Peers not being purged corrected but not optimal. HTTP for inter-peer networking replaced with ENet.

This commit is contained in:
Scott Duensing 2023-09-09 19:59:29 -05:00
parent c68ec92614
commit ac145b6d6b
28 changed files with 849 additions and 876 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

3
hamncheese/godot-uuid/.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
*.gd linguist-language=GDScript

2
hamncheese/godot-uuid/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Godot 4+ specific ignores
.godot/

View file

@ -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.

View file

@ -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)

BIN
hamncheese/godot-uuid/logo.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -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

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="logo.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="-780.35684"
inkscape:cy="736.70042"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
showguides="false" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="opacity:1;fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26499999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
id="rect824"
width="39.823158"
height="39.823158"
x="-239.74075"
y="92.405174"
inkscape:export-xdpi="81.639999"
inkscape:export-ydpi="81.639999" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:14.80780602px;line-height:9.254879px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#eeeeee;fill-opacity:1;stroke:#00adb5;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="-226.37508"
y="125.88918"
id="text821"
inkscape:export-xdpi="57.34"
inkscape:export-ydpi="57.34"><tspan
sodipodi:role="line"
id="tspan819"
x="-226.37508"
y="125.88918"
style="fill:#eeeeee;fill-opacity:1;stroke:#00adb5;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">v4</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.24773884px;line-height:9.5298357px;font-family:'Roboto Mono Medium for Powerline';-inkscape-font-specification:'Roboto Mono Medium for Powerline';letter-spacing:0px;word-spacing:0px;fill:#393e46;fill-opacity:1;stroke:#222831;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
x="-237.8987"
y="107.61478"
id="text828"><tspan
sodipodi:role="line"
id="tspan826"
x="-237.8987"
y="107.61478"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Roboto Mono Medium for Powerline';-inkscape-font-specification:'Roboto Mono Medium for Powerline';fill:#393e46;fill-opacity:1;stroke:#222831;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">UUID</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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})

View file

@ -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 responses 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

View file

@ -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")

View file

@ -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("^(?<method>GET|POST|HEAD|PUT|PATCH|DELETE|OPTIONS) (?<path>[^ ]+) HTTP/1.1$")
_header_regex.compile("^(?<key>[\\w-]+): (?<value>(.*))$")
# 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/(?<id>([^/#?]+?))[/#?]?$"
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 "(?<subpath>$|/.*)"
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

View file

@ -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:

View file

@ -1,21 +1,39 @@
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):
@ -23,16 +41,14 @@ func _process(_delta):
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,21 +58,17 @@ 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):
@ -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

View file

@ -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")

View file

@ -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

View file

@ -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"])

View file

@ -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

View file

@ -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);

View file

@ -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());
}

View file

@ -1,10 +1,10 @@
#include <ctime>
#include <sys/time.h>
#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