Files
leonwww/docs/docs/gurt-server.md
2025-08-15 13:52:01 +03:00

12 KiB

sidebar_position
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:

[dependencies]
gurt = "0.1"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
serde_json = "1.0"

Quick Start

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

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: "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

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 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");
    }
}