diff --git a/.gitignore b/.gitignore index e69de29..3e2ae94 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,2 @@ +*target* +*.pem \ No newline at end of file diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..23b976b --- /dev/null +++ b/SPEC.md @@ -0,0 +1,169 @@ +# GURT Protocol Specification + +GURT is a TCP-based application protocol designed as an HTTP-like alternative with built-in TLS 1.3 encryption. + +### Quick Info + +- **HTTP-like syntax** with familiar methods (GET, POST, PUT, DELETE, etc.) +- **Built-in required TLS 1.3 encryption** for secure communication +- **Binary and text data support** +- **Status codes** compatible with HTTP semantics +- **Default port**: 4878 + +### Version + +Current version: **GURT/1.0.0** + +--- + +## Communication + +- **All connections must start with a HANDSHAKE request.** +- After handshake, all further messages are sent over the encrypted TLS 1.3 connection. + +### Message Types + +1. **HANDSHAKE** - Establishes encrypted connection (method: `HANDSHAKE`) +2. **Standard Requests** - `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH` +3. **Responses** - Status code with optional body + +--- + +## Message Format + +### Request Format + +``` +METHOD /path GURT/1.0.0\r\n +header-name: header-value\r\n +content-length: 123\r\n +user-agent: GURT-Client/1.0.0\r\n +\r\n +[message body] +``` + +- **METHOD**: One of `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH`, `HANDSHAKE` +- **Headers**: Lowercase, separated by `:`, terminated by `\r\n` +- **Header separator**: `\r\n` +- **Body separator**: `\r\n\r\n` +- **Content-Length**: Required for all requests with a body +- **User-Agent**: Sent by default by the Rust client + +### Response Format + +``` +GURT/1.0.0 200 OK\r\n +header-name: header-value\r\n +content-length: 123\r\n +server: GURT/1.0.0\r\n +date: Wed, 01 Jan 2020 00:00:00 GMT\r\n +\r\n +[message body] +``` + +- **Status line**: `GURT/1.0.0 ` +- **Headers**: Lowercase, separated by `:`, terminated by `\r\n` +- **Header separator**: `\r\n` +- **Body separator**: `\r\n\r\n` +- **Content-Length**: Required for all responses with a body +- **Server**: Sent by default by the Rust server +- **Date**: RFC 7231 format, sent by default + +### Header Notes + +- All header names are **lowercased** in the protocol implementation. +- Unknown headers are ignored by default. +- Header order is not significant. + +### Status Codes + +- **1xx Informational** + - `101` - Switching Protocols (handshake success) + +- **2xx Success** + - `200` - OK + - `201` - Created + - `202` - Accepted + - `204` - No Content + +- **4xx Client Error** + - `400` - Bad Request + - `401` - Unauthorized + - `403` - Forbidden + - `404` - Not Found + - `405` - Method Not Allowed + - `408` - Timeout + - `413` - Too Large + +- **5xx Server Error** + - `500` - Internal Server Error + - `501` - Not Implemented + - `502` - Bad Gateway + - `503` - Service Unavailable + - `504` - Gateway Timeout + +--- + +## Security + +### TLS 1.3 Handshake + +- **All connections must use TLS 1.3**. +- **ALPN**: `"GURT/1.0"` (see `GURT_ALPN` in code) +- **Handshake**: The first message must be a `HANDSHAKE` request. +- **Server responds** with `101 Switching Protocols` and headers: + - `gurt-version: 1.0.0` + - `encryption: TLS/1.3` + - `alpn: GURT/1.0` + +--- + +## Example Request + +Below is a full example of the TCP communication for a GURT session, including handshake and a POST request/response. + +```py +# Client +HANDSHAKE / GURT/1.0.0\r\n +host: example.com\r\n +user-agent: GURT-Client/1.0.0\r\n +\r\n + +# Server +GURT/1.0.0 101 SWITCHING_PROTOCOLS\r\n +gurt-version: 1.0.0\r\n +encryption: TLS/1.3\r\n +alpn: gurt/1.0\r\n +server: GURT/1.0.0\r\n +date: Wed, 01 Jan 2020 00:00:00 GMT\r\n +\r\n + +# Handshake is now complete; all further messages are encrypted --- + +# Client +POST /api/data GURT/1.0.0\r\n +host: example.com\r\n +content-type: application/json\r\n +content-length: 17\r\n +user-agent: GURT-Client/1.0.0\r\n +\r\n +{"foo":"bar","x":1} + +# Server +GURT/1.0.0 200 OK\r\n +content-type: application/json\r\n +content-length: 16\r\n +server: GURT/1.0.0\r\n +date: Wed, 01 Jan 2020 00:00:00 GMT\r\n +\r\n +{"result":"ok"} +``` + +## Testing + +```bash +cargo test -- --nocapture +``` + +## Get Started +Check the `cli` folder for **Gurty**, a CLI tool to set up your GURT server. \ No newline at end of file diff --git a/dns/config.template.toml b/dns/config.template.toml index 4811d93..151ff8b 100644 --- a/dns/config.template.toml +++ b/dns/config.template.toml @@ -13,15 +13,37 @@ max_connections = 10 [settings] # Available top-level domains tld_list = [ - "mf", "btw", "fr", "yap", "dev", "scam", "zip", "root", - "web", "rizz", "habibi", "sigma", "now", "it", "soy", - "lol", "uwu", "ohio", "cat" + "shit", "based", "delulu", "aura", "pmo", "sucks", "emo", "twin", + "zorp", "clank", "web", "fent", "yeah", "slop", "job", "goat", + "buss", "dawg", "opium", "gang", "ok", "wtf", "lol", "scam", "cat", + "edge", "miku", "dumb", "balls", "yap", "attic", "ayo", "dev", + + # Country code TLDs + "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", + "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", + "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", + "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", + "cn", "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", + "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "eu", "fi", + "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", + "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", + "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", + "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", + "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", + "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mg", "mh", + "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", + "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", + "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", + "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", + "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", + "sm", "sn", "so", "sr", "ss", "st", "su", "sv", "sx", "sy", "sz", "tc", + "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", + "tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve", + "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "zw" ] # Words that are not allowed in domain names -offensive_words = [ - "nigg", "sex", "porn", "igg" -] +offensive_words = [] [discord] # Discord bot token for domain approval notifications diff --git a/dns/migrations/001_initial.sql b/dns/migrations/001_initial.sql index 08535b1..d895b98 100644 --- a/dns/migrations/001_initial.sql +++ b/dns/migrations/001_initial.sql @@ -1,4 +1,3 @@ --- Create users table CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, @@ -8,7 +7,6 @@ CREATE TABLE IF NOT EXISTS users ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Create invite codes table CREATE TABLE IF NOT EXISTS invite_codes ( id SERIAL PRIMARY KEY, code VARCHAR(32) UNIQUE NOT NULL, @@ -18,7 +16,6 @@ CREATE TABLE IF NOT EXISTS invite_codes ( used_at TIMESTAMP ); --- Create domains table CREATE TABLE IF NOT EXISTS domains ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, @@ -31,7 +28,6 @@ CREATE TABLE IF NOT EXISTS domains ( UNIQUE(name, tld) ); --- Create indexes for faster lookups CREATE INDEX IF NOT EXISTS idx_domains_name_tld ON domains(name, tld); CREATE INDEX IF NOT EXISTS idx_domains_user_id ON domains(user_id); CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status); diff --git a/dns/migrations/002_remove_email.sql b/dns/migrations/002_remove_email.sql index 9df292c..b325bf9 100644 --- a/dns/migrations/002_remove_email.sql +++ b/dns/migrations/002_remove_email.sql @@ -1,5 +1,3 @@ --- Remove email field from users table ALTER TABLE users DROP COLUMN IF EXISTS email; --- Drop email index if it exists DROP INDEX IF EXISTS idx_users_email; \ No newline at end of file diff --git a/dns/migrations/003_fix_timestamp_types.sql b/dns/migrations/003_fix_timestamp_types.sql index 882a0fd..44b23b8 100644 --- a/dns/migrations/003_fix_timestamp_types.sql +++ b/dns/migrations/003_fix_timestamp_types.sql @@ -1,4 +1,3 @@ --- Fix timestamp columns to use TIMESTAMPTZ instead of TIMESTAMP ALTER TABLE users ALTER COLUMN created_at TYPE TIMESTAMPTZ; ALTER TABLE invite_codes ALTER COLUMN created_at TYPE TIMESTAMPTZ; ALTER TABLE invite_codes ALTER COLUMN used_at TYPE TIMESTAMPTZ; diff --git a/dns/migrations/004_add_domain_invite_codes.sql b/dns/migrations/004_add_domain_invite_codes.sql index ee32ace..da0af0f 100644 --- a/dns/migrations/004_add_domain_invite_codes.sql +++ b/dns/migrations/004_add_domain_invite_codes.sql @@ -1,7 +1,5 @@ --- Add domain_invite_codes field to users table ALTER TABLE users ADD COLUMN domain_invite_codes INTEGER DEFAULT 3; --- Create domain invite codes table for domain-specific invites CREATE TABLE IF NOT EXISTS domain_invite_codes ( id SERIAL PRIMARY KEY, code VARCHAR(32) UNIQUE NOT NULL, @@ -11,7 +9,6 @@ CREATE TABLE IF NOT EXISTS domain_invite_codes ( used_at TIMESTAMPTZ ); --- Create indexes for faster lookups CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_code ON domain_invite_codes(code); CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_created_by ON domain_invite_codes(created_by); CREATE INDEX IF NOT EXISTS idx_domain_invite_codes_used_by ON domain_invite_codes(used_by); \ No newline at end of file diff --git a/dns/migrations/005_add_dns_records.sql b/dns/migrations/005_add_dns_records.sql new file mode 100644 index 0000000..d37a381 --- /dev/null +++ b/dns/migrations/005_add_dns_records.sql @@ -0,0 +1,23 @@ +CREATE TABLE dns_records ( + id SERIAL PRIMARY KEY, + domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + record_type VARCHAR(10) NOT NULL CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV')), + name VARCHAR(255) NOT NULL DEFAULT '@', -- @ for root, or subdomain name + value VARCHAR(1000) NOT NULL, + ttl INTEGER DEFAULT 3600, + priority INTEGER, -- For MX records + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_dns_records_domain_type ON dns_records(domain_id, record_type); +CREATE INDEX idx_dns_records_name ON dns_records(name); + +INSERT INTO dns_records (domain_id, record_type, name, value, ttl) +SELECT id, 'A', '@', ip, 3600 +FROM domains +WHERE status = 'approved'; + +INSERT INTO dns_records (domain_id, record_type, name, value, ttl, priority) +SELECT id, 'SRV', '_gurt._tcp', '0 5 4878 @', 3600, 0 +FROM domains +WHERE status = 'approved'; \ No newline at end of file diff --git a/dns/src/http/models.rs b/dns/src/http/models.rs index 3837bde..ff8347b 100644 --- a/dns/src/http/models.rs +++ b/dns/src/http/models.rs @@ -22,6 +22,22 @@ pub struct Domain { pub(crate) created_at: Option>, } +#[derive(Clone, Debug, Deserialize, Serialize, FromRow)] +pub struct DnsRecord { + #[serde(skip_deserializing)] + pub(crate) id: Option, + pub(crate) domain_id: i32, + #[serde(deserialize_with = "deserialize_lowercase")] + pub(crate) record_type: String, // A, AAAA, CNAME, TXT, MX, NS + #[serde(deserialize_with = "deserialize_lowercase")] + pub(crate) name: String, // subdomain or @ for root + pub(crate) value: String, // IP, domain, text value, etc. + pub(crate) ttl: Option, // Time to live in seconds + pub(crate) priority: Option, // For MX records + #[serde(skip_deserializing)] + pub(crate) created_at: Option>, +} + #[derive(Clone, Debug, Deserialize, Serialize, FromRow)] pub struct User { pub(crate) id: i32, @@ -57,6 +73,16 @@ pub(crate) struct ResponseDomain { pub(crate) tld: String, pub(crate) ip: String, pub(crate) name: String, + pub(crate) records: Option>, +} + +#[derive(Debug, Serialize)] +pub(crate) struct ResponseDnsRecord { + pub(crate) record_type: String, + pub(crate) name: String, + pub(crate) value: String, + pub(crate) ttl: i32, + pub(crate) priority: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/flumi/Scripts/AutoSizingFlexContainer.gd b/flumi/Scripts/AutoSizingFlexContainer.gd index f49c34d..32f8e70 100644 --- a/flumi/Scripts/AutoSizingFlexContainer.gd +++ b/flumi/Scripts/AutoSizingFlexContainer.gd @@ -13,14 +13,14 @@ func _resort() -> void: size_flags_horizontal = Control.SIZE_FILL else: if not has_meta("size_flags_set_by_style_manager"): - size_flags_horizontal = Control.SIZE_SHRINK_CENTER + size_flags_horizontal = Control.SIZE_SHRINK_BEGIN # Check if we should fill vertically (for h-full) if has_meta("should_fill_vertical"): size_flags_vertical = Control.SIZE_FILL else: if not has_meta("size_flags_set_by_style_manager"): - size_flags_vertical = Control.SIZE_SHRINK_CENTER + size_flags_vertical = Control.SIZE_SHRINK_BEGIN if debug_draw: _draw_rects.clear() diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd index 50bf971..c8043fa 100644 --- a/flumi/Scripts/B9/HTMLParser.gd +++ b/flumi/Scripts/B9/HTMLParser.gd @@ -106,7 +106,6 @@ func handle_style_element(style_element: HTMLElement) -> void: var src = style_element.get_attribute("src") if src.length() > 0: # TODO: Handle external CSS loading when Network module is available - print("External CSS not yet supported: " + src) return # Handle inline CSS - we'll get the text content when parsing is complete @@ -119,21 +118,19 @@ func process_styles() -> void: if not parse_result.css_parser: return - # Collect all style element content var css_content = Constants.DEFAULT_CSS var style_elements = find_all("style") for style_element in style_elements: if style_element.get_attribute("src").is_empty(): css_content += style_element.text_content + "\n" - print("Processing CSS: ", css_content) - # Parse CSS if we have any + if css_content.length() > 0: parse_result.css_parser.css_text = css_content parse_result.css_parser.parse() - for child: CSSParser.CSSRule in parse_result.css_parser.stylesheet.rules: - print("INFO: for selector \"%s\" we have props: %s" % [child.selector, child.properties]) func get_element_styles_with_inheritance(element: HTMLElement, event: String = "", visited_elements: Array = []) -> Dictionary: + if !parse_result.css_parser: + return {} # Prevent infinite recursion if element in visited_elements: return {} @@ -335,7 +332,7 @@ func process_fonts() -> void: var weight = font_element.get_attribute("weight", "400") if name_str and src: - FontManager.register_font(name, src, weight) + FontManager.register_font(name_str, src, weight) func get_meta_content(name_: String) -> String: var meta_elements = find_all("meta", "name") @@ -366,9 +363,8 @@ func process_scripts(lua_api: LuaAPI, lua_vm) -> void: if not src.is_empty(): # TODO: add support for external Lua script - print("External script found: ", src) + pass elif not inline_code.is_empty(): - print("Executing inline Lua script") lua_api.execute_lua_script(inline_code, lua_vm) func get_all_stylesheets() -> Array[String]: diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index db9fb60..ed0709f 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -6,7 +6,7 @@ const SECONDARY_COLOR = Color(43/255.0, 43/255.0, 43/255.0, 1) const HOVER_COLOR = Color(0, 0, 0, 1) const DEFAULT_CSS = """ -body { text-base text-[#000000] text-left } +body { text-base text-[#000000] text-left bg-white } h1 { text-5xl font-bold } h2 { text-4xl font-bold } h3 { text-3xl font-bold } diff --git a/flumi/Scripts/GurtProtocol.gd b/flumi/Scripts/GurtProtocol.gd index 5407148..99ae3af 100644 --- a/flumi/Scripts/GurtProtocol.gd +++ b/flumi/Scripts/GurtProtocol.gd @@ -7,32 +7,51 @@ static func is_gurt_domain(url: String) -> bool: if url.begins_with("gurt://"): return true - var parts = url.split(".") - return parts.size() == 2 and not url.contains("://") + if not url.contains("://"): + var parts = url.split(".") + return parts.size() == 2 + + return false static func parse_gurt_domain(url: String) -> Dictionary: - print("Parsing URL: ", url) - var domain_part = url if url.begins_with("gurt://"): - domain_part = url.substr(7) # Remove "gurt://" + domain_part = url.substr(7) + + 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, + "is_direct": true + } var parts = domain_part.split(".") if parts.size() != 2: - print("Invalid domain format: ", domain_part) return {} - print("Parsed domain - name: ", parts[0], ", tld: ", parts[1]) return { "name": parts[0], "tld": parts[1], - "display_url": domain_part + "display_url": domain_part, + "is_direct": false } -static func fetch_domain_info(name: String, tld: String) -> Dictionary: - print("Fetching domain info for: ", name, ".", tld) +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 http_request = HTTPRequest.new() var tree = Engine.get_main_loop() tree.current_scene.add_child(http_request) @@ -53,15 +72,11 @@ static func fetch_domain_info(name: String, tld: String) -> Dictionary: http_request.queue_free() if response[1] == 0 and response[3].size() == 0: - print("DNS API request timed out") return {"error": "DNS server is not responding"} var http_code = response[1] var body = response[3] - print("DNS API response code: ", http_code) - print("DNS API response body: ", body.get_string_from_utf8()) - if http_code != 200: return {"error": "Domain not found or not approved"} @@ -69,65 +84,124 @@ static func fetch_domain_info(name: String, tld: String) -> Dictionary: var parse_result = json.parse(body.get_string_from_utf8()) if parse_result != OK: - print("JSON parse error: ", parse_result) return {"error": "Invalid JSON response from DNS server"} - print("Domain info retrieved: ", json.data) return json.data -static func fetch_index_html(ip: String) -> String: - print("Fetching index.html from IP: ", ip) +static func fetch_content_via_gurt(ip: String, path: String = "/") -> Dictionary: + var client = GurtProtocolClient.new() - var http_request = HTTPRequest.new() - var tree = Engine.get_main_loop() - tree.current_scene.add_child(http_request) + if not client.create_client(30): + return {"error": "Failed to create GURT client"} - http_request.timeout = 5.0 + var gurt_url = "gurt://" + ip + ":4878" + path - var url = "http://" + ip + "/index.html" - print("Fetching from URL: ", url) + var response = client.request(gurt_url, {"method": "GET"}) - var error = http_request.request(url) + client.disconnect() - if error != OK: - print("HTTP request to IP failed with error: ", error) - http_request.queue_free() - return "" + if not response: + return {"error": "No response from GURT server"} - var response = await http_request.request_completed - http_request.queue_free() + if not response.is_success: + var error_msg = "Server returned status " + str(response.status_code) + ": " + response.status_message + return {"error": error_msg} - if response[1] == 0 and response[3].size() == 0: - print("Index.html request timed out") - return "" - - var http_code = response[1] - var body = response[3] - - print("IP response code: ", http_code) - - if http_code != 200: - print("Failed to fetch index.html, HTTP code: ", http_code) - return "" - - var html_content = body.get_string_from_utf8() - print("Successfully fetched HTML content (", html_content.length(), " characters)") - return html_content + var content = response.body + return {"content": content, "headers": response.headers} -static func handle_gurt_domain(url: String) -> Dictionary: - print("Handling GURT domain: ", url) +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() + + 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) + + var finished = false + while not finished: + await Engine.get_main_loop().process_frame + OS.delay_msec(10) + + mutex.lock() + finished = shared_result.get("finished", false) + mutex.unlock() + + 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", "html": create_error_page("Invalid domain format. Use: domain.tld")} + 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 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)} + var target_address: String + var path = "/" - var html_content = await fetch_index_html(domain_info.ip) + if parsed.get("is_direct", false): + target_address = parsed.direct_address + else: + var 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)} + target_address = domain_info.ip + + 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 + " 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 + return {"error": error_msg, "html": create_error_page(error_msg)} + + var html_content = content_result.content if html_content.is_empty(): - var error_msg = "Failed to fetch index.html from " + domain_info.ip + var error_msg = "Empty content received from " + target_address return {"error": error_msg, "html": create_error_page(error_msg)} return {"html": html_content, "display_url": parsed.display_url} @@ -137,7 +211,7 @@ static func get_error_type(error_message: String) -> Dictionary: 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 "HTTP request failed" in error_message: + 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": "⚠️"} diff --git a/flumi/Scripts/Tags/button.gd b/flumi/Scripts/Tags/button.gd index 02a817d..3e31c84 100644 --- a/flumi/Scripts/Tags/button.gd +++ b/flumi/Scripts/Tags/button.gd @@ -45,7 +45,6 @@ func apply_button_styles(element: HTMLParser.HTMLElement, parser: HTMLParser) -> if styles.has("font-size"): var font_size = int(styles["font-size"]) - print("SETTING FONT SIZE: ", font_size, " FOR BUTTON NAME: ", element.tag_name) button_node.add_theme_font_size_override("font_size", font_size) # Apply text color with state-dependent colors diff --git a/flumi/Scripts/Utils/UserAgent.gd b/flumi/Scripts/Utils/UserAgent.gd index 838053f..3bdb65e 100644 --- a/flumi/Scripts/Utils/UserAgent.gd +++ b/flumi/Scripts/Utils/UserAgent.gd @@ -32,5 +32,4 @@ static func get_user_agent() -> String: godot_version.minor, godot_version.patch ] - print(user_agent) return user_agent diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 8a7d53f..14ad07f 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -87,7 +87,7 @@ func _on_search_submitted(url: String) -> void: tab.stop_loading() tab.set_icon(GLOBE_ICON) - var html_bytes = result.html.to_utf8_buffer() + var html_bytes = result.html render_content(html_bytes) if result.has("display_url"): @@ -105,7 +105,6 @@ func _on_search_focus_exited() -> void: if not current_domain.is_empty(): search_bar.text = current_domain - func render() -> void: render_content(Constants.HTML_CONTENT) diff --git a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll new file mode 100644 index 0000000..beac173 Binary files /dev/null and b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll differ diff --git a/flumi/addons/gurt-protocol/gurt_godot.gdextension b/flumi/addons/gurt-protocol/gurt_godot.gdextension new file mode 100644 index 0000000..babeac4 --- /dev/null +++ b/flumi/addons/gurt-protocol/gurt_godot.gdextension @@ -0,0 +1,13 @@ +[configuration] + +entry_symbol = "gdext_rust_init" +compatibility_minimum = 4.1 + +[libraries] + +macos.debug = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib" +macos.release = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib" +windows.debug.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll" +windows.release.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll" +linux.debug.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so" +linux.release.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so" \ No newline at end of file diff --git a/flumi/addons/gurt-protocol/gurt_godot.gdextension.uid b/flumi/addons/gurt-protocol/gurt_godot.gdextension.uid new file mode 100644 index 0000000..7331f55 --- /dev/null +++ b/flumi/addons/gurt-protocol/gurt_godot.gdextension.uid @@ -0,0 +1 @@ +uid://dgbwo0xlp5gya diff --git a/flumi/addons/gurt-protocol/plugin.cfg b/flumi/addons/gurt-protocol/plugin.cfg new file mode 100644 index 0000000..78d49d7 --- /dev/null +++ b/flumi/addons/gurt-protocol/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GURT Protocol" +description="HTTP-like networking extension for Godot games using the GURT protocol" +author="FaceDev" +version="0.1.0" +script="plugin.gd" \ No newline at end of file diff --git a/flumi/addons/gurt-protocol/plugin.gd b/flumi/addons/gurt-protocol/plugin.gd new file mode 100644 index 0000000..fc8d121 --- /dev/null +++ b/flumi/addons/gurt-protocol/plugin.gd @@ -0,0 +1,8 @@ +@tool +extends EditorPlugin + +func _enter_tree(): + print("GURT Protocol plugin enabled") + +func _exit_tree(): + print("GURT Protocol plugin disabled") diff --git a/flumi/addons/gurt-protocol/plugin.gd.uid b/flumi/addons/gurt-protocol/plugin.gd.uid new file mode 100644 index 0000000..1539f5c --- /dev/null +++ b/flumi/addons/gurt-protocol/plugin.gd.uid @@ -0,0 +1 @@ +uid://tt2hh4j8txne diff --git a/flumi/project.godot b/flumi/project.godot index 041210f..26a7e4f 100644 --- a/flumi/project.godot +++ b/flumi/project.godot @@ -29,7 +29,7 @@ window/stretch/aspect="ignore" [editor_plugins] -enabled=PackedStringArray("res://addons/DatePicker/plugin.cfg", "res://addons/SmoothScroll/plugin.cfg", "res://addons/godot-flexbox/plugin.cfg") +enabled=PackedStringArray("res://addons/DatePicker/plugin.cfg", "res://addons/SmoothScroll/plugin.cfg", "res://addons/godot-flexbox/plugin.cfg", "res://addons/gurt-protocol/plugin.cfg") [file_customization] diff --git a/protocol/cli/Cargo.lock b/protocol/cli/Cargo.lock new file mode 100644 index 0000000..a50be58 --- /dev/null +++ b/protocol/cli/Cargo.lock @@ -0,0 +1,1720 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gurt" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "gurty" +version = "0.1.0" +dependencies = [ + "clap", + "colored", + "gurt", + "mime_guess", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/protocol/cli/Cargo.toml b/protocol/cli/Cargo.toml new file mode 100644 index 0000000..994e9a8 --- /dev/null +++ b/protocol/cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "gurty" +version = "0.1.0" +edition = "2021" +authors = ["FaceDev"] +license = "MIT" +repository = "https://github.com/outpoot/gurted" +description = "GURT protocol server CLI tool" + +[[bin]] +name = "gurty" +path = "src/main.rs" + +[dependencies] +gurt = { path = "../library" } + +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1" +tracing-subscriber = "0.3" + +clap = { version = "4.0", features = ["derive"] } +colored = "2.0" +mime_guess = "2.0" \ No newline at end of file diff --git a/protocol/cli/README.md b/protocol/cli/README.md new file mode 100644 index 0000000..94f0d96 --- /dev/null +++ b/protocol/cli/README.md @@ -0,0 +1,56 @@ +# Gurty - a CLI tool to setup your GURT Protocol server + +## Setup for Production + +For production deployments, you'll need to generate your own certificates since traditional Certificate Authorities don't support custom protocols: + +1. **Generate production certificates with OpenSSL:** + ```bash + # Generate private key + openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8 -v + + # Generate certificate signing request + openssl req -new -key gurt-server.key -out gurt-server.csr + + # Generate self-signed certificate (valid for 365 days) + openssl x509 -req -days 365 -in gurt-server.csr -signkey gurt-server.key -out gurt-server.crt + + # Or generate both key and certificate in one step + openssl req -x509 -newkey rsa:4096 -keyout gurt-server.key -out gurt-server.crt -days 365 -nodes + ``` + +2. **Deploy with production certificates:** + ```bash + cargo run --release serve --cert gurt-server.crt --key gurt-server.key --host 0.0.0.0 --port 4878 + ``` + +## Development Environment Setup + +To set up a development environment for GURT, follow these steps: +1. **Install mkcert:** + ```bash + # Windows (with Chocolatey) + choco install mkcert + + # Or download from: https://github.com/FiloSottile/mkcert/releases + ``` + +2. **Install local CA in system:** + ```bash + mkcert -install + ``` + This installs a local CA in your **system certificate store**. + +3. **Generate localhost certificates:** + ```bash + cd gurted/protocol/cli + mkcert localhost 127.0.0.1 ::1 + ``` + This creates: + - `localhost+2.pem` (certificate) + - `localhost+2-key.pem` (private key) + +4. **Start GURT server with certificates:** + ```bash + cargo run --release serve --cert localhost+2.pem --key localhost+2-key.pem + ``` \ No newline at end of file diff --git a/protocol/cli/src/main.rs b/protocol/cli/src/main.rs new file mode 100644 index 0000000..0440288 --- /dev/null +++ b/protocol/cli/src/main.rs @@ -0,0 +1,307 @@ +use clap::{Parser, Subcommand}; +use colored::Colorize; +use gurt::prelude::*; +use std::path::PathBuf; +use tracing::error; +use tracing_subscriber; + +#[derive(Parser)] +#[command(name = "server")] +#[command(about = "GURT Protocol Server")] +#[command(version = "1.0.0")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Serve { + #[arg(short, long, default_value_t = 4878)] + port: u16, + + #[arg(long, default_value = "127.0.0.1")] + host: String, + + #[arg(short, long, default_value = ".")] + dir: PathBuf, + + #[arg(short, long)] + verbose: bool, + + #[arg(long, help = "Path to TLS certificate file")] + cert: Option, + + #[arg(long, help = "Path to TLS private key file")] + key: Option, + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Serve { port, host, dir, verbose, cert, key } => { + if verbose { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .init(); + } else { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .init(); + } + + println!("{}", "GURT Protocol Server".bright_cyan().bold()); + println!("{} {}:{}", "Listening on".bright_blue(), host, port); + println!("{} {}", "Serving from".bright_blue(), dir.display()); + + let server = create_file_server(dir, cert, key)?; + let addr = format!("{}:{}", host, port); + + if let Err(e) = server.listen(&addr).await { + error!("Server error: {}", e); + std::process::exit(1); + } + } + } + + Ok(()) +} + +fn create_file_server(base_dir: PathBuf, cert_path: Option, key_path: Option) -> Result { + let base_dir = std::sync::Arc::new(base_dir); + + let server = match (cert_path, key_path) { + (Some(cert), Some(key)) => { + println!("TLS using certificate: {}", cert.display()); + GurtServer::with_tls_certificates( + cert.to_str().ok_or_else(|| GurtError::invalid_message("Invalid certificate path"))?, + key.to_str().ok_or_else(|| GurtError::invalid_message("Invalid key path"))? + )? + } + (Some(_), None) => { + return Err(GurtError::invalid_message("Certificate provided but no key file specified (use --key)")); + } + (None, Some(_)) => { + return Err(GurtError::invalid_message("Key provided but no certificate file specified (use --cert)")); + } + (None, None) => { + return Err(GurtError::invalid_message("GURT protocol requires TLS encryption. Please provide --cert and --key parameters.")); + } + }; + + let server = server + .get("/", { + let base_dir = base_dir.clone(); + move |ctx| { + let client_ip = ctx.client_ip(); + let base_dir = base_dir.clone(); + async move { + // Try to serve index.html if it exists, otherwise show server info + let index_path = base_dir.join("index.html"); + + if index_path.exists() && index_path.is_file() { + match std::fs::read_to_string(&index_path) { + Ok(content) => { + return Ok(GurtResponse::ok() + .with_header("Content-Type", "text/html") + .with_string_body(content)); + } + Err(_) => { + // Fall through to default page + } + } + } + + // Default server info page + Ok(GurtResponse::ok() + .with_header("Content-Type", "text/html") + .with_string_body(format!(r#" + + + + GURT Protocol Server + + + +

