1 /**
2 * @file
3 * @author R1CH
4 * @brief HTTP downloading is used if the server provides a content server URL in the
5 * connect message. Any missing content the client needs will then use the
6 * HTTP server. CURL is used to enable multiple files to be downloaded in parallel
7 * to improve performance on high latency links when small files such as textures
8 * are needed. Since CURL natively supports gzip content encoding, any files
9 * on the HTTP server should ideally be gzipped to conserve bandwidth.
10 * @sa CL_ConnectionlessPacket
11 * @sa SVC_DirectConnect
12 */
13
14 /*
15 Copyright (C) 1997-2001 Id Software, Inc.
16
17 This program is free software; you can redistribute it and/or
18 modify it under the terms of the GNU General Public License
19 as published by the Free Software Foundation; either version 2
20 of the License, or (at your option) any later version.
21
22 This program is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
25
26 See the GNU General Public License for more details.
27
28 You should have received a copy of the GNU General Public License
29 along with this program; if not, write to the Free Software
30 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
31
32 */
33
34 #include "client.h"
35 #include "cl_http.h"
36 #include "battlescape/cl_parse.h"
37
38 #ifndef NO_HTTP
39
40 static cvar_t* cl_http_downloads;
41 static cvar_t* cl_http_filelists;
42 static cvar_t* cl_http_max_connections;
43
44 enum {
45 HTTPDL_ABORT_NONE,
46 HTTPDL_ABORT_SOFT,
47 HTTPDL_ABORT_HARD
48 };
49
50 static CURLM *multi = nullptr;
51 static int handleCount = 0;
52 static int pendingCount = 0;
53 static int abortDownloads = HTTPDL_ABORT_NONE;
54 static bool downloadingPK3 = false;
55
StripHighBits(char * string)56 static void StripHighBits (char* string)
57 {
58 char* p = string;
59
60 while (string[0]) {
61 const unsigned char c = *(string++);
62
63 if (c >= 32 && c <= 127)
64 *p++ = c;
65 }
66
67 p[0] = '\0';
68 }
69
isvalidchar(int c)70 static inline bool isvalidchar (int c)
71 {
72 if (!isalnum(c) && c != '_' && c != '-')
73 return false;
74 return true;
75 }
76
77 /**
78 * @brief libcurl callback to update progress info. Mainly just used as
79 * a way to cancel the transfer if required.
80 */
CL_HTTP_Progress(void * clientp,double dltotal,double dlnow,double ultotal,double ulnow)81 static int CL_HTTP_Progress (void* clientp, double dltotal, double dlnow, double ultotal, double ulnow)
82 {
83 dlhandle_t* dl;
84
85 dl = (dlhandle_t*)clientp;
86
87 dl->position = (unsigned)dlnow;
88
89 /* don't care which download shows as long as something does :) */
90 if (!abortDownloads) {
91 Q_strncpyz(cls.downloadName, dl->queueEntry->ufoPath, sizeof(cls.downloadName));
92 cls.downloadPosition = dl->position;
93
94 if (dltotal > 0.0)
95 cls.downloadPercent = (int)((dlnow / dltotal) * 100.0f);
96 else
97 cls.downloadPercent = 0;
98 }
99
100 return abortDownloads;
101 }
102
103 /**
104 * @brief Properly escapes a path with HTTP %encoding. libcurl's function
105 * seems to treat '/' and such as illegal chars and encodes almost
106 * the entire URL...
107 */
CL_EscapeHTTPPath(const char * filePath,char * escaped)108 static void CL_EscapeHTTPPath (const char* filePath, char* escaped)
109 {
110 int i;
111 size_t len;
112 char *p;
113
114 p = escaped;
115
116 len = strlen(filePath);
117 for (i = 0; i < len; i++) {
118 if (!isalnum(filePath[i]) && filePath[i] != ';' && filePath[i] != '/' &&
119 filePath[i] != '?' && filePath[i] != ':' && filePath[i] != '@' && filePath[i] != '&' &&
120 filePath[i] != '=' && filePath[i] != '+' && filePath[i] != '$' && filePath[i] != ',' &&
121 filePath[i] != '[' && filePath[i] != ']' && filePath[i] != '-' && filePath[i] != '_' &&
122 filePath[i] != '.' && filePath[i] != '!' && filePath[i] != '~' && filePath[i] != '*' &&
123 filePath[i] != '\'' && filePath[i] != '(' && filePath[i] != ')') {
124 sprintf(p, "%%%02x", filePath[i]);
125 p += 3;
126 } else {
127 *p = filePath[i];
128 p++;
129 }
130 }
131 p[0] = 0;
132
133 /* using ./ in a url is legal, but all browsers condense the path and some IDS / request */
134 /* filtering systems act a bit funky if http requests come in with uncondensed paths. */
135 len = strlen(escaped);
136 p = escaped;
137 while ((p = strstr (p, "./"))) {
138 memmove(p, p + 2, len - (p - escaped) - 1);
139 len -= 2;
140 }
141 }
142
143 /**
144 * @brief Actually starts a download by adding it to the curl multi handle.
145 */
CL_StartHTTPDownload(dlqueue_t * entry,dlhandle_t * dl)146 static void CL_StartHTTPDownload (dlqueue_t* entry, dlhandle_t* dl)
147 {
148 char escapedFilePath[MAX_QPATH * 4];
149 const char* extension = Com_GetExtension(entry->ufoPath);
150
151 /* yet another hack to accomodate filelists, how i wish i could push :(
152 * nullptr file handle indicates filelist. */
153 if (extension != nullptr && Q_streq(extension, "filelist")) {
154 dl->file = nullptr;
155 CL_EscapeHTTPPath(entry->ufoPath, escapedFilePath);
156 } else {
157 char tempFile[MAX_OSPATH];
158 /** @todo use the FS_OpenFile function here */
159 Com_sprintf(dl->filePath, sizeof(dl->filePath), "%s/%s", FS_Gamedir(), entry->ufoPath);
160
161 Com_sprintf(tempFile, sizeof(tempFile), BASEDIRNAME "/%s", entry->ufoPath);
162 CL_EscapeHTTPPath(tempFile, escapedFilePath);
163
164 strcat(dl->filePath, ".tmp");
165
166 FS_CreatePath(dl->filePath);
167
168 /* don't bother with http resume... too annoying if server doesn't support it. */
169 dl->file = fopen(dl->filePath, "wb");
170 if (!dl->file) {
171 Com_Printf("CL_StartHTTPDownload: Couldn't open %s for writing.\n", dl->filePath);
172 entry->state = DLQ_STATE_DONE;
173 /* CL_RemoveHTTPDownload(entry->ufoPath); */
174 return;
175 }
176 }
177
178 dl->tempBuffer = nullptr;
179 dl->speed = 0;
180 dl->fileSize = 0;
181 dl->position = 0;
182 dl->queueEntry = entry;
183
184 if (!dl->curl)
185 dl->curl = curl_easy_init();
186
187 Com_sprintf(dl->URL, sizeof(dl->URL), "%s%s", cls.downloadServer, escapedFilePath);
188
189 curl_easy_setopt(dl->curl, CURLOPT_ENCODING, "");
190 #ifdef PARANOID
191 curl_easy_setopt(dl->curl, CURLOPT_VERBOSE, 1);
192 #endif
193 curl_easy_setopt(dl->curl, CURLOPT_NOPROGRESS, 0);
194 if (dl->file) {
195 curl_easy_setopt(dl->curl, CURLOPT_WRITEDATA, dl->file);
196 curl_easy_setopt(dl->curl, CURLOPT_WRITEFUNCTION, nullptr);
197 } else {
198 curl_easy_setopt(dl->curl, CURLOPT_WRITEDATA, dl);
199 curl_easy_setopt(dl->curl, CURLOPT_WRITEFUNCTION, HTTP_Recv);
200 }
201 curl_easy_setopt(dl->curl, CURLOPT_CONNECTTIMEOUT, http_timeout->integer);
202 curl_easy_setopt(dl->curl, CURLOPT_TIMEOUT, http_timeout->integer);
203 curl_easy_setopt(dl->curl, CURLOPT_FAILONERROR, 1);
204 curl_easy_setopt(dl->curl, CURLOPT_PROXY, http_proxy->string);
205 curl_easy_setopt(dl->curl, CURLOPT_FOLLOWLOCATION, 1);
206 curl_easy_setopt(dl->curl, CURLOPT_MAXREDIRS, 5);
207 curl_easy_setopt(dl->curl, CURLOPT_WRITEHEADER, dl);
208 curl_easy_setopt(dl->curl, CURLOPT_HEADERFUNCTION, HTTP_Header);
209 curl_easy_setopt(dl->curl, CURLOPT_PROGRESSFUNCTION, CL_HTTP_Progress);
210 curl_easy_setopt(dl->curl, CURLOPT_PROGRESSDATA, dl);
211 curl_easy_setopt(dl->curl, CURLOPT_USERAGENT, GAME_TITLE " " UFO_VERSION);
212 curl_easy_setopt(dl->curl, CURLOPT_REFERER, cls.downloadReferer);
213 curl_easy_setopt(dl->curl, CURLOPT_URL, dl->URL);
214 curl_easy_setopt(dl->curl, CURLOPT_NOSIGNAL, 1);
215
216 if (curl_multi_add_handle(multi, dl->curl) != CURLM_OK) {
217 Com_Printf("curl_multi_add_handle: error\n");
218 dl->queueEntry->state = DLQ_STATE_DONE;
219 return;
220 }
221
222 handleCount++;
223 /*Com_Printf("started dl: hc = %d\n", handleCount); */
224 Com_Printf("CL_StartHTTPDownload: Fetching %s...\n", dl->URL);
225 dl->queueEntry->state = DLQ_STATE_RUNNING;
226 }
227
228 /**
229 * @brief A new server is specified, so we nuke all our state.
230 */
CL_SetHTTPServer(const char * URL)231 void CL_SetHTTPServer (const char* URL)
232 {
233 CL_HTTP_Cleanup();
234
235 for (dlqueue_t* q = cls.downloadQueue; q;) {
236 dlqueue_t* const del = q;
237 q = q->next;
238 Mem_Free(del);
239 }
240 cls.downloadQueue = 0;
241
242 if (multi)
243 Com_Error(ERR_DROP, "CL_SetHTTPServer: Still have old handle");
244
245 multi = curl_multi_init();
246
247 abortDownloads = HTTPDL_ABORT_NONE;
248 handleCount = pendingCount = 0;
249
250 Q_strncpyz(cls.downloadServer, URL, sizeof(cls.downloadServer));
251 }
252
253 /**
254 * @brief Cancel all downloads and nuke the queue.
255 */
CL_CancelHTTPDownloads(bool permKill)256 void CL_CancelHTTPDownloads (bool permKill)
257 {
258 if (permKill)
259 abortDownloads = HTTPDL_ABORT_HARD;
260 else
261 abortDownloads = HTTPDL_ABORT_SOFT;
262
263 for (dlqueue_t* q = cls.downloadQueue; q; q = q->next) {
264 if (q->state == DLQ_STATE_NOT_STARTED)
265 q->state = DLQ_STATE_DONE;
266 }
267
268 if (!pendingCount && !handleCount && abortDownloads == HTTPDL_ABORT_HARD)
269 cls.downloadServer[0] = 0;
270
271 pendingCount = 0;
272 }
273
274 /**
275 * @brief Find a free download handle to start another queue entry on.
276 */
CL_GetFreeDLHandle(void)277 static dlhandle_t* CL_GetFreeDLHandle (void)
278 {
279 int i;
280
281 for (i = 0; i < 4; i++) {
282 dlhandle_t* dl = &cls.HTTPHandles[i];
283 if (!dl->queueEntry || dl->queueEntry->state == DLQ_STATE_DONE)
284 return dl;
285 }
286
287 return nullptr;
288 }
289
290 /**
291 * @brief Called from the precache check to queue a download.
292 * @sa CL_CheckOrDownloadFile
293 */
CL_QueueHTTPDownload(const char * ufoPath)294 bool CL_QueueHTTPDownload (const char* ufoPath)
295 {
296 /* no http server (or we got booted) */
297 if (!cls.downloadServer[0] || abortDownloads || !cl_http_downloads->integer)
298 return false;
299
300 dlqueue_t** anchor = &cls.downloadQueue;
301 for (; *anchor; anchor = &(*anchor)->next) {
302 /* avoid sending duplicate requests */
303 if (Q_streq(ufoPath, (*anchor)->ufoPath))
304 return true;
305 }
306
307 dlqueue_t* const q = Mem_AllocType(dlqueue_t);
308 q->next = nullptr;
309 q->state = DLQ_STATE_NOT_STARTED;
310 Q_strncpyz(q->ufoPath, ufoPath, sizeof(q->ufoPath));
311 *anchor = q;
312
313 /* special case for map file lists */
314 if (cl_http_filelists->integer) {
315 const char* extension = Com_GetExtension(ufoPath);
316 if (extension != nullptr && !Q_strcasecmp(extension, "bsp")) {
317 char listPath[MAX_OSPATH];
318 const size_t len = strlen(ufoPath);
319 Com_sprintf(listPath, sizeof(listPath), BASEDIRNAME"/%.*s.filelist", (int)(len - 4), ufoPath);
320 CL_QueueHTTPDownload(listPath);
321 }
322 }
323
324 /* if a download entry has made it this far, CL_FinishHTTPDownload is guaranteed to be called. */
325 pendingCount++;
326
327 return true;
328 }
329
330 /**
331 * @brief See if we're still busy with some downloads. Called by precacher just
332 * before it loads the map since we could be downloading the map. If we're
333 * busy still, it'll wait and CL_FinishHTTPDownload will pick up from where
334 * it left.
335 */
CL_PendingHTTPDownloads(void)336 bool CL_PendingHTTPDownloads (void)
337 {
338 if (cls.downloadServer[0] == '\0')
339 return false;
340
341 return pendingCount + handleCount;
342 }
343
344 /**
345 * @return true if the file exists, otherwise it attempts to start a download via curl
346 * @sa CL_CheckAndQueueDownload
347 * @sa CL_RequestNextDownload
348 */
CL_CheckOrDownloadFile(const char * filename)349 bool CL_CheckOrDownloadFile (const char* filename)
350 {
351 static char lastfilename[MAX_OSPATH] = "";
352
353 if (Q_strnull(filename))
354 return true;
355
356 /* r1: don't attempt same file many times */
357 if (Q_streq(filename, lastfilename))
358 return true;
359
360 Q_strncpyz(lastfilename, filename, sizeof(lastfilename));
361
362 if (strstr(filename, "..")) {
363 Com_Printf("Refusing to check a path with .. (%s)\n", filename);
364 return true;
365 }
366
367 if (strchr(filename, ' ')) {
368 Com_Printf("Refusing to check a path containing spaces (%s)\n", filename);
369 return true;
370 }
371
372 if (strchr(filename, ':')) {
373 Com_Printf("Refusing to check a path containing a colon (%s)\n", filename);
374 return true;
375 }
376
377 if (filename[0] == '/') {
378 Com_Printf("Refusing to check a path starting with / (%s)\n", filename);
379 return true;
380 }
381
382 if (FS_LoadFile(filename, nullptr) != -1) {
383 /* it exists, no need to download */
384 return true;
385 }
386
387 if (CL_QueueHTTPDownload(filename))
388 return false;
389
390 return true;
391 }
392
393 /**
394 * @brief Validate a path supplied by a filelist.
395 * @param[in,out] path Pointer to file (path) to download (high bits will be stripped).
396 * @sa CL_QueueHTTPDownload
397 * @sa CL_ParseFileList
398 */
CL_CheckAndQueueDownload(char * path)399 static void CL_CheckAndQueueDownload (char* path)
400 {
401 size_t length;
402 const char *ext;
403 bool pak;
404 bool gameLocal;
405
406 StripHighBits(path);
407
408 length = strlen(path);
409
410 if (length >= MAX_QPATH)
411 return;
412
413 ext = Com_GetExtension(path);
414 if (ext == nullptr)
415 return;
416
417 if (Q_streq(ext, "pk3")) {
418 Com_Printf("NOTICE: Filelist is requesting a .pk3 file (%s)\n", path);
419 pak = true;
420 } else
421 pak = false;
422
423 if (!pak &&
424 !Q_streq(ext, "bsp") &&
425 !Q_streq(ext, "wav") &&
426 !Q_streq(ext, "md2") &&
427 !Q_streq(ext, "ogg") &&
428 !Q_streq(ext, "md3") &&
429 !Q_streq(ext, "png") &&
430 !Q_streq(ext, "jpg") &&
431 !Q_streq(ext, "obj") &&
432 !Q_streq(ext, "mat") &&
433 !Q_streq(ext, "ump")) {
434 Com_Printf("WARNING: Illegal file type '%s' in filelist.\n", path);
435 return;
436 }
437
438 if (path[0] == '@') {
439 if (pak) {
440 Com_Printf("WARNING: @ prefix used on a pk3 file (%s) in filelist.\n", path);
441 return;
442 }
443 gameLocal = true;
444 path++;
445 length--;
446 } else
447 gameLocal = false;
448
449 if (strstr(path, "..") || !isvalidchar(path[0]) || !isvalidchar(path[length - 1]) || strstr(path, "//") ||
450 strchr(path, '\\') || (!pak && !strchr(path, '/')) || (pak && strchr(path, '/'))) {
451 Com_Printf("WARNING: Illegal path '%s' in filelist.\n", path);
452 return;
453 }
454
455 /* by definition pk3s are game-local */
456 if (gameLocal || pak) {
457 bool exists;
458
459 /* search the user homedir to find the pk3 file */
460 if (pak) {
461 char gamePath[MAX_OSPATH];
462 FILE* f;
463 Com_sprintf(gamePath, sizeof(gamePath), "%s/%s", FS_Gamedir(), path);
464 f = fopen(gamePath, "rb");
465 if (!f)
466 exists = false;
467 else {
468 exists = true;
469 fclose(f);
470 }
471 } else
472 exists = FS_CheckFile("%s", path);
473
474 if (!exists) {
475 if (CL_QueueHTTPDownload(path)) {
476 /* pk3s get bumped to the top and HTTP switches to single downloading.
477 * this prevents someone on 28k dialup trying to do both the main .pk3
478 * and referenced configstrings data at once. */
479 if (pak) {
480 dlqueue_t** anchor = &cls.downloadQueue;
481 while ((*anchor)->next) anchor = &(*anchor)->next;
482 /* Remove the last element from the end of the list ... */
483 dlqueue_t* const d = *anchor;
484 *anchor = 0;
485 /* ... and prepend it to the list. */
486 d->next = cls.downloadQueue;
487 cls.downloadQueue = d;
488 }
489 }
490 }
491 } else
492 CL_CheckOrDownloadFile(path);
493 }
494
495 /**
496 * @brief A filelist is in memory, scan and validate it and queue up the files.
497 */
CL_ParseFileList(dlhandle_t * dl)498 static void CL_ParseFileList (dlhandle_t* dl)
499 {
500 char* list;
501
502 if (!cl_http_filelists->integer)
503 return;
504
505 list = dl->tempBuffer;
506
507 for (;;) {
508 char* p = strchr(list, '\n');
509 if (p) {
510 p[0] = 0;
511 if (list[0])
512 CL_CheckAndQueueDownload(list);
513 list = p + 1;
514 } else {
515 if (list[0])
516 CL_CheckAndQueueDownload(list);
517 break;
518 }
519 }
520
521 Mem_Free(dl->tempBuffer);
522 dl->tempBuffer = nullptr;
523 }
524
525 /**
526 * @brief A pk3 file just downloaded, let's see if we can remove some stuff from
527 * the queue which is in the .pk3.
528 */
CL_ReVerifyHTTPQueue(void)529 static void CL_ReVerifyHTTPQueue (void)
530 {
531 pendingCount = 0;
532
533 for (dlqueue_t* q = cls.downloadQueue; q; q = q->next) {
534 if (q->state == DLQ_STATE_NOT_STARTED) {
535 if (FS_LoadFile(q->ufoPath, nullptr) != -1)
536 q->state = DLQ_STATE_DONE;
537 else
538 pendingCount++;
539 }
540 }
541 }
542
543 /**
544 * @brief UFO is exiting or we're changing servers. Clean up.
545 */
CL_HTTP_Cleanup(void)546 void CL_HTTP_Cleanup (void)
547 {
548 int i;
549
550 for (i = 0; i < 4; i++) {
551 dlhandle_t* dl = &cls.HTTPHandles[i];
552
553 if (dl->file) {
554 fclose(dl->file);
555 remove(dl->filePath);
556 dl->file = nullptr;
557 }
558
559 Mem_Free(dl->tempBuffer);
560 dl->tempBuffer = nullptr;
561
562 if (dl->curl) {
563 if (multi)
564 curl_multi_remove_handle(multi, dl->curl);
565 curl_easy_cleanup(dl->curl);
566 dl->curl = nullptr;
567 }
568
569 dl->queueEntry = nullptr;
570 }
571
572 if (multi) {
573 curl_multi_cleanup(multi);
574 multi = nullptr;
575 }
576 }
577
578 /**
579 * @brief A download finished, find out what it was, whether there were any errors and
580 * if so, how severe. If none, rename file and other such stuff.
581 */
CL_FinishHTTPDownload(void)582 static void CL_FinishHTTPDownload (void)
583 {
584 int messagesInQueue, i;
585 CURLcode result;
586 CURL* curl;
587 long responseCode;
588 double timeTaken, fileSize;
589 char tempName[MAX_OSPATH];
590 bool isFile;
591
592 do {
593 CURLMsg* msg = curl_multi_info_read(multi, &messagesInQueue);
594 dlhandle_t* dl = nullptr;
595
596 if (!msg) {
597 Com_Printf("CL_FinishHTTPDownload: Odd, no message for us...\n");
598 return;
599 }
600
601 if (msg->msg != CURLMSG_DONE) {
602 Com_Printf("CL_FinishHTTPDownload: Got some weird message...\n");
603 continue;
604 }
605
606 curl = msg->easy_handle;
607
608 /* curl doesn't provide reverse-lookup of the void* ptr, so search for it */
609 for (i = 0; i < 4; i++) {
610 if (cls.HTTPHandles[i].curl == curl) {
611 dl = &cls.HTTPHandles[i];
612 break;
613 }
614 }
615
616 if (!dl)
617 Com_Error(ERR_DROP, "CL_FinishHTTPDownload: Handle not found");
618
619 /* we mark everything as done even if it errored to prevent multiple attempts. */
620 dl->queueEntry->state = DLQ_STATE_DONE;
621
622 /* filelist processing is done on read */
623 if (dl->file)
624 isFile = true;
625 else
626 isFile = false;
627
628 if (isFile) {
629 fclose(dl->file);
630 dl->file = nullptr;
631 }
632
633 /* might be aborted */
634 if (pendingCount)
635 pendingCount--;
636 handleCount--;
637 /* Com_Printf("finished dl: hc = %d\n", handleCount); */
638 cls.downloadName[0] = 0;
639 cls.downloadPosition = 0;
640
641 result = msg->data.result;
642
643 switch (result) {
644 /* for some reason curl returns CURLE_OK for a 404... */
645 case CURLE_HTTP_RETURNED_ERROR:
646 case CURLE_OK:
647 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
648 if (responseCode == 404) {
649 const char* extension = Com_GetExtension(dl->queueEntry->ufoPath);
650 if (extension != nullptr && Q_streq(extension, "pk3"))
651 downloadingPK3 = false;
652
653 if (isFile)
654 FS_RemoveFile(dl->filePath);
655 Com_Printf("HTTP(%s): 404 File Not Found [%d remaining files]\n", dl->queueEntry->ufoPath, pendingCount);
656 curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &fileSize);
657 if (fileSize > 512) {
658 /* ick */
659 isFile = false;
660 result = CURLE_FILESIZE_EXCEEDED;
661 Com_Printf("Oversized 404 body received (%d bytes), aborting HTTP downloading.\n", (int)fileSize);
662 } else {
663 curl_multi_remove_handle(multi, dl->curl);
664 continue;
665 }
666 } else if (responseCode == 200) {
667 if (!isFile && !abortDownloads)
668 CL_ParseFileList(dl);
669 break;
670 }
671
672 /* every other code is treated as fatal, fallthrough here */
673
674 /* fatal error, disable http */
675 case CURLE_COULDNT_RESOLVE_HOST:
676 case CURLE_COULDNT_CONNECT:
677 case CURLE_COULDNT_RESOLVE_PROXY:
678 if (isFile)
679 FS_RemoveFile(dl->filePath);
680 Com_Printf("Fatal HTTP error: %s\n", curl_easy_strerror(result));
681 curl_multi_remove_handle(multi, dl->curl);
682 if (abortDownloads)
683 continue;
684 CL_CancelHTTPDownloads(true);
685 continue;
686 default:
687 i = strlen(dl->queueEntry->ufoPath);
688 if (Q_streq(dl->queueEntry->ufoPath + i - 4, ".pk3"))
689 downloadingPK3 = false;
690 if (isFile)
691 FS_RemoveFile(dl->filePath);
692 Com_Printf("HTTP download failed: %s\n", curl_easy_strerror(result));
693 curl_multi_remove_handle(multi, dl->curl);
694 continue;
695 }
696
697 if (isFile) {
698 /* rename the temp file */
699 Com_sprintf(tempName, sizeof(tempName), "%s/%s", FS_Gamedir(), dl->queueEntry->ufoPath);
700
701 if (!FS_RenameFile(dl->filePath, tempName, false))
702 Com_Printf("Failed to rename %s for some odd reason...", dl->filePath);
703
704 /* a pk3 file is very special... */
705 i = strlen(tempName);
706 if (Q_streq(tempName + i - 4, ".pk3")) {
707 FS_RestartFilesystem(nullptr);
708 CL_ReVerifyHTTPQueue();
709 downloadingPK3 = false;
710 }
711 }
712
713 /* show some stats */
714 curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &timeTaken);
715 curl_easy_getinfo(curl, CURLINFO_SIZE_DOWNLOAD, &fileSize);
716
717 /** @todo
718 * technically i shouldn't need to do this as curl will auto reuse the
719 * existing handle when you change the URL. however, the handleCount goes
720 * all weird when reusing a download slot in this way. if you can figure
721 * out why, please let me know. */
722 curl_multi_remove_handle(multi, dl->curl);
723
724 Com_Printf("HTTP(%s): %.f bytes, %.2fkB/sec [%d remaining files]\n",
725 dl->queueEntry->ufoPath, fileSize, (fileSize / 1024.0) / timeTaken, pendingCount);
726 } while (messagesInQueue > 0);
727
728 if (handleCount == 0) {
729 if (abortDownloads == HTTPDL_ABORT_SOFT)
730 abortDownloads = HTTPDL_ABORT_NONE;
731 else if (abortDownloads == HTTPDL_ABORT_HARD)
732 cls.downloadServer[0] = 0;
733 }
734
735 /* done current batch, see if we have more to dl - maybe a .bsp needs downloaded */
736 if (cls.state == ca_connected && !CL_PendingHTTPDownloads())
737 CL_RequestNextDownload();
738 }
739
740 /**
741 * @brief Start another HTTP download if possible.
742 * @sa CL_RunHTTPDownloads
743 */
CL_StartNextHTTPDownload(void)744 static void CL_StartNextHTTPDownload (void)
745 {
746 for (dlqueue_t* q = cls.downloadQueue; q; q = q->next) {
747 if (q->state == DLQ_STATE_NOT_STARTED) {
748 size_t len;
749 dlhandle_t* dl = CL_GetFreeDLHandle();
750 if (!dl)
751 return;
752
753 CL_StartHTTPDownload(q, dl);
754
755 /* ugly hack for pk3 file single downloading */
756 len = strlen(q->ufoPath);
757 if (len > 4 && !Q_strcasecmp(q->ufoPath + len - 4, ".pk3"))
758 downloadingPK3 = true;
759
760 break;
761 }
762 }
763 }
764
765 /**
766 * @brief This calls curl_multi_perform do actually do stuff. Called every frame while
767 * connecting to minimise latency. Also starts new downloads if we're not doing
768 * the maximum already.
769 * @sa CL_Frame
770 */
CL_RunHTTPDownloads(void)771 void CL_RunHTTPDownloads (void)
772 {
773 CURLMcode ret;
774
775 if (!cls.downloadServer[0])
776 return;
777
778 /* Com_Printf("handle %d, pending %d\n", handleCount, pendingCount); */
779
780 /* not enough downloads running, queue some more! */
781 if (pendingCount && abortDownloads == HTTPDL_ABORT_NONE &&
782 !downloadingPK3 && handleCount < cl_http_max_connections->integer)
783 CL_StartNextHTTPDownload();
784
785 do {
786 int newHandleCount;
787 ret = curl_multi_perform(multi, &newHandleCount);
788 if (newHandleCount < handleCount) {
789 /* Com_Printf("runnd dl: hc = %d, nc = %d\n", handleCount, newHandleCount); */
790 /* hmm, something either finished or errored out. */
791 CL_FinishHTTPDownload();
792 handleCount = newHandleCount;
793 }
794 } while (ret == CURLM_CALL_MULTI_PERFORM);
795
796 if (ret != CURLM_OK) {
797 Com_Printf("curl_multi_perform error. Aborting HTTP downloads.\n");
798 CL_CancelHTTPDownloads(true);
799 }
800
801 /* not enough downloads running, queue some more! */
802 if (pendingCount && abortDownloads == HTTPDL_ABORT_NONE &&
803 !downloadingPK3 && handleCount < cl_http_max_connections->integer)
804 CL_StartNextHTTPDownload();
805 }
806
HTTP_InitStartup(void)807 void HTTP_InitStartup (void)
808 {
809 cl_http_filelists = Cvar_Get("cl_http_filelists", "1");
810 cl_http_downloads = Cvar_Get("cl_http_downloads", "1", 0, "Try to download files via http");
811 cl_http_max_connections = Cvar_Get("cl_http_max_connections", "1");
812 }
813 #else
CL_CancelHTTPDownloads(bool permKill)814 void CL_CancelHTTPDownloads(bool permKill) {}
CL_QueueHTTPDownload(const char * ufoPath)815 bool CL_QueueHTTPDownload(const char* ufoPath) {return false;}
CL_RunHTTPDownloads(void)816 void CL_RunHTTPDownloads(void) {}
CL_PendingHTTPDownloads(void)817 bool CL_PendingHTTPDownloads(void) {return false;}
CL_SetHTTPServer(const char * URL)818 void CL_SetHTTPServer(const char* URL) {}
CL_HTTP_Cleanup(void)819 void CL_HTTP_Cleanup(void) {}
CL_RequestNextDownload(void)820 void CL_RequestNextDownload(void) {}
CL_CheckOrDownloadFile(const char * filename)821 bool CL_CheckOrDownloadFile(const char* filename) {return false;}
822
HTTP_InitStartup(void)823 void HTTP_InitStartup(void){}
824 #endif
825