extends Node2D class_name DebuggableHttpRequest signal request_completed(response: HTTPResponse) @export_category("HTTP Server") @export var host: String = "192.168.1.29" @export var port: int = 80 @export var query: String = "/" @export var ssl: bool = false @export_category("HTTP Headers") @export var user_agent: String = "Godot/4.4.1 (Windows)" @export var accept: String = "*/*" var client: StreamPeerTCP var sclient: StreamPeerTLS var state: State = State.CLOSED var buffer: PackedByteArray var headers: PackedStringArray enum State { CONNECTING, REQUESTING, RESPONSE, ERROR, CLOSED } func _ready() -> void: request() request_completed.connect(func(resp: HTTPResponse) -> void: print_rich("[color=green][b]Request Completed:[/b] Status ", resp.code, "[/color]") print_rich("[color=cyan]Headers:\n[/color]") print_rich("[color=cyan]", JSON.stringify(resp.headers, "\t"), "[/color]") print("Body:") print(resp.body.get_string_from_utf8()) ) func request(path: String = "") -> void: assert(state == State.CLOSED, "A request is already being made.") if path != "": query = path if port == 443: ssl = true print_rich("[color=yellow]Starting connection to %s:%d[/color]" % [host, port]) client = StreamPeerTCP.new() client.connect_to_host(host, port) state = State.CONNECTING func _process(_delta: float) -> void: if not client: return if state == State.CLOSED: if ssl and sclient: sclient.disconnect_from_stream() sclient = null client.disconnect_from_host() client = null return client.poll() if client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.CONNECTING: if ssl: _handle_ssl() return print_rich("[color=green]Connected to %s:%d[/color]" % [host, port]) state = State.REQUESTING print_rich("[color=green]Sending Request for %s from server %s:%d[/color]" % [query, host, port]) _send_request(client) elif client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.REQUESTING: print_rich("[color=green]Reading response from %s:%d[/color]" % [host, port]) state = State.RESPONSE _handle_response(sclient if ssl else client) elif client.get_status() == StreamPeerTCP.Status.STATUS_CONNECTED and state == State.RESPONSE: _handle_response(sclient if ssl else client) elif client.get_status() == StreamPeerTCP.Status.STATUS_ERROR: state = State.ERROR _handle_error(client.get_status()) func _send_request(peer: StreamPeer) -> void: var proto = "http" if ssl: proto = "https" var request_str := "GET %s://%s:%d%s HTTP/1.1\r\n" % [proto, host, port, query] if (ssl and port == 443) or (not ssl and port == 80): request_str += "Host: %s\r\n" % host else: request_str += "Host: %s:%d\r\n" % [host,port] # TODO: Handle Body Length here #"3.1.4.stable.mono.official" var ver := Engine.get_version_info() var version := "%d.%d.%d.%s.%s" % [ ver["major"], ver["minor"], ver["patch"], ver["status"], ver["build"] ] request_str += "User-Agent: GodotEngine/%s (%s)\r\n" % [ version, OS.get_name() ] request_str += "Accept: %s\r\n\r\n" % accept print_rich("[color=cyan]Request Headers:",request_str,"[/color]--EOH--") peer.put_data(request_str.to_utf8_buffer()) func _handle_response(peer: StreamPeer) -> void: peer.poll() if headers.size() == 0: while true: var byte := peer.get_partial_data(1) if byte[0] == OK and byte[1].size() == 0: peer.poll() continue elif byte[0] == OK: buffer.push_back(byte[1][0]) else: state = State.ERROR _handle_error(byte[0]) return if buffer.size() < 4: peer.poll() continue if (buffer[-2] == 10 and buffer[-1] == 10) or \ (buffer[-4] == 13 and buffer[-3] == 10 and buffer[-2] == 13 and buffer[-1] == 10): var data := buffer.get_string_from_utf8() buffer = [] headers = data.replace("\r","").rstrip("\n\n").split("\n") break peer.poll() if headers.size() == 0: return var chunk := peer.get_partial_data(1024) if chunk[0] == OK and chunk[1].size() > 0: buffer.append_array(chunk[1]) else: var resp = HTTPResponse.new(headers, buffer) request_completed.emit(resp) state = State.CLOSED func _handle_error(error: Variant) -> void: print_rich("[color=brickred]Error: %s[/color]" % error) func _handle_ssl() -> void: if sclient == null: print_rich("[color=yellow]SSL Connection requested, negotiating SSL Connection with %s:%d[/color]" % [host,port]) sclient = StreamPeerTLS.new() sclient.connect_to_stream(client, host) sclient.poll() if sclient.get_status() == StreamPeerTLS.Status.STATUS_CONNECTED: print_rich("[color=yellow]SSL Connection established, Sending Request for %s from server %s:%d[/color]" % [query, host, port]) state = State.REQUESTING _send_request(sclient) elif sclient.get_status() == StreamPeerTLS.Status.STATUS_ERROR or \ sclient.get_status() == StreamPeerTLS.Status.STATUS_ERROR_HOSTNAME_MISMATCH: _handle_error(sclient.get_status()) class HTTPResponse: var headers: Dictionary[String, String] = {} var body: PackedByteArray = [] var code: String var message: String func _init(p_headers: PackedStringArray, p_buffer: PackedByteArray) -> void: body = p_buffer for line in p_headers: if line.contains(": "): var parts := line.split(": ") headers[parts[0]] = parts[1] else: # HTTP Intro Line: HTTP/1.1 200 OK var parts := Array(line.split(" ")) if parts.pop_front() == "HTTP/1.1": code = parts.pop_front() message = " ".join(parts) else: code = "INVALID_PROTOCOL"