Welcome to the GURT Protocol!

+

This server is successfully running. We couldn't find index.html though :(

+

Protocol: GURT/{}

+

Client IP: {}

+ + + "#, + gurt::GURT_VERSION, + client_ip, + ))) + } + } + }) + .get("/*", { + let base_dir = base_dir.clone(); + move |ctx| { + let base_dir = base_dir.clone(); + let path = ctx.path().to_string(); + async move { + let mut relative_path = path.strip_prefix('/').unwrap_or(&path).to_string(); + // Remove any leading slashes to ensure relative path + while relative_path.starts_with('/') || relative_path.starts_with('\\') { + relative_path = relative_path[1..].to_string(); + } + // If the path is now empty, use "." + let relative_path = if relative_path.is_empty() { ".".to_string() } else { relative_path }; + let file_path = base_dir.join(&relative_path); + + match file_path.canonicalize() { + Ok(canonical_path) => { + let canonical_base = match base_dir.canonicalize() { + Ok(base) => base, + Err(_) => { + return Ok(GurtResponse::internal_server_error() + .with_header("Content-Type", "text/plain") + .with_string_body("Server configuration error")); + } + }; + + if !canonical_path.starts_with(&canonical_base) { + return Ok(GurtResponse::bad_request() + .with_header("Content-Type", "text/plain") + .with_string_body("Access denied: Path outside served directory")); + } + + if canonical_path.is_file() { + match std::fs::read(&canonical_path) { + Ok(content) => { + let content_type = get_content_type(&canonical_path); + Ok(GurtResponse::ok() + .with_header("Content-Type", &content_type) + .with_body(content)) + } + Err(_) => { + Ok(GurtResponse::internal_server_error() + .with_header("Content-Type", "text/plain") + .with_string_body("Failed to read file")) + } + } + } else if canonical_path.is_dir() { + let index_path = canonical_path.join("index.html"); + if index_path.is_file() { + match std::fs::read_to_string(&index_path) { + Ok(content) => { + Ok(GurtResponse::ok() + .with_header("Content-Type", "text/html") + .with_string_body(content)) + } + Err(_) => { + Ok(GurtResponse::internal_server_error() + .with_header("Content-Type", "text/plain") + .with_string_body("Failed to read index file")) + } + } + } else { + match std::fs::read_dir(&canonical_path) { + Ok(entries) => { + let mut listing = String::from(r#" + + + + Directory Listing + + + +

Directory Listing

+

← Parent Directory

+
+"#); + for entry in entries.flatten() { + let file_name = entry.file_name(); + let name = file_name.to_string_lossy(); + let is_dir = entry.path().is_dir(); + let display_name = if is_dir { format!("{}/", name) } else { name.to_string() }; + let class = if is_dir { "file dir" } else { "file" }; + + listing.push_str(&format!( + r#" {}"#, + class, name, display_name + )); + listing.push('\n'); + } + + listing.push_str("
\n"); + + Ok(GurtResponse::ok() + .with_header("Content-Type", "text/html") + .with_string_body(listing)) + } + Err(_) => { + Ok(GurtResponse::internal_server_error() + .with_header("Content-Type", "text/plain") + .with_string_body("Failed to read directory")) + } + } + } + } else { + // File not found + Ok(GurtResponse::not_found() + .with_header("Content-Type", "text/html") + .with_string_body(get_404_html())) + } + } + Err(_e) => { + Ok(GurtResponse::not_found() + .with_header("Content-Type", "text/html") + .with_string_body(get_404_html())) + } + } + } + } + }); + + Ok(server) +} + +fn get_404_html() -> &'static str { + r#" + + + 404 Not Found + + + +

404 Page Not Found

+

The requested path was not found on this GURT server.

+

Back to home

+ + +"# +} + +fn get_content_type(path: &std::path::Path) -> String { + match path.extension().and_then(|ext| ext.to_str()) { + Some("html") | Some("htm") => "text/html".to_string(), + Some("css") => "text/css".to_string(), + Some("js") => "application/javascript".to_string(), + Some("json") => "application/json".to_string(), + Some("png") => "image/png".to_string(), + Some("jpg") | Some("jpeg") => "image/jpeg".to_string(), + Some("gif") => "image/gif".to_string(), + Some("svg") => "image/svg+xml".to_string(), + Some("ico") => "image/x-icon".to_string(), + Some("txt") => "text/plain".to_string(), + Some("xml") => "application/xml".to_string(), + Some("pdf") => "application/pdf".to_string(), + _ => "application/octet-stream".to_string(), + } +} \ No newline at end of file diff --git a/protocol/gdextension/.gitignore b/protocol/gdextension/.gitignore new file mode 100644 index 0000000..a77a137 --- /dev/null +++ b/protocol/gdextension/.gitignore @@ -0,0 +1,25 @@ +# Rust +/target/ +Cargo.lock + +# Build outputs +/bin/ +/addon/ + +# SCons +.sconf_temp/ +.sconsign.dblite +config.log + +# OS specific +.DS_Store +Thumbs.db +*.tmp +*.temp + +# Editor specific +*.swp +*.swo +*~ +.vscode/ +.idea/ \ No newline at end of file diff --git a/protocol/gdextension/Cargo.toml b/protocol/gdextension/Cargo.toml new file mode 100644 index 0000000..967a2c3 --- /dev/null +++ b/protocol/gdextension/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "gurt-godot" +version = "0.1.0" +edition = "2021" +authors = ["FaceDev"] +license = "MIT" +repository = "https://github.com/outpoot/gurted" +description = "GURT protocol GDExtension for Godot" + +[lib] +name = "gurt_godot" +crate-type = ["cdylib"] + +[dependencies] +gurt = { path = "../library" } + +godot = "0.1" + +tokio = { version = "1.0", features = [ + "net", + "io-util", + "rt", + "time" +] } +tokio-rustls = "0.26" +rustls-native-certs = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tracing = "0.1" +url = "2.5" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true \ No newline at end of file diff --git a/protocol/gdextension/README.md b/protocol/gdextension/README.md new file mode 100644 index 0000000..4248b09 --- /dev/null +++ b/protocol/gdextension/README.md @@ -0,0 +1,37 @@ +GURT networking extension for Godot. + +## Quick Start + +1. **Build the extension:** + ```bash + ./build.sh + ``` + +2. **Install in your Godot project:** + - Copy `addon/gurt-protocol/` to your project's `addons/` folder (e.g. `addons/gurt-protocol`) + - Enable the plugin in `Project Settings > Plugins` + +3. **Use in your game:** + ```gdscript + var client = GurtProtocolClient.new() + client.create_client(30) # 30s timeout + + var response = client.request("gurt://127.0.0.1:4878", {"method": "GET"}) + + client.disconnect() # cleanup + + if response.is_success: + print(response.body) // { "content": ..., "headers": {...}, ... } + else: + print("Error: ", response.status_code, " ", response.status_message) + ``` + +## Build Options + +```bash +./build.sh # Release build for current platform +./build.sh -t debug # Debug build +./build.sh -p windows # Build for Windows +./build.sh -p linux # Build for Linux +./build.sh -p macos # Build for macOS +``` \ No newline at end of file diff --git a/protocol/gdextension/build.sh b/protocol/gdextension/build.sh new file mode 100644 index 0000000..90f82ca --- /dev/null +++ b/protocol/gdextension/build.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +print_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +TARGET="release" +PLATFORM="" + +while [[ $# -gt 0 ]]; do + case $1 in + -t|--target) + TARGET="$2" + shift 2 + ;; + -p|--platform) + PLATFORM="$2" + shift 2 + ;; + -h|--help) + echo "GURT Godot Extension Build Script" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -t, --target TARGET Build target (debug|release) [default: release]" + echo " -p, --platform PLATFORM Target platform (windows|linux|macos|current)" + echo " -h, --help Show this help message" + echo "" + exit 0 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done + +if [[ "$TARGET" != "debug" && "$TARGET" != "release" ]]; then + print_error "Invalid target: $TARGET. Must be 'debug' or 'release'" + exit 1 +fi + +if [[ -z "$PLATFORM" ]]; then + case "$(uname -s)" in + Linux*) PLATFORM="linux";; + Darwin*) PLATFORM="macos";; + CYGWIN*|MINGW*|MSYS*) PLATFORM="windows";; + *) PLATFORM="current";; + esac +fi + +print_info "GURT Godot Extension Build Script" +print_info "Target: $TARGET" +print_info "Platform: $PLATFORM" + +print_info "Checking prerequisites..." + +if ! command -v cargo >/dev/null 2>&1; then + print_error "Rust/Cargo not found. Please install Rust: https://rustup.rs/" + exit 1 +fi + +print_success "Prerequisites found" + +case $PLATFORM in + windows) + RUST_TARGET="x86_64-pc-windows-msvc" + LIB_NAME="gurt_godot.dll" + ;; + linux) + RUST_TARGET="x86_64-unknown-linux-gnu" + LIB_NAME="libgurt_godot.so" + ;; + macos) + RUST_TARGET="x86_64-apple-darwin" + LIB_NAME="libgurt_godot.dylib" + ;; + current) + RUST_TARGET="" + case "$(uname -s)" in + Linux*) LIB_NAME="libgurt_godot.so";; + Darwin*) LIB_NAME="libgurt_godot.dylib";; + CYGWIN*|MINGW*|MSYS*) LIB_NAME="gurt_godot.dll";; + *) print_error "Unsupported platform"; exit 1;; + esac + ;; + *) + print_error "Unknown platform: $PLATFORM" + exit 1 + ;; +esac + +# Create addon directory structure +ADDON_DIR="addon/gurt-protocol" +OUTPUT_DIR="$ADDON_DIR/bin/$PLATFORM" +mkdir -p "$OUTPUT_DIR" + +BUILD_CMD="cargo build" +if [[ "$TARGET" == "release" ]]; then + BUILD_CMD="$BUILD_CMD --release" +fi + +if [[ -n "$RUST_TARGET" ]]; then + print_info "Installing Rust target: $RUST_TARGET" + rustup target add "$RUST_TARGET" + BUILD_CMD="$BUILD_CMD --target $RUST_TARGET" +fi + +print_info "Building with Cargo..." +$BUILD_CMD + +if [[ -n "$RUST_TARGET" ]]; then + if [[ "$TARGET" == "release" ]]; then + BUILT_LIB="target/$RUST_TARGET/release/$LIB_NAME" + else + BUILT_LIB="target/$RUST_TARGET/debug/$LIB_NAME" + fi +else + if [[ "$TARGET" == "release" ]]; then + BUILT_LIB="target/release/$LIB_NAME" + else + BUILT_LIB="target/debug/$LIB_NAME" + fi +fi + +if [[ -f "$BUILT_LIB" ]]; then + cp "$BUILT_LIB" "$OUTPUT_DIR/$LIB_NAME" + + # Copy addon files + cp gurt_godot.gdextension "$ADDON_DIR/" + cp plugin.cfg "$ADDON_DIR/" + cp plugin.gd "$ADDON_DIR/" + + print_success "Build completed: $OUTPUT_DIR/$LIB_NAME" + SIZE=$(du -h "$OUTPUT_DIR/$LIB_NAME" | cut -f1) + print_info "Library size: $SIZE" +else + print_error "Built library not found at: $BUILT_LIB" + exit 1 +fi + +print_success "Build process completed!" +print_info "Copy the 'addon/gurt-protocol' folder to your project's 'addons/' directory" \ No newline at end of file diff --git a/protocol/gdextension/gurt_godot.gdextension b/protocol/gdextension/gurt_godot.gdextension new file mode 100644 index 0000000..babeac4 --- /dev/null +++ b/protocol/gdextension/gurt_godot.gdextension @@ -0,0 +1,13 @@ +[configuration] + +entry_symbol = "gdext_rust_init" +compatibility_minimum = 4.1 + +[libraries] + +macos.debug = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib" +macos.release = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib" +windows.debug.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll" +windows.release.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll" +linux.debug.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so" +linux.release.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so" \ No newline at end of file diff --git a/protocol/gdextension/plugin.cfg b/protocol/gdextension/plugin.cfg new file mode 100644 index 0000000..78d49d7 --- /dev/null +++ b/protocol/gdextension/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GURT Protocol" +description="HTTP-like networking extension for Godot games using the GURT protocol" +author="FaceDev" +version="0.1.0" +script="plugin.gd" \ No newline at end of file diff --git a/protocol/gdextension/plugin.gd b/protocol/gdextension/plugin.gd new file mode 100644 index 0000000..94a8a57 --- /dev/null +++ b/protocol/gdextension/plugin.gd @@ -0,0 +1,8 @@ +@tool +extends EditorPlugin + +func _enter_tree(): + print("GURT Protocol plugin enabled") + +func _exit_tree(): + print("GURT Protocol plugin disabled") \ No newline at end of file diff --git a/protocol/gdextension/src/lib.rs b/protocol/gdextension/src/lib.rs new file mode 100644 index 0000000..b521749 --- /dev/null +++ b/protocol/gdextension/src/lib.rs @@ -0,0 +1,375 @@ +use godot::prelude::*; +use gurt::prelude::*; +use gurt::{GurtMethod, GurtRequest}; +use tokio::runtime::Runtime; +use std::sync::Arc; +use std::cell::RefCell; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +struct GurtGodotExtension; + +#[gdextension] +unsafe impl ExtensionLibrary for GurtGodotExtension {} + +#[derive(GodotClass)] +#[class(init)] +struct GurtProtocolClient { + base: Base, + + client: Arc>>, + runtime: Arc>>, +} + +#[derive(GodotClass)] +#[class(init)] +struct GurtGDResponse { + base: Base, + + #[var] + status_code: i32, + + #[var] + status_message: GString, + + #[var] + headers: Dictionary, + + #[var] + is_success: bool, + + #[var] + body: PackedByteArray, // Raw bytes + + #[var] + text: GString, // Decoded text +} + +#[godot_api] +impl GurtGDResponse { + #[func] + fn get_header(&self, key: GString) -> GString { + self.headers.get(key).map_or(GString::new(), |v| v.to::()) + } + + #[func] + fn is_binary(&self) -> bool { + let content_type = self.get_header("content-type".into()).to_string(); + content_type.starts_with("image/") || + content_type.starts_with("application/octet-stream") || + content_type.starts_with("video/") || + content_type.starts_with("audio/") + } + + #[func] + fn is_text(&self) -> bool { + let content_type = self.get_header("content-type".into()).to_string(); + content_type.starts_with("text/") || + content_type.starts_with("application/json") || + content_type.starts_with("application/xml") || + content_type.is_empty() + } + + #[func] + fn debug_info(&self) -> GString { + let content_length = self.get_header("content-length".into()).to_string(); + let actual_size = self.body.len(); + let content_type = self.get_header("content-type".into()).to_string(); + let size_match = content_length.parse::().unwrap_or(0) == actual_size; + + format!( + "Status: {} | Type: {} | Length: {} | Actual: {} | Match: {}", + self.status_code, + content_type, + content_length, + actual_size, + size_match + ).into() + } +} + +#[derive(GodotClass)] +#[class(init)] +struct GurtProtocolServer { + base: Base, +} + +#[godot_api] +impl GurtProtocolClient { + #[signal] + fn request_completed(response: Gd); + + #[func] + fn create_client(&mut self, timeout_seconds: i32) -> bool { + let runtime = match Runtime::new() { + Ok(rt) => rt, + Err(e) => { + godot_print!("Failed to create runtime: {}", e); + return false; + } + }; + + let mut config = ClientConfig::default(); + config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64); + + let client = GurtClient::with_config(config); + + *self.runtime.borrow_mut() = Some(runtime); + *self.client.borrow_mut() = Some(client); + + true + } + + #[func] + fn request(&self, url: GString, options: Dictionary) -> Option> { + let runtime_binding = self.runtime.borrow(); + let runtime = match runtime_binding.as_ref() { + Some(rt) => rt, + None => { + godot_print!("No runtime available"); + return None; + } + }; + + let url_str = url.to_string(); + + // Parse URL to get host and port + let parsed_url = match url::Url::parse(&url_str) { + Ok(u) => u, + Err(e) => { + godot_print!("Invalid URL: {}", e); + return None; + } + }; + + let host = match parsed_url.host_str() { + Some(h) => h, + None => { + godot_print!("URL must have a host"); + return None; + } + }; + + let port = parsed_url.port().unwrap_or(4878); + let path = if parsed_url.path().is_empty() { "/" } else { parsed_url.path() }; + + let method_str = options.get("method").unwrap_or("GET".to_variant()).to::(); + let method = match method_str.to_uppercase().as_str() { + "GET" => GurtMethod::GET, + "POST" => GurtMethod::POST, + "PUT" => GurtMethod::PUT, + "DELETE" => GurtMethod::DELETE, + "PATCH" => GurtMethod::PATCH, + "HEAD" => GurtMethod::HEAD, + "OPTIONS" => GurtMethod::OPTIONS, + _ => { + godot_print!("Unsupported HTTP method: {}", method_str); + GurtMethod::GET + } + }; + + let response = match runtime.block_on(self.gurt_request_with_handshake(host, port, method, path)) { + Ok(resp) => resp, + Err(e) => { + godot_print!("GURT request failed: {}", e); + return None; + } + }; + + Some(self.convert_response(response)) + } + + async fn gurt_request_with_handshake(&self, host: &str, port: u16, method: GurtMethod, path: &str) -> gurt::Result { + let addr = format!("{}:{}", host, port); + let mut stream = TcpStream::connect(&addr).await?; + + let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string()) + .with_header("Host", host) + .with_header("User-Agent", &format!("GURT-Client/{}", gurt::GURT_VERSION)); + + let handshake_data = handshake_request.to_string(); + stream.write_all(handshake_data.as_bytes()).await?; + + let mut buffer = Vec::new(); + let mut temp_buffer = [0u8; 8192]; + + loop { + let bytes_read = stream.read(&mut temp_buffer).await?; + if bytes_read == 0 { + break; + } + buffer.extend_from_slice(&temp_buffer[..bytes_read]); + + let separator = b"\r\n\r\n"; + if buffer.windows(separator.len()).any(|w| w == separator) { + break; + } + } + + let handshake_response = GurtResponse::parse_bytes(&buffer)?; + + if handshake_response.status_code != 101 { + return Err(GurtError::handshake(format!("Handshake failed: {} {}", + handshake_response.status_code, + handshake_response.status_message))); + } + + let tls_stream = self.create_secure_tls_connection(stream, host).await?; + let (mut reader, mut writer) = tokio::io::split(tls_stream); + + let actual_request = GurtRequest::new(method, path.to_string()) + .with_header("Host", host) + .with_header("User-Agent", &format!("GURT-Client/{}", gurt::GURT_VERSION)) + .with_header("Accept", "*/*"); + + let request_data = actual_request.to_string(); + writer.write_all(request_data.as_bytes()).await?; + + let mut response_buffer = Vec::new(); + let mut temp_buf = [0u8; 8192]; + + let mut headers_complete = false; + while !headers_complete { + let bytes_read = reader.read(&mut temp_buf).await?; + if bytes_read == 0 { + break; + } + response_buffer.extend_from_slice(&temp_buf[..bytes_read]); + + let separator = b"\r\n\r\n"; + if response_buffer.windows(separator.len()).any(|w| w == separator) { + headers_complete = true; + } + } + + let response = GurtResponse::parse_bytes(&response_buffer)?; + let content_length = response.header("content-length") + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + let separator_pos = response_buffer.windows(4).position(|w| w == b"\r\n\r\n").unwrap_or(0) + 4; + let current_body_len = response_buffer.len().saturating_sub(separator_pos); + + if content_length > current_body_len { + let remaining = content_length - current_body_len; + let mut remaining_buffer = vec![0u8; remaining]; + match reader.read_exact(&mut remaining_buffer).await { + Ok(_) => { + response_buffer.extend_from_slice(&remaining_buffer); + } + Err(e) => { + godot_error!("Failed to read remaining {} bytes: {}", remaining, e); + // Don't fail completely, try to parse what we have + } + } + } + + drop(reader); + drop(writer); + + let final_response = GurtResponse::parse_bytes(&response_buffer)?; + + Ok(final_response) + } + + async fn create_secure_tls_connection(&self, stream: tokio::net::TcpStream, host: &str) -> gurt::Result> { + use tokio_rustls::rustls::{ClientConfig, RootCertStore}; + use std::sync::Arc; + + let mut root_store = RootCertStore::empty(); + + let cert_result = rustls_native_certs::load_native_certs(); + let mut system_cert_count = 0; + for cert in cert_result.certs { + if root_store.add(cert).is_ok() { + system_cert_count += 1; + } + } + + if system_cert_count <= 0 { + godot_error!("No system certificates found. TLS connections will fail."); + } + + let mut client_config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + client_config.alpn_protocols = vec![gurt::crypto::GURT_ALPN.to_vec()]; + + let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config)); + + let server_name = match host { + "127.0.0.1" => "localhost", + "localhost" => "localhost", + _ => host + }; + + let domain = tokio_rustls::rustls::pki_types::ServerName::try_from(server_name.to_string()) + .map_err(|e| GurtError::connection(format!("Invalid server name '{}': {}", server_name, e)))?; + + match connector.connect(domain, stream).await { + Ok(tls_stream) => { + Ok(tls_stream) + } + Err(e) => { + godot_error!("TLS handshake failed: {}", e); + Err(GurtError::connection(format!("TLS handshake failed: {}", e))) + } + } + } + + #[func] + fn disconnect(&mut self) { + *self.client.borrow_mut() = None; + *self.runtime.borrow_mut() = None; + } + + #[func] + fn is_connected(&self) -> bool { + self.client.borrow().is_some() + } + + #[func] + fn get_version(&self) -> GString { + gurt::GURT_VERSION.to_string().into() + } + + #[func] + fn get_default_port(&self) -> i32 { + gurt::DEFAULT_PORT as i32 + } + + fn convert_response(&self, response: GurtResponse) -> Gd { + let mut gd_response = GurtGDResponse::new_gd(); + + gd_response.bind_mut().status_code = response.status_code as i32; + gd_response.bind_mut().status_message = response.status_message.clone().into(); + gd_response.bind_mut().is_success = response.is_success(); + + let mut headers = Dictionary::new(); + for (key, value) in &response.headers { + headers.set(key.clone(), value.clone()); + } + gd_response.bind_mut().headers = headers; + + let mut body = PackedByteArray::new(); + body.resize(response.body.len()); + for (i, byte) in response.body.iter().enumerate() { + body[i] = *byte; + } + gd_response.bind_mut().body = body; + + match std::str::from_utf8(&response.body) { + Ok(text_str) => { + gd_response.bind_mut().text = text_str.into(); + } + Err(_) => { + let content_type = response.headers.get("content-type").cloned().unwrap_or_default(); + let size = response.body.len(); + gd_response.bind_mut().text = format!("[Binary data: {} ({} bytes)]", content_type, size).into(); + } + } + + gd_response + } +} \ No newline at end of file diff --git a/protocol/library/Cargo.lock b/protocol/library/Cargo.lock new file mode 100644 index 0000000..e948075 --- /dev/null +++ b/protocol/library/Cargo.lock @@ -0,0 +1,1540 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gurt" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-test", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/protocol/library/Cargo.toml b/protocol/library/Cargo.toml new file mode 100644 index 0000000..6d36aa4 --- /dev/null +++ b/protocol/library/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "gurt" +version = "0.1.0" +edition = "2021" +authors = ["FaceDev"] +license = "MIT" +repository = "https://github.com/outpoot/gurted" +description = "Official GURT:// protocol implementation" + +[lib] +name = "gurt" +crate-type = ["cdylib", "lib"] + +[dependencies] +tokio = { version = "1.0", features = [ + "net", + "io-util", + "rt", + "macros", + "rt-multi-thread", + "time", + "fs" +] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tracing = "0.1" +chrono = { version = "0.4", features = ["serde"] } + +tokio-rustls = "0.26" +rustls = "0.23" +rustls-pemfile = "2.0" +base64 = "0.22" +url = "2.5" + +[dev-dependencies] +tokio-test = "0.4" +tracing-subscriber = "0.3" +sha2 = "0.10" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true \ No newline at end of file diff --git a/protocol/library/README.md b/protocol/library/README.md new file mode 100644 index 0000000..73e2ab3 --- /dev/null +++ b/protocol/library/README.md @@ -0,0 +1,3 @@ +The Rust implementation of the Gurt protocol, used internally by the **Gurty CLI** and the **Gurt GDExtension** (which is used by Flumi, the official browser). + +See the `examples/` directory for usage examples. For protocol details and design, refer to `SPEC.md` or the source code. \ No newline at end of file diff --git a/protocol/library/examples/tls_server.rs b/protocol/library/examples/tls_server.rs new file mode 100644 index 0000000..7863776 --- /dev/null +++ b/protocol/library/examples/tls_server.rs @@ -0,0 +1,18 @@ +use gurt::{GurtServer, GurtResponse, ServerContext, Result}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let server = GurtServer::with_tls_certificates("cert.pem", "cert.key.pem")? + .get("/", |_ctx: &ServerContext| async { + Ok(GurtResponse::ok().with_string_body("

