192 lines
5.4 KiB
GDScript
192 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]")
|
|
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 HTTP/1.1\r\n" % [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:\n",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"
|