GURT protocol (lib, cli, gdextension, Flumi integration)

This commit is contained in:
Face
2025-08-14 20:29:19 +03:00
parent 65f3a21890
commit c117e602fe
46 changed files with 6559 additions and 89 deletions

1720
protocol/cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
protocol/cli/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "gurty"
version = "0.1.0"
edition = "2021"
authors = ["FaceDev"]
license = "MIT"
repository = "https://github.com/outpoot/gurted"
description = "GURT protocol server CLI tool"
[[bin]]
name = "gurty"
path = "src/main.rs"
[dependencies]
gurt = { path = "../library" }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
clap = { version = "4.0", features = ["derive"] }
colored = "2.0"
mime_guess = "2.0"

56
protocol/cli/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Gurty - a CLI tool to setup your GURT Protocol server
## Setup for Production
For production deployments, you'll need to generate your own certificates since traditional Certificate Authorities don't support custom protocols:
1. **Generate production certificates with OpenSSL:**
```bash
# Generate private key
openssl genpkey -algorithm RSA -out gurt-server.key -pkcs8 -v
# Generate certificate signing request
openssl req -new -key gurt-server.key -out gurt-server.csr
# Generate self-signed certificate (valid for 365 days)
openssl x509 -req -days 365 -in gurt-server.csr -signkey gurt-server.key -out gurt-server.crt
# Or generate both key and certificate in one step
openssl req -x509 -newkey rsa:4096 -keyout gurt-server.key -out gurt-server.crt -days 365 -nodes
```
2. **Deploy with production certificates:**
```bash
cargo run --release serve --cert gurt-server.crt --key gurt-server.key --host 0.0.0.0 --port 4878
```
## Development Environment Setup
To set up a development environment for GURT, follow these steps:
1. **Install mkcert:**
```bash
# Windows (with Chocolatey)
choco install mkcert
# Or download from: https://github.com/FiloSottile/mkcert/releases
```
2. **Install local CA in system:**
```bash
mkcert -install
```
This installs a local CA in your **system certificate store**.
3. **Generate localhost certificates:**
```bash
cd gurted/protocol/cli
mkcert localhost 127.0.0.1 ::1
```
This creates:
- `localhost+2.pem` (certificate)
- `localhost+2-key.pem` (private key)
4. **Start GURT server with certificates:**
```bash
cargo run --release serve --cert localhost+2.pem --key localhost+2-key.pem
```

307
protocol/cli/src/main.rs Normal file
View File

