master uint/magnolia / source / net / mc_client.c
  1#define _POSIX_C_SOURCE 200112L
  2
  3#include <errno.h>
  4#include <netdb.h>
  5#include <poll.h>
  6#include <stdio.h>
  7#include <stdlib.h>
  8#include <string.h>
  9#include <sys/socket.h>
 10#include <unistd.h>
 11
 12#include <zlib.h>
 13
 14#include "mc_internal.h"
 15
 16#ifdef MG_ENABLE_CRYPTO
 17#include "mgrandom.h"
 18#endif
 19
 20#ifndef MSG_NOSIGNAL
 21	#define MSG_NOSIGNAL 0
 22#endif
 23
 24#ifdef MG_ENABLE_CRYPTO
 25static i8 handle_encryption(MCClient* client, const MCAccount* account, MCPacket* packet, char* error,
 26                            size_t error_size);
 27#endif
 28static i8 login_loop(MCClient* client, const MCAccount* account, char* error, size_t error_size);
 29static i8 send_handshake(MCClient* client);
 30static i8 send_login_start(MCClient* client);
 31static i8 socket_read(MCClient* client, u8* data, size_t size);
 32static i8 socket_read_varint(MCClient* client, i32* value);
 33static i8 socket_write(MCClient* client, const u8* data, size_t size);
 34static i8 split_address(const char* address, char* host, size_t host_size, u16* port, char* error, size_t error_size);
 35static i8 tcp_connect(MCClient* client, char* error, size_t error_size);
 36
 37/* handle server encryption request
 38   returns 1 on success, 0 on failure */
 39#ifdef MG_ENABLE_CRYPTO
 40static i8 handle_encryption(MCClient* client, const MCAccount* account, MCPacket* packet, char* error,
 41                            size_t error_size)
 42{
 43	MCBuffer  input = { packet->data, packet->size, packet->size, 0 };
 44	MCBuffer  response;
 45	char      server_id[256];
 46	const u8* public_key;
 47	const u8* verify_token;
 48	size_t    public_key_size;
 49	size_t    verify_token_size;
 50	u8        shared_secret[16];
 51	u8*       encrypted_secret = NULL;
 52	u8*       encrypted_token = NULL;
 53	size_t    encrypted_secret_size = 0;
 54	size_t    encrypted_token_size = 0;
 55	i8        ok = 0;
 56
 57	/* parse the server encryption request */
 58	if (!bufread_string(&input, server_id, sizeof(server_id)) ||
 59	    !bufread_bytes(&input, &public_key, &public_key_size) ||
 60	    !bufread_bytes(&input, &verify_token, &verify_token_size)) {
 61		mc_set_error(MC_EC_MALFORMED_PACKET, error, error_size, "malformed encryption request");
 62		return 0;
 63	}
 64	/* create the AES shared secret for the connection */
 65	if (!mg_random_bytes(shared_secret, sizeof(shared_secret))) {
 66		mc_set_error(MC_EC_RANDOM, error, error_size, "could not obtain secure random bytes");
 67		return 0;
 68	}
 69	/* join the mojang session, encrypt the secret, verify token */
 70	if (!mc_join_server(account, server_id, shared_secret, public_key, public_key_size, error, error_size) ||
 71	    !mc_rsa_encrypt(public_key, public_key_size, shared_secret, sizeof(shared_secret), &encrypted_secret,
 72	                    &encrypted_secret_size, error, error_size) ||
 73	    !mc_rsa_encrypt(public_key, public_key_size, verify_token, verify_token_size, &encrypted_token,
 74	                    &encrypted_token_size, error, error_size))
 75		goto cleanup;
 76
 77	bufinit(&response);
 78	if (bufwrite_varint(&response, (i32)encrypted_secret_size) &&
 79	    bufwrite(&response, encrypted_secret, encrypted_secret_size) &&
 80	    bufwrite_varint(&response, (i32)encrypted_token_size) &&
 81	    bufwrite(&response, encrypted_token, encrypted_token_size) &&
 82	    mc_client_write(client, 0x01, response.data, response.size)) {
 83		/* enable encryption after sending the unencrypted response */
 84		mc_cipher_init(&client->read_cipher, shared_secret);
 85		mc_cipher_init(&client->write_cipher, shared_secret);
 86		client->encrypted = 1;
 87		ok = 1;
 88	}
 89	else {
 90		mc_set_error(MC_EC_SOCKET_WRITE, error, error_size, "could not send encryption response");
 91	}
 92	buffree(&response);
 93
 94cleanup:
 95	memset(shared_secret, 0, sizeof(shared_secret));
 96	free(encrypted_secret);
 97	free(encrypted_token);
 98	return ok;
 99}
