docs, gurt:// <a> url
This commit is contained in:
169
SPEC.md
169
SPEC.md
@@ -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
13
docs/docs/dns-system.md
Normal 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
|
||||
8
docs/docs/flumi-browser.md
Normal file
8
docs/docs/flumi-browser.md
Normal 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
352
docs/docs/gurt-client.md
Normal 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
272
docs/docs/gurt-protocol.md
Normal 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
471
docs/docs/gurt-server.md
Normal 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
105
docs/docs/gurty-cli.md
Normal 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
|
||||
```
|
||||
@@ -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**.
|
||||
|
||||
@@ -137,7 +137,7 @@ const config: Config = {
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
additionalLanguages: ['lua'],
|
||||
additionalLanguages: ['lua', 'bash', 'http'],
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
7
protocol/README.md
Normal file
7
protocol/README.md
Normal 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).
|
||||
61
protocol/cli/Cargo.lock
generated
61
protocol/cli/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
61
protocol/library/Cargo.lock
generated
61
protocol/library/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"))??;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
Reference in New Issue
Block a user