@@ -0,0 +1,307 @@
use clap::{Parser, Subcommand};
use colored::Colorize;
use gurt::prelude::*;
use std::path::PathBuf;
use tracing::error;
use tracing_subscriber;
#[derive(Parser)]
#[command(name = "server")]
#[command(about = "GURT Protocol Server")]
#[command(version = "1.0.0")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Serve {
#[arg(short, long, default_value_t = 4878)]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(short, long, default_value = ".")]
dir: PathBuf,
#[arg(short, long)]
verbose: bool,
#[arg(long, help = "Path to TLS certificate file")]
cert: Option<PathBuf>,
#[arg(long, help = "Path to TLS private key file")]
key: Option<PathBuf>,
}
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Serve { port, host, dir, verbose, cert, key } => {
if verbose {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
} else {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
}
println!("{}", "GURT Protocol Server".bright_cyan().bold());
println!("{} {}:{}", "Listening on".bright_blue(), host, port);
println!("{} {}", "Serving from".bright_blue(), dir.display());
let server = create_file_server(dir, cert, key)?;
let addr = format!("{}:{}", host, port);
if let Err(e) = server.listen(&addr).await {
error!("Server error: {}", e);
std::process::exit(1);
}
}
}
Ok(())
}
fn create_file_server(base_dir: PathBuf, cert_path: Option<PathBuf>, key_path: Option<PathBuf>) -> Result<GurtServer> {
let base_dir = std::sync::Arc::new(base_dir);
let server = match (cert_path, key_path) {
(Some(cert), Some(key)) => {
println!("TLS using certificate: {}", cert.display());
GurtServer::with_tls_certificates(
cert.to_str().ok_or_else(|| GurtError::invalid_message("Invalid certificate path"))?,
key.to_str().ok_or_else(|| GurtError::invalid_message("Invalid key path"))?
)?
}
(Some(_), None) => {
return Err(GurtError::invalid_message("Certificate provided but no key file specified (use --key)"));
}
(None, Some(_)) => {
return Err(GurtError::invalid_message("Key provided but no certificate file specified (use --cert)"));
}
(None, None) => {
return Err(GurtError::invalid_message("GURT protocol requires TLS encryption. Please provide --cert and --key parameters."));
}
};
let server = server
.get("/", {
let base_dir = base_dir.clone();
move |ctx| {
let client_ip = ctx.client_ip();
let base_dir = base_dir.clone();
async move {
// Try to serve index.html if it exists, otherwise show server info
let index_path = base_dir.join("index.html");
if index_path.exists() && index_path.is_file() {
match std::fs::read_to_string(&index_path) {
Ok(content) => {
return Ok(GurtResponse::ok()
.with_header("Content-Type", "text/html")
.with_string_body(content));
}
Err(_) => {
// Fall through to default page
}
}
}
// Default server info page
Ok(GurtResponse::ok()
.with_header("Content-Type", "text/html")
.with_string_body(format!(r#"
<!DOCTYPE html>
<html>
<head>
<title>GURT Protocol Server</title>
<style>
body {{ font-sans m-[30px] bg-[#f5f5f5] }}
.header {{ text-[#0066cc] }}
.status {{ text-[#28a745] font-bold }}
</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,
)))
}
}
})
.get("/*", {
let base_dir = base_dir.clone();
move |ctx| {
let base_dir = base_dir.clone();
let path = ctx.path().to_string();
async move {
let mut relative_path = path.strip_prefix('/').unwrap_or(&path).to_string();
// Remove any leading slashes to ensure relative path
while relative_path.starts_with('/') || relative_path.starts_with('\\') {
relative_path = relative_path[1..].to_string();
}
// If the path is now empty, use "."
let relative_path = if relative_path.is_empty() { ".".to_string() } else { relative_path };
let file_path = base_dir.join(&relative_path);
match file_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = match base_dir.canonicalize() {
Ok(base) => base,
Err(_) => {
return Ok(GurtResponse::internal_server_error()
.with_header("Content-Type", "text/plain")
.with_string_body("Server configuration error"));
}
};
if !canonical_path.starts_with(&canonical_base) {
return Ok(GurtResponse::bad_request()
.with_header("Content-Type", "text/plain")
.with_string_body("Access denied: Path outside served directory"));
}
if canonical_path.is_file() {
match std::fs::read(&canonical_path) {
Ok(content) => {
let content_type = get_content_type(&canonical_path);
Ok(GurtResponse::ok()
.with_header("Content-Type", &content_type)
.with_body(content))
}
Err(_) => {
Ok(GurtResponse::internal_server_error()
.with_header("Content-Type", "text/plain")
.with_string_body("Failed to read file"))
}
}
} else if canonical_path.is_dir() {
let index_path = canonical_path.join("index.html");
if index_path.is_file() {
match std::fs::read_to_string(&index_path) {
Ok(content) => {
Ok(GurtResponse::ok()
.with_header("Content-Type", "text/html")
.with_string_body(content))
}
Err(_) => {
Ok(GurtResponse::internal_server_error()
.with_header("Content-Type", "text/plain")
.with_string_body("Failed to read index file"))
}
}
} else {
match std::fs::read_dir(&canonical_path) {
Ok(entries) => {
let mut listing = String::from(r#"
<!DOCTYPE html>
<html>
<head>
<title>Directory Listing</title>
<style>
body { font-sans m-[40px] }
.file { my-1 }
.dir { font-bold text-[#0066cc] }
</style>
</head>
<body>
<h1>Directory Listing</h1>
<p><a href="../">← Parent Directory</a></p>
<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 { "file dir" } else { "file" };
listing.push_str(&format!(
r#" <a style={} 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"))
}
}
}
} else {
// File not found
Ok(GurtResponse::not_found()
.with_header("Content-Type", "text/html")
.with_string_body(get_404_html()))
}
}
Err(_e) => {
Ok(GurtResponse::not_found()
.with_header("Content-Type", "text/html")
.with_string_body(get_404_html()))
}
}
}
}
});
Ok(server)
}
fn get_404_html() -> &'static str {
r#"<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
<style>
body { font-sans m-[40px] text-center }
</style>
</head>
<body>
<h1>404 Page Not Found</h1>
<p>The requested path was not found on this GURT server.</p>
<p><a href="/">Back to home</a></p>
</body>
</html>
"#
}
fn get_content_type(path: &std::path::Path) -> String {
match path.extension().and_then(|ext| ext.to_str()) {
Some("html") | Some("htm") => "text/html".to_string(),
Some("css") => "text/css".to_string(),
Some("js") => "application/javascript".to_string(),
Some("json") => "application/json".to_string(),
Some("png") => "image/png".to_string(),
Some("jpg") | Some("jpeg") => "image/jpeg".to_string(),
Some("gif") => "image/gif".to_string(),
Some("svg") => "image/svg+xml".to_string(),
Some("ico") => "image/x-icon".to_string(),
Some("txt") => "text/plain".to_string(),
Some("xml") => "application/xml".to_string(),
Some("pdf") => "application/pdf".to_string(),
_ => "application/octet-stream".to_string(),
}
}

25
protocol/gdextension/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Rust
/target/
Cargo.lock
# Build outputs
/bin/
/addon/
# SCons
.sconf_temp/
.sconsign.dblite
config.log
# OS specific
.DS_Store
Thumbs.db
*.tmp
*.temp
# Editor specific
*.swp
*.swo
*~
.vscode/
.idea/

View File

@@ -0,0 +1,37 @@
[package]
name = "gurt-godot"
version = "0.1.0"
edition = "2021"
authors = ["FaceDev"]
license = "MIT"
repository = "https://github.com/outpoot/gurted"
description = "GURT protocol GDExtension for Godot"
[lib]
name = "gurt_godot"
crate-type = ["cdylib"]
[dependencies]
gurt = { path = "../library" }
godot = "0.1"
tokio = { version = "1.0", features = [
"net",
"io-util",
"rt",
"time"
] }
tokio-rustls = "0.26"
rustls-native-certs = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
url = "2.5"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true

View File

@@ -0,0 +1,37 @@
GURT networking extension for Godot.
## Quick Start
1. **Build the extension:**
```bash
./build.sh
```
2. **Install in your Godot project:**
- Copy `addon/gurt-protocol/` to your project's `addons/` folder (e.g. `addons/gurt-protocol`)
- Enable the plugin in `Project Settings > Plugins`
3. **Use in your game:**
```gdscript
var client = GurtProtocolClient.new()
client.create_client(30) # 30s timeout
var response = client.request("gurt://127.0.0.1:4878", {"method": "GET"})
client.disconnect() # cleanup
if response.is_success:
print(response.body) // { "content": ..., "headers": {...}, ... }
else:
print("Error: ", response.status_code, " ", response.status_message)
```
## Build Options
```bash
./build.sh # Release build for current platform
./build.sh -t debug # Debug build
./build.sh -p windows # Build for Windows
./build.sh -p linux # Build for Linux
./build.sh -p macos # Build for macOS
```

View File

@@ -0,0 +1,153 @@
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
TARGET="release"
PLATFORM=""
while [[ $# -gt 0 ]]; do
case $1 in
-t|--target)
TARGET="$2"
shift 2
;;
-p|--platform)
PLATFORM="$2"
shift 2
;;
-h|--help)
echo "GURT Godot Extension Build Script"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -t, --target TARGET Build target (debug|release) [default: release]"
echo " -p, --platform PLATFORM Target platform (windows|linux|macos|current)"
echo " -h, --help Show this help message"
echo ""
exit 0
;;
*)
print_error "Unknown option: $1"
exit 1
;;
esac
done
if [[ "$TARGET" != "debug" && "$TARGET" != "release" ]]; then
print_error "Invalid target: $TARGET. Must be 'debug' or 'release'"
exit 1
fi
if [[ -z "$PLATFORM" ]]; then
case "$(uname -s)" in
Linux*) PLATFORM="linux";;
Darwin*) PLATFORM="macos";;
CYGWIN*|MINGW*|MSYS*) PLATFORM="windows";;
*) PLATFORM="current";;
esac
fi
print_info "GURT Godot Extension Build Script"
print_info "Target: $TARGET"
print_info "Platform: $PLATFORM"
print_info "Checking prerequisites..."
if ! command -v cargo >/dev/null 2>&1; then
print_error "Rust/Cargo not found. Please install Rust: https://rustup.rs/"
exit 1
fi
print_success "Prerequisites found"
case $PLATFORM in
windows)
RUST_TARGET="x86_64-pc-windows-msvc"
LIB_NAME="gurt_godot.dll"
;;
linux)
RUST_TARGET="x86_64-unknown-linux-gnu"
LIB_NAME="libgurt_godot.so"
;;
macos)
RUST_TARGET="x86_64-apple-darwin"
LIB_NAME="libgurt_godot.dylib"
;;
current)
RUST_TARGET=""
case "$(uname -s)" in
Linux*) LIB_NAME="libgurt_godot.so";;
Darwin*) LIB_NAME="libgurt_godot.dylib";;
CYGWIN*|MINGW*|MSYS*) LIB_NAME="gurt_godot.dll";;
*) print_error "Unsupported platform"; exit 1;;
esac
;;
*)
print_error "Unknown platform: $PLATFORM"
exit 1
;;
esac
# Create addon directory structure
ADDON_DIR="addon/gurt-protocol"
OUTPUT_DIR="$ADDON_DIR/bin/$PLATFORM"
mkdir -p "$OUTPUT_DIR"
BUILD_CMD="cargo build"
if [[ "$TARGET" == "release" ]]; then
BUILD_CMD="$BUILD_CMD --release"
fi
if [[ -n "$RUST_TARGET" ]]; then
print_info "Installing Rust target: $RUST_TARGET"
rustup target add "$RUST_TARGET"
BUILD_CMD="$BUILD_CMD --target $RUST_TARGET"
fi
print_info "Building with Cargo..."
$BUILD_CMD
if [[ -n "$RUST_TARGET" ]]; then
if [[ "$TARGET" == "release" ]]; then
BUILT_LIB="target/$RUST_TARGET/release/$LIB_NAME"
else
BUILT_LIB="target/$RUST_TARGET/debug/$LIB_NAME"
fi
else
if [[ "$TARGET" == "release" ]]; then
BUILT_LIB="target/release/$LIB_NAME"
else
BUILT_LIB="target/debug/$LIB_NAME"
fi
fi
if [[ -f "$BUILT_LIB" ]]; then
cp "$BUILT_LIB" "$OUTPUT_DIR/$LIB_NAME"
# Copy addon files
cp gurt_godot.gdextension "$ADDON_DIR/"
cp plugin.cfg "$ADDON_DIR/"
cp plugin.gd "$ADDON_DIR/"
print_success "Build completed: $OUTPUT_DIR/$LIB_NAME"
SIZE=$(du -h "$OUTPUT_DIR/$LIB_NAME" | cut -f1)
print_info "Library size: $SIZE"
else
print_error "Built library not found at: $BUILT_LIB"
exit 1
fi
print_success "Build process completed!"
print_info "Copy the 'addon/gurt-protocol' folder to your project's 'addons/' directory"

View File

@@ -0,0 +1,13 @@
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1
[libraries]
macos.debug = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib"
macos.release = "res://addons/gurt-protocol/bin/macos/libgurt_godot.dylib"
windows.debug.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll"
windows.release.x86_64 = "res://addons/gurt-protocol/bin/windows/gurt_godot.dll"
linux.debug.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so"
linux.release.x86_64 = "res://addons/gurt-protocol/bin/linux/libgurt_godot.so"

View File

@@ -0,0 +1,7 @@
[plugin]
name="GURT Protocol"
description="HTTP-like networking extension for Godot games using the GURT protocol"
author="FaceDev"
version="0.1.0"
script="plugin.gd"

View File

@@ -0,0 +1,8 @@
@tool
extends EditorPlugin
func _enter_tree():
print("GURT Protocol plugin enabled")
func _exit_tree():
print("GURT Protocol plugin disabled")

View File

