From ac145b6d6b9fd61704fb0d8d9ee10755cc6bb8d3 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 9 Sep 2023 19:59:29 -0500 Subject: [PATCH] Peers not being purged corrected but not optimal. HTTP for inter-peer networking replaced with ENet. --- build.sh | 9 +- custom.py | 2 +- hamncheese/dialog.gd | 18 +- hamncheese/godot-uuid/.gitattributes | 3 + hamncheese/godot-uuid/.gitignore | 2 + .../{httpd/LICENSE.md => godot-uuid/LICENSE} | 3 +- hamncheese/godot-uuid/README.md | 21 ++ hamncheese/godot-uuid/logo.png | 3 + hamncheese/godot-uuid/logo.png.import | 34 ++ hamncheese/godot-uuid/logo.svg | 90 +++++ hamncheese/godot-uuid/logo.svg.import | 37 ++ hamncheese/godot-uuid/test.gd | 317 ++++++++++++++++++ hamncheese/godot-uuid/uuid.gd | 115 +++++++ hamncheese/httpd/README.md | 44 --- hamncheese/httpd/http_file_router.gd | 169 ---------- hamncheese/httpd/http_request.gd | 53 --- hamncheese/httpd/http_response.gd | 170 ---------- hamncheese/httpd/http_router.gd | 32 -- hamncheese/httpd/http_server.gd | 243 -------------- hamncheese/main.gd | 71 +++- hamncheese/peers.gd | 77 +++-- hamncheese/router.gd | 32 -- hamncheese/util.gd | 9 + modules/n2nvpn/SCsub | 80 ++--- modules/n2nvpn/n2n/include/n2n_define.h | 8 +- modules/n2nvpn/n2n/src/edge_utils.c | 12 +- modules/n2nvpn/n2n/src/sn_utils.c | 8 +- modules/n2nvpn/n2nvpn.cpp | 63 +++- 28 files changed, 849 insertions(+), 876 deletions(-) create mode 100644 hamncheese/godot-uuid/.gitattributes create mode 100644 hamncheese/godot-uuid/.gitignore rename hamncheese/{httpd/LICENSE.md => godot-uuid/LICENSE} (96%) create mode 100644 hamncheese/godot-uuid/README.md create mode 100644 hamncheese/godot-uuid/logo.png create mode 100644 hamncheese/godot-uuid/logo.png.import create mode 100644 hamncheese/godot-uuid/logo.svg create mode 100644 hamncheese/godot-uuid/logo.svg.import create mode 100644 hamncheese/godot-uuid/test.gd create mode 100644 hamncheese/godot-uuid/uuid.gd delete mode 100644 hamncheese/httpd/README.md delete mode 100644 hamncheese/httpd/http_file_router.gd delete mode 100644 hamncheese/httpd/http_request.gd delete mode 100644 hamncheese/httpd/http_response.gd delete mode 100644 hamncheese/httpd/http_router.gd delete mode 100644 hamncheese/httpd/http_server.gd delete mode 100644 hamncheese/router.gd diff --git a/build.sh b/build.sh index d7d35df..4b43f1d 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Eventially provide a build_profile file to reduce file size. +# Eventually provide a build_profile file to reduce file size. TEMPLATE="disable_3d=yes svg=no" LTO="lto=none" # Use "lto=full" for releases. @@ -10,19 +10,18 @@ pushd godot ln -f -s ../custom.py . # Clean Prior Builds. -scons --clean +#scons --clean # Build Editor. scons platform=linuxbsd target=editor arch=x86_64 ${LTO} # Create JSON for IDE support. -if [[ ! -f compile_commands.json ]]; then +#if [[ ! -f compile_commands.json ]]; then scons compiledb=yes -fi +#fi # Build Templates. scons platform=linuxbsd target=template_release arch=x86_64 ${TEMPLATE} ${LTO} -scons platform=windows target=template_release arch=x86_64 ${TEMPLATE} ${LTO} popd diff --git a/custom.py b/custom.py index 9411304..5d3554c 100644 --- a/custom.py +++ b/custom.py @@ -14,7 +14,7 @@ module_bullet_enabled = "no" module_camera_enabled = "no" module_csg_enabled = "no" module_dds_enabled = "no" -module_enet_enabled = "no" +module_enet_enabled = "yes" module_gltf_enabled = "no" module_gridmap_enabled = "no" module_hdr_enabled = "no" diff --git a/hamncheese/dialog.gd b/hamncheese/dialog.gd index 92212b2..7efa118 100644 --- a/hamncheese/dialog.gd +++ b/hamncheese/dialog.gd @@ -28,27 +28,33 @@ func _confirmCanceled(): emit_signal("_dialog_closed") -func alert(title: String, text: String): +func alert(title: String, text: String, parent: Node = null): _dialog = AcceptDialog.new() _dialog.dialog_text = text _dialog.title = title _dialog.unresizable = true _dialog.get_ok_button().pressed.connect(_alertClosed) - var scene_tree = Engine.get_main_loop() - scene_tree.current_scene.add_child(_dialog) + if parent == null: + var scene_tree = Engine.get_main_loop() + scene_tree.current_scene.add_child(_dialog) + else: + parent.add_child(_dialog) _dialog.popup_centered() await _dialog_closed -func confirm(title: String, text: String): +func confirm(title: String, text: String, parent: Node = null): _dialog = ConfirmationDialog.new() _dialog.dialog_text = text _dialog.title = title _dialog.unresizable = true _dialog.get_ok_button().pressed.connect(_confirmAccepted) _dialog.get_cancel_button().pressed.connect(_confirmCanceled) - var scene_tree = Engine.get_main_loop() - scene_tree.current_scene.add_child(_dialog) + if parent == null: + var scene_tree = Engine.get_main_loop() + scene_tree.current_scene.add_child(_dialog) + else: + parent.add_child(_dialog) _dialog.popup_centered() await _dialog_closed return _result diff --git a/hamncheese/godot-uuid/.gitattributes b/hamncheese/godot-uuid/.gitattributes new file mode 100644 index 0000000..8d1bead --- /dev/null +++ b/hamncheese/godot-uuid/.gitattributes @@ -0,0 +1,3 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf +*.gd linguist-language=GDScript diff --git a/hamncheese/godot-uuid/.gitignore b/hamncheese/godot-uuid/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/hamncheese/godot-uuid/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/hamncheese/httpd/LICENSE.md b/hamncheese/godot-uuid/LICENSE similarity index 96% rename from hamncheese/httpd/LICENSE.md rename to hamncheese/godot-uuid/LICENSE index 85ff935..6497e24 100644 --- a/hamncheese/httpd/LICENSE.md +++ b/hamncheese/godot-uuid/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 deep Entertainment +Copyright (c) 2023 Xavier Sellier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/hamncheese/godot-uuid/README.md b/hamncheese/godot-uuid/README.md new file mode 100644 index 0000000..6d431bf --- /dev/null +++ b/hamncheese/godot-uuid/README.md @@ -0,0 +1,21 @@ +uuid - static uuid generator for Godot Engine +=========================================== + +The *uuid* class is a GDScript 'static' class that provides a unique identifier generation for [Godot Engine](https://godotengine.org). + +Usage +----- + +Copy the `uuid.gd` file in your project folder, and preload it using a constant. + +```gdscript +const uuid_util = preload('res://uuid.gd') + +func _init(): + print(uuid_util.v4()) +``` + +Licensing +--------- + +MIT (See license file for more informations) diff --git a/hamncheese/godot-uuid/logo.png b/hamncheese/godot-uuid/logo.png new file mode 100644 index 0000000..8dcf9f0 --- /dev/null +++ b/hamncheese/godot-uuid/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197de386c0ccd7e36cd8a49fd890b0fadb577cca3a7cc226499934ce1ab38444 +size 5760 diff --git a/hamncheese/godot-uuid/logo.png.import b/hamncheese/godot-uuid/logo.png.import new file mode 100644 index 0000000..e0bce0b --- /dev/null +++ b/hamncheese/godot-uuid/logo.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6r2x1vsb8dpk" +path="res://.godot/imported/logo.png-59310b81088d909d5316ec25c93188c1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://godot-uuid/logo.png" +dest_files=["res://.godot/imported/logo.png-59310b81088d909d5316ec25c93188c1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/hamncheese/godot-uuid/logo.svg b/hamncheese/godot-uuid/logo.svg new file mode 100644 index 0000000..fac99d1 --- /dev/null +++ b/hamncheese/godot-uuid/logo.svg @@ -0,0 +1,90 @@ + + + + + + + + + + image/svg+xml + + + + + + + + v4 + UUID + + diff --git a/hamncheese/godot-uuid/logo.svg.import b/hamncheese/godot-uuid/logo.svg.import new file mode 100644 index 0000000..2c6255f --- /dev/null +++ b/hamncheese/godot-uuid/logo.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bexnj8lbqvhkw" +path="res://.godot/imported/logo.svg-cba474a05619d1d235b2f72ba685d199.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://godot-uuid/logo.svg" +dest_files=["res://.godot/imported/logo.svg-cba474a05619d1d235b2f72ba685d199.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/hamncheese/godot-uuid/test.gd b/hamncheese/godot-uuid/test.gd new file mode 100644 index 0000000..0c6f576 --- /dev/null +++ b/hamncheese/godot-uuid/test.gd @@ -0,0 +1,317 @@ +extends SceneTree + +# To run this script +# godot -s test.gd + +const NUMBER_OF_TESTS = 500000 +const NUMBER_OF_OBJECTS = 50000 # enough to test and to not run out of memory + +var uuid_util = preload('uuid.gd') + +func benchmark_raw(): + print('Benchmarking ...') + + var begin = Time.get_unix_time_from_system() + + var index := 0 + while index < NUMBER_OF_TESTS: + uuid_util.v4() + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_TESTS / duration, + (duration / NUMBER_OF_TESTS) * 1000000, + duration + ]) + print('Benchmark done') + +func benchmark_raw_rng(): + print('Benchmarking ...') + + var rng = RandomNumberGenerator.new() + var begin = Time.get_unix_time_from_system() + var index = 0 + + while index < NUMBER_OF_TESTS: + uuid_util.v4_rng(rng) + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_TESTS / duration, + (duration / NUMBER_OF_TESTS) * 1000000, + duration + ]) + print('Benchmark done') + +func benchmark_obj(): + print('Benchmarking ...') + + var begin = Time.get_unix_time_from_system() + var index = 0 + + while index < NUMBER_OF_TESTS: + uuid_util.new().free() # immediately freeing does not seem to add much overhead + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_TESTS / duration, + (duration / NUMBER_OF_TESTS) * 1000000, + duration + ]) + print('Benchmark done') + +func benchmark_obj_rng(): + print('Benchmarking ...') + + var rng = RandomNumberGenerator.new() + var begin = Time.get_unix_time_from_system() + var index = 0 + + while index < NUMBER_OF_TESTS: + uuid_util.new(rng).free() # immediately freeing does not seem to add much overhead + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_TESTS / duration, + (duration / NUMBER_OF_TESTS) * 1000000, + duration + ]) + print('Benchmark done') + +func benchmark_obj_to_dict(): + print('Setting up benchmark ...') + var uuids = [] + var index = 0 + + while index < NUMBER_OF_OBJECTS: + uuids.push_back(uuid_util.new()) + index += 1 + + print('Benchmarking ...') + var begin = Time.get_unix_time_from_system() + + for uuid in uuids: + uuid.as_dict() + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_OBJECTS / duration, + (duration / NUMBER_OF_OBJECTS) * 1000000, + duration + ]) + print('Cleaning up ...') + + for uuid in uuids: + uuid.free() + print('Benchmark done') + +func benchmark_obj_to_str(): + print('Setting up benchmark ...') + var uuids = [] + var index = 0 + + while index < NUMBER_OF_OBJECTS: + uuids.push_back(uuid_util.new()) + index += 1 + + print('Benchmarking ...') + var begin = Time.get_unix_time_from_system() + + for uuid in uuids: + uuid.as_string() + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_OBJECTS / duration, + (duration / NUMBER_OF_OBJECTS) * 1000000, + duration + ]) + print('Cleaning up ...') + + for uuid in uuids: + uuid.free() + + print('Benchmark done') + +func benchmark_comp_raw(): + print('Setting up benchmark ...') + var uuids = [] + var index = 0 + + while index < NUMBER_OF_OBJECTS: + uuids.push_back(uuid_util.v4()) + index += 1 + + index = 0 + + var collisions = 0 + + print('Benchmarking ...') + var begin = Time.get_unix_time_from_system() + + while index < (NUMBER_OF_OBJECTS - 1): + var uuid1 = uuids[index] + var sub_index = index + 1 + + while sub_index < NUMBER_OF_OBJECTS: + if uuid1 == uuids[sub_index]: + # Don't print anything since it slows down the benchmark + collisions += 1 + + sub_index += 1 + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print("%s collisions detected" % [collisions]) + print("%s total comparison operations" % [NUMBER_OF_OBJECTS]) + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_OBJECTS / duration, + (duration / NUMBER_OF_OBJECTS) * 1000000, + duration + ]) + print('Benchmark done') + +func benchmark_comp_obj(): + print('Setting up benchmark ...') + var uuids = [] + var index = 0 + + while index < NUMBER_OF_OBJECTS: + uuids.push_back(uuid_util.new()) + index += 1 + + index = 0 + var collisions = 0 + + print('Benchmarking ...') + var begin = Time.get_unix_time_from_system() + + while index < (NUMBER_OF_OBJECTS - 1): + var uuid1 = uuids[index] + var sub_index = index + 1 + + while sub_index < NUMBER_OF_OBJECTS: + if uuid1.is_equal(uuids[sub_index]): + # Don't print anything since it slows down the benchmark + collisions += 1 + + sub_index += 1 + index += 1 + + var duration = 1.0 * Time.get_unix_time_from_system() - begin + + print("%s collisions detected" % [collisions]) + print("%s total comparison operations" % [NUMBER_OF_OBJECTS]) + print('uuid/sec: %.02f avg time: %.4fus total time: %.2fs' % [ + NUMBER_OF_OBJECTS / duration, + (duration / NUMBER_OF_OBJECTS) * 1000000, + duration + ]) + print('Cleaning up ...') + + for uuid in uuids: + uuid.free() + + print('Benchmark done') + +func detect_collision(): + print('Detecting collision ...') + + var number_of_collision = 0 + var generated_uuid = {} + var index = 0 + + while index < NUMBER_OF_TESTS: + var key = uuid_util.v4() + + if generated_uuid.has(key): + number_of_collision += 1 + + else: + generated_uuid[key] = true + + index += 1 + + print('Number of collision: %s' % [ number_of_collision ]) + print('Collision detection done') + +func detect_collision_with_rng(): + print('Detecting collision with rng ...') + + var rng = RandomNumberGenerator.new() + var number_of_collision = 0 + var generated_uuid = {} + var index = 0 + + while index < NUMBER_OF_TESTS: + var key = uuid_util.v4_rng(rng) + + if generated_uuid.has(key): + number_of_collision += 1 + + else: + generated_uuid[key] = true + + index += 1 + + print('Number of collision: %s' % [ number_of_collision ]) + print('Collision detection done') + +func test_is_equal(): + var uuid_1 = uuid_util.new() + var uuid_2 = uuid_util.new() + var uuid_3 = uuid_util.new() + + uuid_3._uuid = uuid_1.as_array() + + print('Testing is_equal function') + + if uuid_2.is_equal(uuid_1): + print('"is_equal" ain\'t working correctly (different uuid are identicals)') + + elif not uuid_3.is_equal(uuid_1): + print('"is_equal" ain\'t working correctly (duplicated array not identical)') + + print('Done.') + +func _init(): + test_is_equal() + detect_collision() + detect_collision_with_rng() + + print("\n---------------- Raw ----------------") + benchmark_raw() + + print("\n---------------- Raw with rng ----------------") + benchmark_raw_rng() + + print("\n---------------- Simple object ----------------") + benchmark_obj() + + print("\n---------------- Obj with rng ----------------") + benchmark_obj_rng() + + print("\n---------------- Obj to dict ----------------") + benchmark_obj_to_dict() + + print("\n---------------- Obj to string ----------------") + benchmark_obj_to_str() + + print("\n---------------- Compare raw ----------------") + benchmark_comp_raw() + + print("\n---------------- Compare obj ----------------") + benchmark_comp_obj() + + quit() diff --git a/hamncheese/godot-uuid/uuid.gd b/hamncheese/godot-uuid/uuid.gd new file mode 100644 index 0000000..0c8eb35 --- /dev/null +++ b/hamncheese/godot-uuid/uuid.gd @@ -0,0 +1,115 @@ +# Note: The code might not be as pretty it could be, since it's written +# in a way that maximizes performance. Methods are inlined and loops are avoided. +extends Node + +const BYTE_MASK: int = 0b11111111 + +static func uuidbin(): + randomize() + # 16 random bytes with the bytes on index 6 and 8 modified + return [ + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK, + ((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, + ] + +static func uuidbinrng(rng: RandomNumberGenerator): + rng.randomize() + return [ + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK, + ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, + ] + +static func v4(): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbin() + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +static func v4_rng(rng: RandomNumberGenerator): + # 16 random bytes with the bytes on index 6 and 8 modified + var b = uuidbinrng(rng) + + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + b[0], b[1], b[2], b[3], + + # mid + b[4], b[5], + + # hi + b[6], b[7], + + # clock + b[8], b[9], + + # clock + b[10], b[11], b[12], b[13], b[14], b[15] + ] + +var _uuid: Array + +func _init(rng := RandomNumberGenerator.new()) -> void: + _uuid = uuidbinrng(rng) + +func as_array() -> Array: + return _uuid.duplicate() + +func as_dict(big_endian := true) -> Dictionary: + if big_endian: + return { + "low" : (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8 ) + _uuid[3], + "mid" : (_uuid[4] << 8 ) + _uuid[5], + "hi" : (_uuid[6] << 8 ) + _uuid[7], + "clock": (_uuid[8] << 8 ) + _uuid[9], + "node" : (_uuid[10] << 40) + (_uuid[11] << 32) + (_uuid[12] << 24) + (_uuid[13] << 16) + (_uuid[14] << 8 ) + _uuid[15] + } + else: + return { + "low" : _uuid[0] + (_uuid[1] << 8 ) + (_uuid[2] << 16) + (_uuid[3] << 24), + "mid" : _uuid[4] + (_uuid[5] << 8 ), + "hi" : _uuid[6] + (_uuid[7] << 8 ), + "clock": _uuid[8] + (_uuid[9] << 8 ), + "node" : _uuid[10] + (_uuid[11] << 8 ) + (_uuid[12] << 16) + (_uuid[13] << 24) + (_uuid[14] << 32) + (_uuid[15] << 40) + } + +func as_string() -> String: + return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ + # low + _uuid[0], _uuid[1], _uuid[2], _uuid[3], + + # mid + _uuid[4], _uuid[5], + + # hi + _uuid[6], _uuid[7], + + # clock + _uuid[8], _uuid[9], + + # node + _uuid[10], _uuid[11], _uuid[12], _uuid[13], _uuid[14], _uuid[15] + ] + +func is_equal(other) -> bool: + # Godot Engine compares Array recursively + # There's no need for custom comparison here. + return _uuid == other._uuid diff --git a/hamncheese/httpd/README.md b/hamncheese/httpd/README.md deleted file mode 100644 index 763abdd..0000000 --- a/hamncheese/httpd/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# GodotTPD - -A routeable HTTP server for Godot. - -This addon for the [Godot engine](https://godotengine.com) includes classes to start an HTTP server which can handle requests to paths using a set of routers in the way [ExpressJS](https://expressjs.com/) works. - -## Basic workflow - -Create a router class that extends [HttpRouter](HttpRouter.md). Overwrite the methods that handle the required HTTP methods required for the specific path: - -```python -extends HttpRouter -class_name MyExampleRouter - - -func handle_get(request, response): - response.send(200, "Hello!") - -``` - -This router would respond to a GET [request](HttpRequest.md) on its path and send back a [response](HttpResponse.md) with a 200 status code and the body "Hello!". - -Afterwards, create a new [HttpServer](HttpServer.md), add the router and start the server. This needs to be called from a node in the SceneTree. - -```python -var server = HttpServer.new() -server.register_router("/", MyExampleRouter.new()) -add_child(server) -server.start() -``` - -## Documentation - -Further information can be found in the API documentation: - -- [HttpRequest](docs/api/HttpRequest.md) -- [HttpResponse](docs/api/HttpResponse.md) -- [HttpRouter](docs/api/HttpRouter.md) -- [HttpServer](docs/api/HttpServer.md) -- [HttpFileRouter](docs/api/HttpFileRouter.md) - -## Issues and feature requests - -Please check out the [deep entertainment issue repository](https://github.com/deep-entertainment/issues/issues) if you find bugs or have ideas for new features. diff --git a/hamncheese/httpd/http_file_router.gd b/hamncheese/httpd/http_file_router.gd deleted file mode 100644 index 8a5961e..0000000 --- a/hamncheese/httpd/http_file_router.gd +++ /dev/null @@ -1,169 +0,0 @@ -# Class inheriting HttpRouter for handling file serving requests -extends HttpRouter -class_name HttpFileRouter - -# Full path to the folder which will be exposed to web -var path: String = "" - -# Relative path to the index page, which will be served when a request is made to "/" (server root) -var index_page: String = "index.html" - -# Relative path to the fallback page which will be served if the requested file was not found -var fallback_page: String = "" - -# An ordered list of extensions that will be checked -# if no file extension is provided by the request -var extensions: PackedStringArray = ["html"] - -# A list of extensions that will be excluded if requested -var exclude_extensions: PackedStringArray = [] - -# Creates an HttpFileRouter intance -# -# #### Parameters -# - path: Full path to the folder which will be exposed to web -# - options: Optional Dictionary of options which can be configured. -# - fallback_page: Full path to the fallback page which will be served if the requested file was not found -# - extensions: A list of extensions that will be checked if no file extension is provided by the request -# - exclude_extensions: A list of extensions that will be excluded if requested -func _init( - ppath: String, - options: Dictionary = { - index_page = index_page, - fallback_page = fallback_page, - extensions = extensions, - exclude_extensions = exclude_extensions, - } - ) -> void: - self.path = ppath - self.index_page = options.get("index_page", "") - self.fallback_page = options.get("fallback_page", "") - self.extensions = options.get("extensions", []) - self.exclude_extensions = options.get("exclude_extensions", []) - -# Handle a GET request -func handle_get(request: HttpRequest, response: HttpResponse) -> void: - var serving_path: String = path + request.path - var file_exists: bool = _file_exists(serving_path) - - if request.path == "/" and not file_exists: - if index_page.length() > 0: - serving_path = path + "/" + index_page - file_exists = _file_exists(serving_path) - - if request.path.get_extension() == "" and not file_exists: - for extension in extensions: - serving_path = path + request.path + "." + extension - file_exists = _file_exists(serving_path) - if file_exists: - break - - # GDScript must be excluded, unless it is used as a preprocessor (php-like) - if (file_exists and not serving_path.get_extension() in ["gd"] + Array(exclude_extensions)): - response.send_raw( - 200, - _serve_file(serving_path), - _get_mime(serving_path.get_extension()) - ) - else: - if fallback_page.length() > 0: - serving_path = path + "/" + fallback_page - response.send_raw(200 if index_page == fallback_page else 404, _serve_file(serving_path), _get_mime(fallback_page.get_extension())) - else: - response.send_raw(404) - -# Reads a file as text -# -# #### Parameters -# - file_path: Full path to the file -func _serve_file(file_path: String) -> PackedByteArray: - var content: PackedByteArray = [] - var file: FileAccess = FileAccess.open(file_path, FileAccess.READ) - var error = FileAccess.get_open_error() - if error: - content = ("Couldn't serve file, ERROR = %s" % error).to_ascii_buffer() - else: - content = file.get_buffer(file.get_length()) - file.close() - return content - -# Check if a file exists -# -# #### Parameters -# - file_path: Full path to the file -func _file_exists(file_path: String) -> bool: - return FileAccess.file_exists(file_path) - -# Get the full MIME type of a file from its extension -# -# #### Parameters -# - file_extension: Extension of the file to be served -func _get_mime(file_extension: String) -> String: - var type: String = "application" - var subtype : String = "octet-stream" - match file_extension: - # Web files - "css","html","csv","js","mjs": - type = "text" - subtype = "javascript" if file_extension in ["js","mjs"] else file_extension - "php": - subtype = "x-httpd-php" - "ttf","woff","woff2": - type = "font" - subtype = file_extension - # Image - "png","bmp","gif","png","webp": - type = "image" - subtype = file_extension - "jpeg","jpg": - type = "image" - subtype = "jpg" - "tiff", "tif": - type = "image" - subtype = "jpg" - "svg": - type = "image" - subtype = "svg+xml" - "ico": - type = "image" - subtype = "vnd.microsoft.icon" - # Documents - "doc": - subtype = "msword" - "docx": - subtype = "vnd.openxmlformats-officedocument.wordprocessingml.document" - "7z": - subtype = "x-7x-compressed" - "gz": - subtype = "gzip" - "tar": - subtype = "application/x-tar" - "json","pdf","zip": - subtype = file_extension - "txt": - type = "text" - subtype = "plain" - "ppt": - subtype = "vnd.ms-powerpoint" - # Audio - "midi","mp3","wav": - type = "audio" - subtype = file_extension - "mp4","mpeg","webm": - type = "audio" - subtype = file_extension - "oga","ogg": - type = "audio" - subtype = "ogg" - "mpkg": - subtype = "vnd.apple.installer+xml" - # Video - "ogv": - type = "video" - subtype = "ogg" - "avi": - type = "video" - subtype = "x-msvideo" - "ogx": - subtype = "ogg" - return type + "/" + subtype diff --git a/hamncheese/httpd/http_request.gd b/hamncheese/httpd/http_request.gd deleted file mode 100644 index ea1b01c..0000000 --- a/hamncheese/httpd/http_request.gd +++ /dev/null @@ -1,53 +0,0 @@ -# An HTTP request received by the server -extends RefCounted -class_name HttpRequest - - -# A dictionary of the headers of the request -var headers: Dictionary - -# The received raw body -var body: String - -# A match object of the regular expression that matches the path -var query_match: RegExMatch - -# The path that matches the router path -var path: String - -# The method -var method: String - -# A dictionary of request (aka. routing) parameters -var parameters: Dictionary - -# A dictionary of request query parameters -var query: Dictionary - -# Returns the body object based on the raw body and the content type of the request -func get_body_parsed() -> Variant: - var content_type: String = "" - - if(headers.has("content-type")): - content_type = headers["content-type"] - elif(headers.has("Content-Type")): - content_type = headers["Content-Type"] - - if(content_type == "application/json"): - return JSON.parse_string(body) - - if(content_type == "application/x-www-form-urlencoded"): - var data = {} - - for body_part in body.split("&"): - var key_and_value = body_part.split("=") - data[key_and_value[0]] = key_and_value[1] - - return data - - # Not supported contenty type parsing... for now - return null - -# Override `str()` method, automatically called in `print()` function -func _to_string() -> String: - return JSON.stringify({headers=headers, method=method, path=path}) diff --git a/hamncheese/httpd/http_response.gd b/hamncheese/httpd/http_response.gd deleted file mode 100644 index 4fb280b..0000000 --- a/hamncheese/httpd/http_response.gd +++ /dev/null @@ -1,170 +0,0 @@ -# A response object useful to send out responses -extends RefCounted -class_name HttpResponse - - -# The client currently talking to the server -var client: StreamPeer - -# The server identifier to use on responses [GodotTPD] -var server_identifier: String = "GodotTPD" - -# A dictionary of headers -# Headers can be set using the `set(name, value)` function -var headers: Dictionary = {} - -# An array of cookies -# Cookies can be set using the `cookie(name, value, options)` function -# Cookies will be automatically sent via "Set-Cookie" headers to clients -var cookies: Array = [] - -# Send out a raw (Bytes) response to the client -# Useful to send files faster or raw data which will be converted by the client -# -# #### Parameters -# - status: The HTTP status code to send -# - data: The body data to send [] -# - content_type: The type of the content to send ["text/html"] -func send_raw(status_code: int, data: PackedByteArray = PackedByteArray([]), content_type: String = "application/octet-stream") -> void: - client.put_data(("HTTP/1.1 %d %s\r\n" % [status_code, _match_status_code(status_code)]).to_ascii_buffer()) - client.put_data(("Server: %s\r\n" % server_identifier).to_ascii_buffer()) - for header in headers.keys(): - client.put_data(("%s: %s\r\n" % [header, headers[header]]).to_ascii_buffer()) - for cook in cookies: - client.put_data(("Set-Cookie: %s\r\n" % cook).to_ascii_buffer()) - client.put_data(("Content-Length: %d\r\n" % data.size()).to_ascii_buffer()) - client.put_data("Connection: close\r\n".to_ascii_buffer()) - client.put_data(("Content-Type: %s\r\n\r\n" % content_type).to_ascii_buffer()) - client.put_data(data) - -# Send out a response to the client -# -# #### Parameters -# - status: The HTTP status code to send -# - data: The body data to send [] -# - content_type: The type of the content to send ["text/html"] -func send(status_code: int, data: String = "", content_type = "text/html") -> void: - send_raw(status_code, data.to_ascii_buffer(), content_type) - -# Send out a JSON response to the client -# This function will internally call the `send()` method -# -# #### Parameters -# - status_code: The HTTP status code to send -# - data: The body data to send, must be a Dictionary or an Array -func json(status_code: int, data) -> void: - send(status_code, JSON.stringify(data), "application/json") - - -# Sets the response’s header "field" to "value" -# -# #### Parameters -# - field: the name of the header i.e. "Accept-Type" -# - value: the value of this header i.e. "application/json" -func set_header(field: StringName, value: Variant) -> void: - headers[field] = value - - -# Sets cookie "name" to "value" -# -# #### Parameters -# - name: the name of the cookie i.e. "user-id" -# - value: the value of this cookie i.e. "abcdef" -# - options: a Dictionary of ![cookie attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) -# for this specific cookie, in the { "secure" : "true" } format -func cookie(name: String, value: String, options: Dictionary = {}) -> void: - var cook: String = name+"="+value - if options.has("domain"): cook+="; Domain="+options["domain"] - if options.has("max-age"): cook+="; Max-Age="+options["max-age"] - if options.has("expires"): cook+="; Expires="+options["expires"] - if options.has("path"): cook+="; Path="+options["path"] - if options.has("secure"): cook+="; Secure="+options["secure"] - if options.has("httpOnly"): cook+="; HttpOnly="+options["httpOnly"] - if options.has("sameSite"): - match (options["sameSite"]): - true: cook += "; SameSite=Strict" - "lax": cook += "; SameSite=Lax" - "strict": cook += "; SameSite=Strict" - "none": cook += "; SameSite=None" - _: pass - cookies.append(cook) - - -# Automatically matches a "status_code" to an RFC 7231 compliant "status_text" -# -# #### Parameters -# - code: HTTP Status Code to be matched -# -# Returns: the matched "status_text" -func _match_status_code(code: int) -> String: - var text: String = "OK" - match(code): - # 1xx - Informational Responses - 100: text="Continue" - 101: text="Switching protocols" - 102: text="Processing" - 103: text="Early Hints" - # 2xx - Successful Responses - 200: text="OK" - 201: text="Created" - 202: text="Accepted" - 203: text="Non-Authoritative Information" - 204: text="No Content" - 205: text="Reset Content" - 206: text="Partial Content" - 207: text="Multi-Status" - 208: text="Already Reported" - 226: text="IM Used" - # 3xx - Redirection Messages - 300: text="Multiple Choices" - 301: text="Moved Permanently" - 302: text="Found (Previously 'Moved Temporarily')" - 303: text="See Other" - 304: text="Not Modified" - 305: text="Use Proxy" - 306: text="Switch Proxy" - 307: text="Temporary Redirect" - 308: text="Permanent Redirect" - # 4xx - Client Error Responses - 400: text="Bad Request" - 401: text="Unauthorized" - 402: text="Payment Required" - 403: text="Forbidden" - 404: text="Not Found" - 405: text="Method Not Allowed" - 406: text="Not Acceptable" - 407: text="Proxy Authentication Required" - 408: text="Request Timeout" - 409: text="Conflict" - 410: text="Gone" - 411: text="Length Required" - 412: text="Precondition Failed" - 413: text="Payload Too Large" - 414: text="URI Too Long" - 415: text="Unsupported Media Type" - 416: text="Range Not Satisfiable" - 417: text="Expectation Failed" - 418: text="I'm a Teapot" - 421: text="Misdirected Request" - 422: text="Unprocessable Entity" - 423: text="Locked" - 424: text="Failed Dependency" - 425: text="Too Early" - 426: text="Upgrade Required" - 428: text="Precondition Required" - 429: text="Too Many Requests" - 431: text="Request Header Fields Too Large" - 451: text="Unavailable For Legal Reasons" - # 5xx - Server Error Responses - 500: text="Internal Server Error" - 501: text="Not Implemented" - 502: text="Bad Gateway" - 503: text="Service Unavailable" - 504: text="Gateway Timeout" - 505: text="HTTP Version Not Supported" - 506: text="Variant Also Negotiates" - 507: text="Insufficient Storage" - 508: text="Loop Detected" - 510: text="Not Extended" - 511: text="Network Authentication Required" - return text diff --git a/hamncheese/httpd/http_router.gd b/hamncheese/httpd/http_router.gd deleted file mode 100644 index 2e92111..0000000 --- a/hamncheese/httpd/http_router.gd +++ /dev/null @@ -1,32 +0,0 @@ -# A base class for all HTTP routers -extends RefCounted -class_name HttpRouter - - -# Handle a GET request -func handle_get(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "GET not allowed") - -# Handle a POST request -func handle_post(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "POST not allowed") - -# Handle a HEAD request -func handle_head(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "HEAD not allowed") - -# Handle a PUT request -func handle_put(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "PUT not allowed") - -# Handle a PATCH request -func handle_patch(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "PATCH not allowed") - -# Handle a DELETE request -func handle_delete(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "DELETE not allowed") - -# Handle an OPTIONS request -func handle_options(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(405, "OPTIONS not allowed") diff --git a/hamncheese/httpd/http_server.gd b/hamncheese/httpd/http_server.gd deleted file mode 100644 index 7ccb291..0000000 --- a/hamncheese/httpd/http_server.gd +++ /dev/null @@ -1,243 +0,0 @@ -# A routable HTTP server for Godot -extends Node -class_name HttpServer - -# If `HttpRequest`s and `HttpResponse`s should be logged -var _logging: bool = false - -# The ip address to bind the server to. Use * for all IP addresses [*] -var bind_address: String = "*" - -# The port to bind the server to. [8080] -var port: int = 8080 - -# The server identifier to use when responding to requests [GodotTPD] -var server_identifier: String = "GodotTPD" - - -# The TCP server instance used -var _server: TCPServer - -# An array of StraemPeerTCP objects who are currently talking to the server -var _clients: Array - -# A list of HttpRequest routers who could handle a request -var _routers: Array = [] - -# A regex identifiying the method line -var _method_regex: RegEx = RegEx.new() - -# A regex for header lines -var _header_regex: RegEx = RegEx.new() - -# The base path used in a project to serve files -#var _local_base_path: String = "res://src" - -# Compile the required regex -func _init(_plogging: bool = false): - self._logging = _plogging - set_process(false) - _method_regex.compile("^(?GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS) (?[^ ]+) HTTP/1.1$") - _header_regex.compile("^(?[\\w-]+): (?(.*))$") - -# Print a debug message in console, if the debug mode is enabled -# -# #### Parameters -# - message: The message to be printed (only in debug mode) -func _print_debug(message: String) -> void: - var time = Time.get_datetime_dict_from_system() - var time_return = "%02d-%02d-%02d %02d:%02d:%02d" % [time.year, time.month, time.day, time.hour, time.minute, time.second] - print_debug("[SERVER] ",time_return," >> ", message) - -# Register a new router to handle a specific path -# -# #### Parameters -# - path: The path the router will handle. Supports a regular expression and the -# group matches will be available in HttpRequest.query_match. -# - router: The HttpRouter that will handle the request -func register_router(path: String, router: HttpRouter): - var path_regex = RegEx.new() - var params: Array = [] - if path.left(0) == "^": - path_regex.compile(path) - else: - var regexp: Array = _path_to_regexp(path, router is HttpFileRouter) - path_regex.compile(regexp[0]) - params = regexp[1] - _routers.push_back({ - "path": path_regex, - "params": params, - "router": router - }) - - -# Handle possibly incoming requests -func _process(_delta: float) -> void: - if _server: - var new_client = _server.take_connection() - if new_client: - self._clients.append(new_client) - for client in self._clients: - if client.get_status() == StreamPeerTCP.STATUS_CONNECTED: - var bytes = client.get_available_bytes() - if bytes > 0: - var request_string = client.get_string(bytes) - self._handle_request(client, request_string) - - -# Start the server -func start(): - set_process(true) - self._server = TCPServer.new() - var err: int = self._server.listen(self.port, self.bind_address) - match err: - 22: - _print_debug("Could not bind to port %d, already in use" % [self.port]) - stop() - _: - _print_debug("HTTP Server listening on http://%s:%s" % [self.bind_address, self.port]) - - -# Stop the server and disconnect all clients -func stop(): - for client in self._clients: - client.disconnect_from_host() - self._clients.clear() - self._server.stop() - set_process(false) - _print_debug("Server stopped.") - - -# Interpret a request string and perform the request -# -# #### Parameters -# - client: The client that send the request -# - request: The received request as a String -func _handle_request(client: StreamPeer, request_string: String): - var request = HttpRequest.new() - for line in request_string.split("\r\n"): - var method_matches = _method_regex.search(line) - var header_matches = _header_regex.search(line) - if method_matches: - request.method = method_matches.get_string("method") - var request_path: String = method_matches.get_string("path") - # Check if request_path contains "?" character, could be a query parameter - if not "?" in request_path: - request.path = request_path - else: - var path_query: PackedStringArray = request_path.split("?") - request.path = path_query[0] - request.query = _extract_query_params(path_query[1]) - request.headers = {} - request.body = "" - elif header_matches: - request.headers[header_matches.get_string("key")] = \ - header_matches.get_string("value") - else: - request.body += line - self._perform_current_request(client, request) - - -# Handle a specific request and send it to a router -# If no router matches, send a 404 -# -# #### Parameters -# - client: The client that send the request -# - request_info: A dictionary with information about the request -# - method: The method of the request (e.g. GET, POST) -# - path: The requested path -# - headers: A dictionary of headers of the request -# - body: The raw body of the request -func _perform_current_request(client: StreamPeer, request: HttpRequest): - _print_debug("HTTP Request: " + str(request)) - var found = false - var response = HttpResponse.new() - response.client = client - response.server_identifier = server_identifier - for router in self._routers: - var matches = router.path.search(request.path) - if matches: - request.query_match = matches - if request.query_match.get_string("subpath"): - request.path = request.query_match.get_string("subpath") - if router.params.size() > 0: - for parameter in router.params: - request.parameters[parameter] = request.query_match.get_string(parameter) - match request.method: - "GET": - found = true - router.router.handle_get(request, response) - "POST": - found = true - router.router.handle_post(request, response) - "HEAD": - found = true - router.router.handle_head(request, response) - "PUT": - found = true - router.router.handle_put(request, response) - "PATCH": - found = true - router.router.handle_patch(request, response) - "DELETE": - found = true - router.router.handle_delete(request, response) - "OPTIONS": - found = true - router.router.handle_options(request, response) - if not found: - response.send(404, "Not found") - - -# Converts a URL path to @regexp RegExp, providing a mechanism to fetch groups from the expression -# indexing each parameter by name in the @params array -# -# #### Parameters -# - path: The path of the HttpRequest -# - should_match_subfolder: (dafult [false]) if subfolders should be matched and grouped, -# used for HttpFileRouter -# -# Returns: A 2D array, containing a @regexp String and Dictionary of @params -# [0] = @regexp --> the output expression as a String, to be compiled in RegExp -# [1] = @params --> an Array of parameters, indexed by names -# ex. "/user/:id" --> "^/user/(?([^/#?]+?))[/#?]?$" -func _path_to_regexp(path: String, should_match_subfolders: bool = false) -> Array: - var regexp: String = "^" - var params: Array = [] - var fragments: Array = path.split("/") - fragments.pop_front() - for fragment in fragments: - if fragment.left(1) == ":": - fragment = fragment.lstrip(":") - regexp += "/(?<%s>([^/#?]+?))" % fragment - params.append(fragment) - else: - regexp += "/" + fragment - regexp += "[/#?]?$" if not should_match_subfolders else "(?$|/.*)" - return [regexp, params] - - -# Extracts query parameters from a String query, -# building a Query Dictionary of param:value pairs -# -# #### Parameters -# - query_string: the query string, extracted from the HttpRequest.path -# -# Returns: A Dictionary of param:value pairs -func _extract_query_params(query_string: String) -> Dictionary: - var query: Dictionary = {} - if query_string == "": - return query - var parameters: Array = query_string.split("&") - for param in parameters: - if not "=" in param: - continue - var kv : Array = param.split("=") - var value: String = kv[1] - if value.is_valid_int(): - query[kv[0]] = value.to_int() - elif value.is_valid_float(): - query[kv[0]] = value.to_float() - else: - query[kv[0]] = value - return query diff --git a/hamncheese/main.gd b/hamncheese/main.gd index 2a2078d..434364f 100644 --- a/hamncheese/main.gd +++ b/hamncheese/main.gd @@ -13,7 +13,9 @@ extends Control -const SERVER_PORT = 10042 +const uuid_util = preload('res://godot-uuid/uuid.gd') + +const PEER_BASE_PORT = 20000 # We use the next 256 ports. const FILE_ID_SETTINGS = 0 const FILE_ID_EXIT = 2 @@ -43,14 +45,20 @@ const HELP_ID_MANUAL = 1 @onready var manual_window = %ManualWindow var net; +var uuid; func _go_offline(): - #***TODO*** This can take some time. Show a message. + #***TODO*** This can take some time. Show a message. Not working right. + online_check_button.text = "Wait" + online_check_button.disabled = true + online_check_button.propagate_notification(NOTIFICATION_VISIBILITY_CHANGED) + await get_tree().process_frame Peers.stop_server() net.stop_network() Peers.clear() - peers_tree.clear() + online_check_button.text = "Online" + online_check_button.disabled = false func _go_online(): @@ -80,8 +88,10 @@ func _go_online(): else: # Successful connection. var userInfo = {} + userInfo["ip"] = net.get_ip() userInfo["user"] = user_name_line_edit.text - Peers.start_server(net.get_ip(), SERVER_PORT, userInfo) + userInfo["uuid"] = uuid + Peers.start_server(net.get_ip(), PEER_BASE_PORT, userInfo) func _notification(what): @@ -136,6 +146,23 @@ func _on_online_check_button_toggled(button_pressed): func _on_save_settings_button_pressed(): + # Validate IP address, if needed. + if !ip_check_button.button_pressed: + var ipCheck = RegEx.create_from_string("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$") + if ipCheck.search(ip_line_edit.text) == null: + # Not an IP address. + await Dialog.alert("Invalid IP Address", "The IP address entered is invalid.", settings_window) + return + + # Validate MAC address, if needed. + if !mac_check_button.button_pressed: + var macCheck = RegEx.create_from_string("^(?:[[:xdigit:]]{2}([-:]))(?:[[:xdigit:]]{2}\\1){4}[[:xdigit:]]{2}$") + if macCheck.search(mac_line_edit.text) == null: + # Not a MAC address. + await Dialog.alert("Invalid MAC Address", "The MAC address entered is invalid.", settings_window) + return + + # Save it. var config = ConfigFile.new() config.set_value("settings", "userName", user_name_line_edit.text) config.set_value("settings", "networkName", network_name_line_edit.text) @@ -149,6 +176,7 @@ func _on_save_settings_button_pressed(): config.set_value("settings", "compression", compression_check_button.button_pressed) config.set_value("settings", "autoStart", auto_start_check_button.button_pressed) #config.set_value("settings", "position", DisplayServer.window_get_position()) + config.set_value("settings", "identity", uuid) config.save("user://network.cfg") settings_window.visible = false @@ -168,13 +196,20 @@ func _on_timer_timeout(): print(Peers.peerArray) # Redraw peer tree. peers_tree.clear() + #***TODO**** We should really be displaying things in columns. var root = peers_tree.create_item() for peer in Peers.peerArray: var child = peers_tree.create_item(root) child.set_text(0, peer["user"] + " (" + peer["ip"] + ")") + # Handle empty peer list. + if peers.size() == 0 and peers_tree.get_root() != null: + peers_tree.clear() else: # Clear network information. - my_ip_label.text = "" + if my_ip_label.text != "": + my_ip_label.text = "" + if peers_tree.get_root() != null: + peers_tree.clear() func _ready(): @@ -198,31 +233,33 @@ func _ready(): mac_line_edit.editable = false compression_check_button.button_pressed = true auto_start_check_button.button_pressed = false + uuid = uuid_util.v4() # Show settings dialog. settings_window.visible = true settings_window.move_to_foreground() else: # Load settings. - user_name_line_edit.text = config.get_value("settings", "userName") - network_name_line_edit.text = config.get_value("settings", "networkName") - network_password_line_edit.text = config.get_value("settings", "networkPassword") - server_line_edit.text = config.get_value("settings", "server") - server_port_spin_box.value = config.get_value("settings", "serverPort") - ip_line_edit.text = config.get_value("settings", "ipAddress") - ip_check_button.button_pressed = config.get_value("settings", "ipAutomatic") + user_name_line_edit.text = config.get_value("settings", "userName", "Change Me!") + network_name_line_edit.text = config.get_value("settings", "networkName", "Change Me!") + network_password_line_edit.text = config.get_value("settings", "networkPassword", "Change Me!") + server_line_edit.text = config.get_value("settings", "server", "supernode.kangaroopunch.com") + server_port_spin_box.value = config.get_value("settings", "serverPort", 7777) + ip_line_edit.text = config.get_value("settings", "ipAddress", "10.0.0.10") + ip_check_button.button_pressed = config.get_value("settings", "ipAutomatic", true) ip_line_edit.editable = !ip_check_button.button_pressed - mac_line_edit.text = config.get_value("settings", "macAddress") - mac_check_button.button_pressed = config.get_value("settings", "macAutomatic") + mac_line_edit.text = config.get_value("settings", "macAddress", "DE:AD:BE:EF:00:01") + mac_check_button.button_pressed = config.get_value("settings", "macAutomatic", true) mac_line_edit.editable = !mac_check_button.button_pressed - compression_check_button.button_pressed = config.get_value("settings", "compression") - auto_start_check_button.button_pressed = config.get_value("settings", "autoStart") + compression_check_button.button_pressed = config.get_value("settings", "compression", true) + auto_start_check_button.button_pressed = config.get_value("settings", "autoStart", false) online_check_button.button_pressed = auto_start_check_button.button_pressed + uuid = config.get_value("settings", "identity", uuid_util.v4()) func _show_exit_dialog(): var message = "Are you sure you wish to exit?" if online_check_button.button_pressed: - message = message + "\n\nThis will disconnect you from the network!\n" + message = message + "\n\nThis will disconnect you from the network!\n " var result = await Dialog.confirm("Exit", message) if result: if online_check_button.button_pressed: diff --git a/hamncheese/peers.gd b/hamncheese/peers.gd index fb3435e..0c5380b 100644 --- a/hamncheese/peers.gd +++ b/hamncheese/peers.gd @@ -1,38 +1,54 @@ extends Node -var _server: HttpServer = null -var _client: HTTPRequest +const CHANNELS_MAX = 256 + + +var _enet := ENetMultiplayerPeer.new() +var _basePort: int + +# This array is of dictionary elements that contain: +# "ip": IP address of this peer. +# "port": Listen port of this peer. +# "user": User name on this IP. +# "uuid": UUID for this peer. var _userInfo: Dictionary # This array is of dictionary elements that contain: -# "online": Will be true except when used to update the array. +# "connection": ENet connection to this host. +# "online": Will be true except when used to update the array. +# "port": Listen port of this peer. +# "user": User name on this IP. +# "uuid": UUID for this peer. +# "refresh": Do we need to redraw this entry? +# +# In addition, it contains all the properties of the C++ peer list: # "type": Either "pending" or "known", which is confusing. # "ip": IP address of this peer. -# "user": User name on this IP. -# "refresh": Do we need to redraw this entry? var peerArray: Array -func _http_request_completed(result, response_code, headers, body): - print("HTTP: ", result, " ", response_code) - +func _peer_connected(id): + print("Peer %d connected" % id) + + +func _peer_disconnected(id): + print("Peer %d disconnected" % id) + func _process(_delta): pass func _ready(): - # DEBUG - See if using the VPN fixes our peer removal issue. - _client = HTTPRequest.new() - add_child(_client) - _client.request_completed.connect(self._http_request_completed) clear() - + multiplayer.peer_connected.connect(self._peer_connected) + multiplayer.peer_disconnected.connect(self._peer_disconnected) + func _sort_by_ip(a, b): - #***TODO*** Sort numerically against last three digits. - if a["ip"] < b["ip"]: + # Sort numerically. + if Util.ip_string_to_int(a["ip"]) < Util.ip_string_to_int(b["ip"]): return true return false @@ -42,22 +58,18 @@ func clear(): func start_server(address, port, userInfo): + _basePort = port _userInfo = userInfo - if _server == null: - _server = HttpServer.new() - _server.port = port - _server.bind_address = address - _server.server_identifier = "HamNCheese" - _server.register_router("/", Router.new()) - add_child(_server) - _server.start() + _enet.create_mesh(Util.ip_string_to_int(_userInfo["ip"])) + multiplayer.set_multiplayer_peer(_enet) + _userInfo["port"] = _basePort + _userInfo["ip"].get_slice(".", 3).to_int() + var conn = ENetConnection.new() + conn.create_host_bound(_userInfo["ip"], _userInfo["port"], 256, CHANNELS_MAX) func stop_server(): - if _server != null: - _server.stop() - _server = null - + multiplayer.set_multiplayer_peer(null) + func update(peersFromCPP: Array): var found @@ -84,9 +96,15 @@ func update(peersFromCPP: Array): if !found: # We didn't have them. Add needed records. peerCPP["user"] = "" + peerCPP["uuid"] = null peerCPP["online"] = true peerCPP["refresh"] = false changed = true + # Also create the ENet connection to them. + peerCPP["port"] = _basePort + peerCPP["ip"].get_slice(".", 3).to_int() + var conn = ENetConnection.new() + conn.create_host(1) + conn.connect_to_host(peerCPP["ip"], peerCPP["port"], CHANNELS_MAX) # Sometimes the CPP code will return duplicates. Check. # Also check if we need to redraw anyone. found = false @@ -105,9 +123,4 @@ func update(peersFromCPP: Array): print("Peers After: ", peerArray) - # DEBUG - See if using the VPN fixes our peer removal issue. - var error = _client.request("http://" + _server.bind_address + ":" + str(_server.port)) - if error != OK: - push_error("An error occurred in the HTTP request.") - return changed diff --git a/hamncheese/router.gd b/hamncheese/router.gd deleted file mode 100644 index cf030c4..0000000 --- a/hamncheese/router.gd +++ /dev/null @@ -1,32 +0,0 @@ -extends HttpRouter -class_name Router - - -# Handle a GET request -func handle_get(_request: HttpRequest, _response: HttpResponse): - _response.send(200, "Hello! from GET") - - -# Handle a POST request -func handle_post(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(200, JSON.stringify({ - message = "Hello! from POST", - raw_body = _request.body, - parsed_body = _request.get_body_parsed(), - params = _request.query - }), "application/json") - - -# Handle a PUT request -func handle_put(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(200, "Hello! from PUT") - - -# Handle a PATCH request -func handle_patch(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(200, "Hello! from PATCH") - - -# Handle a DELETE request -func handle_delete(_request: HttpRequest, _response: HttpResponse) -> void: - _response.send(200, "Hello! from DELETE") diff --git a/hamncheese/util.gd b/hamncheese/util.gd index d2019b2..299fb3c 100644 --- a/hamncheese/util.gd +++ b/hamncheese/util.gd @@ -45,3 +45,12 @@ func delete_children(node): for n in node.get_children(): node.remove_child(n) n.queue_free() + + +func ip_string_to_int(ip: String): + var result = 0 + result += ip.get_slice(".", 0).to_int() * 256 * 256 * 256 + result += ip.get_slice(".", 1).to_int() * 256 * 256 + result += ip.get_slice(".", 2).to_int() * 256 + result += ip.get_slice(".", 3).to_int() + return result diff --git a/modules/n2nvpn/SCsub b/modules/n2nvpn/SCsub index 6e7c4f9..d29807b 100644 --- a/modules/n2nvpn/SCsub +++ b/modules/n2nvpn/SCsub @@ -9,51 +9,55 @@ env_n2nvpn.add_source_files(env.modules_sources, "*.cpp") env_n2nvpn.Append(CPPPATH=[".", "n2n", "n2n/include"]) # libn2n. +n2n_dir = "#../modules/n2nvpn/n2n/src/" n2n_src = [ - "n2n/src/aes.c", - "n2n/src/auth.c", - "n2n/src/cc20.c", - "n2n/src/curve25519.c", - "n2n/src/edge_management.c", - "n2n/src/edge_utils.c", - "n2n/src/header_encryption.c", - "n2n/src/hexdump.c", - "n2n/src/json.c", - "n2n/src/management.c", - "n2n/src/minilzo.c", - "n2n/src/n2n.c", - "n2n/src/n2n_port_mapping.c", - "n2n/src/n2n_regex.c", - "n2n/src/network_traffic_filter.c", - "n2n/src/pearson.c", - "n2n/src/random_numbers.c", - "n2n/src/sn_management.c", - "n2n/src/sn_selection.c", - "n2n/src/sn_utils.c", - "n2n/src/speck.c", - "n2n/src/tf.c", - "n2n/src/transform_aes.c", - "n2n/src/transform_cc20.c", - "n2n/src/transform_lzo.c", - "n2n/src/transform_null.c", - "n2n/src/transform_speck.c", - "n2n/src/transform_tf.c", - "n2n/src/transform_zstd.c", - "n2n/src/tuntap_freebsd.c", - "n2n/src/tuntap_linux.c", - "n2n/src/tuntap_netbsd.c", - "n2n/src/tuntap_osx.c", - "n2n/src/wire.c" + "aes.c", + "auth.c", + "cc20.c", + "curve25519.c", + "edge_management.c", + "edge_utils.c", + "header_encryption.c", + "hexdump.c", + "json.c", + "management.c", + "minilzo.c", + "n2n.c", + "n2n_port_mapping.c", + "n2n_regex.c", + "network_traffic_filter.c", + "pearson.c", + "random_numbers.c", + "sn_management.c", + "sn_selection.c", + "sn_utils.c", + "speck.c", + "tf.c", + "transform_aes.c", + "transform_cc20.c", + "transform_lzo.c", + "transform_null.c", + "transform_speck.c", + "transform_tf.c", + "transform_zstd.c", + "tuntap_freebsd.c", + "tuntap_linux.c", + "tuntap_netbsd.c", + "tuntap_osx.c", + "wire.c" ] +n2n_src = [n2n_dir + file for file in n2n_src] env_n2nvpn.add_source_files(env.modules_sources, n2n_src) if env["platform"] == "windows": + n2n_dir_win = "#n2n/src/win32/" n2n_src_win = [ - "n2n/src/win32/edge_utils_win32.c", - "n2n/src/win32/getopt1.c", - "n2n/src/win32/getopt.c", - "n2n/src/win32/wintap.c" + "edge_utils_win32.c", + "getopt1.c", + "getopt.c", + "wintap.c" ] + n2n_src_win = [n2n_dir_win + file for file in n2n_src_win] env_n2nvpn.add_source_files(env.modules_sources, n2n_src_win) env_n2nvpn.Append(CPPPATH=["n2n/src"]) diff --git a/modules/n2nvpn/n2n/include/n2n_define.h b/modules/n2nvpn/n2n/include/n2n_define.h index 12c5dc9..26f1bdf 100644 --- a/modules/n2nvpn/n2n/include/n2n_define.h +++ b/modules/n2nvpn/n2n/include/n2n_define.h @@ -210,12 +210,12 @@ enum skip_add {SN_ADD = 0, SN_ADD_SKIP = 1, SN_ADD_ADDED = 2}; #define N2N_TRANSFORM_ID_USER_START 64 #define N2N_TRANSFORM_ID_MAX 65535 -#ifndef max -#define max(a, b) (((a) < (b)) ? (b) : (a)) +#ifndef n2max +#define n2max(a, b) (((a) < (b)) ? (b) : (a)) #endif -#ifndef min -#define min(a, b) (((a) >(b)) ? (b) : (a)) +#ifndef n2min +#define n2min(a, b) (((a) >(b)) ? (b) : (a)) #endif #endif diff --git a/modules/n2nvpn/n2n/src/edge_utils.c b/modules/n2nvpn/n2n/src/edge_utils.c index 4afdb64..1943987 100644 --- a/modules/n2nvpn/n2n/src/edge_utils.c +++ b/modules/n2nvpn/n2n/src/edge_utils.c @@ -2145,7 +2145,7 @@ void edge_send_packet2net (n2n_edge_t * eee, if(eee->conf.header_encryption == HEADER_ENCRYPTION_ENABLED) // in case of user-password auth, also encrypt the iv of payload assuming ChaCha20 and SPECK having the same iv size - packet_header_encrypt(pktbuf, headerIdx + (NULL != eee->conf.shared_secret) * min(idx - headerIdx, N2N_SPECK_IVEC_SIZE), idx, + packet_header_encrypt(pktbuf, headerIdx + (NULL != eee->conf.shared_secret) * n2min(idx - headerIdx, N2N_SPECK_IVEC_SIZE), idx, eee->conf.header_encryption_ctx_dynamic, eee->conf.header_iv_ctx_dynamic, time_stamp()); @@ -2289,9 +2289,9 @@ void process_udp (n2n_edge_t *eee, const struct sockaddr *sender_sock, const SOC // check static now (very likely to be REGISTER_SUPER_ACK, REGISTER_SUPER_NAK or invalid) if(eee->conf.shared_secret) { // hash the still encrypted packet to eventually be able to check it later (required for REGISTER_SUPER_ACK with user/pw auth) - pearson_hash_128(hash_buf, udp_buf, max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN)); + pearson_hash_128(hash_buf, udp_buf, n2max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN)); } - header_enc = packet_header_decrypt(udp_buf, max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN), + header_enc = packet_header_decrypt(udp_buf, n2max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN), (char *)eee->conf.community_name, eee->conf.header_encryption_ctx_static, eee->conf.header_iv_ctx_static, &stamp); @@ -2918,19 +2918,19 @@ int run_edge_loop (n2n_edge_t *eee) { if(eee->sock >= 0) { FD_SET(eee->sock, &socket_mask); - max_sock = max(eee->sock, eee->udp_mgmt_sock); + max_sock = n2max(eee->sock, eee->udp_mgmt_sock); } #ifndef SKIP_MULTICAST_PEERS_DISCOVERY if((eee->conf.allow_p2p) && (eee->conf.preferred_sock.family == (uint8_t)AF_INVALID)) { FD_SET(eee->udp_multicast_sock, &socket_mask); - max_sock = max(eee->sock, eee->udp_multicast_sock); + max_sock = n2max(eee->sock, eee->udp_multicast_sock); } #endif #ifndef _WIN32 FD_SET(eee->device.fd, &socket_mask); - max_sock = max(max_sock, eee->device.fd); + max_sock = n2max(max_sock, eee->device.fd); #endif wait_time.tv_sec = (eee->sn_wait) ? (SOCKET_TIMEOUT_INTERVAL_SECS / 10 + 1) : (SOCKET_TIMEOUT_INTERVAL_SECS); diff --git a/modules/n2nvpn/n2n/src/sn_utils.c b/modules/n2nvpn/n2n/src/sn_utils.c index 13ac267..13953d8 100644 --- a/modules/n2nvpn/n2n/src/sn_utils.c +++ b/modules/n2nvpn/n2n/src/sn_utils.c @@ -1630,8 +1630,8 @@ static int process_udp (n2n_sn_t * sss, header_enc = 2; } if(!header_enc) { - pearson_hash_128(hash_buf, udp_buf, max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN)); - header_enc = packet_header_decrypt(udp_buf, max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN), comm->community, + pearson_hash_128(hash_buf, udp_buf, n2max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN)); + header_enc = packet_header_decrypt(udp_buf, n2max(0, (int)udp_size - (int)N2N_REG_SUP_HASH_CHECK_LEN), comm->community, comm->header_encryption_ctx_static, comm->header_iv_ctx_static, &stamp); } @@ -1768,7 +1768,7 @@ static int process_udp (n2n_sn_t * sss, if(comm->header_encryption == HEADER_ENCRYPTION_ENABLED) { // in case of user-password auth, also encrypt the iv of payload assuming ChaCha20 and SPECK having the same iv size - packet_header_encrypt(rec_buf, oldEncx + (NULL != comm->allowed_users) * min(encx - oldEncx, N2N_SPECK_IVEC_SIZE), encx, + packet_header_encrypt(rec_buf, oldEncx + (NULL != comm->allowed_users) * n2min(encx - oldEncx, N2N_SPECK_IVEC_SIZE), encx, comm->header_encryption_ctx_dynamic, comm->header_iv_ctx_dynamic, time_stamp()); } @@ -1783,7 +1783,7 @@ static int process_udp (n2n_sn_t * sss, if(comm->header_encryption == HEADER_ENCRYPTION_ENABLED) { // in case of user-password auth, also encrypt the iv of payload assuming ChaCha20 and SPECK having the same iv size - packet_header_encrypt(rec_buf, idx + (NULL != comm->allowed_users) * min(encx - idx, N2N_SPECK_IVEC_SIZE), encx, + packet_header_encrypt(rec_buf, idx + (NULL != comm->allowed_users) * n2min(encx - idx, N2N_SPECK_IVEC_SIZE), encx, comm->header_encryption_ctx_dynamic, comm->header_iv_ctx_dynamic, time_stamp()); } diff --git a/modules/n2nvpn/n2nvpn.cpp b/modules/n2nvpn/n2nvpn.cpp index 5828be1..d72d453 100644 --- a/modules/n2nvpn/n2nvpn.cpp +++ b/modules/n2nvpn/n2nvpn.cpp @@ -1,10 +1,10 @@ #include #include -#include "stddclmr.h" #include "n2nvpn.h" extern "C" { + #include "stddclmr.h" #include "random_numbers.h" #include "sn_selection.h" #include "pearson.h" @@ -77,26 +77,53 @@ Array N2NVPN::get_peers() { in_addr_t net; peer_info_t *peer; peer_info_t *tempPeer; + peer_info_t *peerList; Array results; int x; + macstr_t macBuf; + char *typeName; + char typeKnown[] = "known"; + char typePending[] = "pending"; - HASH_ITER(hh, _eee->pending_peers, peer, tempPeer) { - if (peer->dev_addr.net_addr != 0) { - net = htonl(peer->dev_addr.net_addr); - Dictionary entry; - entry[Variant("type")] = Variant("pending"); - entry[Variant("ip")] = Variant(inet_ntoa(*(struct in_addr *)&net)); - results.append(entry); - } - } - HASH_ITER(hh, _eee->known_peers, peer, tempPeer) { - if (peer->dev_addr.net_addr != 0) { - net = htonl(peer->dev_addr.net_addr); - Dictionary entry; - entry[Variant("type")] = Variant("known"); - entry[Variant("ip")] = Variant(inet_ntoa(*(struct in_addr *)&net)); - results.append(entry); + // First pass, use these values: + peerList = _eee->pending_peers; + typeName = typePending; + + // Run two passes, each over a different peer list. + for (x=0; x<2; x++) { + HASH_ITER(hh, peerList, peer, tempPeer) { + if (peer->dev_addr.net_addr != 0) { + net = htonl(peer->dev_addr.net_addr); + Dictionary entry; + entry[Variant("mac_addr")] = Variant(macaddr_str(macBuf, peer->mac_addr)); + // dev_addr + // dev_desc + // sock + // socket_fd + // preferred_sock + // last_cookie + // auth + entry[Variant("timeout")] = Variant(peer->timeout); + entry[Variant("purgeable")] = Variant(peer->purgeable); + entry[Variant("last_seen")] = Variant(peer->last_seen); + entry[Variant("last_p2p")] = Variant(peer->last_p2p); + entry[Variant("last_sent_query")] = Variant(peer->last_sent_query); + // selection_criterion + entry[Variant("last_valid_time_stamp")] = Variant(peer->last_valid_time_stamp); + // ip_addr + entry[Variant("local")] = Variant(peer->local); + entry[Variant("uptime")] = Variant(peer->uptime); + // version + entry[Variant("type")] = Variant(typeName); + entry[Variant("ip")] = Variant(inet_ntoa(*(struct in_addr *) &net)); + + results.append(entry); + } } + + // Second pass, use these values: + peerList = _eee->known_peers; + typeName = typeKnown; } return results; @@ -111,7 +138,7 @@ void N2NVPN::reset_configuration() { _conf.disable_pmtu_discovery = 1; // Whether to disable the path MTU discovery _conf.drop_multicast = 0; // Whether to disable multicast _conf.tuntap_ip_mode = TUNTAP_IP_MODE_SN_ASSIGN; // How to set the IP address - _conf.local_port = 0; // What port to use (0 = any port) + _conf.local_port = INADDR_ANY; // What port to use (0 = any port) _conf.mgmt_port = N2N_EDGE_MGMT_PORT; // Edge management port (5644 by default) _conf.register_interval = 1; // Interval for both UDP NAT hole punching and supernode registration _conf.register_ttl = 1; // Interval for UDP NAT hole punching through supernode