1 
2 // central.c - communication with central server
3 
4 #include "qwsvdef.h"
5 #include <curl/curl.h>
6 
7 #define GENERATE_CHALLENGE_PATH     "Authentication/GenerateChallenge"
8 #define VERIFY_RESPONSE_PATH        "Authentication/VerifyResponse"
9 #define CHECKIN_PATH                "ServerApi/Checkin"
10 #define MIN_CHECKIN_PERIOD          60
11 
12 #ifdef _WIN32
13 #pragma comment(lib, "libcurld.lib")
14 #pragma comment(lib, "ws2_32.lib")
15 #pragma comment(lib, "wldap32.lib")
16 #endif
17 
18 static cvar_t sv_www_address = { "sv_www_address", "" };
19 static cvar_t sv_www_authkey = { "sv_www_authkey", "" };
20 static cvar_t sv_www_checkin_period = { "sv_www_checkin_period", "60" };
21 
22 static CURLM* curl_handle = NULL;
23 
24 static double last_checkin_time;
25 
26 #define MAX_RESPONSE_LENGTH    (4096*2)
27 
28 struct web_request_data_s;
29 
30 typedef void(*web_response_func_t)(struct web_request_data_s* req, qbool valid);
31 static void Web_ConstructURL(char* url, const char* path, int sizeof_url);
32 static int utf_encode_string(char* value, char* encoded_value);
33 static void Web_SubmitRequestForm(const char* url, struct curl_httppost *first_form_ptr, struct curl_httppost *last_form_ptr, web_response_func_t callback, const char* requestId, void* internal_data);
34 
35 typedef struct web_request_data_s {
36 	CURL*               handle;
37 	double              time_sent;
38 
39 	// response from server
40 	char                response[MAX_RESPONSE_LENGTH];
41 	size_t              response_length;
42 
43 	// form data
44 	struct curl_httppost *first_form_ptr;
45 	struct curl_httppost *last_form_ptr;
46 
47 	// called when response complete
48 	web_response_func_t onCompleteCallback;
49 	void*               internal_data;
50 	char*               request_id;                 // if set, content will be passed to game-mod. will be Q_free()'d
51 
52 	struct web_request_data_s* next;
53 } web_request_data_t;
54 
utf8Encode(char in,char * out1,char * out2)55 static int utf8Encode(char in, char* out1, char* out2) {
56 	if (in <= 0x7F) {
57 		// <= 127 is encoded as single byte, no translation
58 		*out1 = in;
59 		return 1;
60 	}
61 	else {
62 		// Two byte characters... 5 bits then 6
63 		*out1 = 0xC0 | ((in >> 6) & 0x1F);
64 		*out2 = 0x80 | (in & 0x3F);
65 		return 2;
66 	}
67 }
68 
69 static web_request_data_t* web_requests;
70 
CheckFileExists(const char * path)71 static qbool CheckFileExists(const char* path)
72 {
73 	FILE* f;
74 	if (!(f = fopen(path, "rb"))) {
75 		return false;
76 	}
77 	fclose(f);
78 	return true;
79 }
80 
Web_StandardTokenWrite(void * buffer,size_t size,size_t nmemb,void * userp)81 size_t Web_StandardTokenWrite(void* buffer, size_t size, size_t nmemb, void* userp)
82 {
83 	web_request_data_t* data = (web_request_data_t*)userp;
84 	size_t available = sizeof(data->response) - data->response_length;
85 	if (size * nmemb > available) {
86 		Con_DPrintf("WWW: Response too large, size*nmemb = %d, available %d\n", size * nmemb, available);
87 		return 0;
88 	}
89 	else if (size * nmemb > 0) {
90 		Con_DPrintf("WWW: response received, writing %d bytes\n", size * nmemb);
91 		memcpy(data->response + data->response_length, buffer, size * nmemb);
92 		data->response_length += size * nmemb;
93 		Con_DPrintf("WWW: response_length now %d bytes\n", data->response_length);
94 	}
95 	return size * nmemb;
96 }
97 
98 typedef struct response_field_s {
99 	const char* name;
100 	const char** value;
101 } response_field_t;
102 
ProcessWebResponse(web_request_data_t * req,response_field_t * fields,int field_count)103 static int ProcessWebResponse(web_request_data_t* req, response_field_t* fields, int field_count)
104 {
105 	char* colon, *newline;
106 	char* start = req->response;
107 	int i;
108 	int total_fields = 0;
109 
110 	// Response should be multiple lines, <Key>:<Value>\n
111 	// For multi-line values, prefix end of line with $
112 	req->response[req->response_length] = '\0';
113 	while ((colon = strchr(start, ':'))) {
114 		newline = strchr(colon, '\n');
115 		if (newline) {
116 			while (newline && newline != colon + 1 && *(newline - 1) == '$') {
117 				*(newline - 1) = ' ';
118 				newline = strchr(newline + 1, '\n');
119 			}
120 			if (newline) {
121 				*newline = '\0';
122 			}
123 		}
124 
125 		*colon = '\0';
126 		for (i = 0; i < field_count; ++i) {
127 			if (!strcmp(start, fields[i].name)) {
128 				*fields[i].value = colon + 1;
129 			}
130 		}
131 
132 		if (newline == NULL) {
133 			break;
134 		}
135 
136 		start = newline + 1;
137 		while (*start && (*start == 10 || *start == 13)) {
138 			++start;
139 		}
140 		++total_fields;
141 	}
142 
143 	return total_fields;
144 }
145 
Auth_GenerateChallengeResponse(web_request_data_t * req,qbool valid)146 void Auth_GenerateChallengeResponse(web_request_data_t* req, qbool valid)
147 {
148 	client_t* client = (client_t*) req->internal_data;
149 	const char* response = NULL;
150 	const char* challenge = NULL;
151 	const char* message = NULL;
152 	response_field_t fields[] = {
153 		{ "Result", &response },
154 		{ "Challenge", &challenge },
155 		{ "Message", &message }
156 	};
157 
158 	if (client->login_request_time != req->time_sent) {
159 		// Ignore result, subsequent request sent
160 		return;
161 	}
162 
163 	req->internal_data = NULL;
164 	ProcessWebResponse(req, fields, sizeof(fields) / sizeof(fields[0]));
165 
166 	if (response && !strcmp(response, "Success") && challenge) {
167 		char buffer[128];
168 		strlcpy(buffer, "//challenge ", sizeof(buffer));
169 		strlcat(buffer, challenge, sizeof(buffer));
170 		strlcat(buffer, "\n", sizeof(buffer));
171 
172 		strlcpy(client->login_challenge, challenge, sizeof(client->login_challenge));
173 		SV_ClientPrintf2(client, PRINT_HIGH, "Challenge stored...\n", message);
174 
175 		ClientReliableWrite_Begin (client, svc_stufftext, 2+strlen(buffer));
176 		ClientReliableWrite_String (client, buffer);
177 	}
178 	else if (message) {
179 		SV_ClientPrintf2(client, PRINT_HIGH, "Error: %s\n", message);
180 	}
181 	else {
182 		// Maybe add CURLOPT_ERRORBUFFER?
183 		SV_ClientPrintf2(client, PRINT_HIGH, "Error: unknown error\n");
184 	}
185 }
186 
Auth_ProcessLoginAttempt(web_request_data_t * req,qbool valid)187 void Auth_ProcessLoginAttempt(web_request_data_t* req, qbool valid)
188 {
189 	client_t* client = (client_t*) req->internal_data;
190 	const char* response = NULL;
191 	const char* login = NULL;
192 	const char* preferred_alias = NULL;
193 	const char* message = NULL;
194 	const char* flag = NULL;
195 	const char* confirmation = NULL;
196 	response_field_t fields[] = {
197 		{ "Result", &response },
198 		{ "Alias", &preferred_alias },
199 		{ "Login", &login },
200 		{ "Message", &message },
201 		{ "Flag", &flag },
202 		{ "Confirmation", &confirmation }
203 	};
204 
205 	req->internal_data = NULL;
206 	if (client->login_request_time != req->time_sent) {
207 		// Ignore result, subsequent request sent
208 		return;
209 	}
210 
211 	ProcessWebResponse(req, fields, sizeof(fields) / sizeof(fields[0]));
212 
213 	if (response && !strcmp(response, "Success")) {
214 		if (login) {
215 			char oldval[MAX_EXT_INFO_STRING];
216 
217 			strlcpy(oldval, Info_Get(&client->_userinfo_ctx_, "*auth"), sizeof(oldval));
218 			strlcpy(client->login, login, sizeof(client->login));
219 
220 			Info_SetStar(&client->_userinfo_ctx_, "*auth", login);
221 			ProcessUserInfoChange(client, "*auth", oldval);
222 		}
223 
224 		if (confirmation) {
225 			strlcpy(client->login_confirmation, confirmation, sizeof(client->login_confirmation));
226 		}
227 
228 		{
229 			char oldval[MAX_EXT_INFO_STRING];
230 
231 			flag = (flag ? flag : "");
232 			strlcpy(oldval, Info_Get(&client->_userinfo_ctx_, "*flag"), sizeof(oldval));
233 			strlcpy(client->login_flag, flag, sizeof(client->login_flag));
234 			Info_SetStar(&client->_userinfo_ctx_, "*flag", flag);
235 			ProcessUserInfoChange(client, "*flag", oldval);
236 		}
237 
238 		preferred_alias = preferred_alias ? preferred_alias : login;
239 		if (preferred_alias) {
240 			strlcpy(client->login_alias, preferred_alias, sizeof(client->login_alias));
241 		}
242 		client->logged_in_via_web = true;
243 
244 		SV_LoginWebCheck(client);
245 	}
246 	else if (message) {
247 		SV_ClientPrintf2(client, PRINT_HIGH, "Error: %s\n", message);
248 		SV_LoginWebFailed(client);
249 	}
250 	else {
251 		// Maybe add CURLOPT_ERRORBUFFER?
252 		SV_ClientPrintf2(client, PRINT_HIGH, "Error: unknown error (invalid response from server)\n");
253 		SV_LoginWebFailed(client);
254 	}
255 }
256 
Web_PostResponse(web_request_data_t * req,qbool valid)257 void Web_PostResponse(web_request_data_t* req, qbool valid)
258 {
259 	if (valid) {
260 		const char* broadcast = NULL;
261 		const char* upload = NULL;
262 		const char* uploadPath = NULL;
263 
264 		response_field_t fields[] = {
265 			{ "Broadcast", &broadcast },
266 			{ "UploadPath", &uploadPath },
267 			{ "Upload", &upload }
268 		};
269 
270 		req->response[req->response_length] = '\0';
271 
272 		Con_DPrintf("Response from web server:\n");
273 		Con_DPrintf(req->response);
274 
275 		ProcessWebResponse(req, fields, sizeof(fields) / sizeof(fields[0]));
276 
277 		if (broadcast) {
278 			// Server is making announcement
279 			SV_BroadcastPrintfEx(PRINT_HIGH, 0, "%s\n", broadcast);
280 		}
281 		if (upload && uploadPath) {
282 			if (strstr(uploadPath, "//") || FS_UnsafeFilename(upload)) {
283 				Con_Printf("Upload request deemed unsafe, ignoring...\n");
284 			}
285 			else {
286 				if (!strncmp(upload, "demos/", 6)) {
287 					// Ok - could be demo_dir, or full path
288 					char demoName[MAX_OSPATH];
289 
290 					if (sv_demoDir.string[0]) {
291 						char url[512];
292 						struct curl_httppost *first_form_ptr = NULL;
293 						struct curl_httppost *last_form_ptr = NULL;
294 
295 						snprintf(demoName, sizeof(demoName), "%s/%s/%s", fs_gamedir, sv_demoDir.string, upload + 6);
296 						if (!CheckFileExists(demoName) && sv_demoDirAlt.string[0]) {
297 							snprintf(demoName, sizeof(demoName), "%s/%s/%s", fs_gamedir, sv_demoDirAlt.string, upload + 6);
298 						}
299 
300 						if (CheckFileExists(demoName)) {
301 							Web_ConstructURL(url, uploadPath, sizeof(url));
302 
303 							curl_formadd(&first_form_ptr, &last_form_ptr,
304 								CURLFORM_PTRNAME, "file",
305 								CURLFORM_FILE, demoName,
306 								CURLFORM_END
307 							);
308 
309 							curl_formadd(&first_form_ptr, &last_form_ptr,
310 								CURLFORM_COPYNAME, "localPath",
311 								CURLFORM_COPYCONTENTS, upload,
312 								CURLFORM_END
313 							);
314 
315 							Web_SubmitRequestForm(url, first_form_ptr, last_form_ptr, Web_PostResponse, "upload", NULL);
316 							Con_Printf("Uploading %s...\n", demoName);
317 						}
318 						else {
319 							Con_Printf("Couldn't find file %s, ignoring...\n", demoName);
320 						}
321 					}
322 				}
323 				else {
324 					Con_Printf("Upload request folder not authorised, ignoring...\n");
325 				}
326 			}
327 		}
328 	}
329 	else {
330 		Con_Printf("Failure contacting central server.\n");
331 	}
332 
333 	if (req->internal_data) {
334 		Q_free(req->internal_data);
335 	}
336 }
337 
Web_SubmitRequestForm(const char * url,struct curl_httppost * first_form_ptr,struct curl_httppost * last_form_ptr,web_response_func_t callback,const char * requestId,void * internal_data)338 static void Web_SubmitRequestForm(const char* url, struct curl_httppost *first_form_ptr, struct curl_httppost *last_form_ptr, web_response_func_t callback, const char* requestId, void* internal_data)
339 {
340 	CURL* req = curl_easy_init();
341 	web_request_data_t* data = Q_malloc(sizeof(web_request_data_t));
342 	CURLFORMcode code = curl_formadd(&first_form_ptr, &last_form_ptr,
343 		CURLFORM_PTRNAME, "authKey",
344 		CURLFORM_COPYCONTENTS, sv_www_authkey.string,
345 		CURLFORM_END
346 	);
347 
348 	data->onCompleteCallback = callback;
349 	data->time_sent = curtime;
350 	data->internal_data = internal_data;
351 	data->handle = req;
352 	data->request_id = requestId && requestId[0] ? strdup(requestId) : NULL;
353 	data->first_form_ptr = first_form_ptr;
354 
355 	curl_easy_setopt(req, CURLOPT_URL, url);
356 	curl_easy_setopt(req, CURLOPT_WRITEDATA, data);
357 	curl_easy_setopt(req, CURLOPT_WRITEFUNCTION, Web_StandardTokenWrite);
358 	if (first_form_ptr) {
359 		curl_easy_setopt(req, CURLOPT_POST, 1);
360 		curl_easy_setopt(req, CURLOPT_HTTPPOST, first_form_ptr);
361 	}
362 
363 	curl_multi_add_handle(curl_handle, req);
364 	data->next = web_requests;
365 	web_requests = data;
366 }
367 
Central_VerifyChallengeResponse(client_t * client,const char * challenge,const char * response)368 void Central_VerifyChallengeResponse(client_t* client, const char* challenge, const char* response)
369 {
370 	char url[512];
371 	struct curl_httppost *first_form_ptr = NULL;
372 	struct curl_httppost *last_form_ptr = NULL;
373 	CURLFORMcode code;
374 
375 	if (!sv_www_address.string[0]) {
376 		SV_ClientPrintf2(client, PRINT_HIGH, "Remote logins not supported on this server\n");
377 		return;
378 	}
379 
380 	code = curl_formadd(&first_form_ptr, &last_form_ptr,
381 		CURLFORM_PTRNAME, "challenge",
382 		CURLFORM_COPYCONTENTS, challenge,
383 		CURLFORM_END
384 	);
385 
386 	code = curl_formadd(&first_form_ptr, &last_form_ptr,
387 		CURLFORM_PTRNAME, "response",
388 		CURLFORM_COPYCONTENTS, response,
389 		CURLFORM_END
390 	);
391 
392 	Web_ConstructURL(url, VERIFY_RESPONSE_PATH, sizeof(url));
393 
394 	client->login_request_time = curtime;
395 	Web_SubmitRequestForm(url, first_form_ptr, last_form_ptr, Auth_ProcessLoginAttempt, NULL, client);
396 }
397 
Central_GenerateChallenge(client_t * client,const char * username,qbool during_login)398 void Central_GenerateChallenge(client_t* client, const char* username, qbool during_login)
399 {
400 	char url[512];
401 	struct curl_httppost *first_form_ptr = NULL;
402 	struct curl_httppost *last_form_ptr = NULL;
403 	CURLFORMcode code;
404 
405 	if (!sv_www_address.string[0]) {
406 		SV_ClientPrintf2(client, PRINT_HIGH, "Remote logins not supported on this server\n");
407 		return;
408 	}
409 
410 	Web_ConstructURL(url, GENERATE_CHALLENGE_PATH, sizeof(url));
411 
412 	code = curl_formadd(&first_form_ptr, &last_form_ptr,
413 		CURLFORM_PTRNAME, "userName",
414 		CURLFORM_COPYCONTENTS, username,
415 		CURLFORM_END
416 	);
417 
418 	code = curl_formadd(&first_form_ptr, &last_form_ptr,
419 		CURLFORM_PTRNAME, "status",
420 		CURLFORM_COPYCONTENTS, during_login ? "0" : "1",
421 		CURLFORM_END
422 	);
423 
424 	client->login_request_time = curtime;
425 	Web_SubmitRequestForm(url, first_form_ptr, last_form_ptr, Auth_GenerateChallengeResponse, NULL, client);
426 }
427 
Web_ConstructURL(char * url,const char * path,int sizeof_url)428 static void Web_ConstructURL(char* url, const char* path, int sizeof_url)
429 {
430 	strlcpy(url, sv_www_address.string, sizeof_url);
431 	if (url[strlen(url) - 1] != '/') {
432 		strlcat(url, "/", sizeof_url);
433 	}
434 	while (*path == '/') {
435 		++path;
436 	}
437 	strlcat(url, path, sizeof_url);
438 }
439 
440 #define MAX_ENCODED_STRINGLEN 128
441 
Web_AddParametersToRequest(int first_param,struct curl_httppost ** first_form_ptr,struct curl_httppost ** last_form_ptr)442 static qbool Web_AddParametersToRequest(int first_param, struct curl_httppost** first_form_ptr, struct curl_httppost** last_form_ptr)
443 {
444 	int i;
445 
446 	for (i = first_param; i < Cmd_Argc() - 1; i += 2) {
447 		char encoded_value[MAX_ENCODED_STRINGLEN];
448 		int encoded_length = 0;
449 		int j;
450 		char* name;
451 		char* value;
452 		CURLFORMcode code;
453 
454 		name = Cmd_Argv(i);
455 		value = Cmd_Argv(i + 1);
456 		if (!strcmp(name, "*internal")) {
457 			if (!strcmp(value, "authinfo")) {
458 				int login_count = 0;
459 
460 				// Also submit authentication challenges/responses
461 				for (j = 0; j < MAX_CLIENTS; ++j) {
462 					if (svs.clients[j].state > cs_free) {
463 						if (svs.clients[j].login[0] && svs.clients[j].logged_in_via_web) {
464 							name = va("logins[%d].name", login_count);
465 							utf_encode_string(svs.clients[j].login, encoded_value);
466 							code = curl_formadd(first_form_ptr, last_form_ptr,
467 								CURLFORM_COPYNAME, name,
468 								CURLFORM_COPYCONTENTS, encoded_value,
469 								CURLFORM_END
470 							);
471 
472 							if (code != CURLE_OK) {
473 								Con_Printf("Request failed (adding to form)\n");
474 								return false;
475 							}
476 
477 							name = va("logins[%d].token", login_count);
478 							utf_encode_string(svs.clients[j].login_confirmation, encoded_value);
479 							code = curl_formadd(first_form_ptr, last_form_ptr,
480 								CURLFORM_COPYNAME, name,
481 								CURLFORM_COPYCONTENTS, encoded_value,
482 								CURLFORM_END
483 							);
484 
485 							if (code != CURLE_OK) {
486 								Con_Printf("Request failed (adding to form)\n");
487 								return false;
488 							}
489 
490 							++login_count;
491 						}
492 					}
493 				}
494 
495 				snprintf(encoded_value, sizeof(encoded_value), "%d", login_count);
496 				code = curl_formadd(first_form_ptr, last_form_ptr,
497 					CURLFORM_COPYNAME, "logincount",
498 					CURLFORM_COPYCONTENTS, encoded_value,
499 					CURLFORM_END
500 				);
501 
502 				if (code != CURLE_OK) {
503 					Con_Printf("Request failed (adding to form)\n");
504 					return false;
505 				}
506 			}
507 			continue;
508 		}
509 		else {
510 			utf_encode_string(value, encoded_value);
511 		}
512 
513 		code = curl_formadd(first_form_ptr, last_form_ptr,
514 			CURLFORM_COPYNAME, name,
515 			CURLFORM_COPYCONTENTS, encoded_value,
516 			CURLFORM_END
517 		);
518 
519 		if (code != CURLE_OK) {
520 			Con_Printf("Request failed (adding to form)\n");
521 			return false;
522 		}
523 	}
524 
525 	return true;
526 }
527 
Web_SendRequest(qbool post)528 static void Web_SendRequest(qbool post)
529 {
530 	struct curl_httppost *first_form_ptr = NULL;
531 	struct curl_httppost *last_form_ptr = NULL;
532 	char url[512];
533 	char* requestId = NULL;
534 
535 	if (!sv_www_address.string[0]) {
536 		Con_Printf("Address not set - functionality disabled\n");
537 		return;
538 	}
539 
540 	if (Cmd_Argc() < 3) {
541 		Con_Printf("Usage: %s <url> <request-id> (<key> <value>)*\n", Cmd_Argv(0));
542 		return;
543 	}
544 
545 	Web_ConstructURL(url, Cmd_Argv(1), sizeof(url));
546 
547 	requestId = Cmd_Argv(2);
548 	if (requestId[0]) {
549 		requestId = Q_strdup(requestId);
550 	}
551 	else {
552 		requestId = NULL;
553 	}
554 
555 	if (!Web_AddParametersToRequest(3, &first_form_ptr, &last_form_ptr)) {
556 		curl_formfree(first_form_ptr);
557 		return;
558 	}
559 
560 	Web_SubmitRequestForm(url, first_form_ptr, last_form_ptr, Web_PostResponse, requestId, NULL);
561 }
562 
utf_encode_string(char * value,char * encoded_value)563 int utf_encode_string(char* value, char* encoded_value)
564 {
565 	int j;
566 	int encoded_length = 0;
567 	for (j = 0; j < strlen(value) && encoded_length < MAX_ENCODED_STRINGLEN - 2; ++j) {
568 		encoded_length += utf8Encode(value[j], &encoded_value[encoded_length], &encoded_value[encoded_length + 1]);
569 	}
570 	encoded_value[encoded_length] = '\0';
571 	return encoded_length;
572 }
573 
Web_GetRequest_f(void)574 static void Web_GetRequest_f(void)
575 {
576 	Web_SendRequest(false);
577 }
578 
Web_PostRequest_f(void)579 static void Web_PostRequest_f(void)
580 {
581 	Web_SendRequest(true);
582 }
583 
Web_PostFileRequest_f(void)584 static void Web_PostFileRequest_f(void)
585 {
586 	struct curl_httppost *first_form_ptr = NULL;
587 	struct curl_httppost *last_form_ptr = NULL;
588 	char* requestId = NULL;
589 	char url[512];
590 	char path[MAX_OSPATH];
591 	CURLFORMcode code = 0;
592 	const char* specified;
593 
594 	if (!sv_www_address.string[0]) {
595 		Con_Printf("Address not set - functionality disabled\n");
596 		return;
597 	}
598 
599 	if (Cmd_Argc() < 4) {
600 		Con_Printf("Usage: %s <url> <request-id> <file> (<key> <value>)*\n", Cmd_Argv(0));
601 		return;
602 	}
603 
604 	requestId = Cmd_Argv(2);
605 	if (requestId[0]) {
606 		requestId = Q_strdup(requestId);
607 	}
608 	else {
609 		requestId = NULL;
610 	}
611 
612 	specified = Cmd_Argv(3);
613 	if (specified[0] == '*' && specified[1] == '\0') {
614 		const char* demoname = SV_MVDDemoName();
615 
616 		if (!sv.mvdrecording || !demoname) {
617 			Con_Printf("Not recording demo!\n");
618 			return;
619 		}
620 
621 		snprintf(path, MAX_OSPATH, "%s/%s/%s", fs_gamedir, sv_demoDir.string, SV_MVDName2Txt(demoname));
622 	}
623 	else {
624 		if (strstr(specified, ".cfg") || FS_UnsafeFilename(specified)) {
625 			Con_Printf("Filename invalid\n");
626 			return;
627 		}
628 
629 		snprintf(path, MAX_OSPATH, "%s/%s", fs_gamedir, specified);
630 	}
631 
632 	if (FS_UnsafeFilename(path)) {
633 		Con_Printf("Filename invalid\n");
634 		return;
635 	}
636 
637 	Web_ConstructURL(url, Cmd_Argv(1), sizeof(url));
638 
639 	if (! CheckFileExists(path)) {
640 		Con_Printf("Failed to open file\n");
641 		return;
642 	}
643 
644 	code = curl_formadd(&first_form_ptr, &last_form_ptr,
645 		CURLFORM_PTRNAME, "file",
646 		CURLFORM_FILE, path,
647 		CURLFORM_END
648 	);
649 	if (code != CURLE_OK) {
650 		Con_Printf("Request failed (adding file to form)\n");
651 		return;
652 	}
653 
654 	// Optional parameters to be sent to web server
655 	if (!Web_AddParametersToRequest(4, &first_form_ptr, &last_form_ptr)) {
656 		curl_formfree(first_form_ptr);
657 		return;
658 	}
659 
660 	Web_SubmitRequestForm(url, first_form_ptr, last_form_ptr, Web_PostResponse, requestId, NULL);
661 }
662 
Central_ProcessResponses(void)663 void Central_ProcessResponses(void)
664 {
665 	CURLMsg* msg;
666 	int running_handles = 0;
667 	int messages_in_queue = 0;
668 	qbool server_busy = false;
669 
670 	if (!last_checkin_time) {
671 		last_checkin_time = curtime;
672 		return;
673 	}
674 
675 	curl_multi_perform(curl_handle, &running_handles);
676 
677 	server_busy = running_handles || GameStarted();
678 
679 	while ((msg = curl_multi_info_read(curl_handle, &messages_in_queue))) {
680 		if (msg->msg == CURLMSG_DONE) {
681 			CURL* handle = msg->easy_handle;
682 			CURLcode result = msg->data.result;
683 			web_request_data_t** list_pointer = &web_requests;
684 
685 			curl_multi_remove_handle(curl_handle, handle);
686 
687 			while (*list_pointer) {
688 				web_request_data_t* this = *list_pointer;
689 
690 				if (this->handle == handle) {
691 					// Remove from queue immediately
692 					*list_pointer = this->next;
693 
694 					if (this->request_id && !strcmp(this->request_id, "upload")) {
695 						this = this;
696 					}
697 					if (this->onCompleteCallback) {
698 						if (result == CURLE_OK) {
699 							this->onCompleteCallback(this, true);
700 						}
701 						else {
702 							Con_DPrintf("ERROR: %s\n", curl_easy_strerror(result));
703 							this->onCompleteCallback(this, false);
704 						}
705 					}
706 					else {
707 						if (result != CURLE_OK) {
708 							Con_DPrintf("ERROR: %s\n", curl_easy_strerror(result));
709 						}
710 						else {
711 							Con_DPrintf("WEB OK, no callback\n");
712 						}
713 					}
714 
715 					// free memory
716 					curl_formfree(this->first_form_ptr);
717 					Q_free(this->request_id);
718 					Q_free(this);
719 
720 					break;
721 				}
722 
723 				list_pointer = &this->next;
724 			}
725 
726 			curl_easy_cleanup(handle);
727 		}
728 	}
729 
730 	if (sv_www_address.string[0] && !server_busy && curtime - last_checkin_time > max(MIN_CHECKIN_PERIOD, sv_www_checkin_period.value)) {
731 		char url[512];
732 
733 		Web_ConstructURL(url, CHECKIN_PATH, sizeof(url));
734 
735 		Web_SubmitRequestForm(url, NULL, NULL, Web_PostResponse, NULL, NULL);
736 
737 		last_checkin_time = curtime;
738 	}
739 	else if (server_busy) {
740 		last_checkin_time = curtime;
741 	}
742 }
743 
Central_Shutdown(void)744 void Central_Shutdown(void)
745 {
746 	if (curl_handle) {
747 		curl_multi_cleanup(curl_handle);
748 		curl_handle = NULL;
749 	}
750 
751 	curl_global_cleanup();
752 }
753 
Central_Init(void)754 void Central_Init(void)
755 {
756 	curl_global_init(CURL_GLOBAL_DEFAULT);
757 
758 	Cvar_Register(&sv_www_address);
759 	Cvar_Register(&sv_www_authkey);
760 	Cvar_Register(&sv_www_checkin_period);
761 
762 	curl_handle = curl_multi_init();
763 
764 	if (curl_handle) {
765 		Cmd_AddCommand("sv_web_get", Web_GetRequest_f);
766 		Cmd_AddCommand("sv_web_post", Web_PostRequest_f);
767 		Cmd_AddCommand("sv_web_postfile", Web_PostFileRequest_f);
768 	}
769 }
770