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:
parent
e622341f95
commit
eb240ff4f2
12 changed files with 830 additions and 100 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
output.log
|
||||
bin/
|
||||
hamncheese/export_presets.cfg
|
||||
temp/
|
||||
|
|
22
hamncheese/httpd/LICENSE.md
Normal file
22
hamncheese/httpd/LICENSE.md
Normal 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.
|
||||
|
44
hamncheese/httpd/README.md
Normal file
44
hamncheese/httpd/README.md
Normal 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.
|
169
hamncheese/httpd/http_file_router.gd
Normal file
169
hamncheese/httpd/http_file_router.gd
Normal 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
|
53
hamncheese/httpd/http_request.gd
Normal file
53
hamncheese/httpd/http_request.gd
Normal 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})
|
170
hamncheese/httpd/http_response.gd
Normal file
170
hamncheese/httpd/http_response.gd
Normal 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 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 
|
||||
# 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
|
32
hamncheese/httpd/http_router.gd
Normal file
32
hamncheese/httpd/http_router.gd
Normal 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")
|
243
hamncheese/httpd/http_server.gd
Normal file
243
hamncheese/httpd/http_server.gd
Normal 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
|
|
@ -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
|
||||
|
|
32
hamncheese/router.gd
Normal file
32
hamncheese/router.gd
Normal 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")
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
# Linux Support
|
||||
apt-get -y install \
|
||||
autotools \
|
||||
build-essential \
|
||||
scons \
|
||||
pkg-config \
|
||||
|
|
15
supernode.sh
Executable file
15
supernode.sh
Executable 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
|
Loading…
Add table
Reference in a new issue