From b4beaa9f438824435274e5537a3441e6269efc8d Mon Sep 17 00:00:00 2001
From: Lactozilla <jp6781615@gmail.com>
Date: Sun, 4 Feb 2024 20:00:51 -0300
Subject: [PATCH] HTTP downloader port

---
 src/netcode/client_connection.c | 526 +++++++++++++++++++++++---------
 src/netcode/client_connection.h |   5 +-
 src/netcode/d_clisrv.c          |  81 ++++-
 src/netcode/d_clisrv.h          |   3 +-
 src/netcode/d_netcmd.c          |   3 +-
 src/netcode/d_netfil.c          | 446 ++++++++++++++++++++-------
 src/netcode/d_netfil.h          |  58 +++-
 src/netcode/protocol.h          |   6 +-
 src/netcode/server_connection.c |  15 +-
 9 files changed, 872 insertions(+), 271 deletions(-)

diff --git a/src/netcode/client_connection.c b/src/netcode/client_connection.c
index 907021e7d..178ceff63 100644
--- a/src/netcode/client_connection.c
+++ b/src/netcode/client_connection.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -43,11 +43,45 @@ tic_t firstconnectattempttime = 0;
 UINT8 mynode;
 static void *snake = NULL;
 
-static void CL_DrawConnectionStatusBox(void)
+static boolean IsDownloadingFile(void)
+{
+	if (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES)
+		return filedownload.current != -1;
+
+	return false;
+}
+
+static void DrawConnectionStatusBox(void)
 {
 	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
-	if (cl_mode != CL_CONFIRMCONNECT)
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+
+	if (cl_mode == CL_CONFIRMCONNECT || IsDownloadingFile())
+		return;
+
+	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
+}
+
+static void DrawFileProgress(fileneeded_t *file, int y)
+{
+	Net_GetNetStat();
+
+	INT32 dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
+	if (dldlength > 256)
+		dldlength = 256;
+	V_DrawFill(BASEVIDWIDTH/2-128, y, 256, 8, 111);
+	V_DrawFill(BASEVIDWIDTH/2-128, y, dldlength, 8, 96);
+
+	const char *progress_str;
+	if (file->totalsize >= 1024*1024)
+		progress_str = va(" %.2fMiB/%.2fMiB", (double)file->currentsize / (1024*1024), (double)file->totalsize / (1024*1024));
+	else if (file->totalsize < 1024)
+		progress_str = va(" %4uB/%4uB", file->currentsize, file->totalsize);
+	else
+		progress_str = va(" %.2fKiB/%.2fKiB", (double)file->currentsize / 1024, (double)file->totalsize / 1024);
+
+	V_DrawString(BASEVIDWIDTH/2-128, y, V_20TRANS|V_ALLOWLOWERCASE, progress_str);
+
+	V_DrawRightAlignedString(BASEVIDWIDTH/2+128, y, V_20TRANS|V_MONOSPACE, va("%3.1fK/s ", ((double)getbps)/1024));
 }
 
 //
@@ -55,21 +89,21 @@ static void CL_DrawConnectionStatusBox(void)
 //
 // Keep the local client informed of our status.
 //
-static inline void CL_DrawConnectionStatus(void)
+static void CL_DrawConnectionStatus(void)
 {
 	INT32 ccstime = I_GetTime();
 
 	// Draw background fade
 	V_DrawFadeScreen(0xFF00, 16); // force default
 
-	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_LOADFILES)
+	if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADHTTPFILES && cl_mode != CL_LOADFILES)
 	{
 		INT32 animtime = ((ccstime / 4) & 15) + 16;
 		UINT8 palstart;
 		const char *cltext;
 
 		// Draw the bottom box.
-		CL_DrawConnectionStatusBox();
+		DrawConnectionStatusBox();
 
 		if (cl_mode == CL_SEARCHING)
 			palstart = 32; // Red
@@ -78,33 +112,20 @@ static inline void CL_DrawConnectionStatus(void)
 		else
 			palstart = 96; // Green
 
-		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
+		if (!(cl_mode == CL_DOWNLOADSAVEGAME && filedownload.current != -1))
 			for (INT32 i = 0; i < 16; ++i) // 15 pal entries total.
 				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
 
 		switch (cl_mode)
 		{
 			case CL_DOWNLOADSAVEGAME:
-				if (fileneeded && lastfilenum != -1)
+				if (fileneeded && filedownload.current != -1)
 				{
-					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
-					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
-					INT32 dldlength;
+					fileneeded_t *file = &fileneeded[filedownload.current];
 
 					cltext = M_GetText("Downloading game state...");
-					Net_GetNetStat();
-
-					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
-					if (dldlength > 256)
-						dldlength = 256;
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
 
-					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
-
-					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-						va("%3.1fK/s ", ((double)getbps)/1024));
+					DrawFileProgress(file, BASEVIDHEIGHT-16);
 				}
 				else
 					cltext = M_GetText("Waiting to download game state...");
@@ -156,12 +177,11 @@ static inline void CL_DrawConnectionStatus(void)
 			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
 			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, totalfileslength, 8, 96);
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %2u/%2u Files",loadcompletednum,fileneedednum));
+				va(" %2u/%2u files",loadcompletednum,fileneedednum));
 		}
-		else if (lastfilenum != -1)
+		else if (filedownload.current != -1)
 		{
-			INT32 dldlength;
-			static char tempname[28];
+			char tempname[28];
 			fileneeded_t *file;
 			char *filename;
 
@@ -169,24 +189,16 @@ static inline void CL_DrawConnectionStatus(void)
 				Snake_Draw(snake);
 
 			// Draw the bottom box.
-			CL_DrawConnectionStatusBox();
+			DrawConnectionStatusBox();
 
 			if (fileneeded)
 			{
-				file = &fileneeded[lastfilenum];
+				file = &fileneeded[filedownload.current];
 				filename = file->filename;
 			}
 			else
 				return;
 
-			Net_GetNetStat();
-			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
-			if (dldlength > 256)
-				dldlength = 256;
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
-
-			memset(tempname, 0, sizeof(tempname));
 			// offset filename to just the name only part
 			filename += strlen(filename) - nameonlylength(filename);
 
@@ -199,22 +211,56 @@ static inline void CL_DrawConnectionStatus(void)
 			}
 			else // we can copy the whole thing in safely
 			{
-				strncpy(tempname, filename, sizeof(tempname)-1);
+				strlcpy(tempname, filename, sizeof(tempname));
 			}
 
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
-				va(M_GetText("Downloading \"%s\""), tempname));
-			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
-			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
-				va("%3.1fK/s ", ((double)getbps)/1024));
+			// Lactozilla: Disabled, see below change
+			// (also it doesn't really fit on a typical SRB2 screen)
+#if 0
+			const char *download_str = cl_mode == CL_DOWNLOADHTTPFILES
+				? M_GetText("HTTP downloading \"%s\"")
+				: M_GetText("Downloading \"%s\"");
+#else
+			const char *download_str = M_GetText("Downloading \"%s\"");
+#endif
+
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_ALLOWLOWERCASE|V_YELLOWMAP,
+				va(download_str, tempname));
+
+			// Rusty: actually lets do this instead
+			if (cl_mode == CL_DOWNLOADHTTPFILES)
+			{
+				const char *http_source = filedownload.http_source;
+
+				if (strlen(http_source) > sizeof(tempname)-1) // too long to display fully
+				{
+					size_t endhalfpos = strlen(http_source)-10;
+					// display as first 14 chars + ... + last 10 chars
+					// which should add up to 27 if our math(s) is correct
+					snprintf(tempname, sizeof(tempname), "%.14s...%.10s", http_source, http_source+endhalfpos);
+				}
+				else // we can copy the whole thing in safely
+				{
+					strlcpy(tempname, http_source, sizeof(tempname));
+				}
+
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+					va(M_GetText("from %s"), tempname));
+			}
+			else
+			{
+				V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_ALLOWLOWERCASE|V_YELLOWMAP,
+					M_GetText("from the server"));
+			}
+
+			DrawFileProgress(file, BASEVIDHEIGHT-16);
 		}
 		else
 		{
 			if (snake)
 				Snake_Draw(snake);
 
-			CL_DrawConnectionStatusBox();
+			DrawConnectionStatusBox();
 			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				M_GetText("Waiting to download files..."));
 		}
