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
|
output.log
|
||||||
bin/
|
bin/
|
||||||
hamncheese/export_presets.cfg
|
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
|
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
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
|
# 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
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