From b4beaa9f438824435274e5537a3441e6269efc8d Mon Sep 17 00:00:00 2001 From: Lactozilla 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 const char *GetPrintableFileSize(UINT64 filesize) +{ + 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); + + 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; - char *downloadsize = NULL; - - //CONS_Printf(M_GetText("Checking files...\n")); - i = CL_CheckFiles(); + 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)); + if (!filedownload.http_failed) + { + // show download consent modal ONCE! + ShowDownloadConsentMessage(); + } 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); - 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 [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 +#ifdef HAVE_CURL +#include +#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 (direct && !dlstatus && M_CheckParm("-nodownload")) + dlstatus = DLSTATUS_NODOWNLOAD; - 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 (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)); - CONS_Printf("Resuming download...\n"); - file->currentsize = pauseddownload->currentsize; - file->receivedfragments = pauseddownload->receivedfragments; - file->ackresendposition = 0; + free(pauseddownload->receivedfragments); + + 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));