100#endif
101
102/* process login packets until play state
103   returns 1 on success, 0 on failure */
104static i8 login_loop(MCClient* client, const MCAccount* account, char* error, size_t error_size)
105{
106#ifndef MG_ENABLE_CRYPTO
107	(void)account;
108#endif
109	/* login state packet ids for protocol 47:
110	   0x00 disconnect
111	   0x01 encryption request
112	   0x02 login success
113	   0x03 set compression
114
115	   https://prismarinejs.github.io/minecraft-data/protocol/pc/1.8/#login.toClient.types */
116	for (;;) {
117		MCPacket packet;
118		MCBuffer input;
119		if (!mc_client_read(client, &packet, error, error_size))
120			return 0;
121		input.data = packet.data;
122		input.size = packet.size;
123		input.capacity = packet.size;
124		input.cursor = 0;
125		if (packet.id == 0x00) {
126			char reason[1024];
127			if (!bufread_string(&input, reason, sizeof(reason)))
128				strcpy(reason, "malformed disconnect reason");
129			mc_set_error(MC_EC_LOGIN_DISCONNECT, error, error_size, "server disconnected during login: %s",
130			             reason);
131			mc_packet_free(&packet);
132			return 0;
133		}
134		if (packet.id == 0x01) {
135#ifdef MG_ENABLE_CRYPTO
136			i8 ok = handle_encryption(client, account, &packet, error, error_size);
137			mc_packet_free(&packet);
138			if (!ok)
139				return 0;
140			continue;
141#else
142			mc_set_error(MC_EC_CRYPTO_DISABLED, error, error_size,
143			             "server is online-mode, i can't do that");
144			mc_packet_free(&packet);
145			return 0;
146#endif
147		}
148		if (packet.id == 0x02) {
149			char username[64];
150			if (!bufread_string(&input, client->uuid, sizeof(client->uuid)) ||
151			    !bufread_string(&input, username, sizeof(username))) {
152				mc_set_error(MC_EC_MALFORMED_PACKET, error, error_size,
153				             "malformed login success packet");
154				mc_packet_free(&packet);
155				return 0;
156			}
157			printf("login success: uuid=%s username=%s\n", client->uuid, username);
158			mc_packet_free(&packet);
159			return 1;
160		}
161		if (packet.id == 0x03) {
162			i32 threshold;
163			if (!bufread_varint(&input, &threshold) || threshold < 0) {
164				mc_set_error(MC_EC_INVALID_COMPRESSED_PACKET, error, error_size,
165				             "invalid compression threshold");
166				mc_packet_free(&packet);
167				return 0;
168			}
169			client->compression_threshold = threshold;
170			printf("compression enabled: threshold=%d\n", threshold);
171			mc_packet_free(&packet);
172			continue;
173		}
174		mc_set_error(MC_EC_UNEXPECTED_LOGIN_PACKET, error, error_size, "unexpected login packet 0x%02x",
175		             packet.id);
176		mc_packet_free(&packet);
177		return 0;
178	}
179}
180
181/* send handshake packet
182   returns 1 on success, 0 on failure */
183static i8 send_handshake(MCClient* client)
184{
185	MCBuffer payload;
186	i8       ok;
187	bufinit(&payload);
188	/* handshake to next state 2 switches the connection into login state */
189	ok = bufwrite_varint(&payload, MC_PROTOCOL_1_8_9) && bufwrite_string(&payload, client->host) &&
190	     bufwrite_u16(&payload, client->port) && bufwrite_varint(&payload, 2) &&
191	     mc_client_write(client, 0x00, payload.data, payload.size);
192	buffree(&payload);
193	return ok;
194}
195
196/* send login start packet
197   returns 1 on success, 0 on failure */
198static i8 send_login_start(MCClient* client)
199{
200	MCBuffer payload;
201	i8       ok;
202	bufinit(&payload);
203	ok = bufwrite_string(&payload, client->username) && mc_client_write(client, 0x00, payload.data, payload.size);
204	buffree(&payload);
205	return ok;
206}
207
208/* read bytes from socket
209   returns 1 on success, 0 on failure */
210static i8 socket_read(MCClient* client, u8* data, size_t size)
211{
212	size_t offset = 0;
213	while (offset < size) {
214		ssize_t count = recv(client->socket_fd, data + offset, size - offset, 0);
215		if (count > 0) {
216			offset += (size_t)count;
217		}
218		else if (count < 0 && errno == EINTR) {
219			continue;
220		}
221		else {
222			return 0;
223		}
224	}
225#ifdef MG_ENABLE_CRYPTO
226	if (client->encrypted)
227		mc_cipher_decrypt(&client->read_cipher, data, size);
228#endif
229	return 1;
230}
231
232/* read VarInt from socket
233   returns 1 on success, 0 on failure */
234static i8 socket_read_varint(MCClient* client, i32* value)
235{
236	u32 result = 0;
237	i32 shift;
238	for (shift = 0; shift < 35; shift += 7) {
239		u8 byte;
240		if (!socket_read(client, &byte, 1))
241			return 0;
242		result |= (u32)(byte & 0x7f) << shift;
243		if (!(byte & 0x80)) {
244			*value = (i32)result;
245			return 1;
246		}
247	}
248	return 0;
249}
250
251/* write bytes to socket
252   returns 1 on success, 0 on failure */
253static i8 socket_write(MCClient* client, const u8* data, size_t size)
254{
255	u8     output[4096];
256	size_t offset = 0;
257
258	while (offset < size) {
259		size_t chunk = size - offset;
260		size_t sent = 0;
261		if (chunk > sizeof(output))
262			chunk = sizeof(output);
263		memcpy(output, data + offset, chunk);
264#ifdef MG_ENABLE_CRYPTO
265		if (client->encrypted)
266			mc_cipher_encrypt(&client->write_cipher, output, chunk);
267#endif
268		while (sent < chunk) {
269			ssize_t count = send(client->socket_fd, output + sent, chunk - sent, MSG_NOSIGNAL);
270			if (count > 0) {
271				sent += (size_t)count;
272			}
273			else if (count < 0 && errno == EINTR) {
274				continue;
275			}
276			else {
277				return 0;
278			}
279		}
280		offset += chunk;
281	}
282	return 1;
283}
284
285/* parse address into host and port
286   returns 1 on success, 0 on failure */
287static i8 split_address(const char* address, char* host, size_t host_size, u16* port, char* error, size_t error_size)
288{
289	const char* colon = strrchr(address, ':');
290	const char* end = address + strlen(address);
291	char*       port_end = NULL;
292	size_t      length;
293	long        parsed_port = 25565;
294
295	if (address[0] == '[') {
296		const char* close = strchr(address, ']');
297		if (!close) {
298			mc_set_error(MC_EC_INVALID_ADDRESS, error, error_size, "invalid bracketed IPv6 address");
299			return 0;
300		}
301		length = (size_t)(close - address - 1);
302		address += 1;
303		if (close[1] == ':') {
304			parsed_port = strtol(close + 2, &port_end, 10);
305		}
306		else if (close[1] != '\0') {
307			mc_set_error(MC_EC_INVALID_ADDRESS, error, error_size, "invalid address suffix");
308			return 0;
309		}
310	}
311	else if (colon && strchr(address, ':') == colon) {
312		length = (size_t)(colon - address);
313		parsed_port = strtol(colon + 1, &port_end, 10);
314	}
315	else {
316		length = (size_t)(end - address);
317	}
318	if ((port_end && *port_end) || length == 0 || length >= host_size || parsed_port < 1 || parsed_port > 65535) {
319		mc_set_error(MC_EC_INVALID_ADDRESS, error, error_size, "invalid server address");
320		return 0;
321	}
322	memcpy(host, address, length);
323	host[length] = '\0';
324	*port = (u16)parsed_port;
325	return 1;
326}
327
328/* connect to the configured server address
329   returns 1 on success, 0 on failure */
330static i8 tcp_connect(MCClient* client, char* error, size_t error_size)
331{
332	struct addrinfo  hints;
333	struct addrinfo* addresses = NULL;
334	struct addrinfo* address;
335	char             service[6];
336	int              result;
337
338	memset(&hints, 0, sizeof(hints));
339	hints.ai_family = AF_UNSPEC;
340	hints.ai_socktype = SOCK_STREAM;
341	snprintf(service, sizeof(service), "%u", client->port);
342
343	result = getaddrinfo(client->host, service, &hints, &addresses);
344	if (result != 0) {
345		mc_set_error(MC_EC_RESOLVE_FAILED, error, error_size, "could not resolve %s: %s", client->host,
346		             gai_strerror(result));
347		return 0;
348	}
349
350	for (address = addresses; address; address = address->ai_next) {
351		client->socket_fd = socket(address->ai_family, address->ai_socktype, address->ai_protocol);
352		if (client->socket_fd < 0)
353			continue;
354		if (connect(client->socket_fd, address->ai_addr, address->ai_addrlen) == 0)
355			break;
356		close(client->socket_fd);
357		client->socket_fd = -1;
358	}
359	freeaddrinfo(addresses);
360
361	if (client->socket_fd < 0) {
362		mc_set_error(MC_EC_CONNECT_FAILED, error, error_size, "could not connect to %s:%u: %s", client->host,
363		             client->port, strerror(errno));
364		return 0;
365	}
366	return 1;
367}
368
369i8 mc_client_write(MCClient* client, i32 packet_id, const void* data, size_t size)
370{
371	MCBuffer body;
372	MCBuffer framed;
373	MCBuffer length;
374	u8*      compressed = NULL;
375	uLongf   compressed_size;
376	i8       ok = 0;
377
378	if (!client || client->socket_fd < 0 || size > MC_MAX_PACKET_SIZE)
379		return 0;
380	bufinit(&body);
381	bufinit(&framed);
382	bufinit(&length);
383	/* build the packet body... */
384	if (!bufwrite_varint(&body, packet_id) || !bufwrite(&body, data, size))
385		goto cleanup;
386	/* packet layout:
387	   length VarInt
388	   [data length VarInt if compression is enabled]
389	   packet id VarInt
390	   payload
391
392	   https://wikivg.booky.dev/Protocol#Packet_format */
393
394	/* ...then add the packet length and compression header */
395	if (client->compression_threshold >= 0) {
396		/* if in compressed mode, data length 0 means this packet body
397		   is not actually compressed. otherwise it is the size after
398		   zlib decompression
399		   https://wikivg.booky.dev/Protocol#With_compression */
400		if (body.size >= (size_t)client->compression_threshold) {
401			compressed_size = compressBound((uLong)body.size);
402			compressed = (u8*)malloc(compressed_size);
403			if (!compressed || compress2(compressed, &compressed_size, body.data, (uLong)body.size,
404			                             Z_DEFAULT_COMPRESSION) != Z_OK)
405				goto cleanup;
406			if (!bufwrite_varint(&framed, (i32)body.size) ||
407			    !bufwrite(&framed, compressed, compressed_size))
408				goto cleanup;
409		}
410		else if (!bufwrite_varint(&framed, 0) || !bufwrite(&framed, body.data, body.size)) {
411			goto cleanup;
412		}
413	}
414	else if (!bufwrite(&framed, body.data, body.size)) {
415		goto cleanup;
416	}
417	if (bufwrite_varint(&length, (i32)framed.size) && socket_write(client, length.data, length.size) &&
418	    socket_write(client, framed.data, framed.size))
419		ok = 1;
420
421cleanup:
422	free(compressed);
423	buffree(&length);
424	buffree(&framed);
425	buffree(&body);
426	return ok;
427}
428
429i8 mc_client_read(MCClient* client, MCPacket* packet, char* error, size_t error_size)
430{
431	i32      frame_length;
432	MCBuffer frame;
433	MCBuffer decoded;
434	i32      data_length;
435	i32      packet_id;
436	i8       decoded_allocated = 0;
437	i8       ok = 0;
438
439	memset(packet, 0, sizeof(*packet));
440	bufinit(&frame);
441	bufinit(&decoded);
442	if (!socket_read_varint(client, &frame_length)) {
443		mc_set_error(MC_EC_SOCKET_READ, error, error_size, "connection closed while reading packet length");
444		goto cleanup;
445	}
446	if (frame_length <= 0 || (u32)frame_length > MC_MAX_PACKET_SIZE || !bufreserve(&frame, (size_t)frame_length)) {
447		mc_set_error(MC_EC_INVALID_PACKET_LENGTH, error, error_size, "invalid packet length: %d", frame_length);
448		goto cleanup;
449	}
450	frame.size = (size_t)frame_length;
451	if (!socket_read(client, frame.data, frame.size)) {
452		mc_set_error(MC_EC_SOCKET_READ, error, error_size, "connection closed while reading packet");
453		goto cleanup;
454	}
455	if (client->compression_threshold >= 0) {
456		/* same as before data length 0 means no compression */
457		if (!bufread_varint(&frame, &data_length) || data_length < 0 || (u32)data_length > MC_MAX_PACKET_SIZE) {
458			mc_set_error(MC_EC_INVALID_COMPRESSED_PACKET, error, error_size,
459			             "invalid compressed packet header");
460			goto cleanup;
461		}
462		if (data_length == 0) {
463			decoded.data = frame.data + frame.cursor;
464			decoded.size = frame.size - frame.cursor;
465		}
466		else {
467			uLongf output_size = (uLongf)data_length;
468			if (!bufreserve(&decoded, (size_t)data_length) ||
469			    uncompress(decoded.data, &output_size, frame.data + frame.cursor,
470			               (uLong)(frame.size - frame.cursor)) != Z_OK ||
471			    output_size != (uLongf)data_length) {
472				mc_set_error(MC_EC_ZLIB, error, error_size, "invalid zlib packet");
473				goto cleanup;
474			}
475			decoded_allocated = 1;
476			decoded.size = (size_t)output_size;
477		}
478	}
479	else {
480		decoded.data = frame.data;
481		decoded.size = frame.size;
482	}
483	if (!bufread_varint(&decoded, &packet_id)) {
484		mc_set_error(MC_EC_MALFORMED_PACKET, error, error_size, "packet has no valid id");
485		goto cleanup;
486	}
487	packet->size = decoded.size - decoded.cursor;
488	packet->data = (u8*)malloc(packet->size ? packet->size : 1);
489	if (!packet->data) {
490		mc_set_error(MC_EC_OUT_OF_MEMORY, error, error_size, "out of memory reading packet");
491		goto cleanup;
492	}
493	memcpy(packet->data, decoded.data + decoded.cursor, packet->size);
494	packet->id = packet_id;
495	ok = 1;
496
497cleanup:
498	if (decoded_allocated) {
499		buffree(&decoded);
500	}
501	else {
502		decoded.data = NULL;
503	}
504	buffree(&frame);
505	if (!ok)
506		mc_packet_free(packet);
507	return ok;
508}
509
510void mc_packet_free(MCPacket* packet)
511{
512	if (!packet)
513		return;
514	free(packet->data);
515	memset(packet, 0, sizeof(*packet));
516}
517
518/*create a client */
519i8 mc_client_login(MCClient** out, const char* address, const MCAccount* account, char* error, size_t error_size)
520{
521	MCClient* client;
522	size_t    username_size;
523	/*validate inputs*/
524	*out = NULL;
525	if (error && error_size)
526		error[0] = '\0';
527	if (!address || !account || !account->username) {
528		mc_set_error(MC_EC_INVALID_ACCOUNT, error, error_size, "missing server address or account");
529		return 0;
530	}
531	username_size = strlen(account->username);
532	if (username_size == 0 || username_size > 16) {
533		mc_set_error(MC_EC_INVALID_ACCOUNT, error, error_size, "Minecraft username must be 1 to 16 characters");
534		return 0;
535	}
536	client = (MCClient*)calloc(1, sizeof(*client));
537	if (!client) {
538		mc_set_error(MC_EC_OUT_OF_MEMORY, error, error_size, "out of memory creating client");
539		return 0;
540	}
541	client->socket_fd = -1;
542	client->compression_threshold = -1;
543	memcpy(client->username, account->username, username_size + 1);
544	printf("connecting to %s as %s\n", address, client->username);
545	/*parse server addr */
546	if (!split_address(address, client->host, sizeof(client->host), &client->port, error, error_size) ||
547	    /*open tcp */
548	    !tcp_connect(client, error, error_size) || !send_handshake(client) || !send_login_start(client)) {
549		if (!error || !error_size || !error[0])
550			mc_set_error(MC_EC_LOGIN_START, error, error_size, "could not start Minecraft login");
551		mc_client_destroy(client);
552		return 0;
553	}
554	printf("sent handshake and login start\n");
555	if (!login_loop(client, account, error, error_size)) {
556		mc_client_destroy(client);
557		return 0;
558	}
559	*out = client;
560	return 1;
561}
562
563void mc_client_interrupt(MCClient* client)
564{
565	if (client && client->socket_fd >= 0)
566		shutdown(client->socket_fd, SHUT_RDWR);
567}
568
569void mc_client_destroy(MCClient* client)
570{
571	if (!client)
572		return;
573	if (client->socket_fd >= 0)
574		close(client->socket_fd);
575	memset(client, 0, sizeof(*client));
576	free(client);
577}
578
579i8 mc_client_poll(MCClient* client, MCPacket* packet, char* error, size_t error_size)
580{
581	struct pollfd fd;
582	int           result;
583
584	if (!client || client->socket_fd < 0) {
585		mc_set_error(MC_EC_INVALID_CLIENT, error, error_size, "invalid Minecraft client");
586		return -1;
587	}
588
589	fd.fd = client->socket_fd;
590	fd.events = POLLIN;
591	fd.revents = 0;
592
593	do {
594		result = poll(&fd, 1, 0);
595	} while (result < 0 && errno == EINTR);
596
597	if (result < 0) {
598		mc_set_error(MC_EC_POLL, error, error_size, "socket poll failed: %s", strerror(errno));
599		return -1;
600	}
601
602	if (result == 0)
603		return 0;
604
605	if (fd.revents & POLLIN) {
606		if (!mc_client_read(client, packet, error, error_size))
607			return -1;
608
609		return 1;
610	}
611
612	if (fd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
613		mc_set_error(MC_EC_CONNECTION_CLOSED, error, error_size, "Minecraft connection closed");
614		return -1;
615	}
616
617	return 0;
618}
619
620i8 mc_client_send_position_look(MCClient* client, double x, double feet_y, double z, float yaw, float pitch,
621                                i8 grounded)
622{
623	MCBuffer payload;
624	i8       ok;
625
626	bufinit(&payload);
627
628	ok = bufwrite_f64(&payload, x) && bufwrite_f64(&payload, feet_y) && bufwrite_f64(&payload, z) &&
629	     bufwrite_f32(&payload, yaw) && bufwrite_f32(&payload, pitch) && bufwrite_u8(&payload, grounded ? 1 : 0) &&
630	     /* player (look) position */
631	     mc_client_write(client, 0x06, payload.data, payload.size);
632
633	buffree(&payload);
634	return ok;
635}
636
637const char* mc_client_username(const MCClient* client)
638{
639	return client ? client->username : "";
640}
641
642const char* mc_client_uuid(const MCClient* client)
643{
644	return client ? client->uuid : "";
645}