@@ -479,23 +525,98 @@ void CL_UpdateServerList(boolean internetsearch, INT32 room)
 #endif // MASTERSERVER
 }
 
+static boolean IsFileDownloadable(fileneeded_t *file)
+{
+	return file->status == FS_NOTFOUND || file->status == FS_MD5SUMBAD;
+}
+
+static boolean UseDirectDownloader(void)
+{
+	return filedownload.http_source[0] == '\0' || filedownload.http_failed;
+}
+
+static void DoLoadFiles(void)
+{
+	Snake_Free(&snake);
+
+	cl_mode = CL_LOADFILES;
+}
+
+static void AbortConnection(void)
+{
+	Snake_Free(&snake);
+
+	D_QuitNetGame();
+	CL_Reset();
+	D_StartTitle();
+
+	// Will be reset by caller. Signals refusal.
+	cl_mode = CL_ABORTED;
+}
+
+static void BeginDownload(boolean direct)
+{
+	filedownload.current = 0;
+	filedownload.remaining = 0;
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		// Lactozilla: Rusty had fixed this SLIGHTLY incorrectly.
+		// Since [redacted] doesn't HAVE its own file transmission code - it spins an HTTP server - it wasn't
+		// instantly obvious to us where to do this, and we didn't want to check the original implementation.
+		if (fileneeded[i].status == FS_FALLBACK)
+			fileneeded[i].status = FS_NOTFOUND;
+
+		if (IsFileDownloadable(&fileneeded[i]))
+			filedownload.remaining++;
+	}
+
+	if (!filedownload.remaining)
+	{
+		DoLoadFiles();
+		return;
+	}
+
+	if (!direct)
+	{
+		cl_mode = CL_DOWNLOADHTTPFILES;
+		Snake_Allocate(&snake);
+
+		// Discard any paused downloads
+		CL_AbortDownloadResume();
+	}
+	else
+	{
+		// do old LEGACY request
+		if (CL_SendFileRequest())
+		{
+			cl_mode = CL_DOWNLOADFILES;
+
+			// don't alloc snake if already alloced
+			if (!snake)
+				Snake_Allocate(&snake);
+		}
+		else
+		{
+			AbortConnection();
+
+			// why was this its own cl_mode_t?
+			M_StartMessage(M_GetText(
+				"The direct downloader encountered an error.\n"
+				"See the logfile for more info.\n\n"
+				"Press ESC\n"
+			), NULL, MM_NOTHING);
+		}
+	}
+}
+
 static void M_ConfirmConnect(event_t *ev)
 {
 	if (ev->type == ev_keydown)
 	{
 		if (ev->key == ' ' || ev->key == 'y' || ev->key == KEY_ENTER || ev->key == KEY_JOY1)
 		{
-			if (totalfilesrequestednum > 0)
-			{
-				if (CL_SendFileRequest())
-				{
-					cl_mode = CL_DOWNLOADFILES;
-					Snake_Allocate(&snake);
-				}
-			}
-			else
-				cl_mode = CL_LOADFILES;
-
+			BeginDownload(UseDirectDownloader());
 			M_ClearMenus(true);
 		}
 		else if (ev->key == 'n' || ev->key == KEY_ESCAPE || ev->key == KEY_JOY1 + 3)
@@ -506,22 +627,122 @@ static void M_ConfirmConnect(event_t *ev)
 	}
 }
 