@@ -0,0 +1,375 @@
use godot::prelude::*;
use gurt::prelude::*;
use gurt::{GurtMethod, GurtRequest};
use tokio::runtime::Runtime;
use std::sync::Arc;
use std::cell::RefCell;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
struct GurtGodotExtension;
#[gdextension]
unsafe impl ExtensionLibrary for GurtGodotExtension {}
#[derive(GodotClass)]
#[class(init)]
struct GurtProtocolClient {
base: Base<RefCounted>,
client: Arc<RefCell<Option<GurtClient>>>,
runtime: Arc<RefCell<Option<Runtime>>>,
}
#[derive(GodotClass)]
#[class(init)]
struct GurtGDResponse {
base: Base<RefCounted>,
#[var]
status_code: i32,
#[var]
status_message: GString,
#[var]
headers: Dictionary,
#[var]
is_success: bool,
#[var]
body: PackedByteArray, // Raw bytes
#[var]
text: GString, // Decoded text
}
#[godot_api]
impl GurtGDResponse {
#[func]
fn get_header(&self, key: GString) -> GString {
self.headers.get(key).map_or(GString::new(), |v| v.to::<GString>())
}
#[func]
fn is_binary(&self) -> bool {
let content_type = self.get_header("content-type".into()).to_string();
content_type.starts_with("image/") ||
content_type.starts_with("application/octet-stream") ||
content_type.starts_with("video/") ||
content_type.starts_with("audio/")
}
#[func]
fn is_text(&self) -> bool {
let content_type = self.get_header("content-type".into()).to_string();
content_type.starts_with("text/") ||
content_type.starts_with("application/json") ||
content_type.starts_with("application/xml") ||
content_type.is_empty()
}
#[func]
fn debug_info(&self) -> GString {
let content_length = self.get_header("content-length".into()).to_string();
let actual_size = self.body.len();
let content_type = self.get_header("content-type".into()).to_string();
let size_match = content_length.parse::<usize>().unwrap_or(0) == actual_size;
format!(
"Status: {} | Type: {} | Length: {} | Actual: {} | Match: {}",
self.status_code,
content_type,
content_length,
actual_size,
size_match
).into()
}
}
#[derive(GodotClass)]
#[class(init)]
struct GurtProtocolServer {
base: Base<RefCounted>,
}
#[godot_api]
impl GurtProtocolClient {
#[signal]
fn request_completed(response: Gd<GurtGDResponse>);
#[func]
fn create_client(&mut self, timeout_seconds: i32) -> bool {
let runtime = match Runtime::new() {
Ok(rt) => rt,
Err(e) => {
godot_print!("Failed to create runtime: {}", e);
return false;
}
};
let mut config = ClientConfig::default();
config.request_timeout = tokio::time::Duration::from_secs(timeout_seconds as u64);
let client = GurtClient::with_config(config);
*self.runtime.borrow_mut() = Some(runtime);
*self.client.borrow_mut() = Some(client);
true
}
#[func]
fn request(&self, url: GString, options: Dictionary) -> Option<Gd<GurtGDResponse>> {
let runtime_binding = self.runtime.borrow();
let runtime = match runtime_binding.as_ref() {
Some(rt) => rt,
None => {
godot_print!("No runtime available");
return None;
}
};
let url_str = url.to_string();
// Parse URL to get host and port
let parsed_url = match url::Url::parse(&url_str) {
Ok(u) => u,
Err(e) => {
godot_print!("Invalid URL: {}", e);
return None;
}
};
let host = match parsed_url.host_str() {
Some(h) => h,
None => {
godot_print!("URL must have a host");
return None;
}
};
let port = parsed_url.port().unwrap_or(4878);
let path = if parsed_url.path().is_empty() { "/" } else { parsed_url.path() };
let method_str = options.get("method").unwrap_or("GET".to_variant()).to::<String>();
let method = match method_str.to_uppercase().as_str() {
"GET" => GurtMethod::GET,
"POST" => GurtMethod::POST,
"PUT" => GurtMethod::PUT,
"DELETE" => GurtMethod::DELETE,
"PATCH" => GurtMethod::PATCH,
"HEAD" => GurtMethod::HEAD,
"OPTIONS" => GurtMethod::OPTIONS,
_ => {
godot_print!("Unsupported HTTP method: {}", method_str);
GurtMethod::GET
}
};
let response = match runtime.block_on(self.gurt_request_with_handshake(host, port, method, path)) {
Ok(resp) => resp,
Err(e) => {
godot_print!("GURT request failed: {}", e);
return None;
}
};
Some(self.convert_response(response))
}
async fn gurt_request_with_handshake(&self, host: &str, port: u16, method: GurtMethod, path: &str) -> gurt::Result<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;
*self.runtime.borrow_mut() = None;
}
#[func]
fn is_connected(&self) -> bool {
self.client.borrow().is_some()
}
#[func]
fn get_version(&self) -> GString {
gurt::GURT_VERSION.to_string().into()
}
#[func]
fn get_default_port(&self) -> i32 {
gurt::DEFAULT_PORT as i32
}
fn convert_response(&self, response: GurtResponse) -> Gd<GurtGDResponse> {
let mut gd_response = GurtGDResponse::new_gd();
gd_response.bind_mut().status_code = response.status_code as i32;
gd_response.bind_mut().status_message = response.status_message.clone().into();
gd_response.bind_mut().is_success = response.is_success();
let mut headers = Dictionary::new();
for (key, value) in &response.headers {
headers.set(key.clone(), value.clone());
}
gd_response.bind_mut().headers = headers;
let mut body = PackedByteArray::new();
body.resize(response.body.len());
for (i, byte) in response.body.iter().enumerate() {
body[i] = *byte;
}
gd_response.bind_mut().body = body;
match std::str::from_utf8(&response.body) {
Ok(text_str) => {
gd_response.bind_mut().text = text_str.into();
}
Err(_) => {
let content_type = response.headers.get("content-type").cloned().unwrap_or_default();
let size = response.body.len();
gd_response.bind_mut().text = format!("[Binary data: {} ({} bytes)]", content_type, size).into();
}
}
gd_response
}
}

1540
protocol/library/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
[package]
name = "gurt"
version = "0.1.0"
edition = "2021"
authors = ["FaceDev"]
license = "MIT"
repository = "https://github.com/outpoot/gurted"
description = "Official GURT:// protocol implementation"
[lib]
name = "gurt"
crate-type = ["cdylib", "lib"]
[dependencies]
tokio = { version = "1.0", features = [
"net",
"io-util",
"rt",
"macros",
"rt-multi-thread",
"time",
"fs"
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] }
tokio-rustls = "0.26"
rustls = "0.23"
rustls-pemfile = "2.0"
base64 = "0.22"
url = "2.5"
[dev-dependencies]
tokio-test = "0.4"
tracing-subscriber = "0.3"
sha2 = "0.10"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true

View File

@@ -0,0 +1,3 @@
The Rust implementation of the Gurt protocol, used internally by the **Gurty CLI** and the **Gurt GDExtension** (which is used by Flumi, the official browser).
See the `examples/` directory for usage examples. For protocol details and design, refer to `SPEC.md` or the source code.

View File

@@ -0,0 +1,18 @@
use gurt::{GurtServer, GurtResponse, ServerContext, Result};
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let server = GurtServer::with_tls_certificates("cert.pem", "cert.key.pem")?
.get("/", |_ctx: &ServerContext| async {
Ok(GurtResponse::ok().with_string_body("<h1>Hello from GURT!</h1>"))
})
.get("/test", |_ctx: &ServerContext| async {
Ok(GurtResponse::ok().with_string_body("Test endpoint working!"))
});
println!("Starting GURT server on gurt://127.0.0.1:4878");
server.listen("127.0.0.1:4878").await
}

View File

