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