2025-08-09 14:03:46 +03:00
|
|
|
class_name LuaNetworkUtils
|
|
|
|
|
extends RefCounted
|
|
|
|
|
|
|
|
|
|
static func setup_network_api(vm: LuauVM):
|
|
|
|
|
vm.lua_pushcallable(_lua_fetch_handler, "fetch")
|
|
|
|
|
vm.lua_setglobal("fetch")
|
|
|
|
|
|
|
|
|
|
static func _lua_fetch_handler(vm: LuauVM) -> int:
|
|
|
|
|
var url: String = vm.luaL_checkstring(1)
|
|
|
|
|
var options: Dictionary = {}
|
|
|
|
|
|
|
|
|
|
if vm.lua_gettop() >= 2 and vm.lua_istable(2):
|
|
|
|
|
options = vm.lua_todictionary(2)
|
|
|
|
|
|
|
|
|
|
# Default options
|
|
|
|
|
var method = options.get("method", "GET").to_upper()
|
|
|
|
|
var headers = options.get("headers", {})
|
|
|
|
|
var body = options.get("body", "")
|
|
|
|
|
|
|
|
|
|
# Set request options
|
|
|
|
|
var headers_array: PackedStringArray = []
|
2025-08-09 20:39:04 +03:00
|
|
|
|
|
|
|
|
var has_user_agent = false
|
2025-08-09 14:03:46 +03:00
|
|
|
for header_name in headers:
|
2025-08-09 20:39:04 +03:00
|
|
|
if str(header_name).to_lower() == "user-agent":
|
|
|
|
|
has_user_agent = true
|
2025-08-09 14:03:46 +03:00
|
|
|
headers_array.append(str(header_name) + ": " + str(headers[header_name]))
|
|
|
|
|
|
2025-08-09 20:39:04 +03:00
|
|
|
if not has_user_agent:
|
|
|
|
|
headers_array.append("User-Agent: " + UserAgent.get_user_agent())
|
|
|
|
|
|
2025-08-09 14:03:46 +03:00
|
|
|
var response_data = make_http_request(url, method, headers_array, body)
|
|
|
|
|
|
|
|
|
|
# Create response object with actual data
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
|
|
|
|
|
# Add response properties
|
|
|
|
|
vm.lua_pushinteger(response_data.status)
|
|
|
|
|
vm.lua_setfield(-2, "status")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushstring(response_data.status_text)
|
|
|
|
|
vm.lua_setfield(-2, "statusText")
|
|
|
|
|
|
|
|
|
|
# Convert response headers to table
|
|
|
|
|
vm.lua_newtable()
|
|
|
|
|
for header_name in response_data.headers:
|
|
|
|
|
vm.lua_pushstring(response_data.headers[header_name])
|
|
|
|
|
vm.lua_setfield(-2, header_name.to_lower())
|
|
|
|
|
vm.lua_setfield(-2, "headers")
|
|
|
|
|
|
|
|
|
|
# Store response body
|
|
|
|
|
vm.lua_pushstring(response_data.body)
|
|
|
|
|
vm.lua_setfield(-2, "_response_body")
|
|
|
|
|
|
|
|
|
|
# Add response methods
|
|
|
|
|
vm.lua_pushcallable(_response_text_handler, "response.text")
|
|
|
|
|
vm.lua_setfield(-2, "text")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_response_json_handler, "response.json")
|
|
|
|
|
vm.lua_setfield(-2, "json")
|
|
|
|
|
|
|
|
|
|
vm.lua_pushcallable(_response_ok_handler, "response.ok")
|
|
|
|
|
vm.lua_setfield(-2, "ok")
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func _response_text_handler(vm: LuauVM) -> int:
|
|
|
|
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
|
|
|
|
|
|
|
|
|
vm.lua_getfield(1, "_response_body")
|
|
|
|
|
var response_text = vm.lua_tostring(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
vm.lua_pushstring(response_text)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func _response_json_handler(vm: LuauVM) -> int:
|
|
|
|
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
|
|
|
|
|
|
|
|
|
vm.lua_getfield(1, "_response_body")
|
|
|
|
|
var response_text = vm.lua_tostring(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
var json = JSON.new()
|
|
|
|
|
var parse_result = json.parse(response_text)
|
|
|
|
|
|
|
|
|
|
if parse_result == OK:
|
|
|
|
|
vm.lua_pushvariant(json.data)
|
|
|
|
|
else:
|
|
|
|
|
vm.luaL_error("Invalid JSON in response")
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func _response_ok_handler(vm: LuauVM) -> int:
|
|
|
|
|
vm.luaL_checktype(1, vm.LUA_TTABLE)
|
|
|
|
|
|
|
|
|
|
vm.lua_getfield(1, "status")
|
|
|
|
|
var status = vm.lua_tointeger(-1)
|
|
|
|
|
vm.lua_pop(1)
|
|
|
|
|
|
|
|
|
|
vm.lua_pushboolean(status >= 200 and status < 300)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
static func make_http_request(url: String, method: String, headers: PackedStringArray, body: String) -> Dictionary:
|
|
|
|
|
var http_client = HTTPClient.new()
|
|
|
|
|
var response_data = {
|
|
|
|
|
"status": 0,
|
|
|
|
|
"status_text": "Network Error",
|
|
|
|
|
"headers": {},
|
|
|
|
|
"body": ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Parse URL
|
|
|
|
|
var url_parts = url.split("://")
|
|
|
|
|
if url_parts.size() < 2:
|
|
|
|
|
response_data.status = 400
|
|
|
|
|
response_data.status_text = "Bad Request - Invalid URL"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
var protocol = url_parts[0].to_lower()
|
|
|
|
|
var use_ssl = (protocol == "https" or protocol == "gurt+ssl")
|
|
|
|
|
var remaining_url = url_parts[1]
|
|
|
|
|
|
|
|
|
|
var host_path = remaining_url.split("/", false, 1)
|
|
|
|
|
var host = host_path[0]
|
|
|
|
|
var path = "/" + (host_path[1] if host_path.size() > 1 else "")
|
|
|
|
|
|
|
|
|
|
# Extract port if specified
|
|
|
|
|
var port = 80
|
|
|
|
|
if use_ssl:
|
|
|
|
|
port = 443
|
|
|
|
|
|
|
|
|
|
var host_parts = host.split(":")
|
|
|
|
|
if host_parts.size() > 1:
|
|
|
|
|
host = host_parts[0]
|
|
|
|
|
port = host_parts[1].to_int()
|
|
|
|
|
|
|
|
|
|
# Connect to host
|
|
|
|
|
var error: Error
|
|
|
|
|
if use_ssl:
|
|
|
|
|
var tls_options = TLSOptions.client()
|
|
|
|
|
error = http_client.connect_to_host(host, port, tls_options)
|
|
|
|
|
else:
|
|
|
|
|
error = http_client.connect_to_host(host, port)
|
|
|
|
|
if error != OK:
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Connection Failed"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
# Wait for connection
|
|
|
|
|
var timeout_count = 0
|
|
|
|
|
while http_client.get_status() == HTTPClient.STATUS_CONNECTING or http_client.get_status() == HTTPClient.STATUS_RESOLVING:
|
|
|
|
|
http_client.poll()
|
|
|
|
|
OS.delay_msec(10)
|
|
|
|
|
timeout_count += 1
|
|
|
|
|
if timeout_count > 300: # 3 second timeout
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Connection Timeout"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
if http_client.get_status() != HTTPClient.STATUS_CONNECTED:
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Connection Failed"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
# Convert method string to HTTPClient.Method enum
|
|
|
|
|
var http_method: HTTPClient.Method
|
|
|
|
|
match method:
|
|
|
|
|
"GET":
|
|
|
|
|
http_method = HTTPClient.METHOD_GET
|
|
|
|
|
"POST":
|
|
|
|
|
http_method = HTTPClient.METHOD_POST
|
|
|
|
|
"PUT":
|
|
|
|
|
http_method = HTTPClient.METHOD_PUT
|
|
|
|
|
"DELETE":
|
|
|
|
|
http_method = HTTPClient.METHOD_DELETE
|
|
|
|
|
"HEAD":
|
|
|
|
|
http_method = HTTPClient.METHOD_HEAD
|
|
|
|
|
"OPTIONS":
|
|
|
|
|
http_method = HTTPClient.METHOD_OPTIONS
|
|
|
|
|
"PATCH":
|
|
|
|
|
http_method = HTTPClient.METHOD_PATCH
|
|
|
|
|
_:
|
|
|
|
|
http_method = HTTPClient.METHOD_GET
|
|
|
|
|
|
|
|
|
|
# Make request
|
|
|
|
|
error = http_client.request(http_method, path, headers, body)
|
|
|
|
|
if error != OK:
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Request Failed"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
# Wait for response
|
|
|
|
|
timeout_count = 0
|
|
|
|
|
while http_client.get_status() == HTTPClient.STATUS_REQUESTING:
|
|
|
|
|
http_client.poll()
|
|
|
|
|
OS.delay_msec(10)
|
|
|
|
|
timeout_count += 1
|
|
|
|
|
if timeout_count > 1000: # 10 second timeout
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Request Timeout"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
if http_client.get_status() != HTTPClient.STATUS_BODY and http_client.get_status() != HTTPClient.STATUS_CONNECTED:
|
|
|
|
|
response_data.status = 0
|
|
|
|
|
response_data.status_text = "Request Failed"
|
|
|
|
|
return response_data
|
|
|
|
|
|
|
|
|
|
# Get response
|
|
|
|
|
if http_client.has_response():
|
|
|
|
|
response_data.status = http_client.get_response_code()
|
|
|
|
|
|
|
|
|
|
# Get status text
|
|
|
|
|
match response_data.status:
|
|
|
|
|
200: response_data.status_text = "OK"
|
|
|
|
|
201: response_data.status_text = "Created"
|
|
|
|
|
204: response_data.status_text = "No Content"
|
|
|
|
|
400: response_data.status_text = "Bad Request"
|
|
|
|
|
401: response_data.status_text = "Unauthorized"
|
|
|
|
|
403: response_data.status_text = "Forbidden"
|
|
|
|
|
404: response_data.status_text = "Not Found"
|
|
|
|
|
500: response_data.status_text = "Internal Server Error"
|
|
|
|
|
_: response_data.status_text = "Unknown"
|
|
|
|
|
|
|
|
|
|
# Get response headers
|
|
|
|
|
var response_headers = http_client.get_response_headers_as_dictionary()
|
|
|
|
|
response_data.headers = response_headers
|
|
|
|
|
|
|
|
|
|
# Get response body
|
|
|
|
|
var body_bytes = PackedByteArray()
|
|
|
|
|
timeout_count = 0
|
|
|
|
|
while http_client.get_status() == HTTPClient.STATUS_BODY:
|
|
|
|
|
http_client.poll()
|
|
|
|
|
var chunk = http_client.read_response_body_chunk()
|
|
|
|
|
if chunk.size() > 0:
|
|
|
|
|
body_bytes.append_array(chunk)
|
|
|
|
|
timeout_count = 0
|
|
|
|
|
else:
|
|
|
|
|
OS.delay_msec(10)
|
|
|
|
|
timeout_count += 1
|
|
|
|
|
if timeout_count > 1000: # 10 second timeout for body
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
response_data.body = body_bytes.get_string_from_utf8()
|
|
|
|
|
|
|
|
|
|
http_client.close()
|
|
|
|
|
return response_data
|