-static boolean CL_FinishedFileList(void)
+static const char *GetPrintableFileSize(UINT64 filesize)
 {
-	INT32 i;
-	char *downloadsize = NULL;
+	static char downloadsize[32];
+
+	if (filesize >= 1024*1024)
+		snprintf(downloadsize, sizeof(downloadsize), "%.2fMiB", (double)filesize / (1024*1024));
+	else if (filesize < 1024)
+		snprintf(downloadsize, sizeof(downloadsize), "%luB", filesize);
+	else
+		snprintf(downloadsize, sizeof(downloadsize), "%.2fKiB", (double)filesize / 1024);
+
+	return downloadsize;
+}
+
+static void ShowDownloadConsentMessage(void)
+{
+	UINT64 totalsize = 0;
+
+	filedownload.completednum = 0;
+	filedownload.completedsize = 0;
+
+	if (fileneeded == NULL)
+		I_Error("CL_FinishedFileList: fileneeded == NULL");
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		if (IsFileDownloadable(&fileneeded[i]))
+			totalsize += fileneeded[i].totalsize;
+	}
+
+	const char *downloadsize = GetPrintableFileSize(totalsize);
+
+	if (serverisfull)
+		M_StartMessage(va(M_GetText(
+			"This server is full!\n"
+			"Download of %s of additional\ncontent is required to join.\n"
+			"\n"
+			"You may download server addons,\nand wait for a slot.\n"
+			"\n"
+			"Press ENTER to continue\nor ESC to cancel.\n"
+		), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+	else
+		M_StartMessage(va(M_GetText(
+			"Download of %s of additional\ncontent is required to join.\n"
+			"\n"
+			"Press ENTER to continue\nor ESC to cancel.\n"
+		), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+
+	cl_mode = CL_CONFIRMCONNECT;
+	curfadevalue = 0;
+}
+
+static const char *GetDirectDownloadFailReason(UINT8 dlstatus)
+{
+	switch (dlstatus)
+	{
+		case DLSTATUS_TOOLARGE:
+			return M_GetText("Some addons are larger than the server is willing to send.");
+		case DLSTATUS_WONTSEND:
+			return M_GetText("The server is not allowing download requests.");
+		case DLSTATUS_NODOWNLOAD:
+			return M_GetText("All addons downloadable, but you have chosen to disable addon downloading.");
+		case DLSTATUS_FOLDER:
+			return M_GetText("One or more addons were added as a folder, which the server cannot send.");
+	}
+
+	return "Unknown reason";
+}
+
+static void HandleDirectDownloadFail(UINT8 dlstatus)
+{
+	// not downloadable, put reason in console
+	CONS_Alert(CONS_NOTICE, M_GetText("You need additional addons to connect to this server:\n"));
+
+	for (UINT8 i = 0; i < fileneedednum; i++)
+	{
+		if (fileneeded[i].folder || (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN))
+		{
+			CONS_Printf(" * \"%s\" ", fileneeded[i].filename);
 
-	//CONS_Printf(M_GetText("Checking files...\n"));
-	i = CL_CheckFiles();
+			if (fileneeded[i].folder)
+			{
+				CONS_Printf("(folder)");
+			}
+			else
+			{
+				CONS_Printf("(%s)", GetPrintableFileSize(fileneeded[i].totalsize));
+
+				if (fileneeded[i].status == FS_NOTFOUND)
+					CONS_Printf(M_GetText(" not found, md5: "));
+				else if (fileneeded[i].status == FS_MD5SUMBAD)
+					CONS_Printf(M_GetText(" wrong version, md5: "));
+
+				char md5tmp[33];
+				for (INT32 j = 0; j < 16; j++)
+					sprintf(&md5tmp[j*2], "%02x", fileneeded[i].md5sum[j]);
+				CONS_Printf("%s", md5tmp);
+			}
+
+			CONS_Printf("\n");
+		}
+	}
+
+	CONS_Printf("%s\n", GetDirectDownloadFailReason(dlstatus));
+}
+
+static boolean CL_FinishedFileList(void)
+{
+	INT32 i = CL_CheckFiles();
 	if (i == 4) // still checking ...
 	{
 		return true;
 	}
 	else if (i == 3) // too many files
 	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 		M_StartMessage(M_GetText(
 			"You have too many WAD files loaded\n"
 			"to add ones the server is using.\n"
@@ -532,15 +753,15 @@ static boolean CL_FinishedFileList(void)
 	}
 	else if (i == 2) // cannot join for some reason
 	{
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 		M_StartMessage(M_GetText(
-			"You have the wrong addons loaded.\n\n"
+			"You have the wrong addons loaded.\n"
+			"\n"
 			"To play on this server, restart\n"
 			"the game and don't load any addons.\n"
 			"SRB2 will automatically add\n"
-			"everything you need when you join.\n\n"
+			"everything you need when you join.\n"
+			"\n"
 			"Press ESC\n"
 		), NULL, MM_NOTHING);
 		return false;
@@ -566,63 +787,38 @@ static boolean CL_FinishedFileList(void)
 	{
 		// must download something
 		// can we, though?
-		if (!CL_CheckDownloadable()) // nope!
+		// Rusty: always check downloadable
+		UINT8 status = CL_CheckDownloadable(UseDirectDownloader());
+		if (status != DLSTATUS_OK)
 		{
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
+			HandleDirectDownloadFail(status);
+			AbortConnection();
 			M_StartMessage(M_GetText(
 				"An error occurred when trying to\n"
 				"download missing addons.\n"
 				"(This is almost always a problem\n"
-				"with the server, not your game.)\n\n"
+				"with the server, not your game.)\n"
+				"\n"
 				"See the console or log file\n"
-				"for additional details.\n\n"
+				"for additional details.\n"
+				"\n"
 				"Press ESC\n"
 			), NULL, MM_NOTHING);
 			return false;
 		}
 
-		downloadcompletednum = 0;
-		downloadcompletedsize = 0;
-		totalfilesrequestednum = 0;
-		totalfilesrequestedsize = 0;
-
-		if (fileneeded == NULL)
-			I_Error("CL_FinishedFileList: fileneeded == NULL");
-
-		for (i = 0; i < fileneedednum; i++)
-			if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
-			{
-				totalfilesrequestednum++;
-				totalfilesrequestedsize += fileneeded[i].totalsize;
-			}
-
-		if (totalfilesrequestedsize>>20 >= 100)
-			downloadsize = Z_StrDup(va("%uM",totalfilesrequestedsize>>20));
-		else
-			downloadsize = Z_StrDup(va("%uK",totalfilesrequestedsize>>10));
-
-		if (serverisfull)
-			M_StartMessage(va(M_GetText(
-				"This server is full!\n"
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"You may download, load server addons,\nand wait for a slot.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
+		if (!filedownload.http_failed)
+		{
+			// show download consent modal ONCE!
+			ShowDownloadConsentMessage();
+		}
 		else
-			M_StartMessage(va(M_GetText(
-				"Download of %s additional content\nis required to join.\n"
-				"\n"
-				"Press ENTER to continue\nor ESC to cancel.\n"
-			), downloadsize), M_ConfirmConnect, MM_EVENTHANDLER);
-
-		Z_Free(downloadsize);
-		cl_mode = CL_CONFIRMCONNECT;
-		curfadevalue = 0;
+		{
+			// do a direct download
+			BeginDownload(true);
+		}
 	}
+
 	return true;
 }
 
@@ -734,15 +930,18 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 				if (reason)
 				{
 					char *message = Z_StrDup(reason);
-					D_QuitNetGame();
-					CL_Reset();
-					D_StartTitle();
+					AbortConnection();
 					M_StartMessage(message, NULL, MM_NOTHING);
 					Z_Free(message);
 					return false;
 				}
 			}
 
+			if (serverlist[i].info.httpsource[0])
+				strlcpy(filedownload.http_source, serverlist[i].info.httpsource, MAX_MIRROR_LENGTH);
+			else
+				filedownload.http_source[0] = '\0';
+
 			D_ParseFileneeded(info->fileneedednum, info->fileneeded, 0);
 
 			if (info->flags & SV_LOTSOFADDONS)
@@ -773,6 +972,42 @@ static boolean CL_ServerConnectionSearchTicker(tic_t *asksent)
 	return true;
 }
 
+static void HandleHTTPDownloadFail(void)
+{
+	char filename[MAX_WADPATH];
+
+	CONS_Alert(CONS_WARNING, M_GetText("One or more addons failed to download:\n"));
+
+	for (int i = 0; i < fileneedednum; i++)
+	{
+		if (fileneeded[i].failed == FDOWNLOAD_FAIL_NONE)
+			continue;
+
+		strlcpy(filename, fileneeded[i].filename, sizeof filename);
+		nameonly(filename);
+
+		CONS_Printf(" * \"%s\" (%s)", filename, GetPrintableFileSize(fileneeded[i].totalsize));
+
+		if (fileneeded[i].failed == FDOWNLOAD_FAIL_NOTFOUND)
+			CONS_Printf(M_GetText(" not found, md5: "));
+		else if (fileneeded[i].failed == FDOWNLOAD_FAIL_MD5SUMBAD)
+			CONS_Printf(M_GetText(" wrong version, md5: "));
+		else
+			CONS_Printf(M_GetText(" other error, md5: "));
+
+		char md5tmp[33];
+		for (INT32 j = 0; j < 16; j++)
+			snprintf(&md5tmp[j*2], sizeof(md5tmp), "%02x", fileneeded[i].md5sum[j]);
+		CONS_Printf("%s\n", md5tmp);
+
+		fileneeded[i].failed = FDOWNLOAD_FAIL_NONE;
+	}
+
+	CONS_Printf(M_GetText("Falling back to direct downloader.\n"));
+
+	cl_mode = CL_CHECKFILES;
+}
+
 /** Called by CL_ConnectToServer
   *
   * \param tmpsave The name of the gamestate file???
@@ -810,21 +1045,43 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			if (!CL_FinishedFileList())
 				return false;
 			break;
-		case CL_DOWNLOADFILES:
+
+		case CL_DOWNLOADHTTPFILES:
 			waitmore = false;
-			for (INT32 i = 0; i < fileneedednum; i++)
-				if (fileneeded[i].status == FS_DOWNLOADING
-					|| fileneeded[i].status == FS_REQUESTED)
+			for (int i = filedownload.current; i < fileneedednum; i++)
+			{
+				if (IsFileDownloadable(&fileneeded[i]))
 				{
+					if (!filedownload.http_running)
+					{
+						if (!CURLPrepareFile(filedownload.http_source, i))
+							HandleHTTPDownloadFail();
+					}
 					waitmore = true;
 					break;
 				}
+			}
+
+			// Rusty TODO: multithread
+			if (filedownload.http_running)
+				CURLGetFile();
+
 			if (waitmore)
 				break; // exit the case
 
-			Snake_Free(&snake);
-
-			cl_mode = CL_LOADFILES;
+			// Done downloading files
+			if (!filedownload.remaining)
+			{
+				if (filedownload.http_failed)
+					HandleHTTPDownloadFail();
+				else
+					DoLoadFiles();
+			}
+			break;
+		case CL_DOWNLOADFILES:
+			// Done downloading files
+			if (!filedownload.remaining)
+				DoLoadFiles();
 			break;
 		case CL_LOADFILES:
 			if (CL_LoadServerFiles())
@@ -839,10 +1096,7 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			if (firstconnectattempttime + NEWTICRATE*300 < I_GetTime() && !server)
 			{
 				CONS_Printf(M_GetText("5 minute wait time exceeded.\n"));
-				CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
-				D_QuitNetGame();
-				CL_Reset();
-				D_StartTitle();
+				AbortConnection();
 				M_StartMessage(M_GetText(
 					"5 minute wait time exceeded.\n"
 					"You may retry connection.\n"
@@ -914,15 +1168,12 @@ static boolean CL_ServerConnectionTicker(const char *tmpsave, tic_t *oldtic, tic
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
 			M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
 
-			Snake_Free(&snake);
+			AbortConnection();
 
-			D_QuitNetGame();
-			CL_Reset();
-			D_StartTitle();
 			memset(gamekeydown, 0, NUMKEYS);
 			return false;
 		}
-		else if (cl_mode == CL_DOWNLOADFILES && snake)
+		else if ((cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADHTTPFILES) && snake)
 			Snake_Update(snake);
 
 		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
@@ -973,7 +1224,7 @@ void CL_ConnectToServer(void)
 
 	sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
 
-	lastfilenum = -1;
+	filedownload.current = -1;
 
 	cl_mode = CL_SEARCHING;
 
@@ -1121,14 +1372,9 @@ void PT_ServerRefuse(SINT8 node)
 		M_StartMessage(va(M_GetText("Server refuses connection\n\nReason:\n%s"),
 			reason), NULL, MM_NOTHING);
 
-		D_QuitNetGame();
-		CL_Reset();
-		D_StartTitle();
+		AbortConnection();
 
 		free(reason);
-
-		// Will be reset by caller. Signals refusal.
-		cl_mode = CL_ABORTED;
 	}
 }
 
diff --git a/src/netcode/client_connection.h b/src/netcode/client_connection.h
index 4d75160d4..ff054236b 100644
--- a/src/netcode/client_connection.h
+++ b/src/netcode/client_connection.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -36,7 +36,8 @@ typedef enum
 	CL_CONNECTED,
 	CL_ABORTED,
 	CL_ASKFULLFILELIST,
-	CL_CONFIRMCONNECT
+	CL_CONFIRMCONNECT,
+	CL_DOWNLOADHTTPFILES
 } cl_mode_t;
 
 extern serverelem_t serverlist[MAXSERVERLIST];
diff --git a/src/netcode/d_clisrv.c b/src/netcode/d_clisrv.c
index 0d5d3fa90..d735e8132 100644
--- a/src/netcode/d_clisrv.c
+++ b/src/netcode/d_clisrv.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -116,6 +116,8 @@ consvar_t cv_playbackspeed = CVAR_INIT ("playbackspeed", "1", 0, playbackspeed_c
 consvar_t cv_idletime = CVAR_INIT ("idletime", "0", CV_SAVE, CV_Unsigned, NULL);
 consvar_t cv_dedicatedidletime = CVAR_INIT ("dedicatedidletime", "10", CV_SAVE, CV_Unsigned, NULL);
 
+consvar_t cv_httpsource = CVAR_INIT ("http_source", "", CV_SAVE, NULL, NULL);
+
 void ResetNode(INT32 node)
 {
 	memset(&netnodes[node], 0, sizeof(*netnodes));
@@ -153,12 +155,15 @@ void CL_Reset(void)
 	FreeFileNeeded();
 	fileneedednum = 0;
 
-	totalfilesrequestednum = 0;
-	totalfilesrequestedsize = 0;
 	firstconnectattempttime = 0;
 	serverisfull = false;
 	connectiontimeout = (tic_t)cv_nettimeout.value; //reset this temporary hack
 
+	filedownload.remaining = 0;
+	filedownload.http_failed = false;
+	filedownload.http_running = false;
+	filedownload.http_source[0] = '\0';
+
 	// D_StartTitle should get done now, but the calling function will handle it
 }
 
@@ -892,6 +897,74 @@ static void PT_Login(SINT8 node, INT32 netconsole)
 #endif
 }
 
+/** Add a login for HTTP downloads. If the
+  * user/password is missing, remove it.
+  *
+  * \sa Command_list_http_logins
+  */
+static void Command_set_http_login (void)
+{
+	HTTP_login  *login;
+	HTTP_login **prev_next;
+
+	if (COM_Argc() < 2)
+	{
+		CONS_Printf(
+				"set_http_login <URL> [user:password]: Set or remove a login to "
+				"authenticate HTTP downloads.\n"
+		);
+		return;
+	}
+
+	login = CURLGetLogin(COM_Argv(1), &prev_next);
+
+	if (COM_Argc() == 2)
+	{
+		if (login)
+		{
+			(*prev_next) = login->next;
+			CONS_Printf("Login for '%s' removed.\n", login->url);
+			Z_Free(login);
+		}
+	}
+	else
+	{
+		if (login)
+			Z_Free(login->auth);
+		else
+		{
+			login = ZZ_Alloc(sizeof *login);
+			login->url = Z_StrDup(COM_Argv(1));
+		}
+
+		login->auth = Z_StrDup(COM_Argv(2));
+
+		login->next = curl_logins;
+		curl_logins = login;
+	}
+}
+
+/** List logins for HTTP downloads.
+  *
+  * \sa Command_set_http_login
+  */
+static void Command_list_http_logins (void)
+{
+	HTTP_login *login;
+
+	for (
+			login = curl_logins;
+			login;
+			login = login->next
+	){
+		CONS_Printf(
+				"'%s' -> '%s'\n",
+				login->url,
+				login->auth
+		);
+	}
+}
+
 static void PT_AskLuaFile(SINT8 node)
 {
 	if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
@@ -1568,6 +1641,8 @@ void D_ClientServerInit(void)
 	COM_AddCommand("reloadbans", Command_ReloadBan, COM_LUA);
 	COM_AddCommand("connect", Command_connect, COM_LUA);
 	COM_AddCommand("nodes", Command_Nodes, COM_LUA);
+	COM_AddCommand("set_http_login", Command_set_http_login, 0);
+	COM_AddCommand("list_http_logins", Command_list_http_logins, 0);
 	COM_AddCommand("resendgamestate", Command_ResendGamestate, COM_LUA);
 #ifdef PACKETDROP
 	COM_AddCommand("drop", Command_Drop, COM_LUA);
diff --git a/src/netcode/d_clisrv.h b/src/netcode/d_clisrv.h
index 61823e65d..5aac4693d 100644
--- a/src/netcode/d_clisrv.h
+++ b/src/netcode/d_clisrv.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -74,6 +74,7 @@ extern UINT32 playerpingtable[MAXPLAYERS];
 extern tic_t servermaxping;
 
 extern consvar_t cv_netticbuffer, cv_resynchattempts, cv_blamecfail, cv_playbackspeed, cv_idletime, cv_dedicatedidletime;
+extern consvar_t cv_httpsource;
 
 // Used in d_net, the only dependence
 void D_ClientServerInit(void);
diff --git a/src/netcode/d_netcmd.c b/src/netcode/d_netcmd.c
index 3cda17895..87f0110a9 100644
--- a/src/netcode/d_netcmd.c
+++ b/src/netcode/d_netcmd.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -606,6 +606,7 @@ void D_RegisterServerCommands(void)
 	CV_RegisterVar(&cv_blamecfail);
 	CV_RegisterVar(&cv_dedicatedidletime);
 	CV_RegisterVar(&cv_idletime);
+	CV_RegisterVar(&cv_httpsource);
 
 	COM_AddCommand("ping", Command_Ping_f, COM_LUA);
 	CV_RegisterVar(&cv_nettimeout);
diff --git a/src/netcode/d_netfil.c b/src/netcode/d_netfil.c
index 7782939c3..dd9a07f6f 100644
--- a/src/netcode/d_netfil.c
+++ b/src/netcode/d_netfil.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -53,6 +53,10 @@
 
 #include <errno.h>
 
+#ifdef HAVE_CURL
+#include <curl/curl.h>
+#endif
+
 // Prototypes
 static boolean AddFileToSendQueue(INT32 node, UINT8 fileid);
 
@@ -104,12 +108,19 @@ typedef struct
 } pauseddownload_t;
 static pauseddownload_t *pauseddownload = NULL;
 
-// for cl loading screen
-INT32 lastfilenum = -1;
-INT32 downloadcompletednum = 0;
-UINT32 downloadcompletedsize = 0;
-INT32 totalfilesrequestednum = 0;
-UINT32 totalfilesrequestedsize = 0;
+file_download_t filedownload;
+
+static CURL *http_handle;
+static CURLM *multi_handle;
+static UINT32 curl_dlnow;
+static UINT32 curl_dltotal;
+static time_t curl_starttime;
+static int curl_runninghandles = 0;
+static UINT32 curl_origfilesize;
+static UINT32 curl_origtotalfilesize;
+static char *curl_realname = NULL;
+static fileneeded_t *curl_curfile = NULL;
+HTTP_login *curl_logins;
 
 luafiletransfer_t *luafiletransfers = NULL;
 boolean waitingforluafiletransfer = false;
@@ -140,6 +151,13 @@ static UINT16 GetWadNumFromFileNeededId(UINT8 id)
 	return UINT16_MAX;
 }
 
+enum
+{
+	WILLSEND_YES,
+	WILLSEND_NO,
+	WILLSEND_TOOLARGE
+};
+
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
@@ -186,9 +204,9 @@ UINT8 *PutFileNeeded(UINT16 firstfile)
 		{
 			// Store in the upper four bits
 			if (!cv_downloading.value)
-				filestatus += (2 << 4); // Won't send
+				filestatus += (WILLSEND_NO << 4); // Won't send
 			else if (wadfiles[i]->filesize <= (UINT32)cv_maxsend.value * 1024)
-				filestatus += (1 << 4); // Will send if requested
+				filestatus += (WILLSEND_YES << 4); // Will send if requested
 			// else
 				// filestatus += (0 << 4); -- Won't send, too big
 		}
@@ -250,6 +268,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 		fileneeded[i].willsend = (UINT8)(filestatus >> 4);
 		fileneeded[i].totalsize = READUINT32(p); // The four next bytes are the file size
 		fileneeded[i].file = NULL; // The file isn't open yet
+		fileneeded[i].failed = FDOWNLOAD_FAIL_NONE;
 		READSTRINGN(p, fileneeded[i].filename, MAX_WADPATH); // The next bytes are the file name
 		READMEM(p, fileneeded[i].md5sum, 16); // The last 16 bytes are the file checksum
 	}
@@ -257,7 +276,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr, UINT16 fi
 
 void CL_PrepareDownloadSaveGame(const char *tmpsave)
 {
-	lastfilenum = -1;
+	filedownload.current = -1;
 
 	FreeFileNeeded();
 	AllocFileNeeded(1);
@@ -275,86 +294,37 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
 /** Checks the server to see if we CAN download all the files,
   * before starting to create them and requesting.
   *
+  * \param direct Game will do a direct download
   * \return True if we can download all the files
   *
   */
-boolean CL_CheckDownloadable(void)
+UINT8 CL_CheckDownloadable(boolean direct)
 {
-	UINT8 dlstatus = 0;
+	UINT8 dlstatus = DLSTATUS_OK;
 
 	for (UINT8 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN)
 		{
 			if (fileneeded[i].folder)
 			{
-				dlstatus = 4;
+				dlstatus = DLSTATUS_FOLDER;
 				break;
 			}
 
-			if (fileneeded[i].willsend == 1)
+			if (!direct || fileneeded[i].willsend == WILLSEND_YES)
 				continue;
 
-			if (fileneeded[i].willsend == 0)
-				dlstatus = 1;
-			else //if (fileneeded[i].willsend == 2)
-				dlstatus = 2;
+			if (fileneeded[i].willsend == WILLSEND_TOOLARGE)
+				dlstatus = DLSTATUS_TOOLARGE;
+			else //if (fileneeded[i].willsend == WILLSEND_NO)
+				dlstatus = DLSTATUS_WONTSEND;
 		}
 
 	// Downloading locally disabled
-	if (!dlstatus && M_CheckParm("-nodownload"))
-		dlstatus = 3;
-
-	if (!dlstatus)
-		return true;
-
-	// not downloadable, put reason in console
-	CONS_Alert(CONS_NOTICE, M_GetText("You need additional addons to connect to this server:\n"));
-
-	for (UINT8 i = 0; i < fileneedednum; i++)
-	{
-		if (fileneeded[i].folder || (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN))
-		{
-			CONS_Printf(" * \"%s\" ", fileneeded[i].filename);
+	if (direct && !dlstatus && M_CheckParm("-nodownload"))
+		dlstatus = DLSTATUS_NODOWNLOAD;
 
-			if (fileneeded[i].folder)
-			{
-				CONS_Printf("(folder)");
-			}
-			else
-			{
-				CONS_Printf("(%dK)", fileneeded[i].totalsize >> 10);
-
-				if (fileneeded[i].status == FS_NOTFOUND)
-					CONS_Printf(M_GetText(" not found, md5: "));
-				else if (fileneeded[i].status == FS_MD5SUMBAD)
-					CONS_Printf(M_GetText(" wrong version, md5: "));
-
-				char md5tmp[33];
-				for (INT32 j = 0; j < 16; j++)
-					sprintf(&md5tmp[j*2], "%02x", fileneeded[i].md5sum[j]);
-				CONS_Printf("%s", md5tmp);
-			}
-
-			CONS_Printf("\n");
-		}
-	}
-
-	switch (dlstatus)
-	{
-		case 1:
-			CONS_Printf(M_GetText("Some addons are larger than the server is willing to send.\n"));
-			break;
-		case 2:
-			CONS_Printf(M_GetText("The server is not allowing download requests.\n"));
-			break;
-		case 3:
-			CONS_Printf(M_GetText("All addons downloadable, but you have chosen to disable addon downloading.\n"));
-			break;
-		case 4:
-			CONS_Printf(M_GetText("One or more addons were added as a folder, which the server cannot send.\n"));
-			break;
-	}
-	return false;
+	return dlstatus;
 }
 
 /** Returns true if a needed file transfer can be resumed
@@ -396,23 +366,42 @@ boolean CL_SendFileRequest(void)
 
 #ifdef PARANOIA
 	if (M_CheckParm("-nodownload"))
-		I_Error("Attempted to download files in -nodownload mode");
+	{
+		CONS_Printf("Attempted to download files in -nodownload mode");
+		return false;
+	}
+#endif
 
 	for (INT32 i = 0; i < fileneedednum; i++)
+	{
 		if (fileneeded[i].status != FS_FOUND && fileneeded[i].status != FS_OPEN
-			&& (fileneeded[i].willsend == 0 || fileneeded[i].willsend == 2))
+			&& (fileneeded[i].willsend == WILLSEND_TOOLARGE || fileneeded[i].willsend == WILLSEND_NO || fileneeded[i].folder))
 		{
-			I_Error("Attempted to download files that were not sendable");
+			CONS_Printf("Attempted to download files that were not sendable");
+			return false;
 		}
-#endif
+
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+		{
+			// Error check for the first time around.
+			totalfreespaceneeded += fileneeded[i].totalsize;
+		}
+	}
+
+	I_GetDiskFreeSpace(&availablefreespace);
+	if (totalfreespaceneeded > availablefreespace)
+	{
+		CONS_Printf("To play on this server you must download %s KB,\n"
+			"but you have only %s KB free space on your device\n",
+			sizeu1((size_t)(totalfreespaceneeded>>10)), sizeu2((size_t)(availablefreespace>>10)));
+		return false;
+	}
 
 	netbuffer->packettype = PT_REQUESTFILE;
 	p = (char *)netbuffer->u.textcmd;
 	for (INT32 i = 0; i < fileneedednum; i++)
 		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
 		{
-			totalfreespaceneeded += fileneeded[i].totalsize;
-
 			WRITEUINT8(p, i); // fileid
 
 			// put it in download dir
@@ -424,15 +413,16 @@ boolean CL_SendFileRequest(void)
 
 	WRITEUINT8(p, 0xFF);
 
-	I_GetDiskFreeSpace(&availablefreespace);
-	if (totalfreespaceneeded > availablefreespace)
-		I_Error("To play on this server you must download %s KB,\n"
-			"but you have only %s KB free space on this drive\n",
-			sizeu1((size_t)(totalfreespaceneeded>>10)), sizeu2((size_t)(availablefreespace>>10)));
+	if (!HSendPacket(servernode, true, 0, p - (char *)netbuffer->u.textcmd))
+	{
+		CONS_Printf("Could not send download request packet to server\n");
+		return false;
+	}
 
 	// prepare to download
 	I_mkdir(downloaddir, 0755);
-	return HSendPacket(servernode, true, 0, p - (char *)netbuffer->u.textcmd);
+
+	return true;
 }
 
 // get request filepak and put it on the send queue
@@ -514,7 +504,7 @@ INT32 CL_CheckFiles(void)
 
 	for (i = 0; i < fileneedednum; i++)
 	{
-		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD)
+		if (fileneeded[i].status == FS_NOTFOUND || fileneeded[i].status == FS_MD5SUMBAD || fileneeded[i].status == FS_FALLBACK)
 			downloadrequired = true;
 
 		if (fileneeded[i].status != FS_OPEN)
@@ -932,8 +922,6 @@ boolean AddLuaFileToSendQueue(INT32 node, const char *filename)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
-	//INT32 i;
-	//char wadfilename[MAX_WADPATH];
 
 	luafiletransfers->nodestatus[node] = LFTNS_SENDING;
 
@@ -1306,6 +1294,21 @@ void FileReceiveTicker(void)
 	}
 }
 
+static void OpenNewFileForDownload(fileneeded_t *file, const char *filename)
+{
+	file->file = fopen(filename, "wb");
+	if (!file->file)
+		I_Error("Can't create file %s: %s", filename, strerror(errno));
+
+	file->currentsize = 0;
+	file->totalsize = LONG(netbuffer->u.filetxpak.filesize);
+	file->ackresendposition = UINT32_MAX; // Only used for resumed downloads
+
+	file->receivedfragments = calloc(file->totalsize / file->fragmentsize + 1, sizeof(*file->receivedfragments));
+	if (!file->receivedfragments)
+		I_Error("FileSendTicker: No more memory\n");
+}
+
 void PT_FileFragment(SINT8 node, INT32 netconsole)
 {
 	if (netnodes[node].ingame)
@@ -1348,8 +1351,6 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		))
 		I_Error("Tried to download \"%s\"", filename);
 
-	filename = file->filename;
-
 	if (filenum >= fileneedednum)
 	{
 		DEBFILE(va("fileframent not needed %d>%d\n", filenum, fileneedednum));
@@ -1372,15 +1373,24 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 
 		if (CL_CanResumeDownload(file))
 		{
-			file->file = fopen(filename, "r+b");
+			file->file = fopen(file->filename, "r+b");
 			if (!file->file)
-				I_Error("Can't reopen file %s: %s", filename, strerror(errno));
-			CONS_Printf("\r%s...\n", filename);
+			{
+				CONS_Alert(CONS_ERROR, "Couldn't reopen file %s: %s\n", file->filename, strerror(errno));
+
+				free(pauseddownload->receivedfragments);
 
-			CONS_Printf("Resuming download...\n");
-			file->currentsize = pauseddownload->currentsize;
-			file->receivedfragments = pauseddownload->receivedfragments;
-			file->ackresendposition = 0;
+				CONS_Printf("Restarting download of addon \"%s\"...\n", filename);
+
+				OpenNewFileForDownload(file, file->filename);
+			}
+			else
+			{
+				CONS_Printf("Resuming download of addon \"%s\"...\n", filename);
+				file->currentsize = pauseddownload->currentsize;
+				file->receivedfragments = pauseddownload->receivedfragments;
+				file->ackresendposition = 0;
+			}
 
 			free(pauseddownload);
 			pauseddownload = NULL;
@@ -1388,20 +1398,8 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		else
 		{
 			CL_AbortDownloadResume();
-
-			file->file = fopen(filename, "wb");
-			if (!file->file)
-				I_Error("Can't create file %s: %s", filename, strerror(errno));
-
-			CONS_Printf("\r%s...\n",filename);
-
-			file->currentsize = 0;
-			file->totalsize = LONG(netbuffer->u.filetxpak.filesize);
-			file->ackresendposition = UINT32_MAX; // Only used for resumed downloads
-
-			file->receivedfragments = calloc(file->totalsize / fragmentsize + 1, sizeof(*file->receivedfragments));
-			if (!file->receivedfragments)
-				I_Error("FileSendTicker: No more memory\n");
+			OpenNewFileForDownload(file, file->filename);
+			CONS_Printf("Downloading addon \"%s\" from the server...\n", filename);
 		}
 
 		lasttimeackpacketsent = I_GetTime();
@@ -1421,7 +1419,7 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 			// We can receive packets in the wrong order, anyway all OSes support gaped files
 			fseek(file->file, fragmentpos, SEEK_SET);
 			if (fragmentsize && fwrite(netbuffer->u.filetxpak.data, boundedfragmentsize, 1, file->file) != 1)
-				I_Error("Can't write to %s: %s\n",filename, M_FileError(file->file));
+				I_Error("Can't write to %s: %s\n",file->filename, M_FileError(file->file));
 			file->currentsize += boundedfragmentsize;
 
 			AddFragmentToAckPacket(file->ackpacket, file->iteration, fragmentpos / fragmentsize, filenum);
@@ -1435,8 +1433,6 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 				free(file->ackpacket);
 				file->status = FS_FOUND;
 				file->justdownloaded = true;
-				CONS_Printf(M_GetText("Downloading %s...(done)\n"),
-					filename);
 
 				// Tell the server we have received the file
 				netbuffer->packettype = PT_FILERECEIVED;
@@ -1449,6 +1445,16 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 					netbuffer->packettype = PT_HASLUAFILE;
 					HSendPacket(servernode, true, 0, 0);
 					FreeFileNeeded();
+
+					CONS_Printf(M_GetText("Downloaded \"%s\"\n"), filename);
+				}
+				else
+				{
+					filedownload.completednum++;
+					filedownload.completedsize += file->totalsize;
+					filedownload.remaining--;
+
+					CONS_Printf(M_GetText("Finished download of addon \"%s\"\n"), filename);
 				}
 			}
 		}
@@ -1483,7 +1489,7 @@ void PT_FileFragment(SINT8 node, INT32 netconsole)
 		I_Error("Received a file not requested (file id: %d, file status: %s)\n", filenum, s);
 	}
 
-	lastfilenum = filenum;
+	filedownload.current = filenum;
 }
 
 /** \brief Checks if a node is downloading a file
@@ -1581,6 +1587,220 @@ void Command_Downloads_f(void)
 		}
 }
 
+static size_t curlwrite_data(void *ptr, size_t size, size_t nmemb, FILE *stream)
+{
+    return fwrite(ptr, size, nmemb, stream);
+}
+
+static int curlprogress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
+{
+	(void)clientp;
+	(void)ultotal;
+	(void)ulnow; // Function prototype requires these but we won't use, so just discard
+	curl_dlnow = (UINT32)dlnow;
+	curl_dltotal = (UINT32)dltotal;
+	getbytes = ((double)dlnow) / (time(NULL) - curl_starttime); // To-do: Make this more accurate???
+	return 0;
+}
+
+boolean CURLPrepareFile(const char* url, int dfilenum)
+{
+	HTTP_login *login;
+
+#ifdef PARANOIA
+	if (M_CheckParm("-nodownload"))
+		I_Error("Attempted to download files in -nodownload mode");
+#endif
+
+	curl_global_init(CURL_GLOBAL_ALL);
+
+	http_handle = curl_easy_init();
+	multi_handle = curl_multi_init();
+
+	if (http_handle && multi_handle)
+	{
+		I_mkdir(downloaddir, 0755);
+
+		curl_curfile = &fileneeded[dfilenum];
+		curl_realname = curl_curfile->filename;
+		nameonly(curl_realname);
+
+		curl_origfilesize = curl_curfile->currentsize;
+		curl_origtotalfilesize = curl_curfile->totalsize;
+
+		char md5tmp[33];
+		for (INT32 j = 0; j < 16; j++)
+			sprintf(&md5tmp[j*2], "%02x", curl_curfile->md5sum[j]);
+
+		curl_easy_setopt(http_handle, CURLOPT_URL, va("%s/%s?md5=%s", url, curl_realname, md5tmp));
+
+		// Only allow HTTP and HTTPS
+		curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS_STR, "http,https");
+
+		// Set user agent, as some servers won't accept invalid user agents.
+		curl_easy_setopt(http_handle, CURLOPT_USERAGENT, va("Sonic Robo Blast 2/v%d.%d", VERSION, SUBVERSION));
+
+		// Authenticate if the user so wishes
+		login = CURLGetLogin(url, NULL);
+
+		if (login)
+		{
+			curl_easy_setopt(http_handle, CURLOPT_USERPWD, login->auth);
+		}
+
+		// Follow a redirect request, if sent by the server.
+		curl_easy_setopt(http_handle, CURLOPT_FOLLOWLOCATION, 1L);
+
+		curl_easy_setopt(http_handle, CURLOPT_FAILONERROR, 1L);
+
+		CONS_Printf("Downloading addon \"%s\" from %s\n", curl_realname, url);
+
+		strcatbf(curl_curfile->filename, downloaddir, "/");
+		curl_curfile->file = fopen(curl_curfile->filename, "wb");
+		curl_easy_setopt(http_handle, CURLOPT_WRITEDATA, curl_curfile->file);
+		curl_easy_setopt(http_handle, CURLOPT_WRITEFUNCTION, curlwrite_data);
+		curl_easy_setopt(http_handle, CURLOPT_NOPROGRESS, 0L);
+		curl_easy_setopt(http_handle, CURLOPT_XFERINFOFUNCTION, curlprogress_callback);
+
+		curl_curfile->status = FS_DOWNLOADING;
+		curl_multi_add_handle(multi_handle, http_handle);
+
+		curl_multi_perform(multi_handle, &curl_runninghandles);
+		curl_starttime = time(NULL);
+
+		filedownload.current = dfilenum;
+		filedownload.http_running = true;
+
+		return true;
+	}
+
+	filedownload.http_running = false;
+
+	return false;
+}
+
+void CURLGetFile(void)
+{
+	CURLMcode mc; /* return code used by curl_multi_wait() */
+	CURLcode easyres; /* Return from easy interface */
+	int numfds;
+	CURLMsg *m; /* for picking up messages with the transfer status */
+	CURL *e;
+	int msgs_left; /* how many messages are left */
+	const char *easy_handle_error;
+
+	if (curl_runninghandles)
+	{
+		curl_multi_perform(multi_handle, &curl_runninghandles);
+
+		/* wait for activity, timeout or "nothing" */
+		mc = curl_multi_wait(multi_handle, NULL, 0, 1000, &numfds);
+
+		if (mc != CURLM_OK)
+		{
+			CONS_Alert(CONS_WARNING, "curl_multi_wait() failed, code %d.\n", mc);
+			return;
+		}
+		curl_curfile->currentsize = curl_dlnow;
+		curl_curfile->totalsize = curl_dltotal;
+	}
+
+	/* See how the transfers went */
+	while ((m = curl_multi_info_read(multi_handle, &msgs_left)))
+	{
+		if (m && (m->msg == CURLMSG_DONE))
+		{
+			e = m->easy_handle;
+			easyres = m->data.result;
+
+			char *filename = Z_StrDup(curl_realname);
+			nameonly(filename);
+
+			if (easyres != CURLE_OK)
+			{
+				long response_code = 0;
+
+				if (easyres == CURLE_HTTP_RETURNED_ERROR)
+					curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
+
+				if (response_code == 404)
+					curl_curfile->failed = FDOWNLOAD_FAIL_NOTFOUND;
+				else
+					curl_curfile->failed = FDOWNLOAD_FAIL_OTHER;
+
+				easy_handle_error = (response_code) ? va("HTTP response code %ld", response_code) : curl_easy_strerror(easyres);
+				curl_curfile->status = FS_FALLBACK;
+				curl_curfile->currentsize = curl_origfilesize;
+				curl_curfile->totalsize = curl_origtotalfilesize;
+				filedownload.http_failed = true;
+				fclose(curl_curfile->file);
+				remove(curl_curfile->filename);
+				CONS_Alert(CONS_ERROR, M_GetText("Failed to download addon \"%s\" (%s)\n"), filename, easy_handle_error);
+			}
+			else
+			{
+				fclose(curl_curfile->file);
+
+				CONS_Printf(M_GetText("Finished download of addon \"%s\"\n"), filename);
+
+				if (checkfilemd5(curl_curfile->filename, curl_curfile->md5sum) == FS_MD5SUMBAD)
+				{
+					CONS_Alert(CONS_WARNING, M_GetText("File \"%s\" does not match the version used by the server\n"), filename);
+					curl_curfile->status = FS_FALLBACK;
+					curl_curfile->failed = FDOWNLOAD_FAIL_MD5SUMBAD;
+					filedownload.http_failed = true;
+				}
+				else
+				{
+					filedownload.completednum++;
+					filedownload.completedsize += curl_curfile->totalsize;
+					curl_curfile->status = FS_FOUND;
+				}
+			}
+
+			Z_Free(filename);
+
+			curl_curfile->file = NULL;
+			filedownload.http_running = false;
+			filedownload.remaining--;
+			curl_multi_remove_handle(multi_handle, e);
+			curl_easy_cleanup(e);
+
+			if (!filedownload.remaining)
+				break;
+		}
+	}
+
+	if (!filedownload.remaining)
+	{
+		curl_multi_cleanup(multi_handle);
+		curl_global_cleanup();
+	}
+}
+
+HTTP_login *
+CURLGetLogin (const char *url, HTTP_login ***return_prev_next)
+{
+	HTTP_login  * login;
+	HTTP_login ** prev_next;
+
+	for (
+			prev_next = &curl_logins;
+			( login = (*prev_next));
+			prev_next = &login->next
+	){
+		if (strcmp(login->url, url) == 0)
+		{
+			if (return_prev_next)
+				(*return_prev_next) = prev_next;
+
+			return login;
+		}
+	}
+
+	return NULL;
+}
+
 // Functions cut and pasted from Doomatic :)
 
 void nameonly(char *s)
diff --git a/src/netcode/d_netfil.h b/src/netcode/d_netfil.h
index fdbec8c53..4039b5e2d 100644
--- a/src/netcode/d_netfil.h
+++ b/src/netcode/d_netfil.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -25,6 +25,23 @@ typedef enum
 	SF_NOFREERAM
 } freemethod_t;
 
+typedef enum
+{
+	DLSTATUS_OK,
+	DLSTATUS_TOOLARGE,
+	DLSTATUS_WONTSEND,
+	DLSTATUS_NODOWNLOAD,
+	DLSTATUS_FOLDER
+} dlstatus_t;
+
+typedef enum
+{
+	FDOWNLOAD_FAIL_NONE,
+	FDOWNLOAD_FAIL_NOTFOUND,
+	FDOWNLOAD_FAIL_MD5SUMBAD,
+	FDOWNLOAD_FAIL_OTHER
+} filedownloadfail_t;
+
 typedef enum
 {
 	FS_NOTCHECKED,
@@ -33,7 +50,8 @@ typedef enum
 	FS_REQUESTED,
 	FS_DOWNLOADING,
 	FS_OPEN, // Is opened and used in w_wad
-	FS_MD5SUMBAD
+	FS_MD5SUMBAD,
+	FS_FALLBACK
 } filestatus_t;
 
 typedef enum
@@ -51,6 +69,7 @@ typedef struct
 	UINT8 willsend; // Is the server willing to send it?
 	UINT8 folder; // File is a folder
 	fileneededtype_t type;
+	filedownloadfail_t failed;
 	boolean justdownloaded; // To prevent late fragments from causing an I_Error
 
 	// Used only for download
@@ -70,11 +89,30 @@ extern INT32 fileneedednum;
 extern fileneeded_t *fileneeded;
 extern char downloaddir[512];
 
-extern INT32 lastfilenum;
-extern INT32 downloadcompletednum;
-extern UINT32 downloadcompletedsize;
-extern INT32 totalfilesrequestednum;
-extern UINT32 totalfilesrequestedsize;
+typedef struct
+{
+	INT32 current;
+	INT32 remaining;
+	INT32 completednum;
+	UINT32 completedsize;
+
+	boolean http_failed;
+	boolean http_running;
+
+	char http_source[MAX_MIRROR_LENGTH];
+} file_download_t;
+
+extern file_download_t filedownload;
+
+typedef struct HTTP_login HTTP_login;
+
+extern struct HTTP_login
+{
+	char       * url;
+	char       * auth;
+	HTTP_login * next;
+}
+*curl_logins;
 
 extern consvar_t cv_maxsend, cv_noticedownload, cv_downloadspeed;
 
@@ -97,10 +135,14 @@ boolean SendingFile(INT32 node);
 void FileReceiveTicker(void);
 void PT_FileFragment(SINT8 node, INT32 netconsole);
 
-boolean CL_CheckDownloadable(void);
+UINT8 CL_CheckDownloadable(boolean direct);
 boolean CL_SendFileRequest(void);
 void PT_RequestFile(SINT8 node);
 
+boolean CURLPrepareFile(const char* url, int dfilenum);
+void CURLGetFile(void);
+HTTP_login * CURLGetLogin (const char *url, HTTP_login ***return_prev_next);
+
 typedef enum
 {
 	LFTNS_NONE,    // This node is not connected
diff --git a/src/netcode/protocol.h b/src/netcode/protocol.h
index c084d920c..4b39fab66 100644
--- a/src/netcode/protocol.h
+++ b/src/netcode/protocol.h
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -26,7 +26,7 @@ packet versions.
 If you change the struct or the meaning of a field
 therein, increment this number.
 */
-#define PACKETVERSION 4
+#define PACKETVERSION 5
 
 // Network play related stuff.
 // There is a data struct that stores network
@@ -200,6 +200,7 @@ enum {
 
 #define MAXSERVERNAME 32
 #define MAXFILENEEDED 915
+#define MAX_MIRROR_LENGTH 256
 
 // This packet is too large
 typedef struct
@@ -230,6 +231,7 @@ typedef struct
 	unsigned char mapmd5[16];
 	UINT8 actnum;
 	UINT8 iszone;
+	char httpsource[MAX_MIRROR_LENGTH];
 	UINT8 fileneeded[MAXFILENEEDED]; // is filled with writexxx (byteptr.h)
 } ATTRPACK serverinfo_pak;
 
diff --git a/src/netcode/server_connection.c b/src/netcode/server_connection.c
index 376700f0d..bbabc8f1d 100644
--- a/src/netcode/server_connection.c
+++ b/src/netcode/server_connection.c
@@ -1,7 +1,7 @@
 // SONIC ROBO BLAST 2
 //-----------------------------------------------------------------------------
 // Copyright (C) 1998-2000 by DooM Legacy Team.
-// Copyright (C) 1999-2023 by Sonic Team Junior.
+// Copyright (C) 1999-2024 by Sonic Team Junior.
 //
 // This program is free software distributed under the
 // terms of the GNU General Public License, version 2.
@@ -127,6 +127,8 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 
 	memset(netbuffer->u.serverinfo.maptitle, 0, sizeof netbuffer->u.serverinfo.maptitle);
 
+	memset(netbuffer->u.serverinfo.httpsource, 0, MAX_MIRROR_LENGTH);
+
 	if (mapheaderinfo[gamemap-1] && *mapheaderinfo[gamemap-1]->lvlttl)
 	{
 		char *read = mapheaderinfo[gamemap-1]->lvlttl, *writ = netbuffer->u.serverinfo.maptitle;
@@ -153,6 +155,17 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
 	if (mapheaderinfo[gamemap-1])
 		netbuffer->u.serverinfo.actnum = mapheaderinfo[gamemap-1]->actnum;
 
+	const char *httpurl = cv_httpsource.string;
+	size_t mirror_length = strlen(httpurl);
+	if (mirror_length > MAX_MIRROR_LENGTH)
+		mirror_length = MAX_MIRROR_LENGTH;
+
+	if (snprintf(netbuffer->u.serverinfo.httpsource, mirror_length+1, "%s", httpurl) < 0)
+		// If there's an encoding error, send nothing, we accept that the above may be truncated
+		strncpy(netbuffer->u.serverinfo.httpsource, "", mirror_length);
+
+	netbuffer->u.serverinfo.httpsource[MAX_MIRROR_LENGTH-1] = '\0';
+
 	p = PutFileNeeded(0);
 
 	HSendPacket(node, false, 0, p - ((UINT8 *)&netbuffer->u));
-- 
GitLab