@@ -0,0 +1,302 @@
use crate::{
GurtError, Result, GurtRequest, GurtResponse,
protocol::{DEFAULT_PORT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, DEFAULT_HANDSHAKE_TIMEOUT, BODY_SEPARATOR},
message::GurtMethod,
};
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{timeout, Duration};
use url::Url;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub handshake_timeout: Duration,
pub user_agent: String,
pub max_redirects: usize,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT),
request_timeout: Duration::from_secs(DEFAULT_REQUEST_TIMEOUT),
handshake_timeout: Duration::from_secs(DEFAULT_HANDSHAKE_TIMEOUT),
user_agent: format!("GURT-Client/{}", crate::GURT_VERSION),
max_redirects: 5,
}
}
}
#[derive(Debug)]
struct PooledConnection {
stream: TcpStream,
}
impl PooledConnection {
fn new(stream: TcpStream) -> Self {
Self { stream }
}
}
pub struct GurtClient {
config: ClientConfig,
}
impl GurtClient {
pub fn new() -> Self {
Self {
config: ClientConfig::default(),
}
}
pub fn with_config(config: ClientConfig) -> Self {
Self {
config,
}
}
async fn create_connection(&self, host: &str, port: u16) -> Result<PooledConnection> {
let addr = format!("{}:{}", host, port);
let stream = timeout(
self.config.connect_timeout,
TcpStream::connect(&addr)
).await
.map_err(|_| GurtError::timeout("Connection timeout"))?
.map_err(|e| GurtError::connection(format!("Failed to connect: {}", e)))?;
let conn = PooledConnection::new(stream);
Ok(conn)
}
async fn read_response_data(&self, stream: &mut TcpStream) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut temp_buffer = [0u8; 8192];
let start_time = std::time::Instant::now();
loop {
if start_time.elapsed() > self.config.request_timeout {
return Err(GurtError::timeout("Response timeout"));
}
let bytes_read = stream.read(&mut temp_buffer).await?;
if bytes_read == 0 {
break; // Connection closed
}
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
// Check for complete message without converting to string
let body_separator = BODY_SEPARATOR.as_bytes();
let has_complete_response = buffer.windows(body_separator.len()).any(|w| w == body_separator) ||
(buffer.starts_with(b"{") && buffer.ends_with(b"}"));
if has_complete_response {
return Ok(buffer);
}
}
if buffer.is_empty() {
Err(GurtError::connection("Connection closed unexpectedly"))
} else {
Ok(buffer)
}
}
async fn send_request_internal(&self, host: &str, port: u16, request: GurtRequest) -> Result<GurtResponse> {
debug!("Sending {} {} to {}:{}", request.method, request.path, host, port);
let mut conn = self.create_connection(host, port).await?;
let request_data = request.to_string();
conn.stream.write_all(request_data.as_bytes()).await?;
let response_bytes = timeout(
self.config.request_timeout,
self.read_response_data(&mut conn.stream)
).await
.map_err(|_| GurtError::timeout("Request timeout"))??;
let response = GurtResponse::parse_bytes(&response_bytes)?;
Ok(response)
}
pub async fn get(&self, url: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::GET, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Accept", "*/*");
self.send_request_internal(&host, port, request).await
}
pub async fn post(&self, url: &str, body: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::POST, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "text/plain")
.with_string_body(body);
self.send_request_internal(&host, port, request).await
}
/// POST request with JSON body
pub async fn post_json<T: serde::Serialize>(&self, url: &str, data: &T) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let json_body = serde_json::to_string(data)?;
let request = GurtRequest::new(GurtMethod::POST, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "application/json")
.with_string_body(json_body);
self.send_request_internal(&host, port, request).await
}
/// PUT request with body
pub async fn put(&self, url: &str, body: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::PUT, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "text/plain")
.with_string_body(body);
self.send_request_internal(&host, port, request).await
}
/// PUT request with JSON body
pub async fn put_json<T: serde::Serialize>(&self, url: &str, data: &T) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let json_body = serde_json::to_string(data)?;
let request = GurtRequest::new(GurtMethod::PUT, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "application/json")
.with_string_body(json_body);
self.send_request_internal(&host, port, request).await
}
pub async fn delete(&self, url: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::DELETE, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent);
self.send_request_internal(&host, port, request).await
}
pub async fn head(&self, url: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::HEAD, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent);
self.send_request_internal(&host, port, request).await
}
pub async fn options(&self, url: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::OPTIONS, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent);
self.send_request_internal(&host, port, request).await
}
/// PATCH request with body
pub async fn patch(&self, url: &str, body: &str) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let request = GurtRequest::new(GurtMethod::PATCH, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "text/plain")
.with_string_body(body);
self.send_request_internal(&host, port, request).await
}
/// PATCH request with JSON body
pub async fn patch_json<T: serde::Serialize>(&self, url: &str, data: &T) -> Result<GurtResponse> {
let (host, port, path) = self.parse_url(url)?;
let json_body = serde_json::to_string(data)?;
let request = GurtRequest::new(GurtMethod::PATCH, path)
.with_header("Host", &host)
.with_header("User-Agent", &self.config.user_agent)
.with_header("Content-Type", "application/json")
.with_string_body(json_body);
self.send_request_internal(&host, port, request).await
}
pub async fn send_request(&self, host: &str, port: u16, request: GurtRequest) -> Result<GurtResponse> {
self.send_request_internal(host, port, request).await
}
fn parse_url(&self, url: &str) -> Result<(String, u16, String)> {
let parsed_url = Url::parse(url).map_err(|e| GurtError::invalid_message(format!("Invalid URL: {}", e)))?;
if parsed_url.scheme() != "gurt" {
return Err(GurtError::invalid_message("URL must use gurt:// scheme"));
}
let host = parsed_url.host_str()
.ok_or_else(|| GurtError::invalid_message("URL must have a host"))?
.to_string();
let port = parsed_url.port().unwrap_or(DEFAULT_PORT);
let path = if parsed_url.path().is_empty() {
"/".to_string()
} else {
parsed_url.path().to_string()
};
Ok((host, port, path))
}
}
impl Default for GurtClient {
fn default() -> Self {
Self::new()
}
}
impl Clone for GurtClient {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_url_parsing() {
let client = GurtClient::new();
let (host, port, path) = client.parse_url("gurt://example.com/test").unwrap();
assert_eq!(host, "example.com");
assert_eq!(port, DEFAULT_PORT);
assert_eq!(path, "/test");
let (host, port, path) = client.parse_url("gurt://example.com:8080/api/v1").unwrap();
assert_eq!(host, "example.com");
assert_eq!(port, 8080);
assert_eq!(path, "/api/v1");
}
}

View File

@@ -0,0 +1,123 @@
use crate::{GurtError, Result};
use rustls::{ClientConfig, ServerConfig};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use tokio_rustls::{TlsConnector, TlsAcceptor};
use std::sync::Arc;
pub const TLS_VERSION: &str = "TLS/1.3";
pub const GURT_ALPN: &[u8] = b"GURT/1.0";
#[derive(Debug, Clone)]
pub struct TlsConfig {
pub client_config: Option<Arc<ClientConfig>>,
pub server_config: Option<Arc<ServerConfig>>,
}
impl TlsConfig {
pub fn new_client() -> Result<Self> {
let mut config = ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
.with_no_client_auth();
config.alpn_protocols = vec![GURT_ALPN.to_vec()];
Ok(Self {
client_config: Some(Arc::new(config)),
server_config: None,
})
}
pub fn new_server(cert_chain: Vec<CertificateDer<'static>>, private_key: PrivateKeyDer<'static>) -> Result<Self> {
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, private_key)
.map_err(|e| GurtError::crypto(format!("TLS server config error: {}", e)))?;
config.alpn_protocols = vec![GURT_ALPN.to_vec()];
Ok(Self {
client_config: None,
server_config: Some(Arc::new(config)),
})
}
pub fn get_connector(&self) -> Result<TlsConnector> {
let config = self.client_config.as_ref()
.ok_or_else(|| GurtError::crypto("No client config available"))?;
Ok(TlsConnector::from(config.clone()))
}
pub fn get_acceptor(&self) -> Result<TlsAcceptor> {
let config = self.server_config.as_ref()
.ok_or_else(|| GurtError::crypto("No server config available"))?;
Ok(TlsAcceptor::from(config.clone()))
}
}
#[derive(Debug)]
pub struct CryptoManager {
tls_config: Option<TlsConfig>,
}
impl CryptoManager {
pub fn new() -> Self {
Self {
tls_config: None,
}
}
pub fn with_tls_config(config: TlsConfig) -> Self {
Self {
tls_config: Some(config),
}
}
pub fn set_tls_config(&mut self, config: TlsConfig) {
self.tls_config = Some(config);
}
pub fn has_tls_config(&self) -> bool {
self.tls_config.is_some()
}
pub fn get_tls_connector(&self) -> Result<TlsConnector> {
let config = self.tls_config.as_ref()
.ok_or_else(|| GurtError::crypto("No TLS config available"))?;
config.get_connector()
}
pub fn get_tls_acceptor(&self) -> Result<TlsAcceptor> {
let config = self.tls_config.as_ref()
.ok_or_else(|| GurtError::crypto("No TLS config available"))?;
config.get_acceptor()
}
}
impl Default for CryptoManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tls_config_creation() {
let client_config = TlsConfig::new_client();
assert!(client_config.is_ok());
let config = client_config.unwrap();
assert!(config.client_config.is_some());
assert!(config.server_config.is_none());
}
#[test]
fn test_crypto_manager() {
let crypto = CryptoManager::new();
assert!(!crypto.has_tls_config());
}
}

View File

@@ -0,0 +1,71 @@
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum GurtError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Cryptographic error: {0}")]
Crypto(String),
#[error("Protocol error: {0}")]
Protocol(String),
#[error("Invalid message format: {0}")]
InvalidMessage(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Handshake failed: {0}")]
Handshake(String),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Server error: {status} {message}")]
Server { status: u16, message: String },
#[error("Client error: {0}")]
Client(String),
}
pub type Result<T> = std::result::Result<T, GurtError>;
impl GurtError {
pub fn crypto<T: fmt::Display>(msg: T) -> Self {
GurtError::Crypto(msg.to_string())
}
pub fn protocol<T: fmt::Display>(msg: T) -> Self {
GurtError::Protocol(msg.to_string())
}
pub fn invalid_message<T: fmt::Display>(msg: T) -> Self {
GurtError::InvalidMessage(msg.to_string())
}
pub fn connection<T: fmt::Display>(msg: T) -> Self {
GurtError::Connection(msg.to_string())
}
pub fn handshake<T: fmt::Display>(msg: T) -> Self {
GurtError::Handshake(msg.to_string())
}
pub fn timeout<T: fmt::Display>(msg: T) -> Self {
GurtError::Timeout(msg.to_string())
}
pub fn server(status: u16, message: String) -> Self {
GurtError::Server { status, message }
}
pub fn client<T: fmt::Display>(msg: T) -> Self {
GurtError::Client(msg.to_string())
}
}

View File

@@ -0,0 +1,24 @@
pub mod protocol;
pub mod crypto;
pub mod server;
pub mod client;
pub mod error;
pub mod message;
pub use error::{GurtError, Result};
pub use message::{GurtMessage, GurtRequest, GurtResponse, GurtMethod};
pub use protocol::{GurtStatusCode, GURT_VERSION, DEFAULT_PORT};
pub use crypto::{CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION};
pub use server::{GurtServer, GurtHandler, ServerContext, Route};
pub use client::{GurtClient, ClientConfig};
pub mod prelude {
pub use crate::{
GurtError, Result,
GurtMessage, GurtRequest, GurtResponse,
GURT_VERSION, DEFAULT_PORT,
CryptoManager, TlsConfig, GURT_ALPN, TLS_VERSION,
GurtServer, GurtHandler, ServerContext, Route,
GurtClient, ClientConfig,
};
}

View File

@@ -0,0 +1,568 @@
use crate::{GurtError, Result, GURT_VERSION};
use crate::protocol::{GurtStatusCode, PROTOCOL_PREFIX, HEADER_SEPARATOR, BODY_SEPARATOR};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::fmt;
use chrono::Utc;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GurtMethod {
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
HANDSHAKE, // Special method for protocol handshake
}
impl GurtMethod {
pub fn parse(s: &str) -> Result<Self> {
match s.to_uppercase().as_str() {
"GET" => Ok(Self::GET),
"POST" => Ok(Self::POST),
"PUT" => Ok(Self::PUT),
"DELETE" => Ok(Self::DELETE),
"HEAD" => Ok(Self::HEAD),
"OPTIONS" => Ok(Self::OPTIONS),
"PATCH" => Ok(Self::PATCH),
"HANDSHAKE" => Ok(Self::HANDSHAKE),
_ => Err(GurtError::invalid_message(format!("Unsupported method: {}", s))),
}
}
}
impl fmt::Display for GurtMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::GET => "GET",
Self::POST => "POST",
Self::PUT => "PUT",
Self::DELETE => "DELETE",
Self::HEAD => "HEAD",
Self::OPTIONS => "OPTIONS",
Self::PATCH => "PATCH",
Self::HANDSHAKE => "HANDSHAKE",
};
write!(f, "{}", s)
}
}
pub type GurtHeaders = HashMap<String, String>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GurtRequest {
pub method: GurtMethod,
pub path: String,
pub version: String,
pub headers: GurtHeaders,
pub body: Vec<u8>,
}
impl GurtRequest {
pub fn new(method: GurtMethod, path: String) -> Self {
Self {
method,
path,
version: GURT_VERSION.to_string(),
headers: GurtHeaders::new(),
body: Vec::new(),
}
}
pub fn with_header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.headers.insert(key.into().to_lowercase(), value.into());
self
}
pub fn with_body<B: Into<Vec<u8>>>(mut self, body: B) -> Self {
self.body = body.into();
self
}
pub fn with_string_body<S: AsRef<str>>(mut self, body: S) -> Self {
self.body = body.as_ref().as_bytes().to_vec();
self
}
pub fn header(&self, key: &str) -> Option<&String> {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_string())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
}
pub fn parse(data: &str) -> Result<Self> {
Self::parse_bytes(data.as_bytes())
}
pub fn parse_bytes(data: &[u8]) -> Result<Self> {
// Find the header/body separator as bytes
let body_separator = BODY_SEPARATOR.as_bytes();
let body_separator_pos = data.windows(body_separator.len())
.position(|window| window == body_separator);
let (headers_section, body) = if let Some(pos) = body_separator_pos {
let headers_part = &data[..pos];
let body_part = &data[pos + body_separator.len()..];
(headers_part, body_part.to_vec())
} else {
(data, Vec::new())
};
// Convert headers section to string (should be valid UTF-8)
let headers_str = std::str::from_utf8(headers_section)
.map_err(|_| GurtError::invalid_message("Invalid UTF-8 in headers"))?;
let lines: Vec<&str> = headers_str.split(HEADER_SEPARATOR).collect();
if lines.is_empty() {
return Err(GurtError::invalid_message("Empty request"));
}
// Parse request line (METHOD path GURT/version)
let request_line = lines[0];
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() != 3 {
return Err(GurtError::invalid_message("Invalid request line format"));
}
let method = GurtMethod::parse(parts[0])?;
let path = parts[1].to_string();
// Parse protocol version
if !parts[2].starts_with(PROTOCOL_PREFIX) {
return Err(GurtError::invalid_message("Invalid protocol identifier"));
}
let version_str = &parts[2][PROTOCOL_PREFIX.len()..];
let version = version_str.to_string();
// Parse headers
let mut headers = GurtHeaders::new();
for line in lines.iter().skip(1) {
if line.is_empty() {
break;
}
if let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim().to_lowercase();
let value = line[colon_pos + 1..].trim().to_string();
headers.insert(key, value);
}
}
Ok(Self {
method,
path,
version,
headers,
body,
})
}
pub fn to_string(&self) -> String {
let mut message = format!("{} {} {}{}{}",
self.method, self.path, PROTOCOL_PREFIX, self.version, HEADER_SEPARATOR);
let mut headers = self.headers.clone();
if !headers.contains_key("content-length") {
headers.insert("content-length".to_string(), self.body.len().to_string());
}
if !headers.contains_key("user-agent") {
headers.insert("user-agent".to_string(), format!("GURT-Client/{}", GURT_VERSION));
}
for (key, value) in &headers {
message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR));
}
message.push_str(HEADER_SEPARATOR);
if !self.body.is_empty() {
if let Ok(body_str) = std::str::from_utf8(&self.body) {
message.push_str(body_str);
}
}
message
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut message = format!("{} {} {}{}{}",
self.method, self.path, PROTOCOL_PREFIX, self.version, HEADER_SEPARATOR);
let mut headers = self.headers.clone();
if !headers.contains_key("content-length") {
headers.insert("content-length".to_string(), self.body.len().to_string());
}
if !headers.contains_key("user-agent") {
headers.insert("user-agent".to_string(), format!("GURT-Client/{}", GURT_VERSION));
}
for (key, value) in &headers {
message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR));
}
message.push_str(HEADER_SEPARATOR);
let mut bytes = message.into_bytes();
bytes.extend_from_slice(&self.body);
bytes
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GurtResponse {
pub version: String,
pub status_code: u16,
pub status_message: String,
pub headers: GurtHeaders,
pub body: Vec<u8>,
}
impl GurtResponse {
pub fn new(status_code: GurtStatusCode) -> Self {
Self {
version: GURT_VERSION.to_string(),
status_code: status_code as u16,
status_message: status_code.message().to_string(),
headers: GurtHeaders::new(),
body: Vec::new(),
}
}
pub fn ok() -> Self {
Self::new(GurtStatusCode::Ok)
}
pub fn not_found() -> Self {
Self::new(GurtStatusCode::NotFound)
}
pub fn bad_request() -> Self {
Self::new(GurtStatusCode::BadRequest)
}
pub fn internal_server_error() -> Self {
Self::new(GurtStatusCode::InternalServerError)
}
pub fn with_header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.headers.insert(key.into().to_lowercase(), value.into());
self
}
pub fn with_body<B: Into<Vec<u8>>>(mut self, body: B) -> Self {
self.body = body.into();
self
}
pub fn with_string_body<S: AsRef<str>>(mut self, body: S) -> Self {
self.body = body.as_ref().as_bytes().to_vec();
self
}
pub fn with_json_body<T: Serialize>(mut self, data: &T) -> Result<Self> {
let json = serde_json::to_string(data)?;
self.body = json.into_bytes();
self.headers.insert("content-type".to_string(), "application/json".to_string());
Ok(self)
}
pub fn header(&self, key: &str) -> Option<&String> {
self.headers.get(&key.to_lowercase())
}
pub fn body_as_string(&self) -> Result<String> {
std::str::from_utf8(&self.body)
.map(|s| s.to_owned())
.map_err(|e| GurtError::invalid_message(format!("Invalid UTF-8 body: {}", e)))
}
pub fn is_success(&self) -> bool {
self.status_code >= 200 && self.status_code < 300
}
pub fn is_client_error(&self) -> bool {
self.status_code >= 400 && self.status_code < 500
}
pub fn is_server_error(&self) -> bool {
self.status_code >= 500
}
pub fn parse(data: &str) -> Result<Self> {
Self::parse_bytes(data.as_bytes())
}
pub fn parse_bytes(data: &[u8]) -> Result<Self> {
// Find the header/body separator as bytes
let body_separator = BODY_SEPARATOR.as_bytes();
let body_separator_pos = data.windows(body_separator.len())
.position(|window| window == body_separator);
let (headers_section, body) = if let Some(pos) = body_separator_pos {
let headers_part = &data[..pos];
let body_part = &data[pos + body_separator.len()..];
(headers_part, body_part.to_vec())
} else {
(data, Vec::new())
};
// Convert headers section to string (should be valid UTF-8)
let headers_str = std::str::from_utf8(headers_section)
.map_err(|_| GurtError::invalid_message("Invalid UTF-8 in headers"))?;
let lines: Vec<&str> = headers_str.split(HEADER_SEPARATOR).collect();
if lines.is_empty() {
return Err(GurtError::invalid_message("Empty response"));
}
// Parse status line (GURT/version status_code status_message)
let status_line = lines[0];
let parts: Vec<&str> = status_line.splitn(3, ' ').collect();
if parts.len() < 2 {
return Err(GurtError::invalid_message("Invalid status line format"));
}
// Parse protocol version
if !parts[0].starts_with(PROTOCOL_PREFIX) {
return Err(GurtError::invalid_message("Invalid protocol identifier"));
}
let version_str = &parts[0][PROTOCOL_PREFIX.len()..];
let version = version_str.to_string();
let status_code: u16 = parts[1].parse()
.map_err(|_| GurtError::invalid_message("Invalid status code"))?;
let status_message = if parts.len() > 2 {
parts[2].to_string()
} else {
GurtStatusCode::from_u16(status_code)
.map(|sc| sc.message().to_string())
.unwrap_or_else(|| "Unknown".to_string())
};
// Parse headers
let mut headers = GurtHeaders::new();
for line in lines.iter().skip(1) {
if line.is_empty() {
break;
}
if let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim().to_lowercase();
let value = line[colon_pos + 1..].trim().to_string();
headers.insert(key, value);
}
}
Ok(Self {
version,
status_code,
status_message,
headers,
body,
})
}
pub fn to_string(&self) -> String {
let mut message = format!("{}{} {} {}{}",
PROTOCOL_PREFIX, self.version, self.status_code, self.status_message, HEADER_SEPARATOR);
let mut headers = self.headers.clone();
if !headers.contains_key("content-length") {
headers.insert("content-length".to_string(), self.body.len().to_string());
}
if !headers.contains_key("server") {
headers.insert("server".to_string(), format!("GURT/{}", GURT_VERSION));
}
if !headers.contains_key("date") {
// RFC 7231 compliant
let now = Utc::now();
let date_str = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
headers.insert("date".to_string(), date_str);
}
for (key, value) in &headers {
message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR));
}
message.push_str(HEADER_SEPARATOR);
if !self.body.is_empty() {
if let Ok(body_str) = std::str::from_utf8(&self.body) {
message.push_str(body_str);
}
}
message
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut message = format!("{}{} {} {}{}",
PROTOCOL_PREFIX, self.version, self.status_code, self.status_message, HEADER_SEPARATOR);
let mut headers = self.headers.clone();
if !headers.contains_key("content-length") {
headers.insert("content-length".to_string(), self.body.len().to_string());
}
if !headers.contains_key("server") {
headers.insert("server".to_string(), format!("GURT/{}", GURT_VERSION));
}
if !headers.contains_key("date") {
// RFC 7231 compliant
let now = Utc::now();
let date_str = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
headers.insert("date".to_string(), date_str);
}
for (key, value) in &headers {
message.push_str(&format!("{}: {}{}", key, value, HEADER_SEPARATOR));
}
message.push_str(HEADER_SEPARATOR);
// Convert headers to bytes and append body as raw bytes
let mut bytes = message.into_bytes();
bytes.extend_from_slice(&self.body);
bytes
}
}
#[derive(Debug, Clone)]
pub enum GurtMessage {
Request(GurtRequest),
Response(GurtResponse),
}
impl GurtMessage {
pub fn parse(data: &str) -> Result<Self> {
Self::parse_bytes(data.as_bytes())
}
pub fn parse_bytes(data: &[u8]) -> Result<Self> {
// Convert first line to string to determine message type
let header_separator = HEADER_SEPARATOR.as_bytes();
let first_line_end = data.windows(header_separator.len())
.position(|window| window == header_separator)
.unwrap_or(data.len());
let first_line = std::str::from_utf8(&data[..first_line_end])
.map_err(|_| GurtError::invalid_message("Invalid UTF-8 in first line"))?;
// Check if it's a response (starts with GURT/version) or request (method first)
if first_line.starts_with(PROTOCOL_PREFIX) {
Ok(GurtMessage::Response(GurtResponse::parse_bytes(data)?))
} else {
Ok(GurtMessage::Request(GurtRequest::parse_bytes(data)?))
}
}
pub fn is_request(&self) -> bool {
matches!(self, GurtMessage::Request(_))
}
pub fn is_response(&self) -> bool {
matches!(self, GurtMessage::Response(_))
}
pub fn as_request(&self) -> Option<&GurtRequest> {
match self {
GurtMessage::Request(req) => Some(req),
_ => None,
}
}
pub fn as_response(&self) -> Option<&GurtResponse> {
match self {
GurtMessage::Response(res) => Some(res),
_ => None,
}
}
}
impl fmt::Display for GurtMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GurtMessage::Request(req) => write!(f, "{}", req.to_string()),
GurtMessage::Response(res) => write!(f, "{}", res.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_parsing() {
let raw = "GET /test GURT/1.0.0\r\nHost: example.com\r\nAccept: text/html\r\n\r\ntest body";
let request = GurtRequest::parse(raw).expect("Failed to parse request");
assert_eq!(request.method, GurtMethod::GET);
assert_eq!(request.path, "/test");
assert_eq!(request.version, GURT_VERSION.to_string());
assert_eq!(request.header("host"), Some(&"example.com".to_string()));
assert_eq!(request.header("accept"), Some(&"text/html".to_string()));
assert_eq!(request.body_as_string().unwrap(), "test body");
}
#[test]
fn test_response_parsing() {
let raw = "GURT/1.0.0 200 OK\r\nContent-Type: text/html\r\n\r\n<html></html>";
let response = GurtResponse::parse(raw).expect("Failed to parse response");
assert_eq!(response.version, GURT_VERSION.to_string());
assert_eq!(response.status_code, 200);
assert_eq!(response.status_message, "OK");
assert_eq!(response.header("content-type"), Some(&"text/html".to_string()));
assert_eq!(response.body_as_string().unwrap(), "<html></html>");
}
#[test]
fn test_request_building() {
let request = GurtRequest::new(GurtMethod::GET, "/test".to_string())
.with_header("Host", "example.com")
.with_string_body("test body");
let raw = request.to_string();
let parsed = GurtRequest::parse(&raw).expect("Failed to parse built request");
assert_eq!(parsed.method, request.method);
assert_eq!(parsed.path, request.path);
assert_eq!(parsed.body, request.body);
}
#[test]
fn test_response_building() {
let response = GurtResponse::ok()
.with_header("Content-Type", "text/html")
.with_string_body("<html></html>");
let raw = response.to_string();
let parsed = GurtResponse::parse(&raw).expect("Failed to parse built response");
assert_eq!(parsed.status_code, response.status_code);
assert_eq!(parsed.body, response.body);
}
}

View File

@@ -0,0 +1,120 @@
use std::fmt;
pub const GURT_VERSION: &str = "1.0.0";
pub const DEFAULT_PORT: u16 = 4878;
pub const PROTOCOL_PREFIX: &str = "GURT/";
pub const HEADER_SEPARATOR: &str = "\r\n";
pub const BODY_SEPARATOR: &str = "\r\n\r\n";
pub const DEFAULT_HANDSHAKE_TIMEOUT: u64 = 5;
pub const DEFAULT_REQUEST_TIMEOUT: u64 = 30;
pub const DEFAULT_CONNECTION_TIMEOUT: u64 = 10;
pub const MAX_MESSAGE_SIZE: usize = 10 * 1024 * 1024;
pub const MAX_POOL_SIZE: usize = 10;
pub const POOL_IDLE_TIMEOUT: u64 = 300;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GurtStatusCode {
// Success
Ok = 200,
Created = 201,
Accepted = 202,
NoContent = 204,
// Handshake
SwitchingProtocols = 101,
// Client errors
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
Timeout = 408,
TooLarge = 413,
UnsupportedMediaType = 415,
// Server errors
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
}
impl GurtStatusCode {
pub fn from_u16(code: u16) -> Option<Self> {
match code {
200 => Some(Self::Ok),
201 => Some(Self::Created),
202 => Some(Self::Accepted),
204 => Some(Self::NoContent),
101 => Some(Self::SwitchingProtocols),
400 => Some(Self::BadRequest),
401 => Some(Self::Unauthorized),
403 => Some(Self::Forbidden),
404 => Some(Self::NotFound),
405 => Some(Self::MethodNotAllowed),
408 => Some(Self::Timeout),
413 => Some(Self::TooLarge),
415 => Some(Self::UnsupportedMediaType),
500 => Some(Self::InternalServerError),
501 => Some(Self::NotImplemented),
502 => Some(Self::BadGateway),
503 => Some(Self::ServiceUnavailable),
504 => Some(Self::GatewayTimeout),
_ => None,
}
}
pub fn message(&self) -> &'static str {
match self {
Self::Ok => "OK",
Self::Created => "CREATED",
Self::Accepted => "ACCEPTED",
Self::NoContent => "NO_CONTENT",
Self::SwitchingProtocols => "SWITCHING_PROTOCOLS",
Self::BadRequest => "BAD_REQUEST",
Self::Unauthorized => "UNAUTHORIZED",
Self::Forbidden => "FORBIDDEN",
Self::NotFound => "NOT_FOUND",
Self::MethodNotAllowed => "METHOD_NOT_ALLOWED",
Self::Timeout => "TIMEOUT",
Self::TooLarge => "TOO_LARGE",
Self::UnsupportedMediaType => "UNSUPPORTED_MEDIA_TYPE",
Self::InternalServerError => "INTERNAL_SERVER_ERROR",
Self::NotImplemented => "NOT_IMPLEMENTED",
Self::BadGateway => "BAD_GATEWAY",
Self::ServiceUnavailable => "SERVICE_UNAVAILABLE",
Self::GatewayTimeout => "GATEWAY_TIMEOUT",
}
}
pub fn is_success(&self) -> bool {
matches!(self, Self::Ok | Self::Created | Self::Accepted | Self::NoContent)
}
pub fn is_client_error(&self) -> bool {
(*self as u16) >= 400 && (*self as u16) < 500
}
pub fn is_server_error(&self) -> bool {
(*self as u16) >= 500
}
}
impl From<GurtStatusCode> for u16 {
fn from(code: GurtStatusCode) -> Self {
code as u16
}
}
impl fmt::Display for GurtStatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", *self as u16)
}
}

View File

@@ -0,0 +1,563 @@
use crate::{
GurtError, Result, GurtRequest, GurtResponse, GurtMessage,
protocol::{BODY_SEPARATOR, MAX_MESSAGE_SIZE},
message::GurtMethod,
protocol::GurtStatusCode,
crypto::{TLS_VERSION, GURT_ALPN, TlsConfig},
};
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_rustls::{TlsAcceptor, server::TlsStream};
use rustls::pki_types::CertificateDer;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::fs;
use tracing::{info, warn, error, debug};
#[derive(Debug, Clone)]
pub struct ServerContext {
pub remote_addr: SocketAddr,
pub request: GurtRequest,
}
impl ServerContext {
pub fn client_ip(&self) -> std::net::IpAddr {
self.remote_addr.ip()
}
pub fn client_port(&self) -> u16 {
self.remote_addr.port()
}
pub fn method(&self) -> &GurtMethod {
&self.request.method
}
pub fn path(&self) -> &str {
&self.request.path
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.request.headers
}
pub fn body(&self) -> &[u8] {
&self.request.body
}
pub fn body_as_string(&self) -> Result<String> {
self.request.body_as_string()
}
pub fn header(&self, key: &str) -> Option<&String> {
self.request.header(key)
}
}
pub trait GurtHandler: Send + Sync {
fn handle(&self, ctx: &ServerContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<GurtResponse>> + Send + '_>>;
}
pub struct FnHandler<F> {
handler: F,
}
impl<F, Fut> GurtHandler for FnHandler<F>
where
F: Fn(&ServerContext) -> Fut + Send + Sync,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
fn handle(&self, ctx: &ServerContext) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<GurtResponse>> + Send + '_>> {
Box::pin((self.handler)(ctx))
}
}
#[derive(Debug, Clone)]
pub struct Route {
method: Option<GurtMethod>,
path_pattern: String,
}
impl Route {
pub fn new(method: Option<GurtMethod>, path_pattern: String) -> Self {
Self { method, path_pattern }
}
pub fn get(path: &str) -> Self {
Self::new(Some(GurtMethod::GET), path.to_string())
}
pub fn post(path: &str) -> Self {
Self::new(Some(GurtMethod::POST), path.to_string())
}
pub fn put(path: &str) -> Self {
Self::new(Some(GurtMethod::PUT), path.to_string())
}
pub fn delete(path: &str) -> Self {
Self::new(Some(GurtMethod::DELETE), path.to_string())
}
pub fn head(path: &str) -> Self {
Self::new(Some(GurtMethod::HEAD), path.to_string())
}
pub fn options(path: &str) -> Self {
Self::new(Some(GurtMethod::OPTIONS), path.to_string())
}
pub fn patch(path: &str) -> Self {
Self::new(Some(GurtMethod::PATCH), path.to_string())
}
pub fn any(path: &str) -> Self {
Self::new(None, path.to_string())
}
pub fn matches(&self, method: &GurtMethod, path: &str) -> bool {
if let Some(route_method) = &self.method {
if route_method != method {
return false;
}
}
self.matches_path(path)
}
pub fn matches_path(&self, path: &str) -> bool {
self.path_pattern == path ||
(self.path_pattern.ends_with('*') && path.starts_with(&self.path_pattern[..self.path_pattern.len()-1]))
}
}
pub struct GurtServer {
routes: Vec<(Route, Arc<dyn GurtHandler>)>,
tls_acceptor: Option<TlsAcceptor>,
}
impl GurtServer {
pub fn new() -> Self {
Self {
routes: Vec::new(),
tls_acceptor: None,
}
}
pub fn with_tls_certificates(cert_path: &str, key_path: &str) -> Result<Self> {
let mut server = Self::new();
server.load_tls_certificates(cert_path, key_path)?;
Ok(server)
}
pub fn load_tls_certificates(&mut self, cert_path: &str, key_path: &str) -> Result<()> {
info!("Loading TLS certificates: cert={}, key={}", cert_path, key_path);
let cert_data = fs::read(cert_path)
.map_err(|e| GurtError::crypto(format!("Failed to read certificate file '{}': {}", cert_path, e)))?;
let key_data = fs::read(key_path)
.map_err(|e| GurtError::crypto(format!("Failed to read private key file '{}': {}", key_path, e)))?;
let mut cursor = std::io::Cursor::new(cert_data);
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cursor)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| GurtError::crypto(format!("Failed to parse certificates: {}", e)))?;
if certs.is_empty() {
return Err(GurtError::crypto("No certificates found in certificate file"));
}
let mut key_cursor = std::io::Cursor::new(key_data);
let private_key = rustls_pemfile::private_key(&mut key_cursor)
.map_err(|e| GurtError::crypto(format!("Failed to parse private key: {}", e)))?
.ok_or_else(|| GurtError::crypto("No private key found in key file"))?;
let tls_config = TlsConfig::new_server(certs, private_key)?;
self.tls_acceptor = Some(tls_config.get_acceptor()?);
info!("TLS certificates loaded successfully");
Ok(())
}
pub fn route<H>(mut self, route: Route, handler: H) -> Self
where
H: GurtHandler + 'static,
{
self.routes.push((route, Arc::new(handler)));
self
}
pub fn get<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::get(path), FnHandler { handler })
}
pub fn post<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::post(path), FnHandler { handler })
}
pub fn put<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::put(path), FnHandler { handler })
}
pub fn delete<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::delete(path), FnHandler { handler })
}
pub fn head<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::head(path), FnHandler { handler })
}
pub fn options<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::options(path), FnHandler { handler })
}
pub fn patch<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::patch(path), FnHandler { handler })
}
pub fn any<F, Fut>(self, path: &str, handler: F) -> Self
where
F: Fn(&ServerContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<GurtResponse>> + Send + 'static,
{
self.route(Route::any(path), FnHandler { handler })
}
pub async fn listen(self, addr: &str) -> Result<()> {
let listener = TcpListener::bind(addr).await?;
info!("GURT server listening on {}", addr);
loop {
match listener.accept().await {
Ok((stream, addr)) => {
info!("Client connected: {}", addr);
let server = self.clone();
tokio::spawn(async move {
if let Err(e) = server.handle_connection(stream, addr).await {
error!("Connection error from {}: {}", addr, e);
}
info!("Client disconnected: {}", addr);
});
}
Err(e) => {
error!("Failed to accept connection: {}", e);
}
}
}
}
async fn handle_connection(&self, mut stream: TcpStream, addr: SocketAddr) -> Result<()> {
self.handle_initial_handshake(&mut stream, addr).await?;
if let Some(tls_acceptor) = &self.tls_acceptor {
info!("Upgrading connection to TLS for {}", addr);
let tls_stream = tls_acceptor.accept(stream).await
.map_err(|e| GurtError::crypto(format!("TLS upgrade failed: {}", e)))?;
info!("TLS upgrade completed for {}", addr);
self.handle_tls_connection(tls_stream, addr).await
} else {
warn!("No TLS configuration available, but handshake completed - this violates GURT protocol");
Err(GurtError::protocol("TLS is required after handshake but no TLS configuration available"))
}
}
async fn handle_initial_handshake(&self, stream: &mut TcpStream, addr: SocketAddr) -> Result<()> {
let mut buffer = Vec::new();
let mut temp_buffer = [0u8; 8192];
loop {
let bytes_read = stream.read(&mut temp_buffer).await?;
if bytes_read == 0 {
return Err(GurtError::connection("Connection closed during handshake"));
}
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
let body_separator = BODY_SEPARATOR.as_bytes();
if buffer.windows(body_separator.len()).any(|w| w == body_separator) {
break;
}
if buffer.len() > MAX_MESSAGE_SIZE {
return Err(GurtError::protocol("Handshake message too large"));
}
}
let message = GurtMessage::parse_bytes(&buffer)?;
match message {
GurtMessage::Request(request) => {
if request.method == GurtMethod::HANDSHAKE {
self.send_handshake_response(stream, addr, &request).await
} else {
Err(GurtError::protocol("First message must be HANDSHAKE"))
}
}
GurtMessage::Response(_) => {
Err(GurtError::protocol("Server received response during handshake"))
}
}
}
async fn handle_tls_connection(&self, mut tls_stream: TlsStream<TcpStream>, addr: SocketAddr) -> Result<()> {
let mut buffer = Vec::new();
let mut temp_buffer = [0u8; 8192];
loop {
let bytes_read = match tls_stream.read(&mut temp_buffer).await {
Ok(n) => n,
Err(e) => {
// Handle UnexpectedEof from clients that don't send close_notify
if e.kind() == std::io::ErrorKind::UnexpectedEof {
debug!("Client {} closed connection without TLS close_notify (benign)", addr);
break;
}
return Err(e.into());
}
};
if bytes_read == 0 {
break; // Connection closed
}
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
let body_separator = BODY_SEPARATOR.as_bytes();
let has_complete_message = buffer.windows(body_separator.len()).any(|w| w == body_separator) ||
(buffer.starts_with(b"{") && buffer.ends_with(b"}"));
if has_complete_message {
if let Err(e) = self.process_tls_message(&mut tls_stream, addr, &buffer).await {
error!("Encrypted message processing error from {}: {}", addr, e);
let error_response = GurtResponse::internal_server_error()
.with_string_body("Internal server error");
let _ = tls_stream.write_all(&error_response.to_bytes()).await;
}
buffer.clear();
}
// Prevent buffer overflow
if buffer.len() > MAX_MESSAGE_SIZE {
warn!("Message too large from {}, closing connection", addr);
break;
}
}
Ok(())
}
async fn send_handshake_response(&self, stream: &mut TcpStream, addr: SocketAddr, _request: &GurtRequest) -> Result<()> {
info!("Sending handshake response to {}", addr);
let response = GurtResponse::new(GurtStatusCode::SwitchingProtocols)
.with_header("GURT-Version", crate::GURT_VERSION.to_string())
.with_header("Encryption", TLS_VERSION)
.with_header("ALPN", std::str::from_utf8(GURT_ALPN).unwrap_or("gurt/1.0"));
let response_bytes = response.to_string().into_bytes();
stream.write_all(&response_bytes).await?;
info!("Handshake response sent to {}, preparing for TLS upgrade", addr);
Ok(())
}
async fn process_tls_message(&self, tls_stream: &mut TlsStream<TcpStream>, addr: SocketAddr, data: &[u8]) -> Result<()> {
let message = GurtMessage::parse_bytes(data)?;
match message {
GurtMessage::Request(request) => {
if request.method == GurtMethod::HANDSHAKE {
Err(GurtError::protocol("Received HANDSHAKE over TLS - protocol violation"))
} else {
self.handle_encrypted_request(tls_stream, addr, &request).await
}
}
GurtMessage::Response(_) => {
warn!("Received response on server, ignoring");
Ok(())
}
}
}
async fn handle_default_options(&self, tls_stream: &mut TlsStream<TcpStream>, request: &GurtRequest) -> Result<()> {
let mut allowed_methods = std::collections::HashSet::new();
for (route, _) in &self.routes {
if route.matches_path(&request.path) {
if let Some(method) = &route.method {
allowed_methods.insert(method.to_string());
} else {
// Route matches any method
allowed_methods.extend(vec![
"GET".to_string(), "POST".to_string(), "PUT".to_string(),
"DELETE".to_string(), "HEAD".to_string(), "PATCH".to_string()
]);
}
}
}
allowed_methods.insert("OPTIONS".to_string());
let mut allowed_methods_vec: Vec<String> = allowed_methods.into_iter().collect();
allowed_methods_vec.sort();
let allow_header = allowed_methods_vec.join(", ");
let response = GurtResponse::ok()
.with_header("Allow", allow_header)
.with_header("Access-Control-Allow-Origin", "*")
.with_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH")
.with_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
tls_stream.write_all(&response.to_bytes()).await?;
Ok(())
}
async fn handle_default_head(&self, tls_stream: &mut TlsStream<TcpStream>, addr: SocketAddr, request: &GurtRequest) -> Result<()> {
for (route, handler) in &self.routes {
if route.method == Some(GurtMethod::GET) && route.matches(&GurtMethod::GET, &request.path) {
let context = ServerContext {
remote_addr: addr,
request: request.clone(),
};
match handler.handle(&context).await {
Ok(mut response) => {
let original_content_length = response.body.len();
response.body.clear();
response = response.with_header("content-length", original_content_length.to_string());
tls_stream.write_all(&response.to_bytes()).await?;
return Ok(());
}
Err(e) => {
error!("Handler error for HEAD {} (via GET): {}", request.path, e);
let error_response = GurtResponse::internal_server_error();
tls_stream.write_all(&error_response.to_bytes()).await?;
return Ok(());
}
}
}
}
let not_found_response = GurtResponse::not_found();
tls_stream.write_all(&not_found_response.to_bytes()).await?;
Ok(())
}
async fn handle_encrypted_request(&self, tls_stream: &mut TlsStream<TcpStream>, addr: SocketAddr, request: &GurtRequest) -> Result<()> {
debug!("Handling encrypted {} request to {} from {}", request.method, request.path, addr);
// Find matching route
for (route, handler) in &self.routes {
if route.matches(&request.method, &request.path) {
let context = ServerContext {
remote_addr: addr,
request: request.clone(),
};
match handler.handle(&context).await {
Ok(response) => {
// Use to_bytes() to avoid corrupting binary data
let response_bytes = response.to_bytes();
tls_stream.write_all(&response_bytes).await?;
return Ok(());
}
Err(e) => {
error!("Handler error for {} {}: {}", request.method, request.path, e);
let error_response = GurtResponse::internal_server_error()
.with_string_body("Internal server error");
tls_stream.write_all(&error_response.to_bytes()).await?;
return Ok(());
}
}
}
}
// No route found - check for default OPTIONS/HEAD handling
match request.method {
GurtMethod::OPTIONS => {
self.handle_default_options(tls_stream, request).await
}
GurtMethod::HEAD => {
self.handle_default_head(tls_stream, addr, request).await
}
_ => {
let not_found_response = GurtResponse::not_found()
.with_string_body("Not found");
tls_stream.write_all(&not_found_response.to_bytes()).await?;
Ok(())
}
}
}
}
impl Clone for GurtServer {
fn clone(&self) -> Self {
Self {
routes: self.routes.clone(),
tls_acceptor: self.tls_acceptor.clone(),
}
}
}
impl Default for GurtServer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::test;
#[test]
async fn test_route_matching() {
let route = Route::get("/test");
assert!(route.matches(&GurtMethod::GET, "/test"));
assert!(!route.matches(&GurtMethod::POST, "/test"));
assert!(!route.matches(&GurtMethod::GET, "/other"));
let wildcard_route = Route::get("/api/*");
assert!(wildcard_route.matches(&GurtMethod::GET, "/api/users"));
assert!(wildcard_route.matches(&GurtMethod::GET, "/api/posts"));
assert!(!wildcard_route.matches(&GurtMethod::GET, "/other"));
}
}