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
:
[dependencies]
gurtlib = "0.1"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
serde_json = "1.0"
Quick Start
use gurtlib::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
let server = GurtServer::new();
Server with TLS Certificates
// 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
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
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
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:
.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
// 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
// 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
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
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: "[email protected]".to_string() },
User { id: 2, name: "Bob".to_string(), email: "[email protected]".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
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
// 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
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:
# 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:
# 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
// 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
#[cfg(test)]
mod tests {
use super::*;
use gurtlib::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");
}
}