diff --git a/src/project.godot b/src/project.godot index d5ed840..3b3e761 100644 --- a/src/project.godot +++ b/src/project.godot @@ -25,6 +25,7 @@ GlobalLogger="*res://scripts/Logger.gd" FileManager="*res://scripts/Files.gd" Util="*res://scripts/Util.gd" CredentialStore="*res://scripts/credential_store.gd" +AccountServers="*res://scripts/account_servers.gd" [display] diff --git a/src/scenes/managers/app/network_manager.gd b/src/scenes/managers/app/network_manager.gd index b0db41a..51d5af3 100644 --- a/src/scenes/managers/app/network_manager.gd +++ b/src/scenes/managers/app/network_manager.gd @@ -1,6 +1,8 @@ extends Node -const n_c = preload("res://scripts/network_compression.gd") +var n_c = preload("res://scripts/network_compression.gd").new() +var jwt = preload("res://scripts/jwt.gd").new() +var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$") # TODO: Bandwidth toggles @onready var scene_manager = get_tree().current_scene.get_node("SceneManager") @@ -156,41 +158,55 @@ func set_networking_config(options: Dictionary) -> void: @rpc("authority", "reliable") func _receive_server_info(server_info: Dictionary): GlobalLogger.log_string("Received server information.") - print(server_info) + # TODO: Do not change scene until connection is finalized. if server_info.level: await scene_manager.load_multiplayer_scene(server_info.level, server_info.level_node_name) - _send_player_info({"display_name": "Client"}) + _send_player_info(CredentialStore.info.token) @rpc("any_peer", "reliable") -func _receive_player_info(player_info: Dictionary): - GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) - +func _receive_player_info(player_info: String): + # TODO: Error checks for JWT if multiplayer.is_server() == false: # We are a client. We should not process any farther. return + + GlobalLogger.log_string("Received '%s' player info." % multiplayer.get_remote_sender_id()) # TODO: Preform validation to determine if the player is allowed to be here # TODO: Preform validation to determine if the player supplied cridentials are good, where they need to be. - player_info = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) + # Preform validation of JWT token + var player_info_dic = _sanity_check_player_info(player_info, multiplayer.get_remote_sender_id()) + var player_decoded_jwt = jwt.decode_jwt(player_info_dic.jwt) - info.clients.append(player_info) + # TODO: util function to break down a url to the key parts. + var url_parts = parse_url(player_decoded_jwt.payload.issuer) + var host_pub_key = await AccountServers._request_server_pem(url_parts.host, url_parts.port) + + var jwt_is_valid = jwt.verify(player_info_dic.jwt, host_pub_key) + + if jwt_is_valid == false: + # TODO: Refuse connection + multiplayer.multiplayer_peer.disconnect_peer(multiplayer.get_remote_sender_id()) + return + + info.clients.append(player_info_dic) # Spawn player - multiplayer_manager.spawn_player(player_info.multiplayer_id) - multiplayer_manager.rpc("spawn_player", player_info.multiplayer_id) + multiplayer_manager.spawn_player(player_info_dic.multiplayer_id) + multiplayer_manager.rpc("spawn_player", player_info_dic.multiplayer_id) # Spawn all connected clients on the new client for client in info.clients: - if client.multiplayer_id == player_info.multiplayer_id: + if client.multiplayer_id == player_info_dic.multiplayer_id: continue - multiplayer_manager.rpc_id(player_info.multiplayer_id, "spawn_player", client.multiplayer_id) + multiplayer_manager.rpc_id(player_info_dic.multiplayer_id, "spawn_player", client.multiplayer_id) send_server_session_info() -func _send_player_info(player_info: Dictionary): +func _send_player_info(player_info: String): GlobalLogger.log_string("Starting server handshake: Sending information about ourself.") _receive_player_info.rpc_id(1, player_info) @@ -210,13 +226,32 @@ func received_server_session_info(received_info: Dictionary) -> void: # TODO: Check if item exists in player inventory # TODO: Get player inventory -func _sanity_check_player_info(player_info: Dictionary, multiplayer_id: int) -> Dictionary: +func _sanity_check_player_info(player_info: String, multiplayer_id: int) -> Dictionary: var sane_player_info = { - "display_name": "", - "multiplayer_id": "" + "jwt": "", + "multiplayer_id": "", + "display_name": "Greetings!" } - sane_player_info.display_name = str(player_info.display_name) + sane_player_info.jwt = str(player_info) sane_player_info.multiplayer_id = int(multiplayer_id) return sane_player_info + + +func parse_url(url: String) -> Dictionary: + var result = { + "scheme": "", + "host": "", + "port": 0, + "path": "" + } + + var matches = url_regex.search(url) + if matches: + result["scheme"] = matches.get_string(1).to_lower() + result["host"] = matches.get_string(2) + result["port"] = int(matches.get_string(3)) if matches.get_string(3) != "" else (443 if result["scheme"] == "https" else 80) + result["path"] = matches.get_string(4) if matches.get_string(4) != "" else "/" + + return result \ No newline at end of file diff --git a/src/scripts/account_servers.gd b/src/scripts/account_servers.gd new file mode 100644 index 0000000..c6c141d --- /dev/null +++ b/src/scripts/account_servers.gd @@ -0,0 +1,23 @@ +extends Node +var http = preload("res://scripts/http.gd").new() +var database = {} +# TODO: Open metadata file, and keep it opened + +# TODO: Validate RSA PEM key +func get_pem(host: String, port: int) -> String: + # TODO: Check if we have the key saved + return "" + +func _request_server_pem(host: String, port: int = 443) -> String: + var key = await http.req(HTTPClient.METHOD_GET, host, "/public_key", port) + if key.ok == true: + return key.body + return "" + + +func _ready(): + _request_server_pem("http://localhost", 40400) + +func _open_or_create_database(): + var dir = DirAccess.open("user://") + dir.make_dir_recursive("user://account_servers/database.json") \ No newline at end of file diff --git a/src/scripts/account_servers.gd.uid b/src/scripts/account_servers.gd.uid new file mode 100644 index 0000000..3be5ba9 --- /dev/null +++ b/src/scripts/account_servers.gd.uid @@ -0,0 +1 @@ +uid://c6heul4elcmbk diff --git a/src/scripts/jwt.gd b/src/scripts/jwt.gd new file mode 100644 index 0000000..fd78598 --- /dev/null +++ b/src/scripts/jwt.gd @@ -0,0 +1,86 @@ +# This provides basic JWT features +extends Node + +func verify(jwt_string: String = "", signature_pem: String = "") -> bool: + # TODO: Error checks + var crypto: Crypto = Crypto.new() + var public_key: CryptoKey = _signature_pem_to_cryptokey(signature_pem) + var jwt_parts: Dictionary = _get_jwt_parts(jwt_string) + var formatted_payload: Dictionary = _format_jwt_payload(jwt_parts.head, jwt_parts.payload) + + return crypto.verify( + HashingContext.HASH_SHA256, + formatted_payload.payload_bytes, + Marshalls.base64_to_raw(jwt_parts.signature), + public_key + ) + +func decode_jwt(jwt_string: String) -> Dictionary: + # TODO: Error checks + var return_dict = {"head": {}, "payload": {}} + + var jwt_parts = _get_jwt_parts(jwt_string) + + jwt_parts.head = _base64url_to_base64(jwt_parts.head) + jwt_parts.head = Marshalls.base64_to_utf8(jwt_parts.head) + return_dict.head = JSON.parse_string(jwt_parts.head) + + jwt_parts.payload = _base64url_to_base64(jwt_parts.payload) + jwt_parts.payload = Marshalls.base64_to_utf8(jwt_parts.payload) + return_dict.payload = JSON.parse_string(jwt_parts.payload) + + return return_dict + +func _base64url_to_base64(base64url: String): + # TODO: Error checks + var fixed: String = base64url + + fixed = fixed.replace("_", "/").replace("-", "+") + var padding = 4 - (fixed.length() % 4) + + if padding < 4: + fixed += "=".repeat(padding) + + return fixed + +func _signature_pem_to_cryptokey(signature_pem: String = "") -> CryptoKey: + # TODO: Error checks + var public_key := CryptoKey.new() + if public_key.load_from_string(signature_pem, true) != OK: + GlobalLogger.log_string("Failed to load signature", 3) + return null + + return public_key + +func _get_jwt_parts(jwt_string: String = "") -> Dictionary: + # TODO: Error checks + var return_dict = {"ok": false, "head": "", "payload": "", "signature": ""} + + var jwt_split = jwt_string.split(".") + + if len(jwt_split) != 3: + GlobalLogger.log_string("JWT token is not formatted correctly.", 2) + return return_dict + + return_dict.head = jwt_split[0] + return_dict.payload = jwt_split[1] + return_dict.signature = _base64url_to_base64(jwt_split[2]) + return_dict.ok = true + + return return_dict + +func _format_jwt_payload(head: String, payload: String) -> Dictionary: + # TODO: Error checks + var return_dict = {"ok": false, "payload_bytes": []} + + var formatted_payload = head + "." + payload + var payload_bytes = formatted_payload.to_utf8_buffer() + + var hasher: HashingContext = HashingContext.new() + hasher.start(HashingContext.HASH_SHA256) + hasher.update(payload_bytes) + + return_dict.payload_bytes = hasher.finish() + return_dict.ok = true + + return return_dict \ No newline at end of file diff --git a/src/scripts/jwt.gd.uid b/src/scripts/jwt.gd.uid new file mode 100644 index 0000000..5809512 --- /dev/null +++ b/src/scripts/jwt.gd.uid @@ -0,0 +1 @@ +uid://bt5gufaix02sa diff --git a/src/scripts/keys.gd b/src/scripts/keys.gd index 188e57a..d9d02b2 100644 --- a/src/scripts/keys.gd +++ b/src/scripts/keys.gd @@ -47,9 +47,8 @@ func _write_keys_to_disk(username): func _generate_keys(): var crypto = Crypto.new() - # keys.private = crypto.generate_rsa(2048) var generated_keys = crypto.generate_rsa(2048) keys.private = generated_keys.save_to_string(false) keys.public = generated_keys.save_to_string(true) - return \ No newline at end of file + return