Hello from GURT!

")) + }) + .get("/test", |_ctx: &ServerContext| async { + Ok(GurtResponse::ok().with_string_body("Test endpoint working!")) + }); + + println!("Starting GURT server on gurt://127.0.0.1:4878"); + + server.listen("127.0.0.1:4878").await +} \ No newline at end of file diff --git a/protocol/library/src/client.rs b/protocol/library/src/client.rs new file mode 100644 index 0000000..3410195 --- /dev/null +++ b/protocol/library/src/client.rs @@ -0,0 +1,302 @@ +use crate::{ + GurtError, Result, GurtRequest, GurtResponse, + protocol::{DEFAULT_PORT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_HANDSHAKE_TIMEOUT, BODY_SEPARATOR}, + message::GurtMethod, +}; +use tokio::net::TcpStream; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::time::{timeout, Duration}; +use url::Url; +use tracing::debug; + +#[derive(Debug, Clone)] +pub struct ClientConfig { + pub connect_timeout: Duration, + pub request_timeout: Duration, + pub handshake_timeout: Duration, + pub user_agent: String, + pub max_redirects: usize, +} + +impl Default for ClientConfig { + fn default() -> Self { + Self { + connect_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT), + request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT), + handshake_timeout: Duration::from_secs(DEFAULT_HANDSHAKE_TIMEOUT), + user_agent: format!("GURT-Client/{}", crate::GURT_VERSION), + max_redirects: 5, + } + } +} + +#[derive(Debug)] +struct PooledConnection { + stream: TcpStream, +} + +impl PooledConnection { + fn new(stream: TcpStream) -> Self { + Self { stream } + } +} + +pub struct GurtClient { + config: ClientConfig, +} + +impl GurtClient { + pub fn new() -> Self { + Self { + config: ClientConfig::default(), + } + } + + pub fn with_config(config: ClientConfig) -> Self { + Self { + config, + } + } + + async fn create_connection(&self, host: &str, port: u16) -> Result { + let addr = format!("{}:{}", host, port); + let stream = timeout( + self.config.connect_timeout, + TcpStream::connect(&addr) + ).await + .map_err(|_| GurtError::timeout("Connection timeout"))? + .map_err(|e| GurtError::connection(format!("Failed to connect: {}", e)))?; + + let conn = PooledConnection::new(stream); + Ok(conn) + } + + async fn read_response_data(&self, stream: &mut TcpStream) -> Result> { + let mut buffer = Vec::new(); + let mut temp_buffer = [0u8; 8192]; + + let start_time = std::time::Instant::now(); + + loop { + if start_time.elapsed() > self.config.request_timeout { + return Err(GurtError::timeout("Response timeout")); + } + + let bytes_read = stream.read(&mut temp_buffer).await?; + if bytes_read == 0 { + break; // Connection closed + } + + buffer.extend_from_slice(&temp_buffer[..bytes_read]); + + // Check for complete message without converting to string + let body_separator = BODY_SEPARATOR.as_bytes(); + let has_complete_response = buffer.windows(body_separator.len()).any(|w| w == body_separator) || + (buffer.starts_with(b"{") && buffer.ends_with(b"}")); + + if has_complete_response { + return Ok(buffer); + } + } + + if buffer.is_empty() { + Err(GurtError::connection("Connection closed unexpectedly")) + } else { + Ok(buffer) + } + } + + async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result { + debug!("Sending {} {} to {}:{}", request.method, request.path, host, port); + + let mut conn = self.create_connection(host, port).await?; + + let request_data = request.to_string(); + conn.stream.write_all(request_data.as_bytes()).await?; + + let response_bytes = timeout( + self.config.request_timeout, + self.read_response_data(&mut conn.stream) + ).await + .map_err(|_| GurtError::timeout("Request timeout"))??; + + let response = GurtResponse::parse_bytes(&response_bytes)?; + + Ok(response) + } + + pub async fn get(&self, url: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::GET, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Accept", "*/*"); + + self.send_request_internal(&host, port, request).await + } + + pub async fn post(&self, url: &str, body: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::POST, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "text/plain") + .with_string_body(body); + + self.send_request_internal(&host, port, request).await + } + + /// POST request with JSON body + pub async fn post_json(&self, url: &str, data: &T) -> Result { + let (host, port, path) = self.parse_url(url)?; + let json_body = serde_json::to_string(data)?; + + let request = GurtRequest::new(GurtMethod::POST, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "application/json") + .with_string_body(json_body); + + self.send_request_internal(&host, port, request).await + } + + /// PUT request with body + pub async fn put(&self, url: &str, body: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::PUT, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "text/plain") + .with_string_body(body); + + self.send_request_internal(&host, port, request).await + } + + /// PUT request with JSON body + pub async fn put_json(&self, url: &str, data: &T) -> Result { + let (host, port, path) = self.parse_url(url)?; + let json_body = serde_json::to_string(data)?; + + let request = GurtRequest::new(GurtMethod::PUT, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "application/json") + .with_string_body(json_body); + + self.send_request_internal(&host, port, request).await + } + + pub async fn delete(&self, url: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::DELETE, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent); + + self.send_request_internal(&host, port, request).await + } + + pub async fn head(&self, url: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::HEAD, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent); + + self.send_request_internal(&host, port, request).await + } + + pub async fn options(&self, url: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::OPTIONS, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent); + + self.send_request_internal(&host, port, request).await + } + + /// PATCH request with body + pub async fn patch(&self, url: &str, body: &str) -> Result { + let (host, port, path) = self.parse_url(url)?; + let request = GurtRequest::new(GurtMethod::PATCH, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "text/plain") + .with_string_body(body); + + self.send_request_internal(&host, port, request).await + } + + /// PATCH request with JSON body + pub async fn patch_json(&self, url: &str, data: &T) -> Result { + let (host, port, path) = self.parse_url(url)?; + let json_body = serde_json::to_string(data)?; + + let request = GurtRequest::new(GurtMethod::PATCH, path) + .with_header("Host", &host) + .with_header("User-Agent", &self.config.user_agent) + .with_header("Content-Type", "application/json") + .with_string_body(json_body); + + self.send_request_internal(&host, port, request).await + } + + pub async fn send_request(&self, host: &str, port: u16, request: GurtRequest) -> Result { + self.send_request_internal(host, port, request).await + } + + fn parse_url(&self, url: &str) -> Result<(String, u16, String)> { + let parsed_url = Url::parse(url).map_err(|e| GurtError::invalid_message(format!("Invalid URL: {}", e)))?; + + if parsed_url.scheme() != "gurt" { + return Err(GurtError::invalid_message("URL must use gurt:// scheme")); + } + + let host = parsed_url.host_str() + .ok_or_else(|| GurtError::invalid_message("URL must have a host"))? + .to_string(); + + let port = parsed_url.port().unwrap_or(DEFAULT_PORT); + + let path = if parsed_url.path().is_empty() { + "/".to_string() + } else { + parsed_url.path().to_string() + }; + + Ok((host, port, path)) + } + +} + +impl Default for GurtClient { + fn default() -> Self { + Self::new() + } +} + +impl Clone for GurtClient { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_url_parsing() { + let client = GurtClient::new(); + + let (host, port, path) = client.parse_url("gurt://example.com/test").unwrap(); + assert_eq!(host, "example.com"); + assert_eq!(port, DEFAULT_PORT); + assert_eq!(path, "/test"); + + let (host, port, path) = client.parse_url("gurt://example.com:8080/api/v1").unwrap(); + assert_eq!(host, "example.com"); + assert_eq!(port, 8080); + assert_eq!(path, "/api/v1"); + } +} \ No newline at end of file diff --git a/protocol/library/src/crypto.rs b/protocol/library/src/crypto.rs new file mode 100644 index 0000000..a6f0507 --- /dev/null +++ b/protocol/library/src/crypto.rs @@ -0,0 +1,123 @@ +use crate::{GurtError, Result}; +use rustls::{ClientConfig, ServerConfig}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use tokio_rustls::{TlsConnector, TlsAcceptor}; +use std::sync::Arc; + +pub const TLS_VERSION: &str = "TLS/1.3"; +pub const GURT_ALPN: &[u8] = b"GURT/1.0"; + +#[derive(Debug, Clone)] +pub struct TlsConfig { + pub client_config: Option>, + pub server_config: Option>, +} + +impl TlsConfig { + pub fn new_client() -> Result { + let mut config = ClientConfig::builder() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + + config.alpn_protocols = vec![GURT_ALPN.to_vec()]; + + Ok(Self { + client_config: Some(Arc::new(config)), + server_config: None, + }) + } + + pub fn new_server(cert_chain: Vec>, private_key: PrivateKeyDer<'static>) -> Result { + let mut config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, private_key) + .map_err(|e| GurtError::crypto(format!("TLS server config error: {}", e)))?; + + config.alpn_protocols = vec![GURT_ALPN.to_vec()]; + + Ok(Self { + client_config: None, + server_config: Some(Arc::new(config)), + }) + } + + pub fn get_connector(&self) -> Result { + let config = self.client_config.as_ref() + .ok_or_else(|| GurtError::crypto("No client config available"))?; + Ok(TlsConnector::from(config.clone())) + } + + pub fn get_acceptor(&self) -> Result { + let config = self.server_config.as_ref() + .ok_or_else(|| GurtError::crypto("No server config available"))?; + Ok(TlsAcceptor::from(config.clone())) + } +} + + +#[derive(Debug)] +pub struct CryptoManager { + tls_config: Option, +} + +impl CryptoManager { + pub fn new() -> Self { + Self { + tls_config: None, + } + } + + + pub fn with_tls_config(config: TlsConfig) -> Self { + Self { + tls_config: Some(config), + } + } + + pub fn set_tls_config(&mut self, config: TlsConfig) { + self.tls_config = Some(config); + } + + pub fn has_tls_config(&self) -> bool { + self.tls_config.is_some() + } + + pub fn get_tls_connector(&self) -> Result { + let config = self.tls_config.as_ref() + .ok_or_else(|| GurtError::crypto("No TLS config available"))?; + config.get_connector() + } + + pub fn get_tls_acceptor(&self) -> Result { + let config = self.tls_config.as_ref() + .ok_or_else(|| GurtError::crypto("No TLS config available"))?; + config.get_acceptor() + } +} + +impl Default for CryptoManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tls_config_creation() { + let client_config = TlsConfig::new_client(); + assert!(client_config.is_ok()); + + let config = client_config.unwrap(); + assert!(config.client_config.is_some()); + assert!(config.server_config.is_none()); + } + + #[test] + fn test_crypto_manager() { + let crypto = CryptoManager::new(); + assert!(!crypto.has_tls_config()); + } +} \ No newline at end of file diff --git a/protocol/library/src/error.rs b/protocol/library/src/error.rs new file mode 100644 index 0000000..78690f7 --- /dev/null +++ b/protocol/library/src/error.rs @@ -0,0 +1,71 @@ +use std::fmt; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GurtError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("Cryptographic error: {0}")] + Crypto(String), + + #[error("Protocol error: {0}")] + Protocol(String), + + #[error("Invalid message format: {0}")] + InvalidMessage(String), + + #[error("Connection error: {0}")] + Connection(String), + + #[error("Handshake failed: {0}")] + Handshake(String), + + #[error("Timeout error: {0}")] + Timeout(String), + + #[error("Server error: {status} {message}")] + Server { status: u16, message: String }, + + #[error("Client error: {0}")] + Client(String), +} + +pub type Result = std::result::Result; + +impl GurtError { + pub fn crypto(msg: T) -> Self { + GurtError::Crypto(msg.to_string()) + } + + pub fn protocol(msg: T) -> Self { + GurtError::Protocol(msg.to_string()) + } + + pub fn invalid_message(msg: T) -> Self { + GurtError::InvalidMessage(msg.to_string()) + } + + pub fn connection(msg: T) -> Self { + GurtError::Connection(msg.to_string()) + } + + pub fn handshake(msg: T) -> Self { + GurtError::Handshake(msg.to_string()) + } + + pub fn timeout(msg: T) -> Self { + GurtError::Timeout(msg.to_string()) + } + + pub fn server(status: u16, message: String) -> Self { + GurtError::Server { status, message } + } + + pub fn client(msg: T) -> Self { + GurtError::Client(msg.to_string()) + } +} \ No newline at end of file diff --git a/protocol/library/src/lib.rs b/protocol/library/src/lib.rs new file mode 100644 index 0000000..08ed0cc --- /dev/null +++ b/protocol/library/src/lib.rs @@ -0,0 +1,24 @@ +pub mod protocol; +pub mod crypto; +pub mod server; +pub mod client; +pub mod error; +pub mod message; + +pub use error::{GurtError, Result}; +pub use message::{GurtMessage, GurtRequest, GurtResponse, GurtMethod}; +pub use protocol::{GurtStatusCode, GURT_VERSION, DEFAULT_PORT}; +pub use crypto::{CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION}; +pub use server::{GurtServer, GurtHandler, ServerContext, Route}; +pub use client::{GurtClient, ClientConfig}; + +pub mod prelude { + pub use crate::{ + GurtError, Result, + GurtMessage, GurtRequest, GurtResponse, + GURT_VERSION, DEFAULT_PORT, + CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION, + GurtServer, GurtHandler, ServerContext, Route, + GurtClient, ClientConfig, + }; +} \ No newline at end of file diff --git a/protocol/library/src/message.rs b/protocol/library/src/message.rs new file mode 100644 index 0000000..b8e2db6 --- /dev/null +++ b/protocol/library/src/message.rs @@ -0,0 +1,568 @@ +use crate::{GurtError, Result, GURT_VERSION}; +use crate::protocol::{GurtStatusCode, PROTOCOL_PREFIX, HEADER_SEPARATOR, BODY_SEPARATOR}; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use std::fmt; +use chrono::Utc; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum GurtMethod { + GET, + POST, + PUT, + DELETE, + HEAD, + OPTIONS, + PATCH, + HANDSHAKE, // Special method for protocol handshake +} + +impl GurtMethod { + pub fn parse(s: &str) -> Result { + match s.to_uppercase().as_str() { + "GET" => Ok(Self::GET), + "POST" => Ok(Self::POST), + "PUT" => Ok(Self::PUT), + "DELETE" => Ok(Self::DELETE), + "HEAD" => Ok(Self::HEAD), + "OPTIONS" => Ok(Self::OPTIONS), + "PATCH" => Ok(Self::PATCH), + "HANDSHAKE" => Ok(Self::HANDSHAKE), + _ => Err(GurtError::invalid_message(format!("Unsupported method: {}", s))), + } + } +} + +impl fmt::Display for GurtMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::GET => "GET", + Self::POST => "POST", + Self::PUT => "PUT", + Self::DELETE => "DELETE", + Self::HEAD => "HEAD", + Self::OPTIONS => "OPTIONS", + Self::PATCH => "PATCH", + Self::HANDSHAKE => "HANDSHAKE", + }; + write!(f, "{}", s) + } +} + +pub type GurtHeaders = HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GurtRequest { + pub method: GurtMethod, + pub path: String, + pub version: String, + pub headers: GurtHeaders, + pub body: Vec, +} + +impl GurtRequest { + pub fn new(method: GurtMethod, path: String) -> Self { + Self { + method, + path, + version: GURT_VERSION.to_string(), + headers: GurtHeaders::new(), + body: Vec::new(), + } + } + + pub fn with_header, V: Into>(mut self, key: K, value: V) -> Self { + self.headers.insert(key.into().to_lowercase(), value.into()); + self + } + + pub fn with_body>>(mut self, body: B) -> Self { + self.body = body.into(); + self + } + + pub fn with_string_body>(mut self, body: S) -> Self { + self.body = body.as_ref().as_bytes().to_vec(); + self + } + + pub fn header(&self, key: &str) -> Option<&String> { + self.headers.get(&key.to_lowercase()) + } + + pub fn body_as_string(&self) -> Result { + std::str::from_utf8(&self.body) + .map(|s| s.to_string()) + .map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e))) + } + + pub fn parse(data: &str) -> Result { + Self::parse_bytes(data.as_bytes()) + } + + pub fn parse_bytes(data: &[u8]) -> Result { + // Find the header/body separator as bytes + let body_separator = BODY_SEPARATOR.as_bytes(); + let body_separator_pos = data.windows(body_separator.len()) + .position(|window| window == body_separator); + + let (headers_section, body) = if let Some(pos) = body_separator_pos { + let headers_part = &data[..pos]; + let body_part = &data[pos + body_separator.len()..]; + (headers_part, body_part.to_vec()) + } else { + (data, Vec::new()) + }; + + // Convert headers section to string (should be valid UTF-8) + let headers_str = std::str::from_utf8(headers_section) + .map_err(|_| GurtError::invalid_message("Invalid UTF-8 in headers"))?; + + let lines: Vec<&str> = headers_str.split(HEADER_SEPARATOR).collect(); + + if lines.is_empty() { + return Err(GurtError::invalid_message("Empty request")); + } + + // Parse request line (METHOD path GURT/version) + let request_line = lines[0]; + let parts: Vec<&str> = request_line.split_whitespace().collect(); + + if parts.len() != 3 { + return Err(GurtError::invalid_message("Invalid request line format")); + } + + let method = GurtMethod::parse(parts[0])?; + let path = parts[1].to_string(); + + // Parse protocol version + if !parts[2].starts_with(PROTOCOL_PREFIX) { + return Err(GurtError::invalid_message("Invalid protocol identifier")); + } + + let version_str = &parts[2][PROTOCOL_PREFIX.len()..]; + let version = version_str.to_string(); + + // Parse headers + let mut headers = GurtHeaders::new(); + + for line in lines.iter().skip(1) { + if line.is_empty() { + break; + } + + if let Some(colon_pos) = line.find(':') { + let key = line[..colon_pos].trim().to_lowercase(); + let value = line[colon_pos + 1..].trim().to_string(); + headers.insert(key, value); + } + } + + Ok(Self { + method, + path, + version, + headers, + body, + }) + } + + pub fn to_string(&self) -> String { + let mut message = format!("{} {} {}{}{}", + self.method, self.path, PROTOCOL_PREFIX, self.version, HEADER_SEPARATOR); + + let mut headers = self.headers.clone(); + if !headers.contains_key("content-length") { + headers.insert("content-length".to_string(), self.body.len().to_string()); + } + + if !headers.contains_key("user-agent") { + headers.insert("user-agent".to_string(), format!("GURT-Client/{}", GURT_VERSION)); + } + + for (key, value) in &headers { + message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR)); + } + + message.push_str(HEADER_SEPARATOR); + + if !self.body.is_empty() { + if let Ok(body_str) = std::str::from_utf8(&self.body) { + message.push_str(body_str); + } + } + + message + } + + pub fn to_bytes(&self) -> Vec { + let mut message = format!("{} {} {}{}{}", + self.method, self.path, PROTOCOL_PREFIX, self.version, HEADER_SEPARATOR); + + let mut headers = self.headers.clone(); + if !headers.contains_key("content-length") { + headers.insert("content-length".to_string(), self.body.len().to_string()); + } + + if !headers.contains_key("user-agent") { + headers.insert("user-agent".to_string(), format!("GURT-Client/{}", GURT_VERSION)); + } + + for (key, value) in &headers { + message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR)); + } + + message.push_str(HEADER_SEPARATOR); + + let mut bytes = message.into_bytes(); + bytes.extend_from_slice(&self.body); + + bytes + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GurtResponse { + pub version: String, + pub status_code: u16, + pub status_message: String, + pub headers: GurtHeaders, + pub body: Vec, +} + +impl GurtResponse { + pub fn new(status_code: GurtStatusCode) -> Self { + Self { + version: GURT_VERSION.to_string(), + status_code: status_code as u16, + status_message: status_code.message().to_string(), + headers: GurtHeaders::new(), + body: Vec::new(), + } + } + + pub fn ok() -> Self { + Self::new(GurtStatusCode::Ok) + } + + pub fn not_found() -> Self { + Self::new(GurtStatusCode::NotFound) + } + + pub fn bad_request() -> Self { + Self::new(GurtStatusCode::BadRequest) + } + + pub fn internal_server_error() -> Self { + Self::new(GurtStatusCode::InternalServerError) + } + + pub fn with_header, V: Into>(mut self, key: K, value: V) -> Self { + self.headers.insert(key.into().to_lowercase(), value.into()); + self + } + + pub fn with_body>>(mut self, body: B) -> Self { + self.body = body.into(); + self + } + + pub fn with_string_body>(mut self, body: S) -> Self { + self.body = body.as_ref().as_bytes().to_vec(); + self + } + + pub fn with_json_body(mut self, data: &T) -> Result { + let json = serde_json::to_string(data)?; + self.body = json.into_bytes(); + self.headers.insert("content-type".to_string(), "application/json".to_string()); + Ok(self) + } + + pub fn header(&self, key: &str) -> Option<&String> { + self.headers.get(&key.to_lowercase()) + } + + pub fn body_as_string(&self) -> Result { + std::str::from_utf8(&self.body) + .map(|s| s.to_owned()) + .map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e))) + } + + pub fn is_success(&self) -> bool { + self.status_code >= 200 && self.status_code < 300 + } + + pub fn is_client_error(&self) -> bool { + self.status_code >= 400 && self.status_code < 500 + } + + pub fn is_server_error(&self) -> bool { + self.status_code >= 500 + } + + pub fn parse(data: &str) -> Result { + Self::parse_bytes(data.as_bytes()) + } + + pub fn parse_bytes(data: &[u8]) -> Result { + // Find the header/body separator as bytes + let body_separator = BODY_SEPARATOR.as_bytes(); + let body_separator_pos = data.windows(body_separator.len()) + .position(|window| window == body_separator); + + let (headers_section, body) = if let Some(pos) = body_separator_pos { + let headers_part = &data[..pos]; + let body_part = &data[pos + body_separator.len()..]; + (headers_part, body_part.to_vec()) + } else { + (data, Vec::new()) + }; + + // Convert headers section to string (should be valid UTF-8) + let headers_str = std::str::from_utf8(headers_section) + .map_err(|_| GurtError::invalid_message("Invalid UTF-8 in headers"))?; + + let lines: Vec<&str> = headers_str.split(HEADER_SEPARATOR).collect(); + + if lines.is_empty() { + return Err(GurtError::invalid_message("Empty response")); + } + + // Parse status line (GURT/version status_code status_message) + let status_line = lines[0]; + let parts: Vec<&str> = status_line.splitn(3, ' ').collect(); + + if parts.len() < 2 { + return Err(GurtError::invalid_message("Invalid status line format")); + } + + // Parse protocol version + if !parts[0].starts_with(PROTOCOL_PREFIX) { + return Err(GurtError::invalid_message("Invalid protocol identifier")); + } + + let version_str = &parts[0][PROTOCOL_PREFIX.len()..]; + let version = version_str.to_string(); + + let status_code: u16 = parts[1].parse() + .map_err(|_| GurtError::invalid_message("Invalid status code"))?; + + let status_message = if parts.len() > 2 { + parts[2].to_string() + } else { + GurtStatusCode::from_u16(status_code) + .map(|sc| sc.message().to_string()) + .unwrap_or_else(|| "Unknown".to_string()) + }; + + // Parse headers + let mut headers = GurtHeaders::new(); + + for line in lines.iter().skip(1) { + if line.is_empty() { + break; + } + + if let Some(colon_pos) = line.find(':') { + let key = line[..colon_pos].trim().to_lowercase(); + let value = line[colon_pos + 1..].trim().to_string(); + headers.insert(key, value); + } + } + + Ok(Self { + version, + status_code, + status_message, + headers, + body, + }) + } + + pub fn to_string(&self) -> String { + let mut message = format!("{}{} {} {}{}", + PROTOCOL_PREFIX, self.version, self.status_code, self.status_message, HEADER_SEPARATOR); + + let mut headers = self.headers.clone(); + if !headers.contains_key("content-length") { + headers.insert("content-length".to_string(), self.body.len().to_string()); + } + + if !headers.contains_key("server") { + headers.insert("server".to_string(), format!("GURT/{}", GURT_VERSION)); + } + + if !headers.contains_key("date") { + // RFC 7231 compliant + let now = Utc::now(); + let date_str = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); + headers.insert("date".to_string(), date_str); + } + + for (key, value) in &headers { + message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR)); + } + + message.push_str(HEADER_SEPARATOR); + + if !self.body.is_empty() { + if let Ok(body_str) = std::str::from_utf8(&self.body) { + message.push_str(body_str); + } + } + + message + } + + pub fn to_bytes(&self) -> Vec { + let mut message = format!("{}{} {} {}{}", + PROTOCOL_PREFIX, self.version, self.status_code, self.status_message, HEADER_SEPARATOR); + + let mut headers = self.headers.clone(); + if !headers.contains_key("content-length") { + headers.insert("content-length".to_string(), self.body.len().to_string()); + } + + if !headers.contains_key("server") { + headers.insert("server".to_string(), format!("GURT/{}", GURT_VERSION)); + } + + if !headers.contains_key("date") { + // RFC 7231 compliant + let now = Utc::now(); + let date_str = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); + headers.insert("date".to_string(), date_str); + } + + for (key, value) in &headers { + message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR)); + } + + message.push_str(HEADER_SEPARATOR); + + // Convert headers to bytes and append body as raw bytes + let mut bytes = message.into_bytes(); + bytes.extend_from_slice(&self.body); + + bytes + } +} + +#[derive(Debug, Clone)] +pub enum GurtMessage { + Request(GurtRequest), + Response(GurtResponse), +} + +impl GurtMessage { + pub fn parse(data: &str) -> Result { + Self::parse_bytes(data.as_bytes()) + } + + pub fn parse_bytes(data: &[u8]) -> Result { + // Convert first line to string to determine message type + let header_separator = HEADER_SEPARATOR.as_bytes(); + let first_line_end = data.windows(header_separator.len()) + .position(|window| window == header_separator) + .unwrap_or(data.len()); + + let first_line = std::str::from_utf8(&data[..first_line_end]) + .map_err(|_| GurtError::invalid_message("Invalid UTF-8 in first line"))?; + + // Check if it's a response (starts with GURT/version) or request (method first) + if first_line.starts_with(PROTOCOL_PREFIX) { + Ok(GurtMessage::Response(GurtResponse::parse_bytes(data)?)) + } else { + Ok(GurtMessage::Request(GurtRequest::parse_bytes(data)?)) + } + } + + pub fn is_request(&self) -> bool { + matches!(self, GurtMessage::Request(_)) + } + + pub fn is_response(&self) -> bool { + matches!(self, GurtMessage::Response(_)) + } + + pub fn as_request(&self) -> Option<&GurtRequest> { + match self { + GurtMessage::Request(req) => Some(req), + _ => None, + } + } + + pub fn as_response(&self) -> Option<&GurtResponse> { + match self { + GurtMessage::Response(res) => Some(res), + _ => None, + } + } +} + +impl fmt::Display for GurtMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GurtMessage::Request(req) => write!(f, "{}", req.to_string()), + GurtMessage::Response(res) => write!(f, "{}", res.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_request_parsing() { + let raw = "GET /test GURT/1.0.0\r\nHost: example.com\r\nAccept: text/html\r\n\r\ntest body"; + let request = GurtRequest::parse(raw).expect("Failed to parse request"); + + assert_eq!(request.method, GurtMethod::GET); + assert_eq!(request.path, "/test"); + assert_eq!(request.version, GURT_VERSION.to_string()); + assert_eq!(request.header("host"), Some(&"example.com".to_string())); + assert_eq!(request.header("accept"), Some(&"text/html".to_string())); + assert_eq!(request.body_as_string().unwrap(), "test body"); + } + + #[test] + fn test_response_parsing() { + let raw = "GURT/1.0.0 200 OK\r\nContent-Type: text/html\r\n\r\n"; + let response = GurtResponse::parse(raw).expect("Failed to parse response"); + + assert_eq!(response.version, GURT_VERSION.to_string()); + assert_eq!(response.status_code, 200); + assert_eq!(response.status_message, "OK"); + assert_eq!(response.header("content-type"), Some(&"text/html".to_string())); + assert_eq!(response.body_as_string().unwrap(), ""); + } + + #[test] + fn test_request_building() { + let request = GurtRequest::new(GurtMethod::GET, "/test".to_string()) + .with_header("Host", "example.com") + .with_string_body("test body"); + + let raw = request.to_string(); + let parsed = GurtRequest::parse(&raw).expect("Failed to parse built request"); + + assert_eq!(parsed.method, request.method); + assert_eq!(parsed.path, request.path); + assert_eq!(parsed.body, request.body); + } + + #[test] + fn test_response_building() { + let response = GurtResponse::ok() + .with_header("Content-Type", "text/html") + .with_string_body(""); + + let raw = response.to_string(); + let parsed = GurtResponse::parse(&raw).expect("Failed to parse built response"); + + assert_eq!(parsed.status_code, response.status_code); + assert_eq!(parsed.body, response.body); + } +} \ No newline at end of file diff --git a/protocol/library/src/protocol.rs b/protocol/library/src/protocol.rs new file mode 100644 index 0000000..9066d4b --- /dev/null +++ b/protocol/library/src/protocol.rs @@ -0,0 +1,120 @@ +use std::fmt; + +pub const GURT_VERSION: &str = "1.0.0"; +pub const DEFAULT_PORT: u16 = 4878; + +pub const PROTOCOL_PREFIX: &str = "GURT/"; + +pub const HEADER_SEPARATOR: &str = "\r\n"; +pub const BODY_SEPARATOR: &str = "\r\n\r\n"; + +pub const DEFAULT_HANDSHAKE_TIMEOUT: u64 = 5; +pub const DEFAULT_REQUEST_TIMEOUT: u64 = 30; +pub const DEFAULT_CONNECTION_TIMEOUT: u64 = 10; + +pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024; + +pub const MAX_POOL_SIZE: usize = 10; +pub const POOL_IDLE_TIMEOUT: u64 = 300; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GurtStatusCode { + // Success + Ok = 200, + Created = 201, + Accepted = 202, + NoContent = 204, + + // Handshake + SwitchingProtocols = 101, + + // Client errors + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403, + NotFound = 404, + MethodNotAllowed = 405, + Timeout = 408, + TooLarge = 413, + UnsupportedMediaType = 415, + + // Server errors + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, + GatewayTimeout = 504, +} + +impl GurtStatusCode { + pub fn from_u16(code: u16) -> Option { + match code { + 200 => Some(Self::Ok), + 201 => Some(Self::Created), + 202 => Some(Self::Accepted), + 204 => Some(Self::NoContent), + 101 => Some(Self::SwitchingProtocols), + 400 => Some(Self::BadRequest), + 401 => Some(Self::Unauthorized), + 403 => Some(Self::Forbidden), + 404 => Some(Self::NotFound), + 405 => Some(Self::MethodNotAllowed), + 408 => Some(Self::Timeout), + 413 => Some(Self::TooLarge), + 415 => Some(Self::UnsupportedMediaType), + 500 => Some(Self::InternalServerError), + 501 => Some(Self::NotImplemented), + 502 => Some(Self::BadGateway), + 503 => Some(Self::ServiceUnavailable), + 504 => Some(Self::GatewayTimeout), + _ => None, + } + } + + pub fn message(&self) -> &'static str { + match self { + Self::Ok => "OK", + Self::Created => "CREATED", + Self::Accepted => "ACCEPTED", + Self::NoContent => "NO_CONTENT", + Self::SwitchingProtocols => "SWITCHING_PROTOCOLS", + Self::BadRequest => "BAD_REQUEST", + Self::Unauthorized => "UNAUTHORIZED", + Self::Forbidden => "FORBIDDEN", + Self::NotFound => "NOT_FOUND", + Self::MethodNotAllowed => "METHOD_NOT_ALLOWED", + Self::Timeout => "TIMEOUT", + Self::TooLarge => "TOO_LARGE", + Self::UnsupportedMediaType => "UNSUPPORTED_MEDIA_TYPE", + Self::InternalServerError => "INTERNAL_SERVER_ERROR", + Self::NotImplemented => "NOT_IMPLEMENTED", + Self::BadGateway => "BAD_GATEWAY", + Self::ServiceUnavailable => "SERVICE_UNAVAILABLE", + Self::GatewayTimeout => "GATEWAY_TIMEOUT", + } + } + + pub fn is_success(&self) -> bool { + matches!(self, Self::Ok | Self::Created | Self::Accepted | Self::NoContent) + } + + pub fn is_client_error(&self) -> bool { + (*self as u16) >= 400 && (*self as u16) < 500 + } + + pub fn is_server_error(&self) -> bool { + (*self as u16) >= 500 + } +} + +impl From for u16 { + fn from(code: GurtStatusCode) -> Self { + code as u16 + } +} + +impl fmt::Display for GurtStatusCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", *self as u16) + } +} \ No newline at end of file diff --git a/protocol/library/src/server.rs b/protocol/library/src/server.rs new file mode 100644 index 0000000..f49e30d --- /dev/null +++ b/protocol/library/src/server.rs @@ -0,0 +1,563 @@ +use crate::{ + GurtError, Result, GurtRequest, GurtResponse, GurtMessage, + protocol::{BODY_SEPARATOR, MAX_MESSAGE_SIZE}, + message::GurtMethod, + protocol::GurtStatusCode, + crypto::{TLS_VERSION, GURT_ALPN, TlsConfig}, +}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_rustls::{TlsAcceptor, server::TlsStream}; +use rustls::pki_types::CertificateDer; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::fs; +use tracing::{info, warn, error, debug}; + +#[derive(Debug, Clone)] +pub struct ServerContext { + pub remote_addr: SocketAddr, + pub request: GurtRequest, +} + +impl ServerContext { + pub fn client_ip(&self) -> std::net::IpAddr { + self.remote_addr.ip() + } + + pub fn client_port(&self) -> u16 { + self.remote_addr.port() + } + + + pub fn method(&self) -> &GurtMethod { + &self.request.method + } + + pub fn path(&self) -> &str { + &self.request.path + } + + pub fn headers(&self) -> &HashMap { + &self.request.headers + } + + pub fn body(&self) -> &[u8] { + &self.request.body + } + + pub fn body_as_string(&self) -> Result { + self.request.body_as_string() + } + + pub fn header(&self, key: &str) -> Option<&String> { + self.request.header(key) + } +} + +pub trait GurtHandler: Send + Sync { + fn handle(&self, ctx: &ServerContext) -> std::pin::Pin> + Send + '_>>; +} + +pub struct FnHandler { + handler: F, +} + +impl GurtHandler for FnHandler +where + F: Fn(&ServerContext) -> Fut + Send + Sync, + Fut: std::future::Future> + Send + 'static, +{ + fn handle(&self, ctx: &ServerContext) -> std::pin::Pin> + Send + '_>> { + Box::pin((self.handler)(ctx)) + } +} + +#[derive(Debug, Clone)] +pub struct Route { + method: Option, + path_pattern: String, +} + +impl Route { + pub fn new(method: Option, path_pattern: String) -> Self { + Self { method, path_pattern } + } + + pub fn get(path: &str) -> Self { + Self::new(Some(GurtMethod::GET), path.to_string()) + } + + pub fn post(path: &str) -> Self { + Self::new(Some(GurtMethod::POST), path.to_string()) + } + + pub fn put(path: &str) -> Self { + Self::new(Some(GurtMethod::PUT), path.to_string()) + } + + pub fn delete(path: &str) -> Self { + Self::new(Some(GurtMethod::DELETE), path.to_string()) + } + + pub fn head(path: &str) -> Self { + Self::new(Some(GurtMethod::HEAD), path.to_string()) + } + + pub fn options(path: &str) -> Self { + Self::new(Some(GurtMethod::OPTIONS), path.to_string()) + } + + pub fn patch(path: &str) -> Self { + Self::new(Some(GurtMethod::PATCH), path.to_string()) + } + + pub fn any(path: &str) -> Self { + Self::new(None, path.to_string()) + } + + pub fn matches(&self, method: &GurtMethod, path: &str) -> bool { + if let Some(route_method) = &self.method { + if route_method != method { + return false; + } + } + + self.matches_path(path) + } + + pub fn matches_path(&self, path: &str) -> bool { + self.path_pattern == path || + (self.path_pattern.ends_with('*') && path.starts_with(&self.path_pattern[..self.path_pattern.len()-1])) + } +} + +pub struct GurtServer { + routes: Vec<(Route, Arc)>, + tls_acceptor: Option, +} + +impl GurtServer { + pub fn new() -> Self { + Self { + routes: Vec::new(), + tls_acceptor: None, + } + } + + pub fn with_tls_certificates(cert_path: &str, key_path: &str) -> Result { + let mut server = Self::new(); + server.load_tls_certificates(cert_path, key_path)?; + Ok(server) + } + + pub fn load_tls_certificates(&mut self, cert_path: &str, key_path: &str) -> Result<()> { + info!("Loading TLS certificates: cert={}, key={}", cert_path, key_path); + + let cert_data = fs::read(cert_path) + .map_err(|e| GurtError::crypto(format!("Failed to read certificate file '{}': {}", cert_path, e)))?; + + let key_data = fs::read(key_path) + .map_err(|e| GurtError::crypto(format!("Failed to read private key file '{}': {}", key_path, e)))?; + + let mut cursor = std::io::Cursor::new(cert_data); + let certs: Vec> = rustls_pemfile::certs(&mut cursor) + .collect::, _>>() + .map_err(|e| GurtError::crypto(format!("Failed to parse certificates: {}", e)))?; + + if certs.is_empty() { + return Err(GurtError::crypto("No certificates found in certificate file")); + } + + let mut key_cursor = std::io::Cursor::new(key_data); + let private_key = rustls_pemfile::private_key(&mut key_cursor) + .map_err(|e| GurtError::crypto(format!("Failed to parse private key: {}", e)))? + .ok_or_else(|| GurtError::crypto("No private key found in key file"))?; + + let tls_config = TlsConfig::new_server(certs, private_key)?; + self.tls_acceptor = Some(tls_config.get_acceptor()?); + + info!("TLS certificates loaded successfully"); + Ok(()) + } + + pub fn route(mut self, route: Route, handler: H) -> Self + where + H: GurtHandler + 'static, + { + self.routes.push((route, Arc::new(handler))); + self + } + + pub fn get(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::get(path), FnHandler { handler }) + } + + pub fn post(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::post(path), FnHandler { handler }) + } + + pub fn put(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::put(path), FnHandler { handler }) + } + + pub fn delete(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::delete(path), FnHandler { handler }) + } + + pub fn head(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::head(path), FnHandler { handler }) + } + + pub fn options(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::options(path), FnHandler { handler }) + } + + pub fn patch(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::patch(path), FnHandler { handler }) + } + + pub fn any(self, path: &str, handler: F) -> Self + where + F: Fn(&ServerContext) -> Fut + Send + Sync + 'static, + Fut: std::future::Future> + Send + 'static, + { + self.route(Route::any(path), FnHandler { handler }) + } + + pub async fn listen(self, addr: &str) -> Result<()> { + let listener = TcpListener::bind(addr).await?; + info!("GURT server listening on {}", addr); + + loop { + match listener.accept().await { + Ok((stream, addr)) => { + info!("Client connected: {}", addr); + let server = self.clone(); + + tokio::spawn(async move { + if let Err(e) = server.handle_connection(stream, addr).await { + error!("Connection error from {}: {}", addr, e); + } + info!("Client disconnected: {}", addr); + }); + } + Err(e) => { + error!("Failed to accept connection: {}", e); + } + } + } + } + + async fn handle_connection(&self, mut stream: TcpStream, addr: SocketAddr) -> Result<()> { + self.handle_initial_handshake(&mut stream, addr).await?; + + if let Some(tls_acceptor) = &self.tls_acceptor { + info!("Upgrading connection to TLS for {}", addr); + let tls_stream = tls_acceptor.accept(stream).await + .map_err(|e| GurtError::crypto(format!("TLS upgrade failed: {}", e)))?; + + info!("TLS upgrade completed for {}", addr); + + self.handle_tls_connection(tls_stream, addr).await + } else { + warn!("No TLS configuration available, but handshake completed - this violates GURT protocol"); + Err(GurtError::protocol("TLS is required after handshake but no TLS configuration available")) + } + } + + async fn handle_initial_handshake(&self, stream: &mut TcpStream, addr: SocketAddr) -> Result<()> { + let mut buffer = Vec::new(); + let mut temp_buffer = [0u8; 8192]; + + loop { + let bytes_read = stream.read(&mut temp_buffer).await?; + if bytes_read == 0 { + return Err(GurtError::connection("Connection closed during handshake")); + } + + buffer.extend_from_slice(&temp_buffer[..bytes_read]); + + let body_separator = BODY_SEPARATOR.as_bytes(); + if buffer.windows(body_separator.len()).any(|w| w == body_separator) { + break; + } + + if buffer.len() > MAX_MESSAGE_SIZE { + return Err(GurtError::protocol("Handshake message too large")); + } + } + + + let message = GurtMessage::parse_bytes(&buffer)?; + + match message { + GurtMessage::Request(request) => { + if request.method == GurtMethod::HANDSHAKE { + self.send_handshake_response(stream, addr, &request).await + } else { + Err(GurtError::protocol("First message must be HANDSHAKE")) + } + } + GurtMessage::Response(_) => { + Err(GurtError::protocol("Server received response during handshake")) + } + } + } + + async fn handle_tls_connection(&self, mut tls_stream: TlsStream, addr: SocketAddr) -> Result<()> { + let mut buffer = Vec::new(); + let mut temp_buffer = [0u8; 8192]; + + loop { + let bytes_read = match tls_stream.read(&mut temp_buffer).await { + Ok(n) => n, + Err(e) => { + // Handle UnexpectedEof from clients that don't send close_notify + if e.kind() == std::io::ErrorKind::UnexpectedEof { + debug!("Client {} closed connection without TLS close_notify (benign)", addr); + break; + } + return Err(e.into()); + } + }; + if bytes_read == 0 { + break; // Connection closed + } + + buffer.extend_from_slice(&temp_buffer[..bytes_read]); + + let body_separator = BODY_SEPARATOR.as_bytes(); + let has_complete_message = buffer.windows(body_separator.len()).any(|w| w == body_separator) || + (buffer.starts_with(b"{") && buffer.ends_with(b"}")); + + if has_complete_message { + if let Err(e) = self.process_tls_message(&mut tls_stream, addr, &buffer).await { + error!("Encrypted message processing error from {}: {}", addr, e); + let error_response = GurtResponse::internal_server_error() + .with_string_body("Internal server error"); + let _ = tls_stream.write_all(&error_response.to_bytes()).await; + } + + buffer.clear(); + } + + // Prevent buffer overflow + if buffer.len() > MAX_MESSAGE_SIZE { + warn!("Message too large from {}, closing connection", addr); + break; + } + } + + Ok(()) + } + + async fn send_handshake_response(&self, stream: &mut TcpStream, addr: SocketAddr, _request: &GurtRequest) -> Result<()> { + info!("Sending handshake response to {}", addr); + + let response = GurtResponse::new(GurtStatusCode::SwitchingProtocols) + .with_header("GURT-Version", crate::GURT_VERSION.to_string()) + .with_header("Encryption", TLS_VERSION) + .with_header("ALPN", std::str::from_utf8(GURT_ALPN).unwrap_or("gurt/1.0")); + + let response_bytes = response.to_string().into_bytes(); + stream.write_all(&response_bytes).await?; + + info!("Handshake response sent to {}, preparing for TLS upgrade", addr); + Ok(()) + } + + async fn process_tls_message(&self, tls_stream: &mut TlsStream, addr: SocketAddr, data: &[u8]) -> Result<()> { + let message = GurtMessage::parse_bytes(data)?; + + match message { + GurtMessage::Request(request) => { + if request.method == GurtMethod::HANDSHAKE { + Err(GurtError::protocol("Received HANDSHAKE over TLS - protocol violation")) + } else { + self.handle_encrypted_request(tls_stream, addr, &request).await + } + } + GurtMessage::Response(_) => { + warn!("Received response on server, ignoring"); + Ok(()) + } + } + } + + async fn handle_default_options(&self, tls_stream: &mut TlsStream, request: &GurtRequest) -> Result<()> { + let mut allowed_methods = std::collections::HashSet::new(); + + for (route, _) in &self.routes { + if route.matches_path(&request.path) { + if let Some(method) = &route.method { + allowed_methods.insert(method.to_string()); + } else { + // Route matches any method + allowed_methods.extend(vec![ + "GET".to_string(), "POST".to_string(), "PUT".to_string(), + "DELETE".to_string(), "HEAD".to_string(), "PATCH".to_string() + ]); + } + } + } + + allowed_methods.insert("OPTIONS".to_string()); + + let mut allowed_methods_vec: Vec = allowed_methods.into_iter().collect(); + allowed_methods_vec.sort(); + let allow_header = allowed_methods_vec.join(", "); + + let response = GurtResponse::ok() + .with_header("Allow", allow_header) + .with_header("Access-Control-Allow-Origin", "*") + .with_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH") + .with_header("Access-Control-Allow-Headers", "Content-Type, Authorization"); + + tls_stream.write_all(&response.to_bytes()).await?; + Ok(()) + } + + async fn handle_default_head(&self, tls_stream: &mut TlsStream, addr: SocketAddr, request: &GurtRequest) -> Result<()> { + for (route, handler) in &self.routes { + if route.method == Some(GurtMethod::GET) && route.matches(&GurtMethod::GET, &request.path) { + let context = ServerContext { + remote_addr: addr, + request: request.clone(), + }; + + match handler.handle(&context).await { + Ok(mut response) => { + let original_content_length = response.body.len(); + response.body.clear(); + response = response.with_header("content-length", original_content_length.to_string()); + + tls_stream.write_all(&response.to_bytes()).await?; + return Ok(()); + } + Err(e) => { + error!("Handler error for HEAD {} (via GET): {}", request.path, e); + let error_response = GurtResponse::internal_server_error(); + tls_stream.write_all(&error_response.to_bytes()).await?; + return Ok(()); + } + } + } + } + + let not_found_response = GurtResponse::not_found(); + tls_stream.write_all(¬_found_response.to_bytes()).await?; + Ok(()) + } + + async fn handle_encrypted_request(&self, tls_stream: &mut TlsStream, addr: SocketAddr, request: &GurtRequest) -> Result<()> { + debug!("Handling encrypted {} request to {} from {}", request.method, request.path, addr); + + // Find matching route + for (route, handler) in &self.routes { + if route.matches(&request.method, &request.path) { + let context = ServerContext { + remote_addr: addr, + request: request.clone(), + }; + + match handler.handle(&context).await { + Ok(response) => { + // Use to_bytes() to avoid corrupting binary data + let response_bytes = response.to_bytes(); + tls_stream.write_all(&response_bytes).await?; + return Ok(()); + } + Err(e) => { + error!("Handler error for {} {}: {}", request.method, request.path, e); + let error_response = GurtResponse::internal_server_error() + .with_string_body("Internal server error"); + tls_stream.write_all(&error_response.to_bytes()).await?; + return Ok(()); + } + } + } + } + + // No route found - check for default OPTIONS/HEAD handling + match request.method { + GurtMethod::OPTIONS => { + self.handle_default_options(tls_stream, request).await + } + GurtMethod::HEAD => { + self.handle_default_head(tls_stream, addr, request).await + } + _ => { + let not_found_response = GurtResponse::not_found() + .with_string_body("Not found"); + tls_stream.write_all(¬_found_response.to_bytes()).await?; + Ok(()) + } + } + } +} + +impl Clone for GurtServer { + fn clone(&self) -> Self { + Self { + routes: self.routes.clone(), + tls_acceptor: self.tls_acceptor.clone(), + } + } +} + +impl Default for GurtServer { + fn default() -> Self { + Self::new() + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use tokio::test; + + #[test] + async fn test_route_matching() { + let route = Route::get("/test"); + assert!(route.matches(&GurtMethod::GET, "/test")); + assert!(!route.matches(&GurtMethod::POST, "/test")); + assert!(!route.matches(&GurtMethod::GET, "/other")); + + let wildcard_route = Route::get("/api/*"); + assert!(wildcard_route.matches(&GurtMethod::GET, "/api/users")); + assert!(wildcard_route.matches(&GurtMethod::GET, "/api/posts")); + assert!(!wildcard_route.matches(&GurtMethod::GET, "/other")); + } + +} \ No newline at end of file