Switching node query from UDP to HTTP. Added script to build supernode binary. Rewrote peer update again - CPP code is not dropping disconnected peers from the list.

This commit is contained in:
Scott Duensing 2023-08-30 20:05:25 -05:00
parent e622341f95
commit eb240ff4f2
12 changed files with 830 additions and 100 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
output.log output.log
bin/ bin/
hamncheese/export_presets.cfg hamncheese/export_presets.cfg
temp/

View file

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 deep Entertainment
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
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,44 @@
# 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

@ -0,0 +1,169 @@
# 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

@ -0,0 +1,53 @@
# 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

@ -0,0 +1,170 @@
# 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

@ -0,0 +1,32 @@
# 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

@ -0,0 +1,243 @@
# 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

@ -1,11 +1,8 @@
extends Node extends Node
var _server := UDPServer.new() var _server: HttpServer = null
var _userInfo: Dictionary var _userInfo: Dictionary
var _pendingPackets: Array
var _metadataTimer: Timer
var _lastPeers: Array
# This array is of dictionary elements that contain: # This array is of dictionary elements that contain:
# "online": Will be true except when used to update the array. # "online": Will be true except when used to update the array.
@ -15,75 +12,15 @@ var _lastPeers: Array
# "refresh": Do we need to redraw this entry? # "refresh": Do we need to redraw this entry?
var peerArray: Array var peerArray: Array
func _metadataTimerTimeout():
if _server.is_listening():
var method = {}
# Request updated metadata from everyone on the network.
method["method"] = "query"
for peer in peerArray:
_sendTo(peer["ip"], method)
func _process(_delta): func _process(_delta):
if _server.is_listening(): pass
# Handle incoming data.
_server.poll()
if _server.is_connection_available():
var client: PacketPeerUDP = _server.take_connection()
var packet = client.get_packet()
var clientIP = client.get_packet_ip()
var raw = packet.get_string_from_utf8()
var jsonIn = JSON.new()
var error = jsonIn.parse(raw)
# DEBUG
print("Raw: ", raw)
if error == OK:
# DEBUG
print("JSON In: ", jsonIn.data)
# Remote node wants our information.
if jsonIn.data["method"] == "query":
var payload = {}
payload["method"] = "queryResponse"
payload["user"] = _userInfo["user"]
client.put_packet(JSON.stringify(payload).to_utf8_buffer())
# Remote node replied to our query.
if jsonIn.data["method"] == "queryResponse":
for i in range(0, peerArray.size() - 1):
if peerArray[i]["ip"] == clientIP:
peerArray[i]["user"] = jsonIn.data["user"]
peerArray[i]["refresh"] = true
break
client.close()
# Handle outgoing data.
for outbound in _pendingPackets:
# DEBUG
print("Sending: ", outbound)
var destination := PacketPeerUDP.new()
destination.connect_to_host(outbound["ip"], _server.get_local_port())
var error = destination.put_packet(JSON.stringify(outbound["message"]).to_utf8_buffer())
if error:
print("Sending packet error: ", error)
destination.close()
_pendingPackets.clear()
func _ready(): func _ready():
clear() clear()
_metadataTimer = Timer.new()
_metadataTimer.timeout.connect(_metadataTimerTimeout)
Engine.get_main_loop().current_scene.add_child(_metadataTimer)
_metadataTimer.start(5)
func _sendTo(ipAddress, message):
var pending = {}
pending["ip"] = ipAddress
pending["message"] = message
_pendingPackets.append(pending)
func _sort_by_ip(a, b): func _sort_by_ip(a, b):
#***TODO*** Sort numerically against last three digits. #***TODO*** Sort numerically against last three digits.
if a["ip"] < b["ip"]: if a["ip"] < b["ip"]:
@ -97,55 +34,66 @@ func clear():
func start_server(address, port, userInfo): func start_server(address, port, userInfo):
_userInfo = userInfo _userInfo = userInfo
if !_server.is_listening(): if _server == null:
_server.listen(port, address) _server = HttpServer.new()
_pendingPackets.clear() _server.port = port
_server.bind_address = address
_server.server_identifier = "HamNCheese"
_server.register_router("/", Router.new())
add_child(_server)
_server.start()
func stop_server(): func stop_server():
if _server.is_listening(): if _server != null:
_server.stop() _server.stop()
_server = null
func update(peers: Array): func update(peersFromCPP: Array):
var found var found
var newArray: Array = [] var changed = false
var oldArray: Array = []
print("Real peers: ", peers.size()) print("\nPeers Raw: ", peersFromCPP)
print("Peers Before: ", peerArray)
peers.sort_custom(_sort_by_ip) # Remember what we started with.
oldArray = peerArray.duplicate(true)
# Did the peer list change? # Start a new peer list.
if Util.deep_equal(peers, _lastPeers): peerArray.clear()
# Does an existing peer need redrawn?
found = false
for peer in peerArray:
if peer["refresh"] == true:
peer["refresh"] = false
found = true
return found
# Add everyone connected. # Add everyone connected.
for peer in peers: for peerCPP in peersFromCPP:
# Do we have this peer's information? # Do we have this peer's information from before?
found = false found = false
for oldPeer in peerArray: for oldPeer in oldArray:
if oldPeer["ip"] == peer["ip"]: if oldPeer["ip"] == peerCPP["ip"]:
print("Updating ", peer)
found = true found = true
peer = oldPeer peerCPP = oldPeer
if !found: if !found:
# We didn't have them. Add needed records. # We didn't have them. Add needed records.
peer["user"] = "" peerCPP["user"] = ""
peer["online"] = true peerCPP["online"] = true
peer["refresh"] = false peerCPP["refresh"] = false
changed = true
# Sometimes the CPP code will return duplicates. Check.
# Also check if we need to redraw anyone.
found = false
for peer in peerArray:
if peer["ip"] == peerCPP["ip"]:
found = true
if peer["refresh"] == true:
peer["refresh"] = false
changed = true
# Add them to the new list. # Add them to the new list.
newArray.append(peer) peerArray.append(peerCPP)
# Use new array. # Sort new array.
peerArray = newArray.duplicate(true) peerArray.sort_custom(_sort_by_ip)
# Remember this pass.
_lastPeers = peers.duplicate(true)
return true print("Peers After: ", peerArray)
return changed

32
hamncheese/router.gd Normal file
View file

@ -0,0 +1,32 @@
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

@ -6,6 +6,7 @@
# Linux Support # Linux Support
apt-get -y install \ apt-get -y install \
autotools \
build-essential \ build-essential \
scons \ scons \
pkg-config \ pkg-config \

15
supernode.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
# Build supernode for current platform.
mkdir -p bin
pushd modules/n2nvpn/n2n
./autogen.sh
./configure
make supernode
mv supernode ../../../bin/.
make clean
rm include/config.h
popd