docs, gurt:// <a> url

This commit is contained in:
Face
2025-08-15 13:52:01 +03:00
parent c117e602fe
commit 5dae5a4868
25 changed files with 1640 additions and 390 deletions

169
SPEC.md
View File

@@ -1,169 +0,0 @@
# 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 <status_code> <status_message>`
- **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.

13
docs/docs/dns-system.md Normal file
View File

@@ -0,0 +1,13 @@
---
sidebar_position: 6
---
# DNS
The Gurted ecosystem features a custom DNS system that enables domain resolution for the gurt:// protocol. Unlike traditional DNS, Gurted DNS is designed specifically for the decentralized web ecosystem, providing:
- domain registration
- approval workflows (to ensure a free-of-charge, spam-free experience)
- archivable domain records
TODO: complete

View File

@@ -0,0 +1,8 @@
---
sidebar_position: 7
---
# Flumi (browser)
**Flumi** is the official browser for the Gurted ecosystem, built using the Godot game engine. It provides a complete web browsing experience for `gurt://` URLs with custom HTML/CSS rendering, Lua scripting support, and integration with the Gurted DNS system.

352
docs/docs/gurt-client.md Normal file
View File

@@ -0,0 +1,352 @@
---
sidebar_position: 3
---
# GURT Client Library
The GURT client library (for Rust) provides a high-level, HTTP-like interface for making requests to GURT servers. It handles TLS encryption, protocol handshakes, and connection management automatically.
## Bindings
- **Godot** by Gurted - [🔗 link](https://gurted.com/download)
- No bidings for other languages are currently available.
## Installation
Install via Cargo:
```bash
cargo add gurt
```
## Quick Start
```rust
use gurt::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
let client = GurtClient::new();
// Make a GET request
let response = client.get("gurt://example.com/").await?;
println!("Status: {}", response.status_code);
println!("Body: {}", response.text()?);
Ok(())
}
```
## Creating a Client
### Default Client
```rust
let client = GurtClient::new();
```
### Custom Configuration
```rust
use tokio::time::Duration;
let config = GurtClientConfig {
connect_timeout: Duration::from_secs(10),
request_timeout: Duration::from_secs(30),
handshake_timeout: Duration::from_secs(5),
user_agent: "MyApp/1.0.0".to_string(),
max_redirects: 5,
};
let client = GurtClient::with_config(config);
```
## Making Requests
### GET Requests
```rust
let response = client.get("gurt://api.example.com/users").await?;
if response.is_success() {
println!("Success: {}", response.text()?);
} else {
println!("Error: {} {}", response.status_code, response.status_message);
}
```
### POST Requests
#### Text Data
```rust
let response = client.post("gurt://api.example.com/submit", "Hello, GURT!").await?;
```
#### JSON Data
```rust
use serde_json::json;
let data = json!({
"name": "John Doe",
"email": "john@example.com"
});
let response = client.post_json("gurt://api.example.com/users", &data).await?;
```
### PUT Requests
```rust
// Text data
let response = client.put("gurt://api.example.com/resource/123", "Updated content").await?;
// JSON data
let update_data = json!({"status": "completed"});
let response = client.put_json("gurt://api.example.com/tasks/456", &update_data).await?;
```
### DELETE Requests
```rust
let response = client.delete("gurt://api.example.com/users/123").await?;
```
### HEAD Requests
```rust
let response = client.head("gurt://api.example.com/large-file").await?;
// Check headers without downloading body
let content_length = response.headers.get("content-length");
```
### OPTIONS Requests
```rust
let response = client.options("gurt://api.example.com/endpoint").await?;
// Check allowed methods
let allowed_methods = response.headers.get("allow");
```
### PATCH Requests
```rust
let patch_data = json!({"name": "Updated Name"});
let response = client.patch_json("gurt://api.example.com/users/123", &patch_data).await?;
```
## Response Handling
### Response Structure
```rust
pub struct GurtResponse {
pub version: String,
pub status_code: u16,
pub status_message: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
```
### Accessing Response Data
```rust
let response = client.get("gurt://api.example.com/data").await?;
// Status information
println!("Status Code: {}", response.status_code);
println!("Status Message: {}", response.status_message);
// Headers
for (name, value) in &response.headers {
println!("{}: {}", name, value);
}
// Body as string
let text = response.text()?;
// Body as bytes
let bytes = &response.body;
// Parse JSON response
let json_data: serde_json::Value = serde_json::from_slice(&response.body)?;
```
### Status Code Checking
```rust
if response.is_success() {
// 2xx status codes
println!("Request successful");
} else if response.is_client_error() {
// 4xx status codes
println!("Client error: {}", response.status_message);
} else if response.is_server_error() {
// 5xx status codes
println!("Server error: {}", response.status_message);
}
```
## Protocol Implementation
The GURT client automatically handles the complete GURT protocol:
1. **TCP Connection**: Establishes initial connection to the server
2. **Handshake**: Sends `HANDSHAKE` request and waits for `101 Switching Protocols`
3. **TLS Upgrade**: Upgrades the connection to TLS 1.3 with GURT ALPN
4. **Request/Response**: Sends the actual HTTP-style request over encrypted connection
All of this happens transparently when you call methods like `get()`, `post()`, etc.
## URL Parsing
The client automatically parses `gurt://` URLs:
```rust
// These are all valid GURT URLs:
client.get("gurt://example.com/").await?; // Port 4878 (default)
client.get("gurt://example.com:8080/api").await?; // Custom port
client.get("gurt://192.168.1.100/test").await?; // IP address
client.get("gurt://localhost:4878/dev").await?; // Localhost
```
### URL Components
The client extracts:
- **Host**: Domain name or IP address
- **Port**: Specified port or default (4878)
- **Path**: Request path (defaults to `/`)
## Error Handling
### Error Types
```rust
use gurt::GurtError;
match client.get("gurt://invalid-url").await {
Ok(response) => {
// Handle successful response
}
Err(GurtError::InvalidMessage(msg)) => {
println!("Invalid request: {}", msg);
}
Err(GurtError::Connection(msg)) => {
println!("Connection error: {}", msg);
}
Err(GurtError::Timeout(msg)) => {
println!("Request timeout: {}", msg);
}
Err(GurtError::Io(err)) => {
println!("IO error: {}", err);
}
Err(err) => {
println!("Other error: {}", err);
}
}
```
### Timeout Configuration
```rust
let config = GurtClientConfig {
connect_timeout: Duration::from_secs(5), // Connection timeout
request_timeout: Duration::from_secs(30), // Overall request timeout
handshake_timeout: Duration::from_secs(3), // GURT handshake timeout
..Default::default()
};
```
## Why Rust-first?
Rust was chosen for the official GURT protocol implementation due to its embedded nature.
To keep the core organized & not write identical code in GDScript, we used a GDExtension. A GDExtension can be created with a multitude of languages, but Rust was the one that provided the best performance, size, and programming ergonomics.
We expect the community to implement bindings for other languages, such as Python and JavaScript, to make GURT accessible for everybody!
## Example: Building a GURT API Client
```rust
use gurt::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
struct ApiClient {
client: GurtClient,
base_url: String,
}
impl ApiClient {
fn new(base_url: String) -> Self {
Self {
client: GurtClient::new(),
base_url,
}
}
async fn create_user(&self, user: CreateUser) -> Result<User> {
let url = format!("{}/users", self.base_url);
let response = self.client.post_json(&url, &user).await?;
if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}
let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}
async fn get_user(&self, id: u64) -> Result<User> {
let url = format!("{}/users/{}", self.base_url, id);
let response = self.client.get(&url).await?;
if response.status_code == 404 {
return Err(GurtError::invalid_message("User not found".to_string()));
}
if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}
let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let api = ApiClient::new("gurt://api.example.com".to_string());
// Create a user
let new_user = CreateUser {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let user = api.create_user(new_user).await?;
println!("Created user: {} (ID: {})", user.name, user.id);
// Retrieve the user
let retrieved_user = api.get_user(user.id).await?;
println!("Retrieved user: {}", retrieved_user.name);
Ok(())
}
```

272
docs/docs/gurt-protocol.md Normal file
View File

@@ -0,0 +1,272 @@
---
sidebar_position: 2
---
# GURT Protocol
**GURT** (version 1.0.0) is a TCP-based application protocol designed as an HTTP-like alternative with built-in TLS 1.3 encryption. It serves as the foundation for the Gurted ecosystem, enabling secure communication between clients and servers using the `gurt://` URL scheme.
## Overview
GURT provides a familiar HTTP-like syntax while offering security through mandatory TLS 1.3 encryption. Unlike HTTP where encryption is optional (HTTPS), all GURT connections are encrypted by default.
### Key Features
- **HTTP-like syntax** with familiar methods (GET, POST, PUT, DELETE, etc.)
- **Built-in required TLS 1.3 encryption** for all connections
- **Binary and text data support**
- **Status codes** compatible with HTTP semantics
- **Default port**: 4878
- **ALPN identifier**: `GURT/1.0`
## URL Scheme
GURT uses the `gurt://` URL scheme:
```
gurt://example.com/path
gurt://192.168.1.100:4878/api/data
gurt://localhost:4878/hello
```
The protocol automatically defaults to port 4878.
## Communication Flow
Every GURT session must begin with a `HANDSHAKE` request:
```http
HANDSHAKE / GURT/1.0.0\r\n
host: example.com\r\n
user-agent: GURT-Client/1.0.0\r\n
\r\n
```
Server responds with protocol confirmation:
```http
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
```
## Message Format
### Request Structure
```http
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]
```
**Components:**
- **Method line**: `METHOD /path GURT/1.0.0`
- **Headers**: Lowercase names, colon-separated values
- **Header terminator**: `\r\n\r\n`
- **Body**: Optional message content
### Response Structure
```http
GURT/1.0.0 200 OK\r\n
content-type: application/json\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
[response body]
```
**Components:**
- **Status line**: `GURT/1.0.0 <code> <message>`
- **Headers**: Lowercase names, required for responses
- **Body**: Optional response content
## HTTP Methods
GURT supports all standard HTTP methods:
| Method | Purpose | Body Allowed |
|--------|---------|--------------|
| `GET` | Retrieve resource | No |
| `POST` | Create/submit data | Yes |
| `PUT` | Update/replace resource | Yes |
| `DELETE` | Remove resource | No |
| `HEAD` | Get headers only | No |
| `OPTIONS` | Get allowed methods | No |
| `PATCH` | Partial update | Yes |
| `HANDSHAKE` | Protocol handshake | No |
## Status Codes
GURT uses HTTP-compatible status codes:
### Success (2xx)
- `200 OK` - Request successful
- `201 CREATED` - Resource created
- `202 ACCEPTED` - Request accepted for processing
- `204 NO_CONTENT` - Success with no response body
### Protocol (1xx)
- `101 SWITCHING_PROTOCOLS` - Handshake successful
### Client Error (4xx)
- `400 BAD_REQUEST` - Invalid request format
- `401 UNAUTHORIZED` - Authentication required
- `403 FORBIDDEN` - Access denied
- `404 NOT_FOUND` - Resource not found
- `405 METHOD_NOT_ALLOWED` - Method not supported
- `408 TIMEOUT` - Request timeout
- `413 TOO_LARGE` - Request too large
- `415 UNSUPPORTED_MEDIA_TYPE` - Unsupported content type
### Server Error (5xx)
- `500 INTERNAL_SERVER_ERROR` - Server error
- `501 NOT_IMPLEMENTED` - Method not implemented
- `502 BAD_GATEWAY` - Gateway error
- `503 SERVICE_UNAVAILABLE` - Service unavailable
- `504 GATEWAY_TIMEOUT` - Gateway timeout
## Security
All connections must use TLS 1.3 for encryption. This means you have to generate and use a valid TLS certificate for your GURT 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
gurty serve --cert localhost+2.pem --key localhost+2-key.pem
```
Install Gurty, the official GURT server tool, [on the Gurted.com download page](https://gurted.com/download/)
## Protocol Limits
| Parameter | Limit |
|-----------|-------|
| Maximum message size | 10 MB |
| Default connection timeout | 10 seconds |
| Default request timeout | 30 seconds |
| Default handshake timeout | 5 seconds |
| Maximum connection pool size | 10 connections |
| Pool idle timeout | 300 seconds |
## Example Session
Complete GURT communication example:
```http
# Client connects and sends handshake
HANDSHAKE / GURT/1.0.0\r\n
host: example.com\r\n
user-agent: GURT-Client/1.0.0\r\n
\r\n
# Server confirms protocol
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, 14 Aug 2025 12:00:00 GMT\r\n
\r\n
# All further communication is encrypted
# Client sends JSON data
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 responds with JSON
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, 14 Aug 2025 12:00:01 GMT\r\n
\r\n
{"result":"ok"}
```
## Domain Resolution
GURT integrates with Gurted's custom DNS system:
### Direct IP Access
```
gurt://192.168.1.100:4878/
gurt://localhost:4878/api
```
### Domain Resolution
```
gurt://example.real/ # Resolves via Gurted DNS
```
The Gurted DNS server resolves domains in the format `name.tld` to IP addresses, enabling human-readable domain names for GURT services. This is done automatically by your GURT browser and is documented in the [DNS System documentation](./dns-system.md).
## Implementation
GURT is implemented in Rust with the following components:
- **Protocol Library**: Core protocol implementation, reusable as a Rust crate
- **CLI Tool (Gurty)**: Server setup and management
- **Godot Extension**: Browser integration for Flumi

471
docs/docs/gurt-server.md Normal file
View File

@@ -0,0 +1,471 @@
---
sidebar_position: 4
---
# GURT Server Library
The GURT server library provides a framework for building HTTP-like servers that use the GURT protocol. It features automatic TLS handling, route-based request handling, and middleware support.
## Installation
Add the GURT library to your `Cargo.toml`:
```toml
[dependencies]
gurt = "0.1"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
serde_json = "1.0"
```
## Quick Start
```rust
use gurt::prelude::*;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let server = GurtServer::with_tls_certificates("cert.pem", "key.pem")?
.get("/", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("<h1>Hello, GURT!</h1>"))
})
.get("/api/users", |_ctx| async {
let users = json!(["Alice", "Bob"]);
Ok(GurtResponse::ok().with_json_body(&users))
});
println!("GURT server starting on gurt://127.0.0.1:4878");
server.listen("127.0.0.1:4878").await
}
```
## Creating a Server
### Basic Server
```rust
let server = GurtServer::new();
```
### Server with TLS Certificates
```rust
// Load TLS certificates during server creation
let server = GurtServer::with_tls_certificates("cert.pem", "key.pem")?;
// Or load certificates later
let mut server = GurtServer::new();
server.load_tls_certificates("cert.pem", "key.pem")?;
```
## Route Handlers
### Method-Specific Routes
```rust
let server = GurtServer::with_tls_certificates("cert.pem", "key.pem")?
.get("/", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("GET request"))
})
.post("/submit", |ctx| async {
let body = ctx.text()?;
println!("Received: {}", body);
Ok(GurtResponse::new(GurtStatusCode::Created).with_string_body("Created"))
})
.put("/update", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("Updated"))
})
.delete("/delete", |_ctx| async {
Ok(GurtResponse::new(GurtStatusCode::NoContent))
})
.patch("/partial", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("Patched"))
});
```
### Any Method Route
```rust
let server = server.any("/webhook", |ctx| async {
match ctx.method() {
GurtMethod::GET => Ok(GurtResponse::ok().with_string_body("GET webhook")),
GurtMethod::POST => Ok(GurtResponse::ok().with_string_body("POST webhook")),
_ => Ok(GurtResponse::new(GurtStatusCode::MethodNotAllowed)),
}
});
```
### Route Patterns
```rust
let server = server
.get("/users", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("All users"))
})
.get("/users/*", |ctx| async {
// Matches /users/123, /users/profile, etc.
let path = ctx.path();
Ok(GurtResponse::ok().with_string_body(format!("User path: {}", path)))
})
.get("/api/*", |_ctx| async {
// Matches any path starting with /api/
Ok(GurtResponse::ok().with_string_body("API endpoint"))
});
```
## Server Context
The `ServerContext` provides access to request information:
```rust
.post("/analyze", |ctx| async {
// Client information
println!("Client IP: {}", ctx.client_ip());
println!("Client Port: {}", ctx.client_port());
// Request details
println!("Method: {:?}", ctx.method());
println!("Path: {}", ctx.path());
// Headers
if let Some(content_type) = ctx.header("content-type") {
println!("Content-Type: {}", content_type);
}
// Iterate all headers
for (name, value) in ctx.headers() {
println!("{}: {}", name, value);
}
// Body data
let body_bytes = ctx.body();
let body_text = ctx.text()?;
Ok(GurtResponse::ok().with_string_body("Analyzed"))
})
```
## Response Building
### Basic Responses
```rust
// Success responses
GurtResponse::ok() // 200 OK
GurtResponse::new(GurtStatusCode::Created) // 201 Created
GurtResponse::new(GurtStatusCode::Accepted) // 202 Accepted
GurtResponse::new(GurtStatusCode::NoContent) // 204 No Content
// Client error responses
GurtResponse::bad_request() // 400 Bad Request
GurtResponse::new(GurtStatusCode::Unauthorized) // 401 Unauthorized
GurtResponse::new(GurtStatusCode::Forbidden) // 403 Forbidden
GurtResponse::not_found() // 404 Not Found
GurtResponse::new(GurtStatusCode::MethodNotAllowed) // 405 Method Not Allowed
// Server error responses
GurtResponse::internal_server_error() // 500 Internal Server Error
GurtResponse::new(GurtStatusCode::NotImplemented) // 501 Not Implemented
GurtResponse::new(GurtStatusCode::ServiceUnavailable) // 503 Service Unavailable
```
### Response with Body
```rust
// String body
GurtResponse::ok().with_string_body("Hello, World!")
// JSON body
use serde_json::json;
let data = json!({"message": "Hello", "status": "success"});
GurtResponse::ok().with_json_body(&data)
// Binary body
let image_data = std::fs::read("image.png")?;
GurtResponse::ok()
.with_header("content-type", "image/png")
.with_body(image_data)
```
### Response with Headers
```rust
GurtResponse::ok()
.with_string_body("Custom response")
.with_header("x-custom-header", "custom-value")
.with_header("cache-control", "no-cache")
.with_header("content-type", "text/plain; charset=utf-8")
```
## Advanced Examples
### JSON API Server
```rust
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
let server = GurtServer::with_tls_certificates("cert.pem", "key.pem")?
.get("/api/users", |ctx| async {
let users = vec![
User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
];
Ok(GurtResponse::ok()
.with_header("content-type", "application/json")
.with_json_body(&users))
})
.post("/api/users", |ctx| async {
let body = ctx.text()?;
let user: User = serde_json::from_str(&body)
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
// Save user to database here...
println!("Creating user: {}", user.name);
Ok(GurtResponse::new(GurtStatusCode::Created)
.with_header("content-type", "application/json")
.with_json_body(&user)?)
})
.get("/api/users/*", |ctx| async {
let path = ctx.path();
if let Some(user_id) = path.strip_prefix("/api/users/") {
if let Ok(id) = user_id.parse::<u64>() {
// Get user from database here...
let user = User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
};
Ok(GurtResponse::ok()
.with_header("content-type", "application/json")
.with_json_body(&user))
} else {
Ok(GurtResponse::bad_request()
.with_string_body("Invalid user ID"))
}
} else {
Ok(GurtResponse::not_found())
}
});
```
### File Server
```rust
use std::path::Path;
use tokio::fs;
let server = server.get("/files/*", |ctx| async {
let path = ctx.path();
let file_path = path.strip_prefix("/files/").unwrap_or("");
// Security: prevent directory traversal
if file_path.contains("..") {
return Ok(GurtResponse::new(GurtStatusCode::Forbidden)
.with_string_body("Access denied"));
}
let full_path = format!("./static/{}", file_path);
match fs::read(&full_path).await {
Ok(data) => {
let content_type = match Path::new(&full_path).extension()
.and_then(|ext| ext.to_str()) {
Some("html") => "text/html",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("json") => "application/json",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("gif") => "image/gif",
_ => "application/octet-stream",
};
Ok(GurtResponse::ok()
.with_header("content-type", content_type)
.with_body(data))
}
Err(_) => {
Ok(GurtResponse::not_found()
.with_string_body("File not found"))
}
}
});
```
### Middleware Pattern
```rust
// Request logging middleware
async fn log_request(ctx: &ServerContext) -> Result<()> {
println!("{} {} from {}",
ctx.method(),
ctx.path(),
ctx.client_ip()
);
Ok(())
}
// Authentication middleware
async fn require_auth(ctx: &ServerContext) -> Result<()> {
if let Some(auth_header) = ctx.header("authorization") {
if auth_header.starts_with("Bearer ") {
// Validate token here...
return Ok(());
}
}
Err(GurtError::invalid_message("Authentication required"))
}
let server = server
.get("/protected", |ctx| async {
// Apply middleware
log_request(ctx).await?;
require_auth(ctx).await?;
Ok(GurtResponse::ok()
.with_string_body("Protected content"))
})
.post("/api/data", |ctx| async {
log_request(ctx).await?;
// Handle request
Ok(GurtResponse::ok()
.with_string_body("Data processed"))
});
```
### Error Handling
```rust
let server = server.post("/api/process", |ctx| async {
match process_data(ctx).await {
Ok(result) => {
Ok(GurtResponse::ok()
.with_json_body(&result))
}
Err(ProcessError::ValidationError(msg)) => {
Ok(GurtResponse::bad_request()
.with_json_body(&json!({"error": msg})))
}
Err(ProcessError::NotFound) => {
Ok(GurtResponse::not_found()
.with_json_body(&json!({"error": "Resource not found"})))
}
Err(_) => {
Ok(GurtResponse::internal_server_error()
.with_json_body(&json!({"error": "Internal server error"})))
}
}
});
async fn process_data(ctx: &ServerContext) -> Result<serde_json::Value, ProcessError> {
// Your processing logic here
todo!()
}
#[derive(Debug)]
enum ProcessError {
ValidationError(String),
NotFound,
InternalError,
}
```
## TLS Configuration
### Development Certificates
For development, use `mkcert` to generate trusted local certificates:
```bash
# Install mkcert
choco install mkcert # Windows
brew install mkcert # macOS
# or download from GitHub releases
# Install local CA
mkcert -install
# Generate certificates
mkcert localhost 127.0.0.1 ::1
```
### Production Certificates
For production, generate certificates with OpenSSL:
```bash
# Generate private key
openssl genpkey -algorithm RSA -out server.key -pkcs8
# Generate certificate signing request
openssl req -new -key server.key -out server.csr
# Generate self-signed certificate
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# Or in one step
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
```
## Listening and Deployment
```rust
// Listen on all interfaces
server.listen("0.0.0.0:4878").await?;
// Listen on specific interface
server.listen("127.0.0.1:8080").await?;
// Listen on IPv6
server.listen("[::1]:4878").await?;
```
## Testing
```rust
#[cfg(test)]
mod tests {
use super::*;
use gurt::GurtClient;
#[tokio::test]
async fn test_server() {
let server = GurtServer::with_tls_certificates("test-cert.pem", "test-key.pem")
.unwrap()
.get("/test", |_ctx| async {
Ok(GurtResponse::ok().with_string_body("test response"))
});
// Start server in background
tokio::spawn(async move {
server.listen("127.0.0.1:9999").await.unwrap();
});
// Give server time to start
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Test with client
let client = GurtClient::new();
let response = client.get("gurt://127.0.0.1:9999/test").await.unwrap();
assert_eq!(response.status_code, 200);
assert_eq!(response.text().unwrap(), "test response");
}
}
```

105
docs/docs/gurty-cli.md Normal file
View File

@@ -0,0 +1,105 @@
---
sidebar_position: 5
---
# Gurty CLI Tool
**Gurty** is a command-line interface tool for setting up and managing GURT protocol servers. It provides an easy way to deploy GURT servers with proper TLS configuration for both development and production environments.
## Installation
Build Gurty from the protocol CLI directory:
```bash
cd protocol/cli
cargo build --release
```
The binary will be available at `target/release/gurty` (or `gurty.exe` on Windows).
## Quick Start
### Development Setup
1. **Install mkcert** for development certificates:
```bash
# Windows (with Chocolatey)
choco install mkcert
# macOS (with Homebrew)
brew install mkcert
# Or download from: https://github.com/FiloSottile/mkcert/releases
```
2. **Install local CA** in your system:
```bash
mkcert -install
```
3. **Generate localhost certificates**:
```bash
cd 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**:
```bash
cargo run --release serve --cert localhost+2.pem --key localhost+2-key.pem
```
### Production Setup
1. **Generate production certificates** with OpenSSL:
```bash
# Generate private key
openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8
# 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 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
```
## Commands
### `serve` Command
Start a GURT server with TLS certificates.
```bash
gurty serve [OPTIONS]
```
#### Options
| Option | Description | Default |
|--------|-------------|---------|
| `--cert <FILE>` | Path to TLS certificate file | Required |
| `--key <FILE>` | Path to TLS private key file | Required |
| `--host <HOST>` | Host address to bind to | `127.0.0.1` |
| `--port <PORT>` | Port number to listen on | `4878` |
| `--dir <DIR>` | Directory to serve files from | None |
| `--log-level <LEVEL>` | Logging level (error, warn, info, debug, trace) | `info` |
#### Examples
```bash
gurty serve --cert localhost+2.pem --key localhost+2-key.pem --dir ./public
```
Debug:
```bash
gurty serve --cert dev.pem --key dev-key.pem --log-level debug
```

