add http server

This commit is contained in:
Lera Elvoé 2023-05-09 18:03:30 +03:00
parent c3d25c3a09
commit c7392c4d96
Signed by: yagich
SSH Key Fingerprint: SHA256:6xjGb6uA7lAVcULa7byPEN//rQ0wPoG+UzYVMfZnbvc
7 changed files with 513 additions and 0 deletions

View File

@ -0,0 +1,162 @@
class_name HTTPServer extends TCPServer
# Public constants
const Method = preload("res://addons/http_server/method.gd")
const Request = preload("res://addons/http_server/request.gd")
const Response = preload("res://addons/http_server/response.gd")
const Status = preload("res://addons/http_server/status.gd")
# Private variables
var __endpoints: Dictionary = {
# key: [Int, String], array with 0 index representing method, 1 index representing endpoint
# value: Callable, reference to function to call
}
var __fallback: Callable
var __server: TCPServer = null
# Public methods
func endpoint(type: int, endpoint: String, function: Callable) -> void:
var endpoint_hash: Array = [type, endpoint]
if endpoint_hash in __endpoints:
print(
"[ERR] Endpoint already defined type: %s, endpoint: %s" % [
Method.type_to_identifier(type),
endpoint,
]
)
return
__endpoints[endpoint_hash] = function
func fallback(function: Callable) -> void:
__fallback = function
func __take_connection() -> StreamPeerTCP:
if !is_listening():
print(
"[ERR] Server is not listening, please initialize and listen before calling `take_connection`"
)
return null
var connection: StreamPeerTCP = take_connection()
if connection:
__process_connection(connection)
return connection
# Private methods
func __process_connection(connection: StreamPeerTCP) -> void:
var content: PackedByteArray = PackedByteArray([])
while true:
var bytes = connection.get_available_bytes()
if bytes == 0:
break
var data = connection.get_partial_data(bytes)
content.append_array(data[1])
if content.is_empty():
return
var content_string: String = content.get_string_from_utf8()
var content_parts: Array = content_string.split("\r\n")
if content_parts.is_empty():
connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8_buffer())
return
var request_line = content_parts[0]
var request_line_parts = request_line.split(" ")
var method: String = request_line_parts[0]
var endpoint: String = request_line_parts[1]
var headers: Dictionary = {}
var header_index: int = content_parts.find("")
if header_index == -1:
print(
"[ERR] Error parsing request data: %s" % [str(content)]
)
connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8_buffer())
return
for i in range(1, header_index):
var header_parts: Array = content_parts[i].split(":", true, 1)
var header = header_parts[0].strip_edges().to_lower()
var value = header_parts[1].strip_edges()
headers[header] = value
var body: String = ""
if header_index != content_parts.size() - 1:
var body_parts: Array = content_parts.slice(header_index + 1, content_parts.size())
body = "\r\n".join(PackedStringArray(body_parts))
var response: Response = __process_request(method, endpoint, headers, body)
connection.put_data(response.to_utf8_buffer())
func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response:
var type: int = Method.description_to_type(method)
var request: Request = Request.new(
type,
endpoint,
headers,
body
)
var endpoint_func: Callable
var endpoint_parts: PackedStringArray = endpoint.split("/", false)
while endpoint_func.is_null():
var endpoint_hash: Array = [type, "/" + "/".join(endpoint_parts)]
if __endpoints.has(endpoint_hash):
endpoint_func = __endpoints[endpoint_hash]
elif endpoint_parts.is_empty():
break
else:
endpoint_parts.remove_at(endpoint_parts.size() - 1)
if endpoint_func.is_null():
print(
"[WRN] Recieved request for unknown endpoint, method: %s, endpoint: %s" % [method, endpoint]
)
if __fallback:
endpoint_func = __fallback
else:
return __response_from_status(Status.NOT_FOUND)
var response: Response = Response.new()
if endpoint_func.is_null():
print(
"[ERR] Callable for endpoint not valid, method: %s, endpoint: %s" % [method, endpoint]
)
else:
print(
"[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint]
)
endpoint_func.call(request, response)
return response
func __response_from_status(code: int) -> Response:
var response: Response = Response.new()
response.status(code)
return response

