@tool extends Node ## Provides a simple HTTP Service to serve web stuff class_name HTTPServer ## Key: int | Value: WeakRef(Server) static var _servers : Dictionary = {} class Server extends TCPServer: var _bind_address: String var _port: int var _clients: Array[Client] = [] var _listeners: int signal request_received(client: Client) signal client_connected(client: Client) signal client_disconnected(client: Client) signal client_error_occured(client: Client, error: Error) func _init(bind_address: String, port: int) -> void: _bind_address = bind_address _port = port HTTPServer._servers[_port] = weakref(self) func start_listening() -> void: _listeners += 1 if !is_listening(): var status: Error = listen(_port, _bind_address) Engine.get_main_loop().process_frame.connect(_process) if status != OK: HTTPServer.logError("Could not listen to port %d: %s" % [_port, error_string(status)]) else: HTTPServer.logInfo("{%s:%s} listening" % [ _bind_address, _port ]) else: HTTPServer.logDebug("{%s:%s} already listening" % [ _bind_address, _port ]) func stop_listening() -> void: _listeners -= 1 HTTPServer.logDebug("{%s:%s} listener node detached %s left" % [ _bind_address, _port, _listeners ]) if _listeners <= 0: _stop_server() func _stop_server() -> void: HTTPServer.logInfo("{%s:%s} stop" % [ _bind_address, _port ]) Engine.get_main_loop().process_frame.disconnect(_process) for client in _clients: client.peer.disconnect_from_host() stop() func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: HTTPServer.logInfo("{%s:%s} removed" % [ _bind_address, _port ]) HTTPServer._servers.erase(_port) func _process() -> void: if !is_listening(): return if is_connection_available(): _handle_connect() for client in _clients: _process_request(client) _handle_disconnect(client) func _process_request(client: Client) -> void: var peer := client.peer if peer.get_status() == StreamPeerTCP.STATUS_CONNECTED: var error = peer.poll() if error != OK: HTTPServer.logError("Could not poll client %d: %s" % [_port, error_string(error)]) client_error_occured.emit(client, error) elif peer.get_available_bytes() > 0: request_received.emit(client) func _handle_connect() -> void: var peer := take_connection() var client := Client.new() client.peer = peer _clients.append(client) client_connected.emit(client) HTTPServer.logInfo("{%s:%s} client connected" % [ _bind_address, _port ]) func _handle_disconnect(client: Client) -> void: if client.peer.get_status() != StreamPeerTCP.STATUS_CONNECTED: client_disconnected.emit(client) HTTPServer.logInfo("{%s:%s} client disconnected" % [ _bind_address, _port ]) _clients.erase(client) class Client extends RefCounted: var peer: StreamPeerTCP ## Called when a new request was made signal request_received(client: Client) @export var _port: int @export var _bind_address: String var _server : Server var _listening: bool static func create(port: int, bind_address: String = "*") -> HTTPServer: var server = HTTPServer.new() server._bind_address = bind_address server._port = port return server func _ready() -> void: if _servers.has(_port) && _servers[_port] != null: _server = _servers[_port].get_ref() else: _server = Server.new(_bind_address, _port) _server.request_received.connect(_on_request_received) logInfo("{%s:%s} start" % [ _bind_address, _port ]) func _on_request_received(client: Client) -> void: request_received.emit(client) func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: stop_listening() func start_listening() -> void: _listening = true _server.start_listening() func stop_listening() -> void: if _listening: _listening = false _server.stop_listening() func send_response(client: Client, response_code : String, body : PackedByteArray) -> void: var peer = client.peer peer.put_data(("HTTP/1.1 %s\r\n" % response_code).to_utf8_buffer()) peer.put_data("Server: Godot Engine (Twitcher)\r\n".to_utf8_buffer()) peer.put_data(("Content-Length: %d\r\n"% body.size()).to_utf8_buffer()) peer.put_data("Connection: close\r\n".to_utf8_buffer()) peer.put_data("Content-Type: text/html charset=UTF-8\r\n".to_utf8_buffer()) peer.put_data("\r\n".to_utf8_buffer()) peer.put_data(body) # === LOGGER === static var logger: Dictionary = {} static func set_logger(error: Callable, info: Callable, debug: Callable) -> void: logger.debug = debug logger.info = info logger.error = error static func logDebug(text: String) -> void: if logger.has("debug"): logger.debug.call(text) static func logInfo(text: String) -> void: if logger.has("info"): logger.info.call(text) static func logError(text: String) -> void: if logger.has("error"): logger.error.call(text)