Files

6.3 KiB

Servme

Un framework web HTTP de alto rendimiento escrito en Rust, construido sobre Hyper.

Primeros pasos

Instalación

Añade servme a tu Cargo.toml:

[dependencies]
servme = "0.1"
tokio = { version = "1", features = ["full"] }

Tu primer servidor

use http_body_util::Full;
use hyper::{body::Bytes, Request, Response};
use servme::{Responder, Server, ServerError};

#[tokio::main]
async fn main() {
    Server::builder()
        .address("127.0.0.1", 8080)
        .build()
        .run(handler)
        .await;
}

async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
    Responder::ok(format!("Hola! Path: {}", req.uri()))
}

Para usuarios nuevos: Ejecuta cargo run y visita http://127.0.0.1:8080 en tu navegador.


Conceptos básicos

El Handler

El handler es una función asíncrona que recibe un Request y retorna una Response:

use http_body_util::Full;
use hyper::{body::Bytes, Request, Response};

async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
    // Tu lógica aquí
    Responder::ok("Respuesta")
}

Responder

Responder proporciona métodos auxiliares para crear respuestas HTTP comunes:

use servme::Responder;

// Respuestas de éxito
Responder::ok("Mensaje")?;                    // 200 OK
Responder::html("<h1>Título</h1>")?;          // 200 con Content-Type: text/html
Responder::json(&datos)?;                      // 200 con Content-Type: application/json
Responder::redirect("/nueva-ruta")?;           // 302 Redirect
Responder::no_content()?;                      // 204 No Content

// Respuestas de error
Responder::not_found()?;                        // 404
Responder::unauthorized()?;                    // 401
Responder::forbidden()?;                        // 403
Responder::bad_request("Datos inválidos")?;    // 400
Responder::internal_error("Algo salió mal")?;   // 500

Extraer datos del Request

use servme::Requester;

// Extraer body JSON
let data: MyStruct = Requester::extract_body(req).await?;

// Extraer como texto
let body_str: String = Requester::extract_body_str(req).await?;

// Extraer como bytes (más eficiente)
let body_bytes: Bytes = Requester::extract_body_bytes(req).await?;

// Extraer parámetros de URL
let url = UrlExtract::new(req.uri());
let name = url.param_str("nombre");           // ?nombre=Juan
let age: Option<i64> = url.param_i64("edad"); // ?edad=25

Middleware

Los middleware se ejecutan antes del handler y pueden authnticar, filtrar o modificar requests.

API Key

use servme::{Server, ServerBuilder};

Server::builder()
    .address("127.0.0.1", 8080)
    .add_api_key_middleware("mi-clave-secreta")
    .build()
    .run(handler)
    .await;

Envía la clave en el header X-API-Key.

JWT Authentication

use servme::{Server, ServerBuilder};

Server::builder()
    .address("0.0.0.0", 8080)
    .add_jwt_middleware(
        rsa_public_key_pem,           // Clave pública RSA en formato PEM
        vec!["/health".to_string()], // Rutas públicas (sin auth)
    )
    .build()
    .run(handler)
    .await;

El JWT se valida desde:

  • Header Authorization: Bearer <token>
  • Cookie access_token

Filtrado por IP

use servme::ServerBuilder;

Server::builder()
    .address("127.0.0.1", 8080)
    .add_ip_filter_middleware(
        vec!["192.168.1.100".to_string(), "10.0.0.1".to_string()],
        true, // allow_private: también permitir IPs privadas (192.168.x.x, 10.x.x.x)
    )
    .build()
    .run(handler)
    .await;

Múltiples middlewares

Server::builder()
    .address("0.0.0.0", 8080)
    .add_ip_filter_middleware(vec![], true) // Permitir todas las IPs privadas
    .add_jwt_middleware(pub_key, vec!["/static".to_string(), "/health".to_string()])
    .build()
    .run(handler)
    .await;

Estado compartido

Puedes compartir datos entre requests usando .data():

use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db: Database,
}

Server::builder()
    .address("127.0.0.1", 8080)
    .data(AppState { db: Database::new() })
    .build()
    .run(|req| async move {
        // Accede al estado desde las extensions del request
        let state = req.extensions().get::<Arc<AppState>>().unwrap();
        // Usa state.db...
        Responder::ok("ok")
    })
    .await;

Estructura del proyecto

src/
├── lib.rs              # Exports públicos
├── main.rs             # Binario de ejemplo
├── builder.rs          # ServerBuilder - configuración del servidor
├── config.rs           # ServerConfig - estructura de configuración
├── server.rs           # Servidor HTTP con graceful shutdown
├── error.rs            # ServerError - tipos de errores
├── constants.rs        # Constantes del framework
├── responder.rs        # Helpers para construir respuestas
├── requester.rs        # Helpers para extraer datos del request
├── url_extract.rs      # Parsing de URLs y query params
└── middleware/
    ├── mod.rs          # Traits y tipos comunes
    ├── api_key.rs      # Autenticación por API Key
    ├── jwt.rs          # Autenticación JWT (RS256)
    ├── ip_filter.rs    # Filtrado por dirección IP
    └── auth_types.rs   # Tipos para autenticación (Claims)

Construcción y testing

# Compilar
cargo build

# Ejecutar tests
cargo test

# Ejecutar con logs de debug
RUST_LOG=debug cargo run

# Ver documentation
cargo doc --open

Errores comunes

"Failed to bind to address"

El puerto está en uso. Prueba con otro puerto:

Server::builder()
    .address("127.0.0.1", 8081) // Cambia el puerto
    .build()
    .run(handler)
    .await;

"JWT validation failed"

  • Verifica que la clave pública RSA sea válida
  • Asegúrate de que el token no esté expirado (exp claim)
  • El token debe contener el claim sub (subject)

Constantes útiles

use servme::{
    DEFAULT_HOST,          // "127.0.0.1"
    DEFAULT_PORT,          // 8080
    JWT_COOKIE_NAME,       // "access_token"
    BEARER_PREFIX,         // "Bearer "
    MAX_ALLOWED_IPS,       // 1000
};

use servme::constants::FILE_EXTENSIONS; // Extensiones de archivos estáticos

License

MIT