diff --git a/.gitignore b/.gitignore index 958a53e..2d971c9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ output.log bin/ hamncheese/export_presets.cfg +temp/ diff --git a/hamncheese/httpd/LICENSE.md b/hamncheese/httpd/LICENSE.md new file mode 100644 index 0000000..85ff935 --- /dev/null +++ b/hamncheese/httpd/LICENSE.md @@ -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. + diff --git a/hamncheese/httpd/README.md b/hamncheese/httpd/README.md new file mode 100644 index 0000000..763abdd --- /dev/null +++ b/hamncheese/httpd/README.md @@ -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. diff --git a/hamncheese/httpd/http_file_router.gd b/hamncheese/httpd/http_file_router.gd new file mode 100644 index 0000000..8a5961e --- /dev/null +++ b/hamncheese/httpd/http_file_router.gd @@ -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 diff --git a/hamncheese/httpd/http_request.gd b/hamncheese/httpd/http_request.gd new file mode 100644 index 0000000..ea1b01c --- /dev/null +++ b/hamncheese/httpd/http_request.gd @@ -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}) diff --git a/hamncheese/httpd/http_response.gd b/hamncheese/httpd/http_response.gd new file mode 100644 index 0000000..4fb280b --- /dev/null +++ b/hamncheese/httpd/http_response.gd @@ -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 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 new file mode 100644 index 0000000..2e92111 --- /dev/null +++ b/hamncheese/httpd/http_router.gd @@ -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") diff --git a/hamncheese/httpd/http_server.gd b/hamncheese/httpd/http_server.gd new file mode 100644 index 0000000..7ccb291 --- /dev/null +++ b/hamncheese/httpd/http_server.gd @@ -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("^(?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/peers.gd b/hamncheese/peers.gd index cb2912a..a745b56 100644 --- a/hamncheese/peers.gd +++ b/hamncheese/peers.gd @@ -1,11 +1,8 @@ extends Node -var _server := UDPServer.new() +var _server: HttpServer = null var _userInfo: Dictionary -var _pendingPackets: Array -var _metadataTimer: Timer -var _lastPeers: Array # This array is of dictionary elements that contain: # "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? 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): - if _server.is_listening(): - # 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() - + pass + func _ready(): 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): #***TODO*** Sort numerically against last three digits. if a["ip"] < b["ip"]: @@ -97,55 +34,66 @@ func clear(): func start_server(address, port, userInfo): _userInfo = userInfo - if !_server.is_listening(): - _server.listen(port, address) - _pendingPackets.clear() + 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() func stop_server(): - if _server.is_listening(): + if _server != null: _server.stop() - + _server = null + -func update(peers: Array): +func update(peersFromCPP: Array): 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? - if Util.deep_equal(peers, _lastPeers): - # Does an existing peer need redrawn? - found = false - for peer in peerArray: - if peer["refresh"] == true: - peer["refresh"] = false - found = true - return found + # Start a new peer list. + peerArray.clear() # Add everyone connected. - for peer in peers: - # Do we have this peer's information? + for peerCPP in peersFromCPP: + # Do we have this peer's information from before? found = false - for oldPeer in peerArray: - if oldPeer["ip"] == peer["ip"]: - print("Updating ", peer) + for oldPeer in oldArray: + if oldPeer["ip"] == peerCPP["ip"]: found = true - peer = oldPeer + peerCPP = oldPeer if !found: # We didn't have them. Add needed records. - peer["user"] = "" - peer["online"] = true - peer["refresh"] = false + peerCPP["user"] = "" + peerCPP["online"] = true + 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. - newArray.append(peer) + peerArray.append(peerCPP) - # Use new array. - peerArray = newArray.duplicate(true) - - # Remember this pass. - _lastPeers = peers.duplicate(true) + # Sort new array. + peerArray.sort_custom(_sort_by_ip) - return true + print("Peers After: ", peerArray) + + return changed diff --git a/hamncheese/router.gd b/hamncheese/router.gd new file mode 100644 index 0000000..cf030c4 --- /dev/null +++ b/hamncheese/router.gd @@ -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") diff --git a/prereqs.sh b/prereqs.sh index 635a9f3..ea4c25d 100755 --- a/prereqs.sh +++ b/prereqs.sh @@ -6,6 +6,7 @@ # Linux Support apt-get -y install \ + autotools \ build-essential \ scons \ pkg-config \ diff --git a/supernode.sh b/supernode.sh new file mode 100755 index 0000000..538b0f1 --- /dev/null +++ b/supernode.sh @@ -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