1 // SONIC ROBO BLAST 2
2 //-----------------------------------------------------------------------------
3 // Copyright (C) 2020 by James R.
4 //
5 // This program is free software distributed under the
6 // terms of the GNU General Public License, version 2.
7 // See the 'LICENSE' file for more details.
8 //-----------------------------------------------------------------------------
9 // \brief HTTP based master server
10 
11 /*
12 Documentation available here.
13 
14                      <http://mb.srb2.org/MS/tools/api/v1/>
15 */
16 
17 #ifdef HAVE_CURL
18 #include <curl/curl.h>
19 #endif
20 
21 #include "doomdef.h"
22 #include "d_clisrv.h"
23 #include "command.h"
24 #include "m_argv.h"
25 #include "m_menu.h"
26 #include "mserv.h"
27 #include "i_tcp.h"/* for current_port */
28 #include "i_threads.h"
29 
30 /* reasonable default I guess?? */
31 #define DEFAULT_BUFFER_SIZE (4096)
32 
33 /* I just stop myself from making macros anymore. */
34 #define Blame( ... ) \
35 	CONS_Printf("\x85" __VA_ARGS__)
36 
37 static void MasterServer_Debug_OnChange (void);
38 
39 consvar_t cv_masterserver_timeout = CVAR_INIT
40 (
41 		"masterserver_timeout", "5", CV_SAVE, CV_Unsigned,
42 		NULL
43 );
44 
45 consvar_t cv_masterserver_debug = CVAR_INIT
46 (
47 	"masterserver_debug", "Off", CV_SAVE|CV_CALL, CV_OnOff,
48 	MasterServer_Debug_OnChange
49 );
50 
51 consvar_t cv_masterserver_token = CVAR_INIT
52 (
53 		"masterserver_token", "", CV_SAVE, NULL,
54 		NULL
55 );
56 
57 #ifdef MASTERSERVER
58 
59 static int hms_started;
60 
61 static char *hms_api;
62 #ifdef HAVE_THREADS
63 static I_mutex hms_api_mutex;
64 #endif
65 
66 static char *hms_server_token;
67 
68 struct HMS_buffer
69 {
70 	CURL *curl;
71 	char *buffer;
72 	int   needle;
73 	int    end;
74 };
75 
76 static void
Contact_error(void)77 Contact_error (void)
78 {
79 	CONS_Alert(CONS_ERROR,
80 			"There was a problem contacting the master server...\n"
81 	);
82 }
83 
84 static size_t
HMS_on_read(char * s,size_t _1,size_t n,void * userdata)85 HMS_on_read (char *s, size_t _1, size_t n, void *userdata)
86 {
87 	struct HMS_buffer *buffer;
88 	size_t blocks;
89 
90 	(void)_1;
91 
92 	buffer = userdata;
93 
94 	if (n >= (size_t)( buffer->end - buffer->needle ))
95 	{
96 		/* resize to next multiple of buffer size */
97 		blocks = ( n / DEFAULT_BUFFER_SIZE + 1 );
98 		buffer->end += ( blocks * DEFAULT_BUFFER_SIZE );
99 
100 		buffer->buffer = realloc(buffer->buffer, buffer->end);
101 	}
102 
103 	memcpy(&buffer->buffer[buffer->needle], s, n);
104 	buffer->needle += n;
105 
106 	return n;
107 }
108 
109 static struct HMS_buffer *
HMS_connect(const char * format,...)110 HMS_connect (const char *format, ...)
111 {
112 	va_list ap;
113 	CURL *curl;
114 	char *url;
115 	char *quack_token;
116 	size_t seek;
117 	size_t token_length;
118 	struct HMS_buffer *buffer;
119 
120 	if (! hms_started)
121 	{
122 		if (curl_global_init(CURL_GLOBAL_ALL) != 0)
123 		{
124 			Contact_error();
125 			Blame("From curl_global_init.\n");
126 			return NULL;
127 		}
128 		else
129 		{
130 			atexit(curl_global_cleanup);
131 			hms_started = 1;
132 		}
133 	}
134 
135 	curl = curl_easy_init();
136 
137 	if (! curl)
138 	{
139 		Contact_error();
140 		Blame("From curl_easy_init.\n");
141 		return NULL;
142 	}
143 
144 	if (cv_masterserver_token.string[0])
145 	{
146 		quack_token = curl_easy_escape(curl, cv_masterserver_token.string, 0);
147 		token_length = ( sizeof "?token="-1 )+ strlen(quack_token);
148 	}
149 	else
150 	{
151 		quack_token = NULL;
152 		token_length = 0;
153 	}
154 
155 #ifdef HAVE_THREADS
156 	I_lock_mutex(&hms_api_mutex);
157 #endif
158 
159 	seek = strlen(hms_api) + 1;/* + '/' */
160 
161 	va_start (ap, format);
162 	url = malloc(seek + vsnprintf(0, 0, format, ap) + token_length + 1);
163 	va_end (ap);
164 
165 	sprintf(url, "%s/", hms_api);
166 
167 #ifdef HAVE_THREADS
168 	I_unlock_mutex(hms_api_mutex);
169 #endif
170 
171 	va_start (ap, format);
172 	seek += vsprintf(&url[seek], format, ap);
173 	va_end (ap);
174 
175 	if (quack_token)
176 		sprintf(&url[seek], "?token=%s", quack_token);
177 
178 	CONS_Printf("HMS: connecting '%s'...\n", url);
179 
180 	buffer = malloc(sizeof *buffer);
181 	buffer->curl = curl;
182 	buffer->end = DEFAULT_BUFFER_SIZE;
183 	buffer->buffer = malloc(buffer->end);
184 	buffer->needle = 0;
185 
186 	if (cv_masterserver_debug.value)
187 	{
188 		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
189 		curl_easy_setopt(curl, CURLOPT_STDERR, logstream);
190 	}
191 
192 	if (M_CheckParm("-bindaddr") && M_IsNextParm())
193 	{
194 		curl_easy_setopt(curl, CURLOPT_INTERFACE, M_GetNextParm());
195 	}
196 
197 	curl_easy_setopt(curl, CURLOPT_URL, url);
198 	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
199 	curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
200 
201 	curl_easy_setopt(curl, CURLOPT_TIMEOUT, cv_masterserver_timeout.value);
202 	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HMS_on_read);
203 	curl_easy_setopt(curl, CURLOPT_WRITEDATA, buffer);
204 
205 	curl_free(quack_token);
206 	free(url);
207 
208 	return buffer;
209 }
210 
211 static int
HMS_do(struct HMS_buffer * buffer)212 HMS_do (struct HMS_buffer *buffer)
213 {
214 	CURLcode cc;
215 	long status;
216 
217 	char *p;
218 
219 	cc = curl_easy_perform(buffer->curl);
220 
221 	if (cc != CURLE_OK)
222 	{
223 		Contact_error();
224 		Blame(
225 				"From curl_easy_perform: %s\n",
226 				curl_easy_strerror(cc)
227 		);
228 		return 0;
229 	}
230 
231 	buffer->buffer[buffer->needle] = '\0';
232 
233 	curl_easy_getinfo(buffer->curl, CURLINFO_RESPONSE_CODE, &status);
234 
235 	if (status != 200)
236 	{
237 		p = strchr(buffer->buffer, '\n');
238 
239 		if (p)
240 			*p = '\0';
241 
242 		Contact_error();
243 		Blame(
244 				"Master server error %ld: %s%s\n",
245 				status,
246 				buffer->buffer,
247 				( (p) ? "" : " (malformed)" )
248 		);
249 
250 		return 0;
251 	}
252 	else
253 		return 1;
254 }
255 
256 static void
HMS_end(struct HMS_buffer * buffer)257 HMS_end (struct HMS_buffer *buffer)
258 {
259 	curl_easy_cleanup(buffer->curl);
260 	free(buffer->buffer);
261 	free(buffer);
262 }
263 
264 int
HMS_fetch_rooms(int joining,int query_id)265 HMS_fetch_rooms (int joining, int query_id)
266 {
267 	struct HMS_buffer *hms;
268 	int ok;
269 
270 	int doing_shit;
271 
272 	char *id;
273 	char *title;
274 	char *room_motd;
275 
276 	int id_no;
277 
278 	char *p;
279 	char *end;
280 
281 	int i;
282 
283 	(void)query_id;
284 
285 	hms = HMS_connect("rooms");
286 
287 	if (! hms)
288 		return 0;
289 
290 	if (HMS_do(hms))
291 	{
292 		doing_shit = 1;
293 
294 		p = hms->buffer;
295 
296 		for (i = 0; i < NUM_LIST_ROOMS && ( end = strstr(p, "\n\n\n") );)
297 		{
298 			*end = '\0';
299 
300 			id    = strtok(p, "\n");
301 			title = strtok(0, "\n");
302 			room_motd = strtok(0, "");
303 
304 			if (id && title && room_motd)
305 			{
306 				id_no = atoi(id);
307 
308 				/*
309 				Don't show the 'All' room if hosting. And it's a hack like this
310 				because I'm way too lazy to add another feature to the MS.
311 				*/
312 				if (joining || id_no != 0)
313 				{
314 #ifdef HAVE_THREADS
315 					I_lock_mutex(&ms_QueryId_mutex);
316 					{
317 						if (query_id != ms_QueryId)
318 							doing_shit = 0;
319 					}
320 					I_unlock_mutex(ms_QueryId_mutex);
321 
322 					if (! doing_shit)
323 						break;
324 #endif
325 
326 					room_list[i].header.buffer[0] = 1;
327 
328 					room_list[i].id = id_no;
329 					strlcpy(room_list[i].name, title, sizeof room_list[i].name);
330 					strlcpy(room_list[i].motd, room_motd, sizeof room_list[i].motd);
331 
332 					i++;
333 				}
334 
335 				p = ( end + 3 );/* skip the three linefeeds */
336 			}
337 			else
338 				break;
339 		}
340 
341 		if (doing_shit)
342 			room_list[i].header.buffer[0] = 0;
343 
344 		ok = 1;
345 
346 		if (doing_shit)
347 		{
348 #ifdef HAVE_THREADS
349 			I_lock_mutex(&m_menu_mutex);
350 #endif
351 			{
352 				for (i = 0; room_list[i].header.buffer[0]; i++)
353 				{
354 					if(*room_list[i].name != '\0')
355 					{
356 						MP_RoomMenu[i+1].text = room_list[i].name;
357 						roomIds[i] = room_list[i].id;
358 						MP_RoomMenu[i+1].status = IT_STRING|IT_CALL;
359 					}
360 				}
361 			}
362 #ifdef HAVE_THREADS
363 			I_unlock_mutex(m_menu_mutex);
364 #endif
365 		}
366 	}
367 	else
368 		ok = 0;
369 
370 	HMS_end(hms);
371 
372 	return ok;
373 }
374 
375 int
HMS_register(void)376 HMS_register (void)
377 {
378 	struct HMS_buffer *hms;
379 	int ok;
380 
381 	char post[256];
382 
383 	char *title;
384 
385 	hms = HMS_connect("rooms/%d/register", ms_RoomId);
386 
387 	if (! hms)
388 		return 0;
389 
390 	title = curl_easy_escape(hms->curl, cv_servername.string, 0);
391 
392 	snprintf(post, sizeof post,
393 			"port=%d&"
394 			"title=%s&"
395 			"version=%s",
396 
397 			current_port,
398 
399 			title,
400 
401 			SRB2VERSION
402 	);
403 
404 	curl_free(title);
405 
406 	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
407 
408 	ok = HMS_do(hms);
409 
410 	if (ok)
411 	{
412 		hms_server_token = strdup(strtok(hms->buffer, "\n"));
413 	}
414 
415 	HMS_end(hms);
416 
417 	return ok;
418 }
419 
420 int
HMS_unlist(void)421 HMS_unlist (void)
422 {
423 	struct HMS_buffer *hms;
424 	int ok;
425 
426 	hms = HMS_connect("servers/%s/unlist", hms_server_token);
427 
428 	if (! hms)
429 		return 0;
430 
431 	curl_easy_setopt(hms->curl, CURLOPT_CUSTOMREQUEST, "POST");
432 
433 	ok = HMS_do(hms);
434 	HMS_end(hms);
435 
436 	free(hms_server_token);
437 
438 	return ok;
439 }
440 
441 int
HMS_update(void)442 HMS_update (void)
443 {
444 	struct HMS_buffer *hms;
445 	int ok;
446 
447 	char post[256];
448 
449 	char *title;
450 
451 	hms = HMS_connect("servers/%s/update", hms_server_token);
452 
453 	if (! hms)
454 		return 0;
455 
456 	title = curl_easy_escape(hms->curl, cv_servername.string, 0);
457 
458 	snprintf(post, sizeof post,
459 			"title=%s",
460 			title
461 	);
462 
463 	curl_free(title);
464 
465 	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);
466 
467 	ok = HMS_do(hms);
468 	HMS_end(hms);
469 
470 	return ok;
471 }
472 
473 void
HMS_list_servers(void)474 HMS_list_servers (void)
475 {
476 	struct HMS_buffer *hms;
477 
478 	char *list;
479 	char *p;
480 
481 	hms = HMS_connect("servers");
482 
483 	if (! hms)
484 		return;
485 
486 	if (HMS_do(hms))
487 	{
488 		list = curl_easy_unescape(hms->curl, hms->buffer, 0, NULL);
489 
490 		p = strtok(list, "\n");
491 
492 		while (p != NULL)
493 		{
494 			CONS_Printf("\x80%s\n", p);
495 			p = strtok(NULL, "\n");
496 		}
497 
498 		curl_free(list);
499 	}
500 
501 	HMS_end(hms);
502 }
503 
504 msg_server_t *
HMS_fetch_servers(msg_server_t * list,int room_number,int query_id)505 HMS_fetch_servers (msg_server_t *list, int room_number, int query_id)
506 {
507 	struct HMS_buffer *hms;
508 
509 	int doing_shit;
510 
511 	char local_version[9];
512 
513 	char *room;
514 
515 	char *address;
516 	char *port;
517 	char *title;
518 	char *version;
519 
520 	char *end;
521 	char *section_end;
522 	char *p;
523 
524 	int i;
525 
526 	(void)query_id;
527 
528 	if (room_number > 0)
529 	{
530 		hms = HMS_connect("rooms/%d/servers", room_number);
531 	}
532 	else
533 		hms = HMS_connect("servers");
534 
535 	if (! hms)
536 		return NULL;
537 
538 	if (HMS_do(hms))
539 	{
540 		doing_shit = 1;
541 
542 		snprintf(local_version, sizeof local_version,
543 				"%s",
544 				SRB2VERSION
545 		);
546 
547 		p = hms->buffer;
548 		i = 0;
549 
550 		do
551 		{
552 			section_end = strstr(p, "\n\n");
553 
554 			room = strtok(p, "\n");
555 
556 			p = strtok(0, "");
557 
558 			if (! p)
559 				break;
560 
561 			while (i < MAXSERVERLIST && ( end = strchr(p, '\n') ))
562 			{
563 				*end = '\0';
564 
565 				address = strtok(p, " ");
566 				port    = strtok(0, " ");
567 				title   = strtok(0, " ");
568 				version = strtok(0, "");
569 
570 				if (address && port && title && version)
571 				{
572 #ifdef HAVE_THREADS
573 					I_lock_mutex(&ms_QueryId_mutex);
574 					{
575 						if (query_id != ms_QueryId)
576 							doing_shit = 0;
577 					}
578 					I_unlock_mutex(ms_QueryId_mutex);
579 
580 					if (! doing_shit)
581 						break;
582 #endif
583 
584 					if (strcmp(version, local_version) == 0)
585 					{
586 						strlcpy(list[i].ip,      address, sizeof list[i].ip);
587 						strlcpy(list[i].port,    port,    sizeof list[i].port);
588 						strlcpy(list[i].name,    title,   sizeof list[i].name);
589 						strlcpy(list[i].version, version, sizeof list[i].version);
590 
591 						list[i].room = atoi(room);
592 
593 						list[i].header.buffer[0] = 1;
594 
595 						i++;
596 					}
597 
598 					if (end == section_end)/* end of list for this room */
599 						break;
600 					else
601 						p = ( end + 1 );/* skip server delimiter */
602 				}
603 				else
604 				{
605 					section_end = 0;/* malformed so quit the parsing */
606 					break;
607 				}
608 			}
609 
610 			if (! doing_shit)
611 				break;
612 
613 			p = ( section_end + 2 );
614 		}
615 		while (section_end) ;
616 
617 		if (doing_shit)
618 			list[i].header.buffer[0] = 0;
619 	}
620 	else
621 		list = NULL;
622 
623 	HMS_end(hms);
624 
625 	return list;
626 }
627 
628 int
HMS_compare_mod_version(char * buffer,size_t buffer_size)629 HMS_compare_mod_version (char *buffer, size_t buffer_size)
630 {
631 	struct HMS_buffer *hms;
632 	int ok;
633 
634 	char *version;
635 	char *version_name;
636 
637 	hms = HMS_connect("versions/%d", MODID);
638 
639 	if (! hms)
640 		return 0;
641 
642 	ok = 0;
643 
644 	if (HMS_do(hms))
645 	{
646 		version      = strtok(hms->buffer, " ");
647 		version_name = strtok(0, "\n");
648 
649 		if (version && version_name)
650 		{
651 			if (atoi(version) != MODVERSION)
652 			{
653 				strlcpy(buffer, version_name, buffer_size);
654 				ok = 1;
655 			}
656 			else
657 				ok = -1;
658 		}
659 	}
660 
661 	HMS_end(hms);
662 
663 	return ok;
664 }
665 
666 void
HMS_set_api(char * api)667 HMS_set_api (char *api)
668 {
669 #ifdef HAVE_THREADS
670 	I_lock_mutex(&hms_api_mutex);
671 #endif
672 	{
673 		free(hms_api);
674 		hms_api = api;
675 	}
676 #ifdef HAVE_THREADS
677 	I_unlock_mutex(hms_api_mutex);
678 #endif
679 }
680 
681 #endif/*MASTERSERVER*/
682 
683 static void
MasterServer_Debug_OnChange(void)684 MasterServer_Debug_OnChange (void)
685 {
686 #ifdef MASTERSERVER
687 	/* TODO: change to 'latest-log.txt' for log files revision. */
688 	if (cv_masterserver_debug.value)
689 		CONS_Printf("Master server debug messages will appear in log.txt\n");
690 #endif
691 }
692