diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bc700af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: face-hh + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +Or code example: +```html +

Hello

+``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f455865 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/README.md b/README.md index 34df8b1..e60be70 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [Website](https://gurted.com/) | [Docs](https://docs.gurted.com/) | [License](LICENSE) | [YouTube video](https://www.youtube.com) Gurted is an ecosystem similar to the World Wide Web, it features: -- ⚡ A custom protocol (TCP-based) named `GURT://` with mendatory TLS secutity with a [spec](docs.gurted.com) +- ⚡ A custom protocol (TCP-based) named `GURT://` with mandatory TLS security with a [spec](docs.gurted.com) - 🌐 A custom **wayfinder** (browser) written in Rust and GDScript with [Godot](https://godotengine.org/) - 📄 A custom engine for HTML, CSS, and ***Lua*** (no JavaScript) - 🏷️ A custom **DNS** that allows users to create domains with TLDs such as `.based`, `.aura`, `.twin`, and many more diff --git a/dns/Cargo.lock b/dns/Cargo.lock index afce782..b84c52f 100644 --- a/dns/Cargo.lock +++ b/dns/Cargo.lock @@ -972,7 +972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gurt" +name = "gurtlib" version = "0.1.0" dependencies = [ "base64 0.22.1", @@ -3410,7 +3410,7 @@ dependencies = [ "clap-verbosity-flag", "colored", "futures", - "gurt", + "gurtlib", "jsonwebtoken", "log", "macros-rs", diff --git a/dns/Cargo.toml b/dns/Cargo.toml index ee2af9e..a7fba23 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -17,7 +17,7 @@ anyhow = "1.0.86" futures = "0.3.30" macros-rs = "1.2.1" prettytable = "0.10.0" -gurt = { path = "../protocol/library" } +gurtlib = { path = "../protocol/library" } pretty_env_logger = "0.5.0" clap-verbosity-flag = "2.2.0" diff --git a/dns/src/auth.rs b/dns/src/auth.rs index 4f267f6..9e78a71 100644 --- a/dns/src/auth.rs +++ b/dns/src/auth.rs @@ -1,4 +1,4 @@ -use gurt::prelude::*; +use gurtlib::prelude::*; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm}; use serde::{Deserialize, Serialize}; use bcrypt::{hash, verify, DEFAULT_COST}; diff --git a/dns/src/crypto.rs b/dns/src/crypto.rs index 6e0b4b6..8400eb8 100644 --- a/dns/src/crypto.rs +++ b/dns/src/crypto.rs @@ -4,7 +4,6 @@ use openssl::rsa::Rsa; use openssl::x509::X509Req; use openssl::x509::X509Name; use openssl::hash::MessageDigest; -use std::process::Command; pub fn generate_ca_cert() -> Result<(String, String)> { let rsa = Rsa::generate(4096)?; @@ -60,7 +59,8 @@ pub fn sign_csr_with_ca( csr_pem: &str, ca_cert_pem: &str, ca_key_pem: &str, - domain: &str + domain: &str, + client_ip: Option<&str> ) -> Result { let ca_cert = openssl::x509::X509::from_pem(ca_cert_pem.as_bytes())?; let ca_key = PKey::private_key_from_pem(ca_key_pem.as_bytes())?; @@ -92,8 +92,11 @@ pub fn sign_csr_with_ca( .dns("localhost") .ip("127.0.0.1"); - if let Ok(public_ip) = get_public_ip() { - san_builder.ip(&public_ip); + if let Some(ip) = client_ip { + if is_valid_ip(ip) && ip != "127.0.0.1" { + san_builder.ip(ip); + println!("Added client IP {} to certificate for {}", ip, domain); + } } let subject_alt_name = san_builder.build(&context)?; @@ -119,50 +122,6 @@ pub fn sign_csr_with_ca( Ok(String::from_utf8(cert_pem)?) } -fn get_public_ip() -> Result> { - // Method 1: Check if we can get it from environment or interface - if let Ok(output) = Command::new("curl") - .args(&["-s", "--max-time", "5", "https://api.ipify.org"]) - .output() - { - if output.status.success() { - let ip = String::from_utf8(output.stdout)?.trim().to_string(); - if is_valid_ip(&ip) { - return Ok(ip); - } - } - } - - // Method 2: Try ifconfig.me - if let Ok(output) = Command::new("curl") - .args(&["-s", "--max-time", "5", "https://ifconfig.me/ip"]) - .output() - { - if output.status.success() { - let ip = String::from_utf8(output.stdout)?.trim().to_string(); - if is_valid_ip(&ip) { - return Ok(ip); - } - } - } - - // Method 3: Try to get from network interfaces - if let Ok(output) = Command::new("hostname") - .args(&["-I"]) - .output() - { - if output.status.success() { - let ips = String::from_utf8(output.stdout)?; - for ip in ips.split_whitespace() { - if is_valid_ip(ip) && !ip.starts_with("127.") && !ip.starts_with("192.168.") && !ip.starts_with("10.") { - return Ok(ip.to_string()); - } - } - } - } - - Err("Could not determine public IP".into()) -} fn is_valid_ip(ip: &str) -> bool { ip.split('.') diff --git a/dns/src/gurt_server.rs b/dns/src/gurt_server.rs index 44ac1be..8462536 100644 --- a/dns/src/gurt_server.rs +++ b/dns/src/gurt_server.rs @@ -8,9 +8,9 @@ use crate::{auth::jwt_middleware_gurt, config::Config, discord_bot}; use colored::Colorize; use macros_rs::fmt::{crashln, string}; use std::{sync::Arc, collections::HashMap}; -use gurt::prelude::*; +use gurtlib::prelude::*; use warp::Filter; -use gurt::{GurtStatusCode, Route}; +use gurtlib::{GurtStatusCode, Route}; #[derive(Debug)] struct CertificateError; diff --git a/dns/src/gurt_server/auth_routes.rs b/dns/src/gurt_server/auth_routes.rs index c9e6d88..9a85972 100644 --- a/dns/src/gurt_server/auth_routes.rs +++ b/dns/src/gurt_server/auth_routes.rs @@ -1,7 +1,7 @@ use super::{models::*, AppState}; use crate::auth::*; -use gurt::prelude::*; -use gurt::GurtStatusCode; +use gurtlib::prelude::*; +use gurtlib::GurtStatusCode; use sqlx::Row; use chrono::Utc; diff --git a/dns/src/gurt_server/routes.rs b/dns/src/gurt_server/routes.rs index c7757f1..c09fb7e 100644 --- a/dns/src/gurt_server/routes.rs +++ b/dns/src/gurt_server/routes.rs @@ -2,7 +2,7 @@ use super::{models::*, AppState}; use crate::auth::Claims; use crate::discord_bot::{send_domain_approval_request, DomainRegistration}; use base64::{engine::general_purpose, Engine as _}; -use gurt::prelude::*; +use gurtlib::prelude::*; use rand::{rngs::OsRng, Rng}; use sha2::{Digest, Sha256}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -1056,7 +1056,7 @@ pub(crate) async fn get_certificate( .map_err(|_| GurtError::invalid_message("Database error"))?; if txt_records.is_empty() { - return Ok(GurtResponse::new(gurt::GurtStatusCode::Accepted) + return Ok(GurtResponse::new(gurtlib::GurtStatusCode::Accepted) .with_string_body("Challenge not completed yet")); } @@ -1072,6 +1072,7 @@ pub(crate) async fn get_certificate( &ca_cert.ca_cert_pem, &ca_cert.ca_key_pem, &domain, + Some(&ctx.client_ip().to_string()), ) .map_err(|e| { log::error!("Failed to sign certificate: {}", e); diff --git a/docs/docs/gurt-client.md b/docs/docs/gurt-client.md index 1c9ef38..286296a 100644 --- a/docs/docs/gurt-client.md +++ b/docs/docs/gurt-client.md @@ -14,13 +14,13 @@ The GURT client library (for Rust) provides a high-level, HTTP-like interface fo Install via Cargo: ```bash -cargo add gurt +cargo add gurtlib ``` ## Quick Start ```rust -use gurt::prelude::*; +use gurtlib::prelude::*; #[tokio::main] async fn main() -> Result<()> { @@ -223,7 +223,7 @@ The client extracts: ### Error Types ```rust -use gurt::GurtError; +use gurtlib::GurtError; match client.get("gurt://invalid-url").await { Ok(response) => { @@ -268,7 +268,7 @@ We expect the community to implement bindings for other languages, such as Pytho ## Example: Building a GURT API Client ```rust -use gurt::prelude::*; +use gurtlib::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Serialize)] diff --git a/docs/docs/gurt-server.md b/docs/docs/gurt-server.md index a081151..8bcdc71 100644 --- a/docs/docs/gurt-server.md +++ b/docs/docs/gurt-server.md @@ -12,7 +12,7 @@ Add the GURT library to your `Cargo.toml`: ```toml [dependencies] -gurt = "0.1" +gurtlib = "0.1" tokio = { version = "1.0", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" @@ -22,7 +22,7 @@ serde_json = "1.0" ## Quick Start ```rust -use gurt::prelude::*; +use gurtlib::prelude::*; use serde_json::json; #[tokio::main] @@ -442,7 +442,7 @@ server.listen("[::1]:4878").await?; #[cfg(test)] mod tests { use super::*; - use gurt::GurtClient; + use gurtlib::GurtClient; #[tokio::test] async fn test_server() { diff --git a/docs/docs/lua/canvas.md b/docs/docs/lua/canvas.md index 0380dbf..df13c1e 100644 --- a/docs/docs/lua/canvas.md +++ b/docs/docs/lua/canvas.md @@ -215,21 +215,18 @@ local shaderCtx = canvas:withContext('shader') shaderCtx:source([[ shader_type canvas_item; - - uniform float time : hint_range(0.0, 10.0) = 1.0; - uniform vec2 resolution; - + void fragment() { vec2 uv = UV; // Create animated rainbow effect vec3 color = vec3( - 0.5 + 0.5 * cos(time + uv.x * 6.0), - 0.5 + 0.5 * cos(time + uv.y * 6.0 + 2.0), - 0.5 + 0.5 * cos(time + (uv.x + uv.y) * 6.0 + 4.0) + 0.5 + 0.5 * cos(TIME + uv.x * 6.0), + 0.5 + 0.5 * cos(TIME + uv.y * 6.0 + 2.0), + 0.5 + 0.5 * cos(TIME + (uv.x + uv.y) * 6.0 + 4.0) ); COLOR = vec4(color, 1.0); } ]]) -``` \ No newline at end of file +``` diff --git a/docs/docs/lua/crumbs.md b/docs/docs/lua/crumbs.md new file mode 100644 index 0000000..dd8df80 --- /dev/null +++ b/docs/docs/lua/crumbs.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 9 +--- + +# Crumbs + +The Crumbs API provides **client-side storage** similar to browser cookies. Crumbs are stored locally and can have optional expiration times. They are domain-specific, meaning each GURT domain has its own isolated storage. + +Storage location: `%APPDATA%/Flumi/crumbs/[domain].json` + +## API Reference + +### gurt.crumbs.set(options) + +Sets a crumb with the specified name and value. + +**Parameters:** +- `options` (table) - Configuration object with the following fields: + - `name` (string, required) - The crumb name/key + - `value` (string, required) - The crumb value + - `lifetime` (number, optional) - Lifetime in seconds. If omitted, the crumb persists indefinitely + +```lua +-- Set a permanent crumb +gurt.crumbs.set({ + name = "username", + value = "gurted_user" +}) + +-- Set a temporary crumb (expires in 10 seconds) +gurt.crumbs.set({ + name = "session_token", + value = "abc123def456", + lifetime = 10 +}) + +-- Set a short-lived crumb (expires in 30 seconds) +gurt.crumbs.set({ + name = "temp_data", + value = "temporary_value", + lifetime = 30 +}) +``` + +### gurt.crumbs.get(name) + +Retrieves a crumb value by name. Returns `nil` if the crumb doesn't exist or has expired. + +**Parameters:** +- `name` (string) - The crumb name to retrieve + +**Returns:** +- `string` - The crumb value, or `nil` if not found/expired + +```lua +-- Get a crumb value +local username = gurt.crumbs.get("username") +if username then + trace.log("Welcome back, " .. username .. "!") +else + trace.log("No username found") +end +``` + +### gurt.crumbs.delete(name) + +Deletes a crumb by name. + +**Parameters:** +- `name` (string) - The crumb name to delete + +**Returns:** +- `boolean` - `true` if the crumb existed and was deleted, `false` if it didn't exist + +```lua +local wasDeleted = gurt.crumbs.delete("session_token") +if wasDeleted then + trace.log("Session token removed") +else + trace.log("Session token was not found") +end + +gurt.crumbs.delete("temp_data") +``` + +### gurt.crumbs.getAll() + +Retrieves all non-expired crumbs for the current domain. + +**Returns:** +- `table` - A table where keys are crumb names and values are crumb objects + +Each crumb object contains: +- `name` (string) - The crumb name +- `value` (string) - The crumb value +- `expiry` (number, optional) - Unix timestamp when the crumb expires (only present for temporary crumbs) + +```lua +local allCrumbs = gurt.crumbs.getAll() + +for name, crumb in pairs(allCrumbs) do + trace.log("Crumb: " .. name .. " = " .. crumb.value) + + if crumb.expiry then + local remaining = crumb.expiry - (Time.now() / 1000) + trace.log(" Expires in " .. math.floor(remaining) .. " seconds") + else + trace.log(" Permanent crumb") + end +end +``` + +### File Storage Format + +Crumbs are stored in JSON files: + +```json +{ + "username": { + "name": "username", + "value": "alice", + "created_at": 1672531200.0, + "lifespan": -1.0 + }, + "session_token": { + "name": "session_token", + "value": "abc123", + "created_at": 1672531200.0, + "lifespan": 3600.0 + } +} +``` diff --git a/docs/docs/lua/elements.md b/docs/docs/lua/elements.md index c03e35b..bcbf8a5 100644 --- a/docs/docs/lua/elements.md +++ b/docs/docs/lua/elements.md @@ -55,6 +55,39 @@ for i = 1, #children do end ``` +### element.size + +Gets the size of an element in pixels. + +```lua +local box = gurt.select('#my-box') +local size = box.size + +trace.log('Width: ' .. size.width .. 'px') +trace.log('Height: ' .. size.height .. 'px') + +if size.width == size.height then + trace.log('Element is square') +end +``` + +### element.position + +Gets the position of an element relative to its parent. + +```lua +local element = gurt.select('#positioned-element') +local pos = element.position + +trace.log('X position: ' .. pos.x .. 'px') +trace.log('Y position: ' .. pos.y .. 'px') + +-- Check if element is at origin +if pos.x == 0 and pos.y == 0 then + trace.log('Element is at origin') +end +``` + ## DOM Traversal ### element.parent @@ -96,11 +129,19 @@ Adds an event listener. Returns a subscription object. local button = gurt.select('#my-button') -- Click event -local subscription = button:on('click', function() - trace.log('Button clicked!') +local subscription = button:on('click', function(event) + trace.log('Button clicked at: ' .. event.x .. ', ' .. event.y) end) -- Mouse events +button:on('mousedown', function(event) + trace.log('Mouse down at: ' .. event.x .. ', ' .. event.y) +end) + +button:on('mouseup', function(event) + trace.log('Mouse up at: ' .. event.x .. ', ' .. event.y) +end) + button:on('mouseenter', function() button.classList:add('hover-effect') end) @@ -128,6 +169,30 @@ end) subscription:unsubscribe() ``` +#### Mouse Event Positioning + +Mouse events (`click`, `mousedown`, `mouseup`) provide position information relative to the element: + +```lua +local element = gurt.select('#interactive-element') + +element:on('click', function(event) + -- event.x and event.y are relative to the element's top-left corner + local elementX = event.x + local elementY = event.y + + trace.log('Clicked at (' .. elementX .. ', ' .. elementY .. ') within element') + + local size = element.size + local pos = element.position + + local xPercent = (elementX / size.width) * 100 + local yPercent = (elementY / size.height) * 100 + + trace.log('Clicked at ' .. xPercent .. '% horizontally, ' .. yPercent .. '% vertically') +end) +``` + ### element:append(childElement) Adds a child element. diff --git a/docs/docs/lua/intro.md b/docs/docs/lua/intro.md index d9b99ca..6201f98 100644 --- a/docs/docs/lua/intro.md +++ b/docs/docs/lua/intro.md @@ -112,6 +112,25 @@ end -- Get all values for a parameter (for repeated params) local tags = gurt.location.query.getAll('tag') ``` + +### gurt.width() + +Gets the available width of the site page viewport in pixels. + +```lua +local pageWidth = gurt.width() +trace.log('Page width: ' .. pageWidth .. ' pixels') +``` + +### gurt.height() + +Gets the available height of the site page viewport in pixels. + +```lua +local pageHeight = gurt.height() +trace.log('Page height: ' .. pageHeight .. ' pixels') +``` + ## Global: trace The global trace table for logging messages to the console. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 7281243..a899cb9 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -45,6 +45,7 @@ const sidebars: SidebarsConfig = { items: [ 'lua/intro', 'lua/elements', + 'lua/crumbs', 'lua/audio', 'lua/canvas', 'lua/network', diff --git a/flumi/Assets/arson-ca.crt b/flumi/Assets/arson-ca.crt index c412709..01ad78d 100644 --- a/flumi/Assets/arson-ca.crt +++ b/flumi/Assets/arson-ca.crt @@ -1,2 +1,28 @@ -----BEGIN CERTIFICATE----- +MIIEvjCCAyagAwIBAgIRAOgNdrOTI5GLs9YoNMGXILkwDQYJKoZIhvcNAQELBQAw +dzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSYwJAYDVQQLDB1zbWFy +dGNvZGVyQG5peG9zIChTbWFydGNvZGVyKTEtMCsGA1UEAwwkbWtjZXJ0IHNtYXJ0 +Y29kZXJAbml4b3MgKFNtYXJ0Y29kZXIpMB4XDTI1MDkwODA4MDIxN1oXDTM1MDkw +ODA4MDIxN1owdzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSYwJAYD +VQQLDB1zbWFydGNvZGVyQG5peG9zIChTbWFydGNvZGVyKTEtMCsGA1UEAwwkbWtj +ZXJ0IHNtYXJ0Y29kZXJAbml4b3MgKFNtYXJ0Y29kZXIpMIIBojANBgkqhkiG9w0B +AQEFAAOCAY8AMIIBigKCAYEAqkhX/N1Mjzoq9CgdREOFkW+TPeo5yshLHVFqWiY/ +8gxmuFd6kK+TfnsUKeDKK2z/KdZWlWjSxFdvZSn19TPsZ1OLgn0SbWXvUiFjjlJk +/bgVU7bwAR+TvEOMJ/TTK/T9LkWLK4pQov5+LtnuLh5s0aLUW/eV1OcYNdQGuEhD +IzN8ITp7vZeKTAB0TGu+hVG+xsVdnRPsmugP0EQy7jBtB4KjK+CCxkMYQRS17h22 +RyDDwvtLIGWWHfza6M1MkEqfTevzBR/3fAt4kFKt19p+pVG7bHuVxHaVdiMLPq0h +vR52ELxNb3v8j7a0ZTQG3sym+1J0Avr5z5onuPx0rxsaLoX7NvzPQZk+hxEYynOP +f0lgoMOJEcMPXcj+dEBpB5Q5igR9OEs53wINYvN5lOw6X466DZH8ofr/h8uAtYBp +y9DXdbOddTFSIHfYDlQsdK8txgrW0kgs1raoS8h2EI9CETCjKzeuID6bp1/6K96n +G5pk5aat+ElBkWa4o4OCiRWRAgMBAAGjRTBDMA4GA1UdDwEB/wQEAwICBDASBgNV +HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQysYpcKzkvcIPrQQDo/olzWnKGoTAN +BgkqhkiG9w0BAQsFAAOCAYEAcjstUc9k2kgwodhyh+R35p+SZRDGuWgBZVn7wHFS +4W7TuSyOw7RLHlGOtJ7SxzqKoOQmMaC0IhFN50znE9FocjcHOXMcRQpc/ql18Ohh +hVIqli5GnR3N+JRWGo234BZU73SPkskQnP4xw6gZKwwTiMjMltuY5KuRuCQpk4/w +Avn9k8fgrFidYu6PAYzx+r/JyVYISzLLoowgK8hK4bNWIujESvF87NxcNGwV/+j/ +q1sI+yKplw35Jjvhg0UBNrRAbujBjIthL6rPLrqk1e/modoOE2shT/QelmkJUtOZ +aAZVMSQfKn6zsqypHqeBdJg8djDT9YqQZDu2l6yOQSiGb81pJxHyPYso17JkjvKA +SyluR0RtiTeib7VQCVIHjgcfFyQP1jBELk2Yq9HeYMg89M1U1AThI4JMCA6ukYVR +oktSBQJdPEQYu1Geve/UU04g5JzstBMrET9EsRlyfg2/B3o2/TbZIlRPbAIRdVhn +XsC+LWBb5waKWhq6Ti+6CNuv -----END CERTIFICATE----- diff --git a/flumi/Scenes/BrowserMenus/help.tscn b/flumi/Scenes/BrowserMenus/help.tscn index 7ad75c3..9cf464d 100644 --- a/flumi/Scenes/BrowserMenus/help.tscn +++ b/flumi/Scenes/BrowserMenus/help.tscn @@ -213,7 +213,6 @@ size_flags_horizontal = 3 theme_override_constants/separation = 0 [node name="ShortcutsPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"] -visible = false layout_mode = 2 theme_override_constants/separation = 20 @@ -304,6 +303,18 @@ theme = ExtResource("2_theme") theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) text = "Focus address bar" +[node name="Label6" type="Label" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel/NavigationSection/VBoxContainer/GridContainer"] +layout_mode = 2 +theme = ExtResource("2_theme") +theme_override_colors/font_color = Color(0.8, 0.8, 0.8, 1) +text = "Ctrl+R" + +[node name="Description6" type="Label" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel/NavigationSection/VBoxContainer/GridContainer"] +layout_mode = 2 +theme = ExtResource("2_theme") +theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) +text = "Refresh tab" + [node name="WindowSection" type="PanelContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack/ShortcutsPanel"] layout_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_section") @@ -461,6 +472,7 @@ text = "GURT is a content delivery protocol similar to HTTPS. It's the core of h autowrap_mode = 3 [node name="ScriptingPanel" type="VBoxContainer" parent="HSplitContainer/Content/ScrollContainer/ContentStack"] +visible = false layout_mode = 2 theme_override_constants/separation = 20 diff --git a/flumi/Scenes/Tags/canvas.tscn b/flumi/Scenes/Tags/canvas.tscn index 73a06b7..84c9a86 100644 --- a/flumi/Scenes/Tags/canvas.tscn +++ b/flumi/Scenes/Tags/canvas.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://08xpof853sfh" path="res://Scripts/Tags/canvas.gd" id="1_canvas"] [node name="canvas" type="ColorRect"] -custom_minimum_size = Vector2(300, 150) +custom_minimum_size = Vector2(0, 0) offset_right = 300.0 offset_bottom = 150.0 size_flags_horizontal = 0 diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd index ad06129..0144b66 100644 --- a/flumi/Scripts/B9/HTMLParser.gd +++ b/flumi/Scripts/B9/HTMLParser.gd @@ -215,7 +215,8 @@ func get_element_styles_with_inheritance(element: HTMLElement, event: String = " if element in visited_elements: return {} - visited_elements.append(element) + var new_visited_for_styles = visited_elements.duplicate() + new_visited_for_styles.append(element) var styles = {} @@ -403,7 +404,7 @@ func get_icon() -> String: var icon_element = find_first("icon") return icon_element.get_attribute("src") if icon_element != null else "" -func process_fonts() -> void: +func process_fonts(base_url: String = "") -> void: var font_elements = find_all("font") for font_element in font_elements: @@ -412,7 +413,8 @@ func process_fonts() -> void: var weight = font_element.get_attribute("weight", "400") if name_str and src: - FontManager.register_font(name_str, src, weight) + var resolved_src = URLUtils.resolve_url(base_url, src) + FontManager.register_font(name_str, resolved_src, weight) func get_meta_content(name_: String) -> String: var meta_elements = find_all("meta", "name") @@ -449,7 +451,7 @@ func process_scripts(lua_api: LuaAPI, _lua_vm) -> void: parse_result.external_scripts = [] parse_result.external_scripts.append(src) elif not inline_code.is_empty(): - lua_api.execute_lua_script(inline_code) + lua_api.execute_lua_script(inline_code, "") func process_external_scripts(lua_api: LuaAPI, _lua_vm, base_url: String = "") -> void: if not lua_api or not parse_result.external_scripts or parse_result.external_scripts.is_empty(): @@ -460,7 +462,7 @@ func process_external_scripts(lua_api: LuaAPI, _lua_vm, base_url: String = "") - for script_url in parse_result.external_scripts: var script_content = await Network.fetch_external_resource(script_url, base_url) if not script_content.is_empty(): - lua_api.execute_lua_script(script_content) + lua_api.execute_lua_script(script_content, script_url) func process_postprocess() -> HTMLParser.HTMLElement: var postprocess_elements = find_all("postprocess") @@ -561,7 +563,7 @@ static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, par for child in element.children: var child_styles = styles if parser != null: - child_styles = parser.get_element_styles_with_inheritance(child, "", new_visited) + child_styles = parser.get_element_styles_with_inheritance(child, "", []) var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser, new_visited) child_content = apply_element_bbcode_formatting(child, child_styles, child_content) text += child_content diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index 8ce5835..71e9a58 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -359,7 +359,8 @@ func _on_gui_input_click(event: InputEvent, subscription: EventSubscription) -> if event is InputEventMouseButton: var mouse_event = event as InputEventMouseButton if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed: - _execute_lua_callback(subscription) + var mouse_info = _get_element_relative_mouse_position(mouse_event, subscription.element_id) + _execute_lua_callback(subscription, [mouse_info]) func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void: if event is InputEventMouseButton: @@ -376,7 +377,8 @@ func _on_gui_input_mouse_universal(event: InputEvent, signal_node: Node) -> void should_trigger = true if should_trigger: - _execute_lua_callback(subscription) + var mouse_info = _get_element_relative_mouse_position(mouse_event, subscription.element_id) + _execute_lua_callback(subscription, [mouse_info]) # Event callback handlers func _on_gui_input_mousemove(event: InputEvent, subscription: EventSubscription) -> void: @@ -441,6 +443,31 @@ func _input(event: InputEvent) -> void: } _execute_lua_callback(subscription, [key_info]) + elif event is InputEventMouseButton: + var mouse_event = event as InputEventMouseButton + for subscription_id in event_subscriptions: + var subscription = event_subscriptions[subscription_id] + if subscription.element_id == "body" and subscription.connected_signal == "input": + var should_trigger = false + match subscription.event_name: + "mousedown": + should_trigger = mouse_event.pressed + "mouseup": + should_trigger = not mouse_event.pressed + + if should_trigger: + var mouse_info = {"x": 0, "y": 0, "button": mouse_event.button_index} + var body_container = _get_body_container() + + if body_container: + var control = body_container as Control + var global_pos = mouse_event.global_position + var element_rect = control.get_global_rect() + mouse_info["x"] = global_pos.x - element_rect.position.x + mouse_info["y"] = global_pos.y - element_rect.position.y + + _execute_lua_callback(subscription, [mouse_info]) + elif event is InputEventMouseMotion: var mouse_event = event as InputEventMouseMotion for subscription_id in event_subscriptions: @@ -449,29 +476,67 @@ func _input(event: InputEvent) -> void: if subscription.event_name == "mousemove": _handle_mousemove_event(mouse_event, subscription) -func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void: - # TODO: pass reference instead of hardcoded path - var body_container = Engine.get_main_loop().current_scene.website_container - - if body_container.get_parent() is MarginContainer: - body_container = body_container.get_parent() +func _get_element_relative_mouse_position(mouse_event: InputEvent, element_id: String) -> Dictionary: + var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) + if not dom_node or not dom_node is Control: + return {"x": 0, "y": 0} + var control = dom_node as Control + var global_pos: Vector2 + + if mouse_event is InputEventMouseButton: + global_pos = (mouse_event as InputEventMouseButton).global_position + elif mouse_event is InputEventMouseMotion: + global_pos = (mouse_event as InputEventMouseMotion).global_position + else: + return {"x": 0, "y": 0} + + var element_rect = control.get_global_rect() + var local_x = global_pos.x - element_rect.position.x + var local_y = global_pos.y - element_rect.position.y + + return { + "x": local_x, + "y": local_y + } + +func _handle_mousemove_event(mouse_event: InputEventMouseMotion, subscription: EventSubscription) -> void: + var body_container = _get_body_container() if not body_container: return - var container_rect = body_container.get_global_rect() - var local_x = mouse_event.global_position.x - container_rect.position.x - var local_y = mouse_event.global_position.y - container_rect.position.y + var control = body_container as Control + var global_pos = mouse_event.global_position + var element_rect = control.get_global_rect() + var local_x = global_pos.x - element_rect.position.x + var local_y = global_pos.y - element_rect.position.y - # Only provide coordinates if mouse is within the container bounds - if local_x >= 0 and local_y >= 0 and local_x <= container_rect.size.x and local_y <= container_rect.size.y: - var mouse_info = { - "x": local_x, - "y": local_y, - "deltaX": mouse_event.relative.x, - "deltaY": mouse_event.relative.y - } - _execute_lua_callback(subscription, [mouse_info]) + var mouse_info = { + "x": local_x, + "y": local_y, + "deltaX": mouse_event.relative.x, + "deltaY": mouse_event.relative.y + } + _execute_lua_callback(subscription, [mouse_info]) + +func _get_body_container() -> Control: + # Try to get body from DOM registry first + var body_container = dom_parser.parse_result.dom_nodes.get("body", null) + + # We fallback to finding the active website container, as it seems theres a bug where body can be null in this context + if not body_container: + var main_scene = Engine.get_main_loop().current_scene + if main_scene and main_scene.has_method("get_active_website_container"): + body_container = main_scene.get_active_website_container() + else: + body_container = Engine.get_main_loop().current_scene.website_container + if body_container and body_container.get_parent() is MarginContainer: + body_container = body_container.get_parent() + + if body_container and body_container is Control: + return body_container as Control + + return null # Input event handlers func _on_input_text_changed(new_text: String, subscription: EventSubscription) -> void: @@ -634,13 +699,13 @@ func get_dom_node(node: Node, purpose: String = "general") -> Node: return node # Main execution function -func execute_lua_script(code: String): +func execute_lua_script(code: String, chunk_name: String = "dostring"): if not threaded_vm.lua_thread or not threaded_vm.lua_thread.is_alive(): # Start the thread if it's not running threaded_vm.start_lua_thread(dom_parser, self) script_start_time = Time.get_ticks_msec() / 1000.0 - threaded_vm.execute_script_async(code) + threaded_vm.execute_script_async(code, chunk_name) func _on_threaded_script_completed(_result: Dictionary): pass @@ -649,7 +714,14 @@ func _on_threaded_script_error(error_message: String): Trace.trace_error("RuntimeError: " + error_message) func _on_print_output(message: Dictionary): - Trace.get_instance().log_message.emit(message, "lua", Time.get_ticks_msec() / 1000.0) + var message_strings: Array[String] = [] + for part in message.parts: + if part.type == "table": + message_strings.append(str(part.data)) + else: + message_strings.append(part.data) + var formatted_message = "\t".join(message_strings) + Trace.get_instance().log_message.emit(formatted_message, "lua", Time.get_ticks_msec() / 1000.0) func kill_script_execution(): threaded_vm.stop_lua_thread() @@ -1003,3 +1075,31 @@ func _handle_download_request(operation: Dictionary): var main_node = Engine.get_main_loop().current_scene main_node.download_manager.handle_download_request(download_data) + +func _get_element_size_sync(result: Array, element_id: String): + var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) + if dom_node and dom_node is Control: + var control = dom_node as Control + result[0] = control.size.x + result[1] = control.size.y + result[2] = true # completion flag + return + + # Fallback + result[0] = 0.0 + result[1] = 0.0 + result[2] = true # completion flag + +func _get_element_position_sync(result: Array, element_id: String): + var dom_node = dom_parser.parse_result.dom_nodes.get(element_id, null) + if dom_node and dom_node is Control: + var control = dom_node as Control + result[0] = control.position.x + result[1] = control.position.y + result[2] = true # completion flag + return + + # Fallback + result[0] = 0.0 + result[1] = 0.0 + result[2] = true # completion flag diff --git a/flumi/Scripts/Browser/DevToolsConsole.gd b/flumi/Scripts/Browser/DevToolsConsole.gd index e6a3658..c81fe36 100644 --- a/flumi/Scripts/Browser/DevToolsConsole.gd +++ b/flumi/Scripts/Browser/DevToolsConsole.gd @@ -245,9 +245,9 @@ func execute_lua_command(code: String) -> void: var is_expression = is_likely_expression(code) if is_expression: var wrapped_code = "print(" + code + ")" - lua_api.execute_lua_script(wrapped_code) + lua_api.execute_lua_script(wrapped_code, "") else: - lua_api.execute_lua_script(code) + lua_api.execute_lua_script(code, "") return add_log_entry("No Lua context available", "error", Time.get_ticks_msec() / 1000.0) diff --git a/flumi/Scripts/Browser/Tab.gd b/flumi/Scripts/Browser/Tab.gd index 9c1c68f..f6e8d4f 100644 --- a/flumi/Scripts/Browser/Tab.gd +++ b/flumi/Scripts/Browser/Tab.gd @@ -153,7 +153,7 @@ func _exit_tree(): if background_panel and is_instance_valid(background_panel): if background_panel.get_parent(): - background_panel.get_parent().remove_child(background_panel) + background_panel.get_parent().remove_child.call_deferred(background_panel) background_panel.queue_free() if dev_tools and is_instance_valid(dev_tools): diff --git a/flumi/Scripts/Browser/TabContainer.gd b/flumi/Scripts/Browser/TabContainer.gd index b969a58..771d3ec 100644 --- a/flumi/Scripts/Browser/TabContainer.gd +++ b/flumi/Scripts/Browser/TabContainer.gd @@ -249,6 +249,8 @@ func _input(_event: InputEvent) -> void: if Input.is_action_just_pressed("FocusSearch"): main.search_bar.grab_focus() main.search_bar.select_all() + if Input.is_action_just_pressed("ReloadPage"): + main.reload_current_page() func _on_new_tab_button_pressed() -> void: create_tab() diff --git a/flumi/Scripts/Engine/FontManager.gd b/flumi/Scripts/Engine/FontManager.gd index a2adecc..fd764ac 100644 --- a/flumi/Scripts/Engine/FontManager.gd +++ b/flumi/Scripts/Engine/FontManager.gd @@ -27,6 +27,10 @@ static func load_font(font_info: Dictionary) -> void: if src.begins_with("http://") or src.begins_with("https://"): load_web_font(font_info) + elif src.begins_with("gurt://"): + load_gurt_font(font_info) + else: + load_local_font(font_info) static func load_web_font(font_info: Dictionary) -> void: var src = font_info["src"] @@ -48,13 +52,8 @@ static func load_web_font(font_info: Dictionary) -> void: font_info["font_resource"] = font loaded_fonts[name] = font - # Trigger font refresh if callback is available if refresh_callback.is_valid(): refresh_callback.call(name) - else: - print("FontManager: Empty font data received for ", name) - else: - print("FontManager: Failed to load font ", name, " - HTTP ", response_code) if is_instance_valid(temp_parent): temp_parent.queue_free() @@ -65,6 +64,50 @@ static func load_web_font(font_info: Dictionary) -> void: http_request.request(src, headers) +static func load_local_font(font_info: Dictionary) -> void: + var src = font_info["src"] + var name = font_info["name"] + + if not FileAccess.file_exists(src): + return + + var file = FileAccess.open(src, FileAccess.READ) + if file == null: + return + + var font_data = file.get_buffer(file.get_length()) + file.close() + + if font_data.size() == 0: + return + + var font = FontFile.new() + font.data = font_data + + font_info["font_resource"] = font + loaded_fonts[name] = font + + if refresh_callback.is_valid(): + refresh_callback.call(name) + +static func load_gurt_font(font_info: Dictionary) -> void: + var src = font_info["src"] + var name = font_info["name"] + + var font_data = Network.fetch_gurt_resource(src, true) + + if font_data.size() == 0: + return + + var font = FontFile.new() + font.data = font_data + + font_info["font_resource"] = font + loaded_fonts[name] = font + + if refresh_callback.is_valid(): + refresh_callback.call(name) + static func get_font(family_name: String) -> Font: if family_name == "sans-serif": var sys_font = SystemFont.new() diff --git a/flumi/Scripts/Engine/StyleManager.gd b/flumi/Scripts/Engine/StyleManager.gd index 7cad2c0..ef88718 100644 --- a/flumi/Scripts/Engine/StyleManager.gd +++ b/flumi/Scripts/Engine/StyleManager.gd @@ -68,13 +68,18 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement, if width == "100%": node.size_flags_horizontal = Control.SIZE_EXPAND_FILL node.custom_minimum_size.x = 0 + if node is PanelContainer and node.get_child_count() > 0: + var vbox = node.get_child(0) + if vbox is VBoxContainer: + vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + node.set_meta("size_flags_set_by_style_manager", true) else: # For other percentages, convert to viewport-relative size var percent = float(width.replace("%", "")) / 100.0 var viewport_width = node.get_viewport().get_visible_rect().size.x if node.get_viewport() else 800 node.custom_minimum_size.x = viewport_width * percent - node.set_meta("size_flags_set_by_style_manager", true) - node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + node.set_meta("size_flags_set_by_style_manager", true) + node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN else: node.custom_minimum_size.x = width node.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN @@ -402,7 +407,7 @@ static func apply_margin_styles_to_container(margin_container: MarginContainer, if margin_val != null: margin_container.add_theme_constant_override(theme_key, margin_val) -static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "") -> void: +static func apply_styles_to_label(label: Control, styles: Dictionary, element: HTMLParser.HTMLElement, parser, text_override: String = "", is_refresh: bool = false) -> void: if label is Button: apply_font_to_button(label, styles) return @@ -410,6 +415,10 @@ static func apply_styles_to_label(label: Control, styles: Dictionary, element: H if not label is RichTextLabel: return + if not is_refresh and styles.has("font-family") and styles["font-family"] not in ["sans-serif", "serif", "monospace"]: + var main_node = Engine.get_main_loop().current_scene + main_node.register_font_dependent_element(label, styles, element, parser) + var text = text_override if text_override != "" else (element.get_preserved_text() if element.tag_name == "pre" else element.get_bbcode_formatted_text(parser)) var font_size = 24 # default @@ -418,15 +427,15 @@ static func apply_styles_to_label(label: Control, styles: Dictionary, element: H var font_family = styles["font-family"] var font_resource = FontManager.get_font(font_family) - # set a sans-serif fallback first if font_family not in ["sans-serif", "serif", "monospace"]: - if not FontManager.loaded_fonts.has(font_family): - # Font not loaded yet, use sans-serif as fallback + if FontManager.loaded_fonts.has(font_family) and font_resource: + apply_font_to_label(label, font_resource, styles) + else: var fallback_font = FontManager.get_font("sans-serif") apply_font_to_label(label, fallback_font, styles) - - if font_resource: - apply_font_to_label(label, font_resource, styles) + else: + if font_resource: + apply_font_to_label(label, font_resource, styles) else: # No custom font family, but check if we need to apply font weight if styles.has("font-thin") or styles.has("font-extralight") or styles.has("font-light") or styles.has("font-normal") or styles.has("font-medium") or styles.has("font-semibold") or styles.has("font-extrabold") or styles.has("font-black"): @@ -567,50 +576,64 @@ static func parse_radius(radius_str: String) -> int: return SizeUtils.parse_radius(radius_str) static func apply_font_to_label(label: RichTextLabel, font_resource: Font, styles: Dictionary = {}) -> void: - # Create normal font with appropriate weight - var normal_font = SystemFont.new() - normal_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"] + if font_resource is FontFile: + label.add_theme_font_override("normal_font", font_resource) + label.add_theme_font_override("bold_font", font_resource) + label.add_theme_font_override("italics_font", font_resource) - # Set weight based on styles - var font_weight = 400 # Default normal weight - if styles.has("font-thin"): - font_weight = 100 - elif styles.has("font-extralight"): - font_weight = 200 - elif styles.has("font-light"): - font_weight = 300 - elif styles.has("font-normal"): - font_weight = 400 - elif styles.has("font-medium"): - font_weight = 500 - elif styles.has("font-semibold"): - font_weight = 600 - elif styles.has("font-bold"): - font_weight = 700 - elif styles.has("font-extrabold"): - font_weight = 800 - elif styles.has("font-black"): - font_weight = 900 + elif font_resource is SystemFont: + var font_weight = 400 + if styles.has("font-thin"): + font_weight = 100 + elif styles.has("font-extralight"): + font_weight = 200 + elif styles.has("font-light"): + font_weight = 300 + elif styles.has("font-normal"): + font_weight = 400 + elif styles.has("font-medium"): + font_weight = 500 + elif styles.has("font-semibold"): + font_weight = 600 + elif styles.has("font-bold"): + font_weight = 700 + elif styles.has("font-extrabold"): + font_weight = 800 + elif styles.has("font-black"): + font_weight = 900 + + var normal_font = SystemFont.new() + normal_font.font_names = font_resource.font_names + normal_font.font_weight = font_weight + label.add_theme_font_override("normal_font", normal_font) + + # Create bold variant + var bold_font = SystemFont.new() + bold_font.font_names = font_resource.font_names + bold_font.font_weight = 700 + label.add_theme_font_override("bold_font", bold_font) + + # Create italic variant + var italic_font = SystemFont.new() + italic_font.font_names = font_resource.font_names + italic_font.font_italic = true + italic_font.font_weight = font_weight + label.add_theme_font_override("italics_font", italic_font) + + else: + label.add_theme_font_override("normal_font", font_resource) + label.add_theme_font_override("bold_font", font_resource) + label.add_theme_font_override("italics_font", font_resource) - normal_font.font_weight = font_weight - - label.add_theme_font_override("normal_font", normal_font) - - var bold_font = SystemFont.new() - bold_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"] - bold_font.font_weight = 700 # Bold weight - label.add_theme_font_override("bold_font", bold_font) - - var italic_font = SystemFont.new() - italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"] - italic_font.font_italic = true - label.add_theme_font_override("italics_font", italic_font) - - var bold_italic_font = SystemFont.new() - bold_italic_font.font_names = font_resource.font_names if font_resource is SystemFont else ["Arial"] - bold_italic_font.font_weight = 700 # Bold weight - bold_italic_font.font_italic = true - label.add_theme_font_override("bold_italics_font", bold_italic_font) + # Handle bold_italics_font + if font_resource is FontFile: + label.add_theme_font_override("bold_italics_font", font_resource) + elif font_resource is SystemFont: + var bold_italic_font = SystemFont.new() + bold_italic_font.font_names = font_resource.font_names + bold_italic_font.font_weight = 700 + bold_italic_font.font_italic = true + label.add_theme_font_override("bold_italics_font", bold_italic_font) static func apply_font_to_button(button: Button, styles: Dictionary) -> void: if styles.has("font-family"): diff --git a/flumi/Scripts/Network.gd b/flumi/Scripts/Network.gd index 7c50011..922eae7 100644 --- a/flumi/Scripts/Network.gd +++ b/flumi/Scripts/Network.gd @@ -142,9 +142,9 @@ func fetch_external_resource(url: String, base_url: String = "") -> String: else: return "" -func fetch_gurt_resource(url: String) -> String: +func fetch_gurt_resource(url: String, as_binary: bool = false): if not GurtProtocol.is_gurt_domain(url): - return "" + return PackedByteArray() if as_binary else "" var gurt_url = url if not gurt_url.begins_with("gurt://"): @@ -184,11 +184,17 @@ func fetch_gurt_resource(url: String) -> String: status_code = response.status_code error_msg += ": " + str(response.status_code) + " " + response.status_message NetworkManager.complete_request(network_request.id, status_code, error_msg, {}, "") - return "" + return PackedByteArray() if as_binary else "" var response_headers = response.headers if response.headers else {} - var response_body = response.body.get_string_from_utf8() - NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body) + var response_body = response.body - return response_body + if as_binary: + var size_info = "Binary data: " + str(response_body.size()) + " bytes" + NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, size_info, response_body) + return response_body + else: + var response_body_str = response_body.get_string_from_utf8() + NetworkManager.complete_request(network_request.id, response.status_code, "OK", response_headers, response_body_str) + return response_body_str diff --git a/flumi/Scripts/Tags/canvas.gd b/flumi/Scripts/Tags/canvas.gd index 93322b5..0952015 100644 --- a/flumi/Scripts/Tags/canvas.gd +++ b/flumi/Scripts/Tags/canvas.gd @@ -8,6 +8,8 @@ var canvas_height: int = 150 var draw_commands: Array = [] var context_2d: CanvasContext2D = null var context_shader: CanvasContextShader = null +var pending_redraw: bool = false +var max_draw_commands: int = 1000 class CanvasContext2D: var canvas: HTMLCanvas @@ -41,8 +43,7 @@ class CanvasContext2D: "color": color, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func strokeRect(x: float, y: float, width: float, height: float, color_hex: String = "", stroke_width: float = 0.0): var color = _parse_color(stroke_style if color_hex.is_empty() else color_hex) @@ -57,10 +58,14 @@ class CanvasContext2D: "stroke_width": width_val, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func clearRect(x: float, y: float, width: float, height: float): + if x == 0 and y == 0 and width >= canvas.canvas_width and height >= canvas.canvas_height: + canvas.draw_commands.clear() + canvas._do_redraw() + return + var cmd = { "type": "clearRect", "x": x, @@ -68,8 +73,7 @@ class CanvasContext2D: "width": width, "height": height } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func drawCircle(x: float, y: float, radius: float, color_hex: String = "#000000", filled: bool = true): var cmd = { @@ -81,8 +85,7 @@ class CanvasContext2D: "filled": filled, "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func drawText(x: float, y: float, text: String, color_hex: String = "#000000"): var color = _parse_color(fill_style if color_hex == "#000000" else color_hex) @@ -95,8 +98,7 @@ class CanvasContext2D: "font_size": _parse_font_size(font), "transform": current_transform } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) # Path-based drawing functions func beginPath(): @@ -149,8 +151,7 @@ class CanvasContext2D: "line_cap": line_cap, "line_join": line_join } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) func fill(): if current_path.size() < 3: @@ -161,8 +162,7 @@ class CanvasContext2D: "path": current_path.duplicate(), "color": _parse_color(fill_style) } - canvas.draw_commands.append(cmd) - canvas.queue_redraw() + canvas._add_draw_command(cmd) # Transformation functions func save(): @@ -328,6 +328,10 @@ func withContext(context_type: String): func _draw(): draw_rect(Rect2(Vector2.ZERO, size), Color.TRANSPARENT) + # Skip if too many commands to prevent frame drops + if draw_commands.size() > max_draw_commands * 2: + return + for cmd in draw_commands: match cmd.type: "fillRect": @@ -407,6 +411,31 @@ func _draw(): if path.size() > 2: draw_colored_polygon(path, clr) +func _add_draw_command(cmd: Dictionary): + _optimize_command(cmd) + + draw_commands.append(cmd) + + if draw_commands.size() > max_draw_commands: + draw_commands = draw_commands.slice(draw_commands.size() - max_draw_commands) + + if not pending_redraw: + pending_redraw = true + call_deferred("_do_redraw") + +func _optimize_command(cmd: Dictionary): + # Remove redundant consecutive clearRect commands + if cmd.type == "clearRect" and draw_commands.size() > 0: + var last_cmd = draw_commands[-1] + if last_cmd.type == "clearRect" and \ + last_cmd.x == cmd.x and last_cmd.y == cmd.y and \ + last_cmd.width == cmd.width and last_cmd.height == cmd.height: + draw_commands.pop_back() + +func _do_redraw(): + pending_redraw = false + queue_redraw() + func clear(): draw_commands.clear() - queue_redraw() + _do_redraw() diff --git a/flumi/Scripts/Utils/Lua/Canvas.gd b/flumi/Scripts/Utils/Lua/Canvas.gd index 6780790..e41b0ea 100644 --- a/flumi/Scripts/Utils/Lua/Canvas.gd +++ b/flumi/Scripts/Utils/Lua/Canvas.gd @@ -3,8 +3,35 @@ extends RefCounted # This file mainly creates operations that are handled by canvas.gd +static var pending_operations: Dictionary = {} +static var batch_timer: SceneTreeTimer = null + static func emit_canvas_operation(lua_api: LuaAPI, operation: Dictionary) -> void: - lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + var element_id = operation.get("element_id", "") + + if not pending_operations.has(element_id): + pending_operations[element_id] = [] + + pending_operations[element_id].append(operation) + + if not batch_timer or batch_timer.time_left <= 0: + var scene_tree = lua_api.get_tree() if lua_api else Engine.get_main_loop() + if scene_tree: + batch_timer = scene_tree.create_timer(0.001) # 1ms batch window + batch_timer.timeout.connect(_flush_pending_operations.bind(lua_api)) + +static func _flush_pending_operations(lua_api: LuaAPI) -> void: + if not lua_api or not lua_api.is_inside_tree(): + pending_operations.clear() + return + + for element_id in pending_operations: + var operations = pending_operations[element_id] + for operation in operations: + lua_api.threaded_vm.call_deferred("_emit_dom_operation_request", operation) + + pending_operations.clear() + batch_timer = null static func _element_withContext_wrapper(vm: LuauVM) -> int: var lua_api = vm.get_meta("lua_api") as LuaAPI diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index 244763a..299162d 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -1054,6 +1054,60 @@ static func _element_index_wrapper(vm: LuauVM) -> int: # Fallback to true (visible by default) vm.lua_pushboolean(true) return 1 + "size": + if lua_api: + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null) + if dom_node and dom_node is Control: + var result = [0.0, 0.0, false] + lua_api.call_deferred("_get_element_size_sync", result, element_id) + while not result[2]: # wait for completion flag + OS.delay_msec(1) + + vm.lua_newtable() + vm.lua_pushnumber(result[0]) + vm.lua_setfield(-2, "width") + vm.lua_pushnumber(result[1]) + vm.lua_setfield(-2, "height") + return 1 + + # Fallback to zero size + vm.lua_newtable() + vm.lua_pushnumber(0) + vm.lua_setfield(-2, "width") + vm.lua_pushnumber(0) + vm.lua_setfield(-2, "height") + return 1 + "position": + if lua_api: + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + var dom_node = lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null) + if dom_node and dom_node is Control: + var result = [0.0, 0.0, false] + lua_api.call_deferred("_get_element_position_sync", result, element_id) + while not result[2]: # wait for completion flag + OS.delay_msec(1) + + vm.lua_newtable() + vm.lua_pushnumber(result[0]) + vm.lua_setfield(-2, "x") + vm.lua_pushnumber(result[1]) + vm.lua_setfield(-2, "y") + return 1 + + # Fallback to zero position + vm.lua_newtable() + vm.lua_pushnumber(0) + vm.lua_setfield(-2, "x") + vm.lua_pushnumber(0) + vm.lua_setfield(-2, "y") + return 1 _: # Check for DOM traversal properties first if lua_api: diff --git a/flumi/Scripts/Utils/Lua/Event.gd b/flumi/Scripts/Utils/Lua/Event.gd index 260925b..35bc705 100644 --- a/flumi/Scripts/Utils/Lua/Event.gd +++ b/flumi/Scripts/Utils/Lua/Event.gd @@ -17,7 +17,7 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri var wrapper = func(): LuaAudioUtils.mark_user_event() LuaDownloadUtils.mark_user_event() - subscription.lua_api._on_event_triggered(subscription) + subscription.lua_api._execute_lua_callback(subscription, [{}]) signal_node.pressed.connect(wrapper) subscription.connected_signal = "pressed" subscription.connected_node = signal_node if signal_node != subscription.lua_api.get_dom_node(signal_node.get_parent(), "signal") else null @@ -189,6 +189,11 @@ static func connect_body_event(event_name: String, subscription, lua_api) -> boo subscription.connected_signal = "input_mousemove" subscription.connected_node = lua_api return true + "mousedown", "mouseup": + lua_api.set_process_input(true) + subscription.connected_signal = "input" + subscription.connected_node = lua_api + return true "mouseenter", "mouseexit": var main_container = lua_api.dom_parser.parse_result.dom_nodes.get("body", null) if main_container: diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index 3d3ed40..9724d0a 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -56,11 +56,12 @@ func stop_lua_thread(): lua_thread.wait_to_finish() lua_thread = null -func execute_script_async(script_code: String): +func execute_script_async(script_code: String, chunk_name: String = "dostring"): queue_mutex.lock() command_queue.append({ "type": "execute_script", - "code": script_code + "code": script_code, + "chunk_name": chunk_name }) queue_mutex.unlock() thread_semaphore.post() @@ -143,21 +144,28 @@ func _process_command_queue(): for command in commands_to_process: match command.type: "execute_script": - _execute_script_in_thread(command.code) + _execute_script_in_thread(command.code, command.get("chunk_name", "dostring")) "execute_callback": _execute_callback_in_thread(command.callback_ref, command.args) "execute_timeout": _execute_timeout_in_thread(command.timeout_id) -func _execute_script_in_thread(script_code: String): +func _execute_script_in_thread(script_code: String, chunk_name: String = "dostring"): if not lua_vm: call_deferred("_emit_script_error", "Lua VM not initialized") return - var result = lua_vm.lua_dostring(script_code) + # Use load_string with custom chunk name, then lua_pcall + var load_result = lua_vm.load_string(script_code, chunk_name) - if result == lua_vm.LUA_OK: - call_deferred("_emit_script_completed", {"success": true}) + if load_result == lua_vm.LUA_OK: + var call_result = lua_vm.lua_pcall(0, 0, 0) + if call_result == lua_vm.LUA_OK: + call_deferred("_emit_script_completed", {"success": true}) + else: + var error_msg = lua_vm.lua_tostring(-1) + lua_vm.lua_pop(1) + call_deferred("_emit_script_error", error_msg) else: var error_msg = lua_vm.lua_tostring(-1) lua_vm.lua_pop(1) @@ -257,6 +265,16 @@ func _print_handler(vm: LuauVM) -> int: "count": message_parts.size() } + # Also call trace.log with the formatted message + var message_strings: Array[String] = [] + for part in message_parts: + if part.type == "table": + message_strings.append(str(part.data)) + else: + message_strings.append(part.data) + var final_message = "\t".join(message_strings) + call_deferred("_emit_trace_message", final_message, "log") + call_deferred("_emit_print_output", print_data) return 0 @@ -359,6 +377,12 @@ func _setup_threaded_gurt_api(): lua_vm.lua_setfield(-2, "on") lua_vm.lua_setfield(-2, "body") + lua_vm.lua_pushcallable(_gurt_get_width_handler, "gurt.width") + lua_vm.lua_setfield(-2, "width") + + lua_vm.lua_pushcallable(_gurt_get_height_handler, "gurt.height") + lua_vm.lua_setfield(-2, "height") + lua_vm.lua_setglobal("gurt") func _setup_additional_lua_apis(): @@ -499,9 +523,49 @@ func _set_interval_handler(vm: LuauVM) -> int: func get_current_href() -> String: var main_node = Engine.get_main_loop().current_scene + if main_node == null: + return "" + + return main_node.current_domain + +func _gurt_get_width_handler(vm: LuauVM) -> int: + var result = [0.0] + call_deferred("_get_width_sync", result) + while result[0] == 0.0: + OS.delay_msec(1) + vm.lua_pushnumber(result[0]) + return 1 + +func _gurt_get_height_handler(vm: LuauVM) -> int: + var result = [0.0] + call_deferred("_get_height_sync", result) + while result[0] == 0.0: + OS.delay_msec(1) + vm.lua_pushnumber(result[0]) + return 1 + +func _get_width_sync(result: Array): + var main_node = Engine.get_main_loop().current_scene if main_node: - return main_node.current_domain - return "" + var active_tab = main_node.get_active_tab() + if active_tab and active_tab.website_container: + result[0] = active_tab.website_container.size.x + else: + result[0] = 1024.0 + else: + result[0] = 1024.0 + +func _get_height_sync(result: Array): + var main_node = Engine.get_main_loop().current_scene + if main_node: + var active_tab = main_node.get_active_tab() + if active_tab and active_tab.website_container: + result[0] = active_tab.website_container.size.y + else: + result[0] = 768.0 + else: + result[0] = 768.0 + func _gurt_select_handler(vm: LuauVM) -> int: var selector: String = vm.luaL_checkstring(1) diff --git a/flumi/Scripts/Utils/Lua/Trace.gd b/flumi/Scripts/Utils/Lua/Trace.gd index f00c0cf..eba871a 100644 --- a/flumi/Scripts/Utils/Lua/Trace.gd +++ b/flumi/Scripts/Utils/Lua/Trace.gd @@ -52,21 +52,21 @@ static func clear_messages() -> void: _messages.clear() static func _lua_trace_log_handler(vm: LuauVM) -> int: - var message = vm.luaL_checkstring(1) + var message = LuaPrintUtils.lua_value_to_string(vm, 1) vm.lua_getglobal("_trace_log") vm.lua_pushstring(message) vm.lua_call(1, 0) return 0 static func _lua_trace_warn_handler(vm: LuauVM) -> int: - var message = vm.luaL_checkstring(1) + var message = LuaPrintUtils.lua_value_to_string(vm, 1) vm.lua_getglobal("_trace_warning") vm.lua_pushstring(message) vm.lua_call(1, 0) return 0 static func _lua_trace_error_handler(vm: LuauVM) -> int: - var message = vm.luaL_checkstring(1) + var message = LuaPrintUtils.lua_value_to_string(vm, 1) vm.lua_getglobal("_trace_error") vm.lua_pushstring(message) vm.lua_call(1, 0) diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index e8242c0..0766ec1 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -388,7 +388,7 @@ func render_content(html_bytes: PackedByteArray) -> void: await parser.process_external_styles(current_domain) # Process and load all custom fonts defined in tags - parser.process_fonts() + parser.process_fonts(current_domain) FontManager.load_all_fonts() if parse_result.errors.size() > 0: @@ -810,7 +810,6 @@ func register_font_dependent_element(label: Control, styles: Dictionary, element }) func refresh_fonts(font_name: String) -> void: - # Find all elements that should use this font and refresh them for element_info in font_dependent_elements: var label = element_info["label"] var styles = element_info["styles"] @@ -819,7 +818,7 @@ func refresh_fonts(font_name: String) -> void: if styles.has("font-family") and styles["font-family"] == font_name: if is_instance_valid(label): - StyleManager.apply_styles_to_label(label, styles, element, parser) + StyleManager.apply_styles_to_label(label, styles, element, parser, "", true) func get_current_url() -> String: return current_domain if not current_domain.is_empty() else "" diff --git a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll index 5e1584f..efaaffc 100644 Binary files a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll and b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll differ diff --git a/flumi/export_presets.cfg b/flumi/export_presets.cfg index ef999b8..ba817c9 100644 --- a/flumi/export_presets.cfg +++ b/flumi/export_presets.cfg @@ -39,3 +39,70 @@ unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\") rm -rf \"{temp_dir}\"" + +[preset.1] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build-scripts/Windows/Flumi" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=0 +binary_format/embed_pck=false +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +binary_format/architecture="x86_64" +codesign/enable=false +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PackedStringArray() +application/modify_resources=true +application/icon="uid://ctpe0lbehepen" +application/console_wrapper_icon="uid://ctpe0lbehepen" +application/icon_interpolation=4 +application/file_version="" +application/product_version="" +application/company_name="Outpoot" +application/product_name="Flumi" +application/file_description="A browser for GURT protocol sites" +application/copyright="2025 Gurted" +application/trademarks="" +application/export_angle=0 +application/export_d3d12=0 +application/d3d12_agility_sdk_multiarch=true +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' +$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' +$trigger = New-ScheduledTaskTrigger -Once -At 00:00 +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings +Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true +Start-ScheduledTask -TaskName godot_remote_debug +while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" +ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue +Remove-Item -Recurse -Force '{temp_dir}'" diff --git a/flumi/project.godot b/flumi/project.godot index 77ac165..6dd1a5c 100644 --- a/flumi/project.godot +++ b/flumi/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Flumi" -config/version="1.0.0" +config/version="1.0.1" run/main_scene="uid://bytm7bt2s4ak8" config/use_custom_user_dir=true config/features=PackedStringArray("4.4", "Forward Plus") @@ -80,3 +80,8 @@ DevTools={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":73,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +ReloadPage={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/protocol/cli/Cargo.lock b/protocol/cli/Cargo.lock index 00d11dd..6f399f3 100644 --- a/protocol/cli/Cargo.lock +++ b/protocol/cli/Cargo.lock @@ -416,7 +416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gurt" +name = "gurtlib" version = "0.1.0" dependencies = [ "base64", @@ -440,7 +440,7 @@ dependencies = [ "async-trait", "clap", "colored", - "gurt", + "gurtlib", "indexmap", "mime_guess", "regex", diff --git a/protocol/cli/Cargo.toml b/protocol/cli/Cargo.toml index 1821c62..8342e2f 100644 --- a/protocol/cli/Cargo.toml +++ b/protocol/cli/Cargo.toml @@ -12,7 +12,7 @@ name = "gurty" path = "src/main.rs" [dependencies] -gurt = { path = "../library" } +gurtlib = { path = "../library" } tokio = { version = "1.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } diff --git a/protocol/cli/src/error.rs b/protocol/cli/src/error.rs index b99c997..4076bbf 100644 --- a/protocol/cli/src/error.rs +++ b/protocol/cli/src/error.rs @@ -29,8 +29,8 @@ impl From for ServerError { } } -impl From for ServerError { - fn from(err: gurt::GurtError) -> Self { +impl From for ServerError { + fn from(err: gurtlib::GurtError) -> Self { ServerError::ServerStartup(err.to_string()) } } diff --git a/protocol/cli/src/request_handler.rs b/protocol/cli/src/request_handler.rs index abff72b..0cfcdef 100644 --- a/protocol/cli/src/request_handler.rs +++ b/protocol/cli/src/request_handler.rs @@ -3,7 +3,7 @@ use crate::{ config::GurtConfig, security::SecurityMiddleware, }; -use gurt::prelude::*; +use gurtlib::prelude::*; use std::path::Path; use std::sync::Arc; use tracing; @@ -238,14 +238,14 @@ impl RequestHandler { } let result = match method { - gurt::message::GurtMethod::GET => { + gurtlib::message::GurtMethod::GET => { if ctx.path() == "/" { self.handle_root_request().await } else { self.handle_file_request(ctx.path()).await } } - gurt::message::GurtMethod::HEAD => { + gurtlib::message::GurtMethod::HEAD => { let mut response = if ctx.path() == "/" { self.handle_root_request().await? } else { @@ -254,7 +254,7 @@ impl RequestHandler { response.body = Vec::new(); Ok(response) } - gurt::message::GurtMethod::OPTIONS => { + gurtlib::message::GurtMethod::OPTIONS => { let allowed_methods = if let Some(config) = &self.config { if let Some(security) = &config.security { security.allowed_methods.join(", ") @@ -272,7 +272,7 @@ impl RequestHandler { Ok(self.apply_global_headers(response)) } _ => { - let response = GurtResponse::new(gurt::protocol::GurtStatusCode::MethodNotAllowed) + let response = GurtResponse::new(gurtlib::protocol::GurtStatusCode::MethodNotAllowed) .with_header("Content-Type", "text/html"); Ok(self.apply_global_headers(response)) } @@ -430,7 +430,7 @@ impl RequestHandler { #[cfg(test)] mod tests { use super::*; - use gurt::GurtStatusCode; + use gurtlib::GurtStatusCode; use std::fs; use std::env; diff --git a/protocol/cli/src/security.rs b/protocol/cli/src/security.rs index f738cc0..7a80063 100644 --- a/protocol/cli/src/security.rs +++ b/protocol/cli/src/security.rs @@ -1,5 +1,5 @@ use crate::config::GurtConfig; -use gurt::{prelude::*, GurtMethod, GurtStatusCode}; +use gurtlib::{prelude::*, GurtMethod, GurtStatusCode}; use std::collections::HashMap; use std::net::IpAddr; use std::sync::{Arc, Mutex}; diff --git a/protocol/cli/src/server.rs b/protocol/cli/src/server.rs index f772f4e..c70fd72 100644 --- a/protocol/cli/src/server.rs +++ b/protocol/cli/src/server.rs @@ -3,7 +3,7 @@ use crate::{ handlers::{FileHandler, DirectoryHandler, DefaultFileHandler, DefaultDirectoryHandler}, request_handler::{RequestHandler, RequestHandlerBuilder}, }; -use gurt::prelude::*; +use gurtlib::prelude::*; use std::{path::PathBuf, sync::Arc}; pub struct FileServerBuilder { diff --git a/protocol/gdextension/Cargo.toml b/protocol/gdextension/Cargo.toml index 9acd6df..845c9de 100644 --- a/protocol/gdextension/Cargo.toml +++ b/protocol/gdextension/Cargo.toml @@ -12,7 +12,7 @@ name = "gurt_godot" crate-type = ["cdylib"] [dependencies] -gurt = { path = "../library" } +gurtlib = { path = "../library" } godot = "0.1" diff --git a/protocol/gdextension/src/lib.rs b/protocol/gdextension/src/lib.rs index 68f7ff6..c123464 100644 --- a/protocol/gdextension/src/lib.rs +++ b/protocol/gdextension/src/lib.rs @@ -1,6 +1,6 @@ use godot::prelude::*; -use gurt::prelude::*; -use gurt::{GurtMethod, GurtClientConfig, GurtRequest}; +use gurtlib::prelude::*; +use gurtlib::{GurtMethod, GurtClientConfig, GurtRequest}; use tokio::runtime::Runtime; use std::sync::Arc; use std::cell::RefCell; @@ -266,12 +266,12 @@ impl GurtProtocolClient { #[func] fn get_version(&self) -> GString { - gurt::GURT_VERSION.to_string().into() + gurtlib::GURT_VERSION.to_string().into() } #[func] fn get_default_port(&self) -> i32 { - gurt::DEFAULT_PORT as i32 + gurtlib::DEFAULT_PORT as i32 } #[func] diff --git a/protocol/gurtca/Cargo.toml b/protocol/gurtca/Cargo.toml index b0fbc3d..c7b45c2 100644 --- a/protocol/gurtca/Cargo.toml +++ b/protocol/gurtca/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.0", features = ["derive"] } tokio = { version = "1.0", features = ["full"] } -gurt = { path = "../library" } +gurtlib = { path = "../library" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" openssl = "0.10" diff --git a/protocol/gurtca/src/client.rs b/protocol/gurtca/src/client.rs index a48f2b9..386a9fa 100644 --- a/protocol/gurtca/src/client.rs +++ b/protocol/gurtca/src/client.rs @@ -1,6 +1,6 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; -use gurt::prelude::*; +use gurtlib::prelude::*; pub struct GurtCAClient { ca_url: String, @@ -67,9 +67,9 @@ impl GurtCAClient { println!("✅ Fetched CA certificate via HTTP bootstrap"); // Create new client with custom CA - let mut config = gurt::client::GurtClientConfig::default(); + let mut config = gurtlib::client::GurtClientConfig::default(); config.custom_ca_certificates = vec![ca_cert]; - let gurt_client = gurt::GurtClient::with_config(config); + let gurt_client = gurtlib::GurtClient::with_config(config); let client_with_ca = Self { ca_url, gurt_client, diff --git a/protocol/library/Cargo.lock b/protocol/library/Cargo.lock index 449666a..1163db4 100644 --- a/protocol/library/Cargo.lock +++ b/protocol/library/Cargo.lock @@ -369,7 +369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "gurt" +name = "gurtlib" version = "0.1.0" dependencies = [ "base64", diff --git a/protocol/library/Cargo.toml b/protocol/library/Cargo.toml index 1b61ca7..5d54bcf 100644 --- a/protocol/library/Cargo.toml +++ b/protocol/library/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "gurt" +name = "gurtlib" version = "0.1.0" edition = "2021" authors = ["FaceDev"] @@ -8,7 +8,7 @@ repository = "https://github.com/outpoot/gurted" description = "Official GURT:// protocol implementation" [lib] -name = "gurt" +name = "gurtlib" crate-type = ["cdylib", "lib"] [dependencies] diff --git a/protocol/library/examples/tls_server.rs b/protocol/library/examples/tls_server.rs index 7863776..2084cc3 100644 --- a/protocol/library/examples/tls_server.rs +++ b/protocol/library/examples/tls_server.rs @@ -1,4 +1,4 @@ -use gurt::{GurtServer, GurtResponse, ServerContext, Result}; +use gurtlib::{GurtServer, GurtResponse, ServerContext, Result}; #[tokio::main] async fn main() -> Result<()> { diff --git a/protocol/library/src/client.rs b/protocol/library/src/client.rs index c66e354..3040524 100644 --- a/protocol/library/src/client.rs +++ b/protocol/library/src/client.rs @@ -109,9 +109,9 @@ impl GurtClient { } } - async fn get_pooled_connection(&self, host: &str, port: u16) -> Result> { + async fn get_pooled_connection(&self, host: &str, port: u16, original_host: Option<&str>) -> Result> { if !self.config.enable_connection_pooling { - return self.perform_handshake(host, port).await; + return self.perform_handshake(host, port, original_host).await; } let key = ConnectionKey { @@ -131,7 +131,7 @@ impl GurtClient { } debug!("Creating new connection for {}:{}", host, port); - self.perform_handshake(host, port).await + self.perform_handshake(host, port, original_host).await } fn return_connection_to_pool(&self, host: &str, port: u16, connection: tokio_rustls::client::TlsStream) { @@ -231,13 +231,16 @@ impl GurtClient { } } - async fn perform_handshake(&self, host: &str, port: u16) -> Result> { + async fn perform_handshake(&self, host: &str, port: u16, original_host: Option<&str>) -> Result> { debug!("Starting GURT handshake with {}:{}", host, port); let mut plain_conn = self.create_connection(host, port).await?; + // Use original_host for the Host header if available, otherwise fall back to host + let host_header = original_host.unwrap_or(host); + let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string()) - .with_header("Host", host) + .with_header("Host", host_header) .with_header("User-Agent", &self.config.user_agent); let handshake_data = handshake_request.to_string(); @@ -261,7 +264,10 @@ impl GurtClient { Connection::Plain(stream) => stream, }; - self.upgrade_to_tls(tcp_stream, host).await + // Use original_host for TLS SNI if available, otherwise fall back to host + let tls_host = original_host.unwrap_or(host); + + self.upgrade_to_tls(tcp_stream, tls_host).await } async fn upgrade_to_tls(&self, stream: TcpStream, host: &str) -> Result> { @@ -323,10 +329,10 @@ impl GurtClient { Ok(tls_stream) } - async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result { + async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest, original_host: Option<&str>) -> Result { debug!("Sending {} {} to {}:{}", request.method, request.path, host, port); - let mut tls_stream = self.get_pooled_connection(host, port).await?; + let mut tls_stream = self.get_pooled_connection(host, port, original_host).await?; let request_data = request.to_string(); tls_stream.write_all(request_data.as_bytes()).await @@ -501,7 +507,7 @@ impl GurtClient { request = request.with_header("Host", host); - self.send_request_internal(&resolved_host, port, request).await + self.send_request_internal(&resolved_host, port, request, Some(host)).await } fn parse_gurt_url(&self, url: &str) -> Result<(String, u16, String)> { @@ -564,7 +570,7 @@ impl GurtClient { .with_header("Content-Type", "application/json") .with_string_body(dns_request_body); - let dns_response = self.send_request_internal(&dns_server_ip, self.config.dns_server_port, dns_request).await?; + let dns_response = self.send_request_internal(&dns_server_ip, self.config.dns_server_port, dns_request, None).await?; if dns_response.status_code != 200 { return Err(GurtError::invalid_message(format!( @@ -675,4 +681,55 @@ mod tests { assert_eq!(key1, key2); assert_ne!(key1, key3); } + + #[tokio::test] + async fn test_host_header_preserved_with_dns_resolution() { + use crate::message::{GurtMethod, GurtRequest}; + + let mut config = GurtClientConfig::default(); + config.enable_connection_pooling = false; + let client = GurtClient::with_config(config); + + { + let mut dns_cache = client.dns_cache.lock().unwrap(); + dns_cache.insert("arson.dev".to_string(), "1.1.1.1".to_string()); + } + + let request = GurtRequest::new(GurtMethod::GET, "/test".to_string()); + + let original_host = "arson.dev"; + + let mut test_request = request.clone(); + test_request = test_request.with_header("Host", original_host); + + assert_eq!(test_request.headers.get("host").unwrap(), original_host); + + let resolved = client.resolve_domain("arson.dev").await.unwrap(); + assert_eq!(resolved, "1.1.1.1"); + + let request_with_host = GurtRequest::new(GurtMethod::GET, "/test".to_string()) + .with_header("Host", original_host); + + assert_eq!(request_with_host.headers.get("host").unwrap(), "arson.dev"); + } + + #[test] + fn test_handshake_request_uses_original_host() { + use crate::message::{GurtMethod, GurtRequest}; + + let original_host = "arson.dev"; + + let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string()) + .with_header("Host", original_host) + .with_header("User-Agent", "GURT-Client/1.0.0"); + + assert_eq!(handshake_request.headers.get("host").unwrap(), "arson.dev"); + assert_ne!(handshake_request.headers.get("host").unwrap(), "1.1.1.1"); + + assert_eq!(handshake_request.method, GurtMethod::HANDSHAKE); + assert_eq!(handshake_request.path, "/"); + + assert!(handshake_request.headers.contains_key("user-agent")); + } + } \ No newline at end of file diff --git a/protocol/library/src/server.rs b/protocol/library/src/server.rs index c744cd2..05c040f 100644 --- a/protocol/library/src/server.rs +++ b/protocol/library/src/server.rs @@ -577,8 +577,8 @@ mod tests { assert!(!route.matches(&GurtMethod::POST, "/test")); assert!(!route.matches(&GurtMethod::GET, "/other")); - assert!(!route.matches(&GurtMethod::GET, "/test?foo=bar")); - assert!(!route.matches(&GurtMethod::GET, "/test?page=1&limit=100")); + assert!(route.matches(&GurtMethod::GET, "/test?foo=bar")); + assert!(route.matches(&GurtMethod::GET, "/test?page=1&limit=100")); let wildcard_route = Route::get("/api/*"); assert!(wildcard_route.matches(&GurtMethod::GET, "/api/users")); diff --git a/search-engine/Cargo.toml b/search-engine/Cargo.toml index 50fc97f..ee18272 100644 --- a/search-engine/Cargo.toml +++ b/search-engine/Cargo.toml @@ -8,7 +8,7 @@ tokio = { version = "1.38.0", features = ["full"] } futures = "0.3.30" tantivy = "0.22" sha2 = "0.10" -gurt = { path = "../protocol/library" } +gurtlib = { path = "../protocol/library" } sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] } scraper = "0.20" lol_html = "1.2" diff --git a/search-engine/src/crawler.rs b/search-engine/src/crawler.rs index b2e47dc..09c6aa3 100644 --- a/search-engine/src/crawler.rs +++ b/search-engine/src/crawler.rs @@ -1,6 +1,6 @@ use anyhow::{Result, Context}; use chrono::Utc; -use gurt::{GurtClient, GurtClientConfig}; +use gurtlib::{GurtClient, GurtClientConfig}; use scraper::{Html, Selector}; use std::collections::{HashSet, VecDeque}; use std::sync::Arc; diff --git a/search-engine/src/server.rs b/search-engine/src/server.rs index 3d8c873..389dbb9 100644 --- a/search-engine/src/server.rs +++ b/search-engine/src/server.rs @@ -1,6 +1,6 @@ use anyhow::{Result, Context}; -use gurt::prelude::*; -use gurt::GurtError; +use gurtlib::prelude::*; +use gurtlib::GurtError; use serde_json::json; use std::sync::Arc; use tracing::{info, error}; diff --git a/site/src/routes/+page.svelte b/site/src/routes/+page.svelte index 8b00778..0ea8223 100644 --- a/site/src/routes/+page.svelte +++ b/site/src/routes/+page.svelte @@ -272,7 +272,7 @@
{`
 
 
@@ -100,7 +100,7 @@
 						Ubuntu 20.04+ / Fedora 35+
 					
 					
-