View File

@@ -15,6 +15,8 @@ sidebar_position: 1
**GURT** is a *content delivery protocol* similar to HTTPS. It's the core of how Gurted applications communicate.
Learn more about the GURT protocol: [Protocol Specification](./gurt-protocol.md)
## Getting Started
Get started by **exploring Gurted sites** or **try creating your first GURT page**.

View File

@@ -137,7 +137,7 @@ const config: Config = {
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
additionalLanguages: ['lua'],
additionalLanguages: ['lua', 'bash', 'http'],
},
} satisfies Preset.ThemeConfig,
};

View File

@@ -13,21 +13,38 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
Create as many sidebars as you want.
*/
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
label: 'GURT Protocol',
items: [
'gurt-protocol',
'gurt-client',
'gurt-server',
'gurty-cli',
],
},
{
type: 'category',
label: 'Ecosystem',
items: [
'dns-system',
'flumi-browser',
],
},
{
type: 'category',
label: 'Web Standards',
items: [
'html',
'css',
],
},
],
*/
// Auto-generated fallback
// tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
};
export default sidebars;

View File

@@ -377,7 +377,7 @@ func apply_element_styles(node: Control, element: HTMLElement, parser: HTMLParse
var text = HTMLParser.get_bbcode_with_styles(element, styles, parser)
label.text = text
static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictionary, content: String) -> String:
static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictionary, content: String, parser: HTMLParser = null) -> String:
match element.tag_name:
"b":
if styles.has("font-bold") and styles["font-bold"]:
@@ -416,6 +416,7 @@ static func apply_element_bbcode_formatting(element: HTMLElement, styles: Dictio
else:
color = str(c)
if href.length() > 0:
# Pass raw href - URL resolution happens in handle_link_click
return "[color=%s][url=%s]%s[/url][/color]" % [color, href, content]
return content
@@ -429,10 +430,10 @@ static func get_bbcode_with_styles(element: HTMLElement, styles: Dictionary, par
if parser != null:
child_styles = parser.get_element_styles_with_inheritance(child, "", [])
var child_content = HTMLParser.get_bbcode_with_styles(child, child_styles, parser)
child_content = apply_element_bbcode_formatting(child, child_styles, child_content)
child_content = apply_element_bbcode_formatting(child, child_styles, child_content, parser)
text += child_content
# Apply formatting to the current element itself
text = apply_element_bbcode_formatting(element, styles, text)
text = apply_element_bbcode_formatting(element, styles, text, parser)
return text

View File

@@ -218,10 +218,10 @@ static func get_error_type(error_message: String) -> Dictionary:
else:
return {"code": "ERR_UNKNOWN", "title": "Something went wrong", "icon": ""}
static func create_error_page(error_message: String) -> String:
static func create_error_page(error_message: String) -> PackedByteArray:
var error_info = get_error_type(error_message)
return """<head>
return ("""<head>
<title>""" + error_info.title + """ - GURT</title>
<meta name="theme-color" content="#f8f9fa">
<style>
@@ -269,4 +269,4 @@ static func create_error_page(error_message: String) -> String:
<button style="retry-button" id="reload">Reload</button>
</div>
</body>"""
</body>""").to_utf8_buffer()

View File

@@ -70,6 +70,54 @@ func recalculate_percentage_elements(node: Node):
var current_domain = "" # Store current domain for display
func resolve_url(href: String) -> String:
if href.begins_with("http://") or href.begins_with("https://") or href.begins_with("gurt://"):
return href
if current_domain.is_empty():
return href
var clean_domain = current_domain.rstrip("/")
var current_parts = clean_domain.split("/")
var host = current_parts[0]
var current_path_parts = Array(current_parts.slice(1)) if current_parts.size() > 1 else []
var final_path_parts = []
if href.begins_with("/"):
var href_path = href.substr(1) if href.length() > 1 else ""
if not href_path.is_empty():
final_path_parts = href_path.split("/")
else:
final_path_parts = current_path_parts.duplicate()
var href_parts = href.split("/")
for part in href_parts:
if part == "..":
if final_path_parts.size() > 0:
final_path_parts.pop_back()
elif part == "." or part == "":
continue
else:
final_path_parts.append(part)
var result = "gurt://" + host
if final_path_parts.size() > 0:
result += "/" + "/".join(final_path_parts)
return result
func handle_link_click(meta: Variant) -> void:
var href = str(meta)
var resolved_url = resolve_url(href)
if GurtProtocol.is_gurt_domain(resolved_url):
_on_search_submitted(resolved_url)
else:
OS.shell_open(resolved_url)
func _on_search_submitted(url: String) -> void:
print("Search submitted: ", url)
@@ -88,12 +136,13 @@ func _on_search_submitted(url: String) -> void:
tab.set_icon(GLOBE_ICON)
var html_bytes = result.html
render_content(html_bytes)
if result.has("display_url"):
current_domain = result.display_url
if not search_bar.has_focus():
search_bar.text = current_domain
render_content(html_bytes)
else:
print("Non-GURT URL entered: ", url)
@@ -178,7 +227,7 @@ func render_content(html_bytes: PackedByteArray) -> void:
safe_add_child(hbox, inline_node)
# Handle hyperlinks for all inline elements
if contains_hyperlink(inline_element) and inline_node is RichTextLabel:
inline_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
inline_node.meta_clicked.connect(handle_link_click)
else:
print("Failed to create inline element node: ", inline_element.tag_name)
@@ -198,9 +247,9 @@ func render_content(html_bytes: PackedByteArray) -> void:
if contains_hyperlink(element):
if element_node is RichTextLabel:
element_node.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
element_node.meta_clicked.connect(handle_link_click)
elif element_node.has_method("get") and element_node.get("rich_text_label"):
element_node.rich_text_label.meta_clicked.connect(func(meta): OS.shell_open(str(meta)))
element_node.rich_text_label.meta_clicked.connect(handle_link_click)
else:
print("Couldn't parse unsupported HTML tag \"%s\"" % element.tag_name)
@@ -328,6 +377,12 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) ->
if child_element.tag_name not in ["input", "textarea", "select", "button", "audio"]:
parser.register_dom_node(child_element, child_node)
safe_add_child(container_for_children, child_node)
if contains_hyperlink(child_element):
if child_node is RichTextLabel:
child_node.meta_clicked.connect(handle_link_click)
elif child_node.has_method("get") and child_node.get("rich_text_label"):
child_node.rich_text_label.meta_clicked.connect(handle_link_click)
return final_node