View File

@ -0,0 +1,61 @@
# Public Constants
enum {
GET = 0,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
PATCH
}
# Private constants
const __DESCRIPTIONS: Dictionary = {
GET: "Get",
HEAD: "Head",
POST: "Post",
PUT: "Put",
DELETE: "Delete",
CONNECT: "Connect",
OPTIONS: "Options",
TRACE: "Trace",
PATCH: "Patch",
}
const __TYPES: Dictionary = {
"GET": GET,
"HEAD": HEAD,
"POST": POST,
"PUT": PUT,
"DELETE": DELETE,
"CONNECT": CONNECT,
"OPTIONS": OPTIONS,
"TRACE": TRACE,
"PATCH": PATCH,
}
# Public methods
static func description_to_type(description: String) -> int:
return identifier_to_type(description.to_upper())
static func identifier_to_type(identifier: String) -> int:
if __TYPES.has(identifier):
return __TYPES[identifier]
return -1
static func type_to_description(type: int) -> String:
return __DESCRIPTIONS[type]
static func type_to_identifier(type: int) -> String:
return type_to_description(type).to_upper()

View File

@ -0,0 +1,7 @@
[plugin]
name="HTTPServer"
description="HTTP Server implementation for receiving HTTP requests from external sources."
author="velopman"
version="0.1"
script="plugin.gd"

View File

@ -0,0 +1,10 @@
@tool
extends EditorPlugin
func _enter_tree() -> void:
pass
func _exit_tree() -> void:
pass

View File

@ -0,0 +1,69 @@
# Public constants
const Method = preload("res://addons/http_server/method.gd")
# Private variables
var __body: String = ""
var __endpoint: String = ""
var __headers: Dictionary = {
# key: String, header name
# value: Variant, header value
}
var __json_data = null # Variant
var __type: int = Method.GET
# Lifecyle methods
func _init(type: int,endpoint: String,headers: Dictionary,body: String):
__body = body
__endpoint = endpoint
__headers = headers
__type = type
# Public methods
func body() -> String:
return __body
func endpoint() -> String:
return __endpoint
func header(name: String = "", default = null): # Variant
return __headers.get(name, default)
func headers() -> Dictionary:
return __headers
func json(): # Variant
if __json_data != null:
return __json_data
var content_type = header("content-type")
if content_type != "application/json":
print(
"[WRN] Attempting to call get_json checked a request with content-type: %s" % [content_type]
)
return null
var json = JSON.new()
var test_json_conv = json.parse(__body)
if test_json_conv != OK:
print(
"[ERR] Error parsing request json at line %s: %s" % [json.get_error_line(), json.get_error_message()]
)
__json_data = json.get_data()
return __json_data
func type() -> int:
return __type

View File

@ -0,0 +1,57 @@
# Public Constants
const Status = preload("res://addons/http_server/status.gd")
# Private variables
var __data = "" # variant
var __headers: Dictionary = {
# key: String, header name
# value: Variant, header value
}
var __status: int = 200
# Public methods
func data(data) -> void: # data: Variant
__data = data
func header(name: String, value) -> void: # value: Variant
__headers[name.to_lower()] = value
func json(data) -> void: # data: Variant
header("content-type", "application/json")
__data = data
func status(status: int) -> void:
__status = status
func to_utf8_buffer() -> PackedByteArray:
var content = PackedStringArray()
content.append(Status.code_to_status_line(__status))
var data = __data
if data.is_empty():
data = Status.code_to_description(__status)
if __headers.get("content-type", "") == "application/json":
data = JSON.stringify(data)
__headers['content-length'] = len(data)
for header in __headers:
content.append("%s: %s" % [header, str(__headers[header])])
content.append("")
if data:
content.append(data)
return "\r\n".join(content).to_utf8_buffer()

View File

