extends RefCounted class_name GurtProtocol const DNS_API_URL = "gurt://localhost:8877" # DNS resolution cache: domain.tld -> IP address static var _dns_cache: Dictionary = {} static func is_gurt_domain(url: String) -> bool: if url.begins_with("gurt://"): return true if not url.contains("://"): var parts = url.split(".") return parts.size() == 2 return false static func parse_gurt_domain(url: String) -> Dictionary: var domain_part = url var path = "/" if url.begins_with("gurt://"): domain_part = url.substr(7) # Extract path from domain_part (e.g., "test.dawg/script.lua" -> "test.dawg" + "/script.lua") var path_start = domain_part.find("/") if path_start != -1: path = domain_part.substr(path_start) domain_part = domain_part.substr(0, path_start) # Check if domain is cached (resolved before) var domain_key = domain_part if _dns_cache.has(domain_key): return { "direct_address": _dns_cache[domain_key], "display_url": domain_part + path, "is_direct": true, "path": path, "full_domain": domain_part } if domain_part.contains(":") or domain_part.begins_with("127.0.0.1") or domain_part.begins_with("localhost") or is_ip_address(domain_part): return { "direct_address": domain_part, "display_url": domain_part + path, "is_direct": true, "path": path, "full_domain": domain_part } var parts = domain_part.split(".") if parts.size() < 2: return {} # Support subdomains (e.g., api.blog.example.com) if parts.size() == 2: return { "name": parts[0], "tld": parts[1], "display_url": domain_part + path, "is_direct": false, "path": path, "full_domain": domain_part, "is_subdomain": false } else: return { "name": parts[parts.size() - 2], # The domain name part "tld": parts[parts.size() - 1], # The TLD part "display_url": domain_part + path, "is_direct": false, "path": path, "full_domain": domain_part, "is_subdomain": true, "subdomain_parts": parts.slice(0, parts.size() - 2) } static func is_ip_address(address: String) -> bool: var parts = address.split(".") if parts.size() != 4: return false for part in parts: if not part.is_valid_int(): return false var num = part.to_int() if num < 0 or num > 255: return false return true static func fetch_domain_info(name: String, tld: String) -> Dictionary: var request_data = JSON.stringify({"name": name, "tld": tld}) var result = await fetch_dns_post_working("localhost:8877", "/resolve", request_data) if result.has("error"): return {"error": result.error} if not result.has("content"): return {"error": "No content in DNS response"} var content_str = result.content.get_string_from_utf8() var json = JSON.new() var parse_result = json.parse(content_str) if parse_result != OK: return {"error": "Invalid JSON in DNS response"} return json.data static func fetch_full_domain_info(full_domain: String, record_type: String = "") -> Dictionary: var request_data = {"domain": full_domain} if not record_type.is_empty(): request_data["record_type"] = record_type var json_data = JSON.stringify(request_data) var result = await fetch_dns_post_working("localhost:8877", "/resolve-full", json_data) if result.has("error"): return {"error": result.error} if not result.has("content"): return {"error": "No content in DNS response"} var content_str = result.content.get_string_from_utf8() var json = JSON.new() var parse_result = json.parse(content_str) if parse_result != OK: return {"error": "Invalid JSON in DNS response"} return json.data static func fetch_dns_post_working(server: String, path: String, json_data: String) -> Dictionary: var shared_result = {"finished": false} var thread = Thread.new() var mutex = Mutex.new() var thread_func = func(): var local_result = {} var client = GurtProtocolClient.new() for ca_cert in CertificateManager.trusted_ca_certificates: client.add_ca_certificate(ca_cert) if not client.create_client(10): local_result = {"error": "Failed to create client"} else: var url = "gurt://" + server + path # Prepare request options var options = { "method": "POST", "headers": {"Content-Type": "application/json"}, "body": json_data } var response = client.request(url, options) client.disconnect() if not response: local_result = {"error": "No response from server"} elif not response.is_success: local_result = {"error": "Server error: " + str(response.status_code) + " " + str(response.status_message)} else: local_result = {"content": response.body} mutex.lock() shared_result.clear() for key in local_result: shared_result[key] = local_result[key] shared_result["finished"] = true mutex.unlock() thread.start(thread_func) # Non-blocking wait while not shared_result.get("finished", false): await Engine.get_main_loop().process_frame thread.wait_to_finish() mutex.lock() var final_result = {} for key in shared_result: if key != "finished": final_result[key] = shared_result[key] mutex.unlock() return final_result static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary: var client = GurtProtocolClient.new() for ca_cert in CertificateManager.trusted_ca_certificates: client.add_ca_certificate(ca_cert) if not client.create_client(30): return {"error": "Failed to create GURT client"} var gurt_url = "gurt://" + ip + ":4878" + path var response = client.request(gurt_url, {"method": "GET"}) client.disconnect() if not response: return {"error": "No response from GURT server"} if not response.is_success: var error_msg = "Server returned status " + str(response.status_code) + ": " + response.status_message return {"error": error_msg} var content = response.body return {"content": content, "headers": response.headers} static func fetch_content_via_gurt_direct(address: String, path: String = "/") -> Dictionary: var shared_result = {"finished": false} var thread = Thread.new() var mutex = Mutex.new() var thread_func = func(): var local_result = {} var client = GurtProtocolClient.new() for ca_cert in CertificateManager.trusted_ca_certificates: client.add_ca_certificate(ca_cert) if not client.create_client(10): local_result = {"error": "Failed to create GURT client"} else: var gurt_url: String if address.contains(":"): gurt_url = "gurt://" + address + path else: gurt_url = "gurt://" + address + ":4878" + path var response = client.request(gurt_url, {"method": "GET"}) client.disconnect() if not response: local_result = {"error": "No response from GURT server"} else: var content = response.body if not response.is_success: var error_msg = "Server returned status " + str(response.status_code) + ": " + response.status_message local_result = {"error": error_msg, "content": content, "headers": response.headers} else: local_result = {"content": content, "headers": response.headers} mutex.lock() shared_result.clear() for key in local_result: shared_result[key] = local_result[key] shared_result["finished"] = true mutex.unlock() thread.start(thread_func) # Non-blocking wait using signals instead of polling while not shared_result.get("finished", false): await Engine.get_main_loop().process_frame # Yield control back to the main thread without blocking delays thread.wait_to_finish() mutex.lock() var final_result = {} for key in shared_result: if key != "finished": final_result[key] = shared_result[key] mutex.unlock() return final_result static func handle_gurt_domain(url: String) -> Dictionary: var parsed = parse_gurt_domain(url) if parsed.is_empty(): return {"error": "Invalid domain format. Use: domain.tld or IP:port", "html": create_error_page("Invalid domain format. Use: domain.tld or IP:port")} var target_address: String var path = parsed.get("path", "/") if parsed.get("is_direct", false): target_address = parsed.direct_address else: var domain_info: Dictionary # Use the new full domain resolution for subdomains if parsed.get("is_subdomain", false): domain_info = await fetch_full_domain_info(parsed.full_domain) else: domain_info = await fetch_domain_info(parsed.name, parsed.tld) if domain_info.has("error"): return {"error": domain_info.error, "html": create_error_page(domain_info.error)} # Process DNS records to find target address var target_result = await resolve_target_address(domain_info, parsed.full_domain) if target_result.has("error"): return {"error": target_result.error, "html": create_error_page(target_result.error)} target_address = target_result.address # Cache the resolved address var domain_key = parsed.full_domain _dns_cache[domain_key] = target_address var content_result = await fetch_content_via_gurt_direct(target_address, path) if content_result.has("error"): var error_msg = "Failed to fetch content from " + target_address + path + " via GURT protocol - " + content_result.error if content_result.has("content") and not content_result.content.is_empty(): return {"html": content_result.content, "display_url": parsed.display_url} return {"error": error_msg, "html": create_error_page(error_msg)} if not content_result.has("content"): var error_msg = "No content received from " + target_address + path return {"error": error_msg, "html": create_error_page(error_msg)} var html_content = content_result.content if html_content.is_empty(): var error_msg = "Empty content received from " + target_address + path return {"error": error_msg, "html": create_error_page(error_msg)} return {"html": html_content, "display_url": parsed.display_url} static func resolve_target_address(domain_info: Dictionary, original_domain: String) -> Dictionary: if not domain_info.has("records") or domain_info.records == null: return {"error": "No DNS records found for domain"} var records = domain_info.records var max_cname_depth = 5 # Prevent infinite CNAME loops var cname_depth = 0 # First pass: Look for direct A/AAAA records var a_records = [] var aaaa_records = [] var cname_records = [] var ns_records = [] for record in records: if not record.has("type") or not record.has("value"): continue match record.type: "A": a_records.append(record.value) "AAAA": aaaa_records.append(record.value) "CNAME": cname_records.append(record.value) "NS": ns_records.append(record.value) # If we have direct A records, use the first one if not a_records.is_empty(): return {"address": a_records[0]} # If we have IPv6 AAAA records and no A records, we need to handle this if not aaaa_records.is_empty() and a_records.is_empty(): return {"error": "Only IPv6 (AAAA) records found, but IPv4 required for GURT protocol"} # Follow CNAME chain if not cname_records.is_empty(): var current_cname = cname_records[0] while cname_depth < max_cname_depth: cname_depth += 1 # Try to resolve the CNAME target var cname_info = await fetch_full_domain_info(current_cname, "A") if cname_info.has("error"): return {"error": "Failed to resolve CNAME target: " + current_cname + " - " + cname_info.error} if not cname_info.has("records") or cname_info.records == null: return {"error": "No records found for CNAME target: " + current_cname} # Look for A records in the CNAME target var found_next_cname = false for record in cname_info.records: if record.has("type") and record.type == "A" and record.has("value"): return {"address": record.value} elif record.has("type") and record.type == "CNAME" and record.has("value"): # Another CNAME, continue the chain current_cname = record.value found_next_cname = true break if not found_next_cname: # No more CNAMEs found, but also no A record return {"error": "CNAME chain ended without A record for: " + current_cname} return {"error": "CNAME chain too deep (max " + str(max_cname_depth) + " levels)"} # If we have NS records, this indicates delegation if not ns_records.is_empty(): return {"error": "Domain is delegated to nameservers: " + str(ns_records) + ". Cannot resolve directly."} return {"error": "No A record found for domain"} static func get_error_type(error_message: String) -> Dictionary: if "DNS server is not responding" in error_message or "Domain not found" in error_message: return {"code": "ERR_NAME_NOT_RESOLVED", "title": "This site can't be reached", "icon": "🌐"} elif "timeout" in error_message.to_lower() or "timed out" in error_message.to_lower(): return {"code": "ERR_CONNECTION_TIMED_OUT", "title": "This site can't be reached", "icon": "⏰"} elif "Failed to fetch" in error_message or "No response" in error_message: return {"code": "ERR_CONNECTION_REFUSED", "title": "This site can't be reached", "icon": "🚫"} elif "Invalid domain format" in error_message: return {"code": "ERR_INVALID_URL", "title": "This page isn't working", "icon": "⚠️"} else: return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": "❌"} static func create_error_page(error_message: String) -> PackedByteArray: var error_info = get_error_type(error_message) return ("""
""" + error_info.icon + """
""" + error_message + """