7
protocol/README.md Normal file
View File

@@ -0,0 +1,7 @@
The implementation of the GURT protocol.
- [cli](./cli) - Gurty, the command-line tool for managing GURT servers
- [library](./library) - client/server GURT protocol library for Rust (the core, used in Flumi)
- [gdextension](./gdextension) - the Godot bindings for the GURT protocol (used in Flumi)
For the full spec, see the [Gurted Documentation](https://docs.gurted.com).

View File

@@ -299,6 +299,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -395,6 +405,7 @@ dependencies = [
"base64",
"chrono",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_json",
@@ -772,6 +783,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "overload"
version = "0.1.1"
@@ -948,6 +965,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -990,12 +1019,44 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.219"

View File

@@ -95,11 +95,10 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
let server = server
.get("/", {
let base_dir = base_dir.clone();
move |ctx| {
let client_ip = ctx.client_ip();
move |_| {
let base_dir = base_dir.clone();
async move {
// Try to serve index.html if it exists, otherwise show server info
// Try to serve index.html if it exists
let index_path = base_dir.join("index.html");
if index_path.exists() && index_path.is_file() {
@@ -110,36 +109,54 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
.with_string_body(content));
}
Err(_) => {
// Fall through to default page
// Fall through to directory listing
}
}
}
// Default server info page
Ok(GurtResponse::ok()
.with_header("Content-Type", "text/html")
.with_string_body(format!(r#"
// No index.html found, show directory listing
match std::fs::read_dir(base_dir.as_ref()) {
Ok(entries) => {
let mut listing = String::from(r#"
<!DOCTYPE html>
<html>
<head>
<title>GURT Protocol Server</title>
<title>Directory Listing</title>
<style>
body {{ font-sans m-[30px] bg-[#f5f5f5] }}
.header {{ text-[#0066cc] }}
.status {{ text-[#28a745] font-bold }}
body { font-sans m-[40px] }
.dir { font-bold text-[#0066cc] }
</style>
</head>
<body>
<h1 class="header">Welcome to the GURT Protocol!</h1>
<p class="status">This server is successfully running. We couldn't find index.html though :(</p>
<p>Protocol: <strong>GURT/{}</strong></p>
<p>Client IP: <strong>{}</strong></p>
</body>
</html>
"#,
gurt::GURT_VERSION,
client_ip,
)))
<h1>Directory Listing</h1>
<div style="flex flex-col gap-2">
"#);
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 { "style=\"dir\"" } else { "" };
listing.push_str(&format!(
r#" <a {} href="/{}">{}</a>"#,
class, name, display_name
));
listing.push('\n');
}
listing.push_str("</div></body>\n</html>");
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"))
}
}
}
}
})
@@ -214,7 +231,6 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
<title>Directory Listing</title>
<style>
body { font-sans m-[40px] }
.file { my-1 }
.dir { font-bold text-[#0066cc] }
</style>
</head>
@@ -228,10 +244,10 @@ fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: O
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" };
let class = if is_dir { "style=\"dir\"" } else { "" };
listing.push_str(&format!(
r#" <a style={} href="/{}">{}</a>"#,
r#" <a {} href="{}">{}</a>"#,
class, name, display_name
));
listing.push('\n');

View File

@@ -16,17 +16,7 @@ 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"
tokio = { version = "1.0", features = ["rt"] }
url = "2.5"
[profile.release]

View File

@@ -1,11 +1,9 @@
use godot::prelude::*;
use gurt::prelude::*;
use gurt::{GurtMethod, GurtRequest};
use gurt::{GurtMethod, GurtClientConfig};
use tokio::runtime::Runtime;
use std::sync::Arc;
use std::cell::RefCell;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
struct GurtGodotExtension;
@@ -109,7 +107,7 @@ impl GurtProtocolClient {
}
};
let mut config = ClientConfig::default();
let mut config = GurtClientConfig::default();
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
let client = GurtClient::with_config(config);
@@ -168,7 +166,31 @@ impl GurtProtocolClient {
}
};
let response = match runtime.block_on(self.gurt_request_with_handshake(host, port, method, path)) {
let client_binding = self.client.borrow();
let client = match client_binding.as_ref() {
Some(c) => c,
None => {
godot_print!("No client available");
return None;
}
};
let url = format!("gurt://{}:{}{}", host, port, path);
let response = match runtime.block_on(async {
match method {
GurtMethod::GET => client.get(&url).await,
GurtMethod::POST => client.post(&url, "").await,
GurtMethod::PUT => client.put(&url, "").await,
GurtMethod::DELETE => client.delete(&url).await,
GurtMethod::HEAD => client.head(&url).await,
GurtMethod::OPTIONS => client.options(&url).await,
GurtMethod::PATCH => client.patch(&url, "").await,
_ => {
godot_print!("Unsupported method: {:?}", method);
return Err(GurtError::invalid_message("Unsupported method"));
}
}
}) {
Ok(resp) => resp,
Err(e) => {
godot_print!("GURT request failed: {}", e);
@@ -179,145 +201,6 @@ impl GurtProtocolClient {
Some(self.convert_response(response))
}
async fn gurt_request_with_handshake(&self, host: &str, port: u16, method: GurtMethod, path: &str) -> gurt::Result<GurtResponse> {
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::<usize>().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<tokio_rustls::client::TlsStream<tokio::net::TcpStream>> {
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;

View File

@@ -224,6 +224,16 @@ dependencies = [
"cc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -365,6 +375,7 @@ dependencies = [
"base64",
"chrono",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_json",
@@ -686,6 +697,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "overload"
version = "0.1.1"
@@ -830,6 +847,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -872,6 +901,38 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "security-framework"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.219"

View File

@@ -30,6 +30,7 @@ chrono = { version = "0.4", features = ["serde"] }
tokio-rustls = "0.26"
rustls = "0.23"
rustls-pemfile = "2.0"
rustls-native-certs = "0.8"
base64 = "0.22"
url = "2.5"

View File

@@ -2,15 +2,18 @@ use crate::{
GurtError, Result, GurtRequest, GurtResponse,
protocol::{DEFAULT_PORT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_HANDSHAKE_TIMEOUT, BODY_SEPARATOR},
message::GurtMethod,
crypto::GURT_ALPN,
};
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{timeout, Duration};
use tokio_rustls::{TlsConnector, rustls::{ClientConfig as TlsClientConfig, RootCertStore, pki_types::ServerName}};
use std::sync::Arc;
use url::Url;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub struct GurtClientConfig {
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub handshake_timeout: Duration,
@@ -18,7 +21,7 @@ pub struct ClientConfig {
pub max_redirects: usize,
}
impl Default for ClientConfig {
impl Default for GurtClientConfig {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT),
@@ -30,29 +33,55 @@ impl Default for ClientConfig {
}
}
#[derive(Debug)]
enum Connection {
Plain(TcpStream),
Tls(tokio_rustls::client::TlsStream<TcpStream>),
}
impl Connection {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self {
Connection::Plain(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
Connection::Tls(stream) => stream.read(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
async fn write_all(&mut self, buf: &[u8]) -> Result<()> {
match self {
Connection::Plain(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
Connection::Tls(stream) => stream.write_all(buf).await.map_err(|e| GurtError::connection(e.to_string())),
}
}
}
#[derive(Debug)]
struct PooledConnection {
stream: TcpStream,
connection: Connection,
}
impl PooledConnection {
fn new(stream: TcpStream) -> Self {
Self { stream }
Self { connection: Connection::Plain(stream) }
}
fn with_tls(stream: tokio_rustls::client::TlsStream<TcpStream>) -> Self {
Self { connection: Connection::Tls(stream) }
}
}
pub struct GurtClient {
config: ClientConfig,
config: GurtClientConfig,
}
impl GurtClient {
pub fn new() -> Self {
Self {
config: ClientConfig::default(),
config: GurtClientConfig::default(),
}
}
pub fn with_config(config: ClientConfig) -> Self {
pub fn with_config(config: GurtClientConfig) -> Self {
Self {
config,
}
@@ -71,7 +100,7 @@ impl GurtClient {
Ok(conn)
}
async fn read_response_data(&self, stream: &mut TcpStream) -> Result<Vec<u8>> {
async fn read_response_data(&self, conn: &mut PooledConnection) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut temp_buffer = [0u8; 8192];
@@ -82,7 +111,7 @@ impl GurtClient {
return Err(GurtError::timeout("Response timeout"));
}
let bytes_read = stream.read(&mut temp_buffer).await?;
let bytes_read = conn.connection.read(&mut temp_buffer).await?;
if bytes_read == 0 {
break; // Connection closed
}
@@ -106,17 +135,92 @@ impl GurtClient {
}
}
async fn perform_handshake(&self, host: &str, port: u16) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
debug!("Starting GURT handshake with {}:{}", host, port);
let mut plain_conn = self.create_connection(host, port).await?;
let handshake_request = GurtRequest::new(GurtMethod::HANDSHAKE, "/".to_string())
.with_header("Host", host)
.with_header("User-Agent", &self.config.user_agent);
let handshake_data = handshake_request.to_string();
plain_conn.connection.write_all(handshake_data.as_bytes()).await?;
let handshake_response_bytes = timeout(
self.config.handshake_timeout,
self.read_response_data(&mut plain_conn)
).await
.map_err(|_| GurtError::timeout("Handshake timeout"))??;
let handshake_response = GurtResponse::parse_bytes(&handshake_response_bytes)?;
if handshake_response.status_code != 101 {
return Err(GurtError::protocol(format!("Handshake failed: {} {}",
handshake_response.status_code,
handshake_response.status_message)));
}
let tcp_stream = match plain_conn.connection {
Connection::Plain(stream) => stream,
_ => return Err(GurtError::protocol("Expected plain connection for handshake")),
};
self.upgrade_to_tls(tcp_stream, host).await
}
async fn upgrade_to_tls(&self, stream: TcpStream, host: &str) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
debug!("Upgrading connection to TLS for {}", host);
let mut root_store = RootCertStore::empty();
let cert_result = rustls_native_certs::load_native_certs();
let mut added = 0;
for cert in cert_result.certs {
if root_store.add(cert).is_ok() {
added += 1;
}
}
if added == 0 {
return Err(GurtError::crypto("No valid system certificates found".to_string()));
}
let mut client_config = TlsClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
client_config.alpn_protocols = vec![GURT_ALPN.to_vec()];
let connector = TlsConnector::from(Arc::new(client_config));
let server_name = match host {
"127.0.0.1" => "localhost",
"localhost" => "localhost",
_ => host
};
let domain = ServerName::try_from(server_name.to_string())
.map_err(|e| GurtError::crypto(format!("Invalid server name '{}': {}", server_name, e)))?;
let tls_stream = connector.connect(domain, stream).await
.map_err(|e| GurtError::crypto(format!("TLS handshake failed: {}", e)))?;
debug!("TLS connection established with {}", host);
Ok(tls_stream)
}
async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result<GurtResponse> {
debug!("Sending {} {} to {}:{}", request.method, request.path, host, port);
let mut conn = self.create_connection(host, port).await?;
let tls_stream = self.perform_handshake(host, port).await?;
let mut conn = PooledConnection::with_tls(tls_stream);
let request_data = request.to_string();
conn.stream.write_all(request_data.as_bytes()).await?;
conn.connection.write_all(request_data.as_bytes()).await?;
let response_bytes = timeout(
self.config.request_timeout,
self.read_response_data(&mut conn.stream)
self.read_response_data(&mut conn)
).await
.map_err(|_| GurtError::timeout("Request timeout"))??;

View File

@@ -10,7 +10,7 @@ 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 use client::{GurtClient, GurtClientConfig};
pub mod prelude {
pub use crate::{
@@ -19,6 +19,6 @@ pub mod prelude {
GURT_VERSION, DEFAULT_PORT,
CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION,
GurtServer, GurtHandler, ServerContext, Route,
GurtClient, ClientConfig,
GurtClient, GurtClientConfig,
};
}

View File

@@ -90,7 +90,7 @@ impl GurtRequest {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
pub fn text(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_string())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
@@ -283,7 +283,7 @@ impl GurtResponse {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
pub fn text(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_owned())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
@@ -524,7 +524,7 @@ mod tests {
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");
assert_eq!(request.text().unwrap(), "test body");
}
#[test]
@@ -536,7 +536,7 @@ mod tests {
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(), "<html></html>");
assert_eq!(response.text().unwrap(), "<html></html>");
}
#[test]

View File

@@ -47,8 +47,8 @@ impl ServerContext {
&self.request.body
}
pub fn body_as_string(&self) -> Result<String> {
self.request.body_as_string()
pub fn text(&self) -> Result<String> {
self.request.text()
}
pub fn header(&self, key: &str) -> Option<&String> {
@@ -387,7 +387,7 @@ impl GurtServer {
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"));
.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?;