@ -0,0 +1,147 @@
# Public constants
enum {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
IM_USED = 226,
MULTIPLE_CHOICE = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
IM_A_TEAPOT = 418,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
TOO_EARLY = 425,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
NOT_EXTENDED = 510,
NETWORK_AUTHENTICATION_REQUIRED = 511,
}
# Private constants
const __DESCRIPTIONS: Dictionary = {
CONTINUE: "Continue",
SWITCHING_PROTOCOLS: "Switching Protocols",
PROCESSING: "Processing",
EARLY_HINTS: "Early Hints",
OK: "Ok",
CREATED: "Created",
ACCEPTED: "Accepted",
NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information",
NO_CONTENT: "No Content",
RESET_CONTENT: "Reset Content",
PARTIAL_CONTENT: "Partial Content",
MULTI_STATUS: "Multi-Status",
ALREADY_REPORTED: "Already Reported",
IM_USED: "IM Used",
MULTIPLE_CHOICE: "Multiple Choice",
MOVED_PERMANENTLY: "Moved Permanently",
FOUND: "Found",
SEE_OTHER: "See Other",
NOT_MODIFIED: "Not Modified",
TEMPORARY_REDIRECT: "Temporary Redirect",
PERMANENT_REDIRECT: "Permanent Redirect",
BAD_REQUEST: "Bad Request",
UNAUTHORIZED: "Unauthorized",
PAYMENT_REQUIRED: "Payment Required",
FORBIDDEN: "Forbidden",
NOT_FOUND: "Not Found",
METHOD_NOT_ALLOWED: "Method Not Allowed",
NOT_ACCEPTABLE: "Not Acceptable",
PROXY_AUTHENTICATION_REQUIRED: "Proxy Authentication Requested",
REQUEST_TIMEOUT: "Request Timeout",
CONFLICT: "Conflict",
GONE: "Gone",
LENGTH_REQUIRED: "Length Required",
PRECONDITION_FAILED: "Precondition Failed",
PAYLOAD_TOO_LARGE: "Payload Too Large",
URI_TOO_LONG: "URI Too long",
UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type",
RANGE_NOT_SATISFIABLE: "Range Not Satisfiable",
EXPECTATION_FAILED: "Expectation Failed",
IM_A_TEAPOT: "I'm A Teapot",
MISDIRECTED_REQUEST: "Misdirected Request",
UNPROCESSABLE_ENTITY: "Unprocessable Entity",
LOCKED: "Locked",
FAILED_DEPENDENCY: "Failed Dependency",
TOO_EARLY: "Too Early",
UPGRADE_REQUIRED: "Upgrade Required",
PRECONDITION_REQUIRED: "Precondition Required",
TOO_MANY_REQUESTS: "Too Many Requests",
REQUEST_HEADER_FIELDS_TOO_LARGE: "Request Header Fields Too Large",
UNAVAILABLE_FOR_LEGAL_REASONS: "Unavailable For Legal Reasons",
INTERNAL_SERVER_ERROR: "Internal Server Error",
NOT_IMPLEMENTED: "Not Implemented",
BAD_GATEWAY: "Bad Gateway",
SERVICE_UNAVAILABLE: "Service Unavailable",
GATEWAY_TIMEOUT: "Gateway Timeout",
HTTP_VERSION_NOT_SUPPORTED: "HTTP Version Not Supported",
VARIANT_ALSO_NEGOTIATES: "Variant Also Negotiates",
INSUFFICIENT_STORAGE: "Insufficient Storage",
LOOP_DETECTED: "Loop detected",
NOT_EXTENDED: "Not Extended",
NETWORK_AUTHENTICATION_REQUIRED: "Network Authentication Required",
}
# Public methods
static func code_to_description(code: int) -> String:
return __DESCRIPTIONS[code]
static func code_to_identifier(code: int) -> String:
return code_to_description(code).to_upper().replace(" ", "_").replace("'", "")
static func code_to_status_line(code: int) -> String:
return "HTTP/1.1 %d %s" % [code, code_to_identifier(code)]