#include "quakedef.h" #include "fs.h" #include "libcurl.h" static cvar_t cl_curl_maxdownloads = {CVAR_SAVE, "cl_curl_maxdownloads","1", "maximum number of concurrent HTTP/FTP downloads"}; static cvar_t cl_curl_maxspeed = {CVAR_SAVE, "cl_curl_maxspeed","100", "maximum download speed (KiB/s)"}; static cvar_t sv_curl_defaulturl = {CVAR_SAVE, "sv_curl_defaulturl","", "default autodownload source URL"}; static cvar_t sv_curl_serverpackages = {CVAR_SAVE, "sv_curl_serverpackages","", "list of required files for the clients, separated by spaces"}; static cvar_t cl_curl_enabled = {CVAR_SAVE, "cl_curl_enabled","1", "whether client's download support is enabled"}; /* ================================================================= Minimal set of definitions from libcurl WARNING: for a matter of simplicity, several pointer types are casted to "void*", and most enumerated values are not included ================================================================= */ typedef struct CURL_s CURL; typedef struct CURLM_s CURLM; typedef enum { CURLE_OK = 0 } CURLcode; typedef enum { CURLM_CALL_MULTI_PERFORM=-1, /* please call curl_multi_perform() soon */ CURLM_OK = 0 } CURLMcode; #define CURL_GLOBAL_NOTHING 0 #define CURL_GLOBAL_SSL 1 #define CURL_GLOBAL_WIN32 2 #define CURLOPTTYPE_LONG 0 #define CURLOPTTYPE_OBJECTPOINT 10000 #define CURLOPTTYPE_FUNCTIONPOINT 20000 #define CURLOPTTYPE_OFF_T 30000 #define CINIT(name,type,number) CURLOPT_ ## name = CURLOPTTYPE_ ## type + number typedef enum { CINIT(WRITEDATA, OBJECTPOINT, 1), CINIT(URL, OBJECTPOINT, 2), CINIT(ERRORBUFFER, OBJECTPOINT, 10), CINIT(WRITEFUNCTION, FUNCTIONPOINT, 11), CINIT(REFERER, OBJECTPOINT, 16), CINIT(USERAGENT, OBJECTPOINT, 18), CINIT(RESUME_FROM, LONG, 21), CINIT(FOLLOWLOCATION, LONG, 52), /* use Location: Luke! */ CINIT(PRIVATE, OBJECTPOINT, 103), CINIT(LOW_SPEED_LIMIT, LONG , 19), CINIT(LOW_SPEED_TIME, LONG, 20), } CURLoption; typedef enum { CURLINFO_TEXT = 0, CURLINFO_HEADER_IN, /* 1 */ CURLINFO_HEADER_OUT, /* 2 */ CURLINFO_DATA_IN, /* 3 */ CURLINFO_DATA_OUT, /* 4 */ CURLINFO_SSL_DATA_IN, /* 5 */ CURLINFO_SSL_DATA_OUT, /* 6 */ CURLINFO_END } curl_infotype; #define CURLINFO_STRING 0x100000 #define CURLINFO_LONG 0x200000 #define CURLINFO_DOUBLE 0x300000 #define CURLINFO_SLIST 0x400000 #define CURLINFO_MASK 0x0fffff #define CURLINFO_TYPEMASK 0xf00000 typedef enum { CURLINFO_NONE, /* first, never use this */ CURLINFO_EFFECTIVE_URL = CURLINFO_STRING + 1, CURLINFO_RESPONSE_CODE = CURLINFO_LONG + 2, CURLINFO_TOTAL_TIME = CURLINFO_DOUBLE + 3, CURLINFO_NAMELOOKUP_TIME = CURLINFO_DOUBLE + 4, CURLINFO_CONNECT_TIME = CURLINFO_DOUBLE + 5, CURLINFO_PRETRANSFER_TIME = CURLINFO_DOUBLE + 6, CURLINFO_SIZE_UPLOAD = CURLINFO_DOUBLE + 7, CURLINFO_SIZE_DOWNLOAD = CURLINFO_DOUBLE + 8, CURLINFO_SPEED_DOWNLOAD = CURLINFO_DOUBLE + 9, CURLINFO_SPEED_UPLOAD = CURLINFO_DOUBLE + 10, CURLINFO_HEADER_SIZE = CURLINFO_LONG + 11, CURLINFO_REQUEST_SIZE = CURLINFO_LONG + 12, CURLINFO_SSL_VERIFYRESULT = CURLINFO_LONG + 13, CURLINFO_FILETIME = CURLINFO_LONG + 14, CURLINFO_CONTENT_LENGTH_DOWNLOAD = CURLINFO_DOUBLE + 15, CURLINFO_CONTENT_LENGTH_UPLOAD = CURLINFO_DOUBLE + 16, CURLINFO_STARTTRANSFER_TIME = CURLINFO_DOUBLE + 17, CURLINFO_CONTENT_TYPE = CURLINFO_STRING + 18, CURLINFO_REDIRECT_TIME = CURLINFO_DOUBLE + 19, CURLINFO_REDIRECT_COUNT = CURLINFO_LONG + 20, CURLINFO_PRIVATE = CURLINFO_STRING + 21, CURLINFO_HTTP_CONNECTCODE = CURLINFO_LONG + 22, CURLINFO_HTTPAUTH_AVAIL = CURLINFO_LONG + 23, CURLINFO_PROXYAUTH_AVAIL = CURLINFO_LONG + 24, CURLINFO_OS_ERRNO = CURLINFO_LONG + 25, CURLINFO_NUM_CONNECTS = CURLINFO_LONG + 26, CURLINFO_SSL_ENGINES = CURLINFO_SLIST + 27, } CURLINFO; typedef enum { CURLMSG_NONE, /* first, not used */ CURLMSG_DONE, /* This easy handle has completed. 'result' contains the CURLcode of the transfer */ CURLMSG_LAST } CURLMSG; typedef struct { CURLMSG msg; /* what this message means */ CURL *easy_handle; /* the handle it concerns */ union { void *whatever; /* message-specific data */ CURLcode result; /* return code for transfer */ } data; } CURLMsg; static void (*qcurl_global_init) (long flags); static void (*qcurl_global_cleanup) (); static CURL * (*qcurl_easy_init) (); static void (*qcurl_easy_cleanup) (CURL *handle); static CURLcode (*qcurl_easy_setopt) (CURL *handle, CURLoption option, ...); static CURLcode (*qcurl_easy_getinfo) (CURL *handle, CURLINFO info, ...); static const char * (*qcurl_easy_strerror) (CURLcode); static CURLM * (*qcurl_multi_init) (); static CURLMcode (*qcurl_multi_perform) (CURLM *multi_handle, int *running_handles); static CURLMcode (*qcurl_multi_add_handle) (CURLM *multi_handle, CURL *easy_handle); static CURLMcode (*qcurl_multi_remove_handle) (CURLM *multi_handle, CURL *easy_handle); static CURLMsg * (*qcurl_multi_info_read) (CURLM *multi_handle, int *msgs_in_queue); static void (*qcurl_multi_cleanup) (CURLM *); static const char * (*qcurl_multi_strerror) (CURLcode); static dllfunction_t curlfuncs[] = { {"curl_global_init", (void **) &qcurl_global_init}, {"curl_global_cleanup", (void **) &qcurl_global_cleanup}, {"curl_easy_init", (void **) &qcurl_easy_init}, {"curl_easy_cleanup", (void **) &qcurl_easy_cleanup}, {"curl_easy_setopt", (void **) &qcurl_easy_setopt}, {"curl_easy_strerror", (void **) &qcurl_easy_strerror}, {"curl_easy_getinfo", (void **) &qcurl_easy_getinfo}, {"curl_multi_init", (void **) &qcurl_multi_init}, {"curl_multi_perform", (void **) &qcurl_multi_perform}, {"curl_multi_add_handle", (void **) &qcurl_multi_add_handle}, {"curl_multi_remove_handle",(void **) &qcurl_multi_remove_handle}, {"curl_multi_info_read", (void **) &qcurl_multi_info_read}, {"curl_multi_cleanup", (void **) &qcurl_multi_cleanup}, {"curl_multi_strerror", (void **) &qcurl_multi_strerror}, {NULL, NULL} }; // Handle for CURL DLL static dllhandle_t curl_dll = NULL; // will be checked at many places to find out if qcurl calls are allowed typedef struct downloadinfo_s { char filename[MAX_QPATH]; char url[256]; char referer[256]; qfile_t *stream; fs_offset_t startpos; CURL *curle; qboolean started; qboolean ispak; unsigned long bytes_received; struct downloadinfo_s *next, *prev; qboolean forthismap; } downloadinfo; static downloadinfo *downloads = NULL; static int numdownloads = 0; static qboolean noclear = FALSE; static int numdownloads_fail = 0; static int numdownloads_success = 0; static int numdownloads_added = 0; static char command_when_done[256] = ""; static char command_when_error[256] = ""; /* ==================== Curl_CommandWhenDone Sets the command which is to be executed when the last download completes AND all downloads since last server connect ended with a successful status. Setting the command to NULL clears it. ==================== */ void Curl_CommandWhenDone(const char *cmd) { if(!curl_dll) return; if(cmd) strlcpy(command_when_done, cmd, sizeof(command_when_done)); else *command_when_done = 0; } /* FIXME Do not use yet. Not complete. Problem: what counts as an error? */ void Curl_CommandWhenError(const char *cmd) { if(!curl_dll) return; if(cmd) strlcpy(command_when_error, cmd, sizeof(command_when_error)); else *command_when_error = 0; } /* ==================== Curl_Clear_forthismap Clears the "will disconnect on failure" flags. ==================== */ void Curl_Clear_forthismap() { downloadinfo *di; if(noclear) return; for(di = downloads; di; di = di->next) di->forthismap = false; Curl_CommandWhenError(NULL); Curl_CommandWhenDone(NULL); numdownloads_fail = 0; numdownloads_success = 0; numdownloads_added = 0; } /* ==================== Curl_Have_forthismap Returns true if a download needed for the current game is running. ==================== */ qboolean Curl_Have_forthismap() { return numdownloads_added; } void Curl_Register_predownload() { Curl_CommandWhenDone("cl_begindownloads"); Curl_CommandWhenError("cl_begindownloads"); } /* ==================== Curl_CheckCommandWhenDone Checks if a "done command" is to be executed. All downloads finished, at least one success since connect, no single failure -> execute the command. */ static void Curl_CheckCommandWhenDone() { if(!curl_dll) return; if(numdownloads_added && (numdownloads_success == numdownloads_added) && *command_when_done) { Con_DPrintf("cURL downloads occurred, executing %s\n", command_when_done); Cbuf_AddText("\n"); Cbuf_AddText(command_when_done); Cbuf_AddText("\n"); Curl_Clear_forthismap(); } else if(numdownloads_added && numdownloads_fail && *command_when_error) { Con_DPrintf("cURL downloads FAILED, executing %s\n", command_when_error); Cbuf_AddText("\n"); Cbuf_AddText(command_when_error); Cbuf_AddText("\n"); Curl_Clear_forthismap(); } } /* ==================== CURL_CloseLibrary Load the cURL DLL ==================== */ static qboolean CURL_OpenLibrary (void) { const char* dllnames [] = { #if defined(WIN64) "libcurl64.dll", #elif defined(WIN32) "libcurl-4.dll", "libcurl-3.dll", #elif defined(MACOSX) "libcurl.4.dylib", // Mac OS X Notyetreleased "libcurl.3.dylib", // Mac OS X Tiger "libcurl.2.dylib", // Mac OS X Panther #else "libcurl.so.4", "libcurl.so.3", "libcurl.so", // FreeBSD #endif NULL }; // Already loaded? if (curl_dll) return true; // Load the DLL if (! Sys_LoadLibrary (dllnames, &curl_dll, curlfuncs)) { Con_Printf ("cURL support disabled\n"); return false; } Con_Printf ("cURL support enabled\n"); return true; } /* ==================== CURL_CloseLibrary Unload the cURL DLL ==================== */ static void CURL_CloseLibrary (void) { Sys_UnloadLibrary (&curl_dll); } static CURLM *curlm = NULL; static unsigned long bytes_received = 0; // used for bandwidth throttling static double curltime = 0; /* ==================== CURL_fwrite fwrite-compatible function that writes the data to a file. libcurl can call this. ==================== */ static size_t CURL_fwrite(void *data, size_t size, size_t nmemb, void *vdi) { fs_offset_t ret; size_t bytes = size * nmemb; downloadinfo *di = (downloadinfo *) vdi; bytes_received += bytes; di->bytes_received += bytes; ret = FS_Write(di->stream, data, bytes); return ret; // why not ret / nmemb? } typedef enum { CURL_DOWNLOAD_SUCCESS = 0, CURL_DOWNLOAD_FAILED, CURL_DOWNLOAD_ABORTED, CURL_DOWNLOAD_SERVERERROR } CurlStatus; /* ==================== Curl_EndDownload stops a download. It receives a status (CURL_DOWNLOAD_SUCCESS, CURL_DOWNLOAD_FAILED or CURL_DOWNLOAD_ABORTED) and in the second case the error code from libcurl, or 0, if another error has occurred. ==================== */ static void Curl_EndDownload(downloadinfo *di, CurlStatus status, CURLcode error) { qboolean ok = false; if(!curl_dll) return; switch(status) { case CURL_DOWNLOAD_SUCCESS: Con_Printf("Download of %s: OK\n", di->filename); ok = true; break; case CURL_DOWNLOAD_FAILED: Con_Printf("Download of %s: FAILED\n", di->filename); if(error) Con_Printf("Reason given by libcurl: %s\n", qcurl_easy_strerror(error)); break; case CURL_DOWNLOAD_ABORTED: Con_Printf("Download of %s: ABORTED\n", di->filename); break; case CURL_DOWNLOAD_SERVERERROR: Con_Printf("Download of %s: %d\n", di->filename, (int) error); // reopen to enforce it to have zero bytes again FS_Close(di->stream); di->stream = FS_Open(di->filename, "w", false, false); break; } if(di->curle) { qcurl_multi_remove_handle(curlm, di->curle); qcurl_easy_cleanup(di->curle); } if(ok && !di->bytes_received) { Con_Printf("ERROR: empty file\n"); ok = false; } if(di->stream) FS_Close(di->stream); if(ok && di->ispak) ok = FS_AddPack(di->filename, NULL, true); if(di->prev) di->prev->next = di->next; else downloads = di->next; if(di->next) di->next->prev = di->prev; --numdownloads; if(di->forthismap) { if(ok) ++numdownloads_success; else ++numdownloads_fail; } Z_Free(di); } /* ==================== CheckPendingDownloads checks if there are free download slots to start new downloads in. To not start too many downloads at once, only one download is added at a time, up to a maximum number of cl_curl_maxdownloads are running. ==================== */ static void CheckPendingDownloads() { if(!curl_dll) return; if(numdownloads < cl_curl_maxdownloads.integer) { downloadinfo *di; for(di = downloads; di; di = di->next) { if(!di->started) { Con_Printf("Downloading %s -> %s", di->url, di->filename); di->stream = FS_Open(di->filename, "ab", false, false); if(!di->stream) { Con_Printf("\nFAILED: Could not open output file %s\n", di->filename); Curl_EndDownload(di, CURL_DOWNLOAD_FAILED, CURLE_OK); return; } FS_Seek(di->stream, 0, SEEK_END); di->startpos = FS_Tell(di->stream); if(di->startpos > 0) Con_Printf(", resuming from position %ld", (long) di->startpos); Con_Print("...\n"); di->curle = qcurl_easy_init(); qcurl_easy_setopt(di->curle, CURLOPT_URL, di->url); qcurl_easy_setopt(di->curle, CURLOPT_USERAGENT, engineversion); qcurl_easy_setopt(di->curle, CURLOPT_REFERER, di->referer); qcurl_easy_setopt(di->curle, CURLOPT_RESUME_FROM, (long) di->startpos); qcurl_easy_setopt(di->curle, CURLOPT_FOLLOWLOCATION, 1); qcurl_easy_setopt(di->curle, CURLOPT_WRITEFUNCTION, CURL_fwrite); qcurl_easy_setopt(di->curle, CURLOPT_LOW_SPEED_LIMIT, (long) 256); qcurl_easy_setopt(di->curle, CURLOPT_LOW_SPEED_TIME, (long) 45); qcurl_easy_setopt(di->curle, CURLOPT_WRITEDATA, (void *) di); qcurl_easy_setopt(di->curle, CURLOPT_PRIVATE, (void *) di); qcurl_multi_add_handle(curlm, di->curle); di->started = true; ++numdownloads; if(numdownloads >= cl_curl_maxdownloads.integer) break; } } } } /* ==================== Curl_Init this function MUST be called before using anything else in this file. On Win32, this must be called AFTER WSAStartup has been done! ==================== */ void Curl_Init() { CURL_OpenLibrary(); if(!curl_dll) return; qcurl_global_init(CURL_GLOBAL_NOTHING); curlm = qcurl_multi_init(); } /* ==================== Curl_Shutdown Surprise... closes all the stuff. Please do this BEFORE shutting down LHNET. ==================== */ void Curl_ClearRequirements(); void Curl_Shutdown() { if(!curl_dll) return; Curl_ClearRequirements(); Curl_CancelAll(); CURL_CloseLibrary(); curl_dll = NULL; } /* ==================== Curl_Find Finds the internal information block for a download given by file name. ==================== */ static downloadinfo *Curl_Find(const char *filename) { downloadinfo *di; if(!curl_dll) return NULL; for(di = downloads; di; di = di->next) if(!strcasecmp(di->filename, filename)) return di; return NULL; } /* ==================== Curl_Begin Starts a download of a given URL to the file name portion of this URL (or name if given) in the "dlcache/" folder. ==================== */ void Curl_Begin(const char *URL, const char *name, qboolean ispak, qboolean forthismap) { if(!curl_dll) return; else { char fn[MAX_QPATH]; const char *p, *q; size_t length; downloadinfo *di; // Note: This extraction of the file name portion is NOT entirely correct. // // It does the following: // // http://host/some/script.cgi/SomeFile.pk3?uid=ABCDE -> SomeFile.pk3 // http://host/some/script.php?uid=ABCDE&file=/SomeFile.pk3 -> SomeFile.pk3 // http://host/some/script.php?uid=ABCDE&file=SomeFile.pk3 -> script.php // // However, I'd like to keep this "buggy" behavior so that PHP script // authors can write download scripts without having to enable // AcceptPathInfo on Apache. They just have to ensure that their script // can be called with such a "fake" path name like // http://host/some/script.php?uid=ABCDE&file=/SomeFile.pk3 // // By the way, such PHP scripts should either send the file or a // "Location:" redirect; PHP code example: // // header("Location: http://www.example.com/"); // // By the way, this will set User-Agent to something like // "Nexuiz build 22:27:55 Mar 17 2006" (engineversion) and Referer to // dp://serverhost:serverport/ so you can filter on this; an example // httpd log file line might be: // // 141.2.16.3 - - [17/Mar/2006:22:32:43 +0100] "GET /maps/tznex07.pk3 HTTP/1.1" 200 1077455 "dp://141.2.16.7:26000/" "Nexuiz Linux 22:07:43 Mar 17 2006" if(!name) name = URL; p = strrchr(name, '/'); p = p ? (p+1) : name; q = strchr(p, '?'); length = q ? (size_t)(q - p) : strlen(p); dpsnprintf(fn, sizeof(fn), "dlcache/%.*s", (int)length, p); // already downloading the file? { downloadinfo *di = Curl_Find(fn); if(di) { Con_Printf("Can't download %s, already getting it from %s!\n", fn, di->url); // however, if it was not for this map yet... if(forthismap && !di->forthismap) { di->forthismap = true; // this "fakes" a download attempt so the client will wait for // the download to finish and then reconnect ++numdownloads_added; } return; } } if(ispak && FS_FileExists(fn)) { qboolean already_loaded; if(FS_AddPack(fn, &already_loaded, true)) { Con_DPrintf("%s already exists, not downloading!\n", fn); if(already_loaded) Con_DPrintf("(pak was already loaded)\n"); else { if(forthismap) { ++numdownloads_added; ++numdownloads_success; } } return; } else { qfile_t *f = FS_Open(fn, "rb", false, false); if(f) { char buf[4] = {0}; FS_Read(f, buf, sizeof(buf)); // no "-1", I will use memcmp if(memcmp(buf, "PK\x03\x04", 4) && memcmp(buf, "PACK", 4)) { Con_DPrintf("Detected non-PAK %s, clearing and NOT resuming.\n", fn); FS_Close(f); f = FS_Open(fn, "w", false, false); if(f) FS_Close(f); } else { // OK FS_Close(f); } } } } if(forthismap) ++numdownloads_added; di = (downloadinfo *) Z_Malloc(sizeof(*di)); strlcpy(di->filename, fn, sizeof(di->filename)); strlcpy(di->url, URL, sizeof(di->url)); dpsnprintf(di->referer, sizeof(di->referer), "dp://%s/", cls.netcon ? cls.netcon->address : "notconnected.invalid"); di->forthismap = forthismap; di->stream = NULL; di->startpos = 0; di->curle = NULL; di->started = false; di->ispak = ispak; di->bytes_received = 0; di->next = downloads; di->prev = NULL; if(di->next) di->next->prev = di; downloads = di; } } /* ==================== Curl_Run call this regularily as this will always download as much as possible without blocking. ==================== */ void Curl_Run() { noclear = FALSE; if(!cl_curl_enabled.integer) return; if(!curl_dll) return; Curl_CheckCommandWhenDone(); if(!downloads) return; if(realtime < curltime) // throttle return; { int remaining; CURLMcode mc; do { mc = qcurl_multi_perform(curlm, &remaining); } while(mc == CURLM_CALL_MULTI_PERFORM); for(;;) { CURLMsg *msg = qcurl_multi_info_read(curlm, &remaining); if(!msg) break; if(msg->msg == CURLMSG_DONE) { downloadinfo *di; CurlStatus failed = CURL_DOWNLOAD_SUCCESS; CURLcode result; qcurl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &di); result = msg->data.result; if(result) { failed = CURL_DOWNLOAD_FAILED; } else { long code; qcurl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &code); switch(code / 100) { case 4: // e.g. 404? case 5: // e.g. 500? failed = CURL_DOWNLOAD_SERVERERROR; result = code; break; } } Curl_EndDownload(di, failed, result); } } } CheckPendingDownloads(); // when will we curl the next time? // we will wait a bit to ensure our download rate is kept. // we now know that realtime >= curltime... so set up a new curltime if(cl_curl_maxspeed.value > 0) { unsigned long bytes = bytes_received; // maybe smoothen a bit? curltime = realtime + bytes / (cl_curl_maxspeed.value * 1024.0); bytes_received -= bytes; } else curltime = realtime; } /* ==================== Curl_CancelAll Stops ALL downloads. ==================== */ void Curl_CancelAll() { if(!curl_dll) return; while(downloads) { Curl_EndDownload(downloads, CURL_DOWNLOAD_ABORTED, CURLE_OK); // INVARIANT: downloads will point to the next download after that! } } /* ==================== Curl_Running returns true iff there is a download running. ==================== */ qboolean Curl_Running() { if(!curl_dll) return false; return downloads != NULL; } /* ==================== Curl_GetDownloadAmount returns a value from 0.0 to 1.0 which represents the downloaded amount of data for the given download. ==================== */ static double Curl_GetDownloadAmount(downloadinfo *di) { if(!curl_dll) return -2; if(di->curle) { double length; qcurl_easy_getinfo(di->curle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length); if(length > 0) return di->bytes_received / length; else return 0; } else return -1; } /* ==================== Curl_GetDownloadSpeed returns the speed of the given download in bytes per second ==================== */ static double Curl_GetDownloadSpeed(downloadinfo *di) { if(!curl_dll) return -2; if(di->curle) { double speed; qcurl_easy_getinfo(di->curle, CURLINFO_SPEED_DOWNLOAD, &speed); return speed; } else return -1; } /* ==================== Curl_Info_f prints the download list ==================== */ // TODO rewrite using Curl_GetDownloadInfo? static void Curl_Info_f() { downloadinfo *di; if(!curl_dll) return; if(Curl_Running()) { Con_Print("Currently running downloads:\n"); for(di = downloads; di; di = di->next) { double speed, percent; Con_Printf(" %s -> %s ", di->url, di->filename); percent = 100.0 * Curl_GetDownloadAmount(di); speed = Curl_GetDownloadSpeed(di); if(percent >= 0) Con_Printf("(%.1f%% @ %.1f KiB/s)\n", percent, speed / 1024.0); else Con_Print("(queued)\n"); } } else { Con_Print("No downloads running.\n"); } } /* ==================== Curl_Curl_f implements the "curl" console command curl --info curl --cancel curl --cancel filename curl url For internal use: curl [--pak] [--forthismap] [--for filename filename...] url --pak: after downloading, load the package into the virtual file system --for filename...: only download of at least one of the named files is missing --forthismap: don't reconnect on failure curl --clear_autodownload clears the download success/failure counters curl --finish_autodownload if at least one download has been started, disconnect and drop to the menu once the last download completes successfully, reconnect to the current server ==================== */ void Curl_Curl_f(void) { int i; int end; qboolean pak = false; qboolean forthismap = false; const char *url; const char *name = 0; if(!curl_dll) { Con_Print("libcurl DLL not found, this command is inactive.\n"); return; } if(!cl_curl_enabled.integer) { Con_Print("curl support not enabled. Set cl_curl_enabled to 1 to enable.\n"); return; } for(i = 0; i != Cmd_Argc(); ++i) Con_DPrintf("%s ", Cmd_Argv(i)); Con_DPrint("\n"); if(Cmd_Argc() < 2) { Con_Print("usage:\ncurl --info, curl --cancel [filename], curl url\n"); return; } url = Cmd_Argv(Cmd_Argc() - 1); end = Cmd_Argc(); for(i = 1; i != end; ++i) { const char *a = Cmd_Argv(i); if(!strcmp(a, "--info")) { Curl_Info_f(); return; } else if(!strcmp(a, "--cancel")) { if(i == end - 1) // last argument Curl_CancelAll(); else { downloadinfo *di = Curl_Find(url); if(di) Curl_EndDownload(di, CURL_DOWNLOAD_ABORTED, CURLE_OK); else Con_Print("download not found\n"); } return; } else if(!strcmp(a, "--pak")) { pak = true; } else if(!strcmp(a, "--for")) { for(i = i + 1; i != end - 1; ++i) { if(!FS_FileExists(Cmd_Argv(i))) goto needthefile; // why can't I have a "double break"? } // if we get here, we have all the files... return; } else if(!strcmp(a, "--forthismap")) { forthismap = true; } else if(!strcmp(a, "--as")) { if(i < end - 1) { ++i; name = Cmd_Argv(i); } } else if(!strcmp(a, "--clear_autodownload")) { // mark all running downloads as "not for this map", so if they // fail, it does not matter Curl_Clear_forthismap(); return; } else if(!strcmp(a, "--finish_autodownload")) { if(numdownloads_added) { char donecommand[256]; if(cls.netcon) { if(cl.loadbegun) // curling won't inhibit loading the map any more when at this stage, so bail out and force a reconnect { dpsnprintf(donecommand, sizeof(donecommand), "connect %s", cls.netcon->address); Curl_CommandWhenDone(donecommand); noclear = TRUE; CL_Disconnect(); noclear = FALSE; Curl_CheckCommandWhenDone(); } else Curl_Register_predownload(); } } return; } else if(*a == '-') { Con_Printf("invalid option %s\n", a); return; } } needthefile: Curl_Begin(url, name, pak, forthismap); } /* ==================== Curl_Init_Commands loads the commands and cvars this library uses ==================== */ void Curl_Init_Commands(void) { Cvar_RegisterVariable (&cl_curl_enabled); Cvar_RegisterVariable (&cl_curl_maxdownloads); Cvar_RegisterVariable (&cl_curl_maxspeed); Cvar_RegisterVariable (&sv_curl_defaulturl); Cvar_RegisterVariable (&sv_curl_serverpackages); Cmd_AddCommand ("curl", Curl_Curl_f, "download data from an URL and add to search path"); } /* ==================== Curl_GetDownloadInfo returns an array of Curl_downloadinfo_t structs for usage by GUIs. The number of elements in the array is returned in int *nDownloads. const char **additional_info may be set to a string of additional user information, or to NULL if no such display shall occur. The returned array must be freed later using Z_Free. ==================== */ Curl_downloadinfo_t *Curl_GetDownloadInfo(int *nDownloads, const char **additional_info) { int n, i; downloadinfo *di; Curl_downloadinfo_t *downinfo; static char addinfo[128]; if(!curl_dll) { *nDownloads = 0; if(additional_info) *additional_info = NULL; return NULL; } n = 0; for(di = downloads; di; di = di->next) ++n; downinfo = (Curl_downloadinfo_t *) Z_Malloc(sizeof(*downinfo) * n); i = 0; for(di = downloads; di; di = di->next) { strlcpy(downinfo[i].filename, di->filename, sizeof(downinfo[i].filename)); if(di->curle) { downinfo[i].progress = Curl_GetDownloadAmount(di); downinfo[i].speed = Curl_GetDownloadSpeed(di); downinfo[i].queued = false; } else { downinfo[i].queued = true; } ++i; } if(additional_info) { // TODO: can I clear command_when_done as soon as the first download fails? if(*command_when_done && !numdownloads_fail && numdownloads_added) { if(!strncmp(command_when_done, "connect ", 8)) dpsnprintf(addinfo, sizeof(addinfo), "(will join %s when done)", command_when_done + 8); else if(!strcmp(command_when_done, "cl_begindownloads")) dpsnprintf(addinfo, sizeof(addinfo), "(will enter the game when done)"); else dpsnprintf(addinfo, sizeof(addinfo), "(will do '%s' when done)", command_when_done); *additional_info = addinfo; } else *additional_info = NULL; } *nDownloads = n; return downinfo; } /* ==================== Curl_FindPackURL finds the URL where to find a given package. For this, it reads a file "curl_urls.txt" of the following format: data*.pk3 - revdm*.pk3 http://revdm/downloads/are/here/ * http://any/other/stuff/is/here/ The URLs should end in /. If not, downloads will still work, but the cached files can't be just put into the data directory with the same download configuration (you might want to do this if you want to tag downloaded files from your server, but you should not). "-" means "don't download". If no single pattern matched, the cvar sv_curl_defaulturl is used as download location instead. Note: pak1.pak and data*.pk3 are excluded from autodownload at another point in this file for obvious reasons. ==================== */ static const char *Curl_FindPackURL(const char *filename) { static char foundurl[256]; fs_offset_t filesize; char *buf = (char *) FS_LoadFile("curl_urls.txt", tempmempool, true, &filesize); if(buf && filesize) { // read lines of format "pattern url" char *p = buf; char *pattern = NULL, *patternend = NULL, *url = NULL, *urlend = NULL; qboolean eof = false; pattern = p; while(!eof) { switch(*p) { case 0: eof = true; // fallthrough case '\n': case '\r': if(pattern && url && patternend) { if(!urlend) urlend = p; *patternend = 0; *urlend = 0; if(matchpattern(filename, pattern, true)) { strlcpy(foundurl, url, sizeof(foundurl)); Z_Free(buf); return foundurl; } } pattern = NULL; patternend = NULL; url = NULL; urlend = NULL; break; case ' ': case '\t': if(pattern && !patternend) patternend = p; else if(url && !urlend) urlend = p; break; default: if(!pattern) pattern = p; else if(pattern && patternend && !url) url = p; break; } ++p; } } if(buf) Z_Free(buf); return sv_curl_defaulturl.string; } typedef struct requirement_s { struct requirement_s *next; char filename[MAX_QPATH]; } requirement; static requirement *requirements = NULL; /* ==================== Curl_RequireFile Adds the given file to the list of requirements. ==================== */ void Curl_RequireFile(const char *filename) { requirement *req = (requirement *) Z_Malloc(sizeof(*requirements)); req->next = requirements; strlcpy(req->filename, filename, sizeof(req->filename)); requirements = req; } /* ==================== Curl_ClearRequirements Clears the list of required files for playing on the current map. This should be called at every map change. ==================== */ void Curl_ClearRequirements() { const char *p; while(requirements) { requirement *req = requirements; requirements = requirements->next; Z_Free(req); } p = sv_curl_serverpackages.string; Con_DPrintf("Require all of: %s\n", p); while(COM_ParseToken_Simple(&p, false, false)) { Con_DPrintf("Require: %s\n", com_token); Curl_RequireFile(com_token); } } /* ==================== Curl_SendRequirements Makes the current host_clients download all files he needs. This is done by sending him the following console commands: curl --clear_autodownload curl --pak --for maps/pushmoddm1.bsp --forthismap http://where/this/darn/map/is/pushmoddm1.pk3 curl --finish_autodownload ==================== */ void Curl_SendRequirements() { // for each requirement, find the pack name char sendbuffer[4096] = ""; requirement *req; qboolean foundone = false; for(req = requirements; req; req = req->next) { const char *p; const char *thispack = FS_WhichPack(req->filename); const char *packurl; if(!thispack) continue; p = strrchr(thispack, '/'); if(p) thispack = p + 1; packurl = Curl_FindPackURL(thispack); if(packurl && *packurl && strcmp(packurl, "-")) { if(!foundone) strlcat(sendbuffer, "curl --clear_autodownload\n", sizeof(sendbuffer)); strlcat(sendbuffer, "curl --pak --forthismap --as ", sizeof(sendbuffer)); strlcat(sendbuffer, thispack, sizeof(sendbuffer)); strlcat(sendbuffer, " --for ", sizeof(sendbuffer)); strlcat(sendbuffer, req->filename, sizeof(sendbuffer)); strlcat(sendbuffer, " ", sizeof(sendbuffer)); strlcat(sendbuffer, packurl, sizeof(sendbuffer)); strlcat(sendbuffer, thispack, sizeof(sendbuffer)); strlcat(sendbuffer, "\n", sizeof(sendbuffer)); foundone = true; } } if(foundone) strlcat(sendbuffer, "curl --finish_autodownload\n", sizeof(sendbuffer)); if(strlen(sendbuffer) + 1 < sizeof(sendbuffer)) Host_ClientCommands("%s", sendbuffer); else Con_Printf("Could not initiate autodownload due to URL buffer overflow\n"); }