httpclientdebugger/test_node.gd
2025-04-29 22:06:23 -05:00

189 lines
5.4 KiB
GDScript

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]\n")
print_rich("[color=cyan]Headers:\n[/color]")
print_rich("[color=cyan]", JSON.stringify(resp.headers, "\t"), "[/color]\n")
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]\n" % [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]\n" % [host, port])
state = State.REQUESTING
print_rich("[color=green]Sending Request for %s from server %s:%d[/color]\n" % [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]\n" % [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 request_str := "GET %s://%s:%d/%s HTTP/1.1\r\n" % [
"https" if ssl else "http",
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)" % [
version,
OS.get_name()
]
request_str += "Accept: %s\r\n\r\n" % accept
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]\n" % [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"