chore: add http2 support with connection tracking and optimize middlewares
This commit is contained in:
@@ -1,139 +1,280 @@
|
||||
# Servme
|
||||
|
||||
Un framework web HTTP de bajo nivel escrito en Rust, construido sobre Hyper.
|
||||
Un framework web HTTP de alto rendimiento escrito en Rust, construido sobre [Hyper](https://hyper.rs/).
|
||||
|
||||
## Características
|
||||
## Primeros pasos
|
||||
|
||||
- **Middleware System**: Pipeline extensible para autenticación (JWT, API Key, IP Filter)
|
||||
- **Builder Pattern**: API fluente para configuración del servidor
|
||||
- **Graceful Shutdown**: Manejo elegante de señales SIGINT/SIGTERM
|
||||
- **Error Handling**: Sistema de errores tipado con `ServerError`
|
||||
- **High Performance**: IP filtering con O(1) lookups usando HashSet
|
||||
### Instalación
|
||||
|
||||
## Uso Básico
|
||||
Añade `servme` a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
servme = "0.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
### Tu primer servidor
|
||||
|
||||
```rust
|
||||
use servme::{ServerBuilder, Responder, UrlExtract};
|
||||
use http_body_util::Full;
|
||||
use hyper::{body::Bytes, Request, Response};
|
||||
use servme::{Responder, Server, ServerError};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let server = ServerBuilder::new()
|
||||
async fn main() {
|
||||
Server::builder()
|
||||
.address("127.0.0.1", 8080)
|
||||
.handler(|req, res| async {
|
||||
let url = UrlExtract::new(req.uri());
|
||||
Responder::ok(format!("Hello, {}!", url.param_str("name").unwrap_or_default()))
|
||||
})
|
||||
.build();
|
||||
.build()
|
||||
.run(handler)
|
||||
.await;
|
||||
}
|
||||
|
||||
server.run().await
|
||||
async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
|
||||
Responder::ok(format!("Hola! Path: {}", req.uri()))
|
||||
}
|
||||
```
|
||||
|
||||
## Middlewares
|
||||
> **Para usuarios nuevos**: Ejecuta `cargo run` y visita `http://127.0.0.1:8080` en tu navegador.
|
||||
|
||||
### API Key Authentication
|
||||
---
|
||||
|
||||
## Conceptos básicos
|
||||
|
||||
### El Handler
|
||||
|
||||
El handler es una función asíncrona que recibe un `Request` y retorna una `Response`:
|
||||
|
||||
```rust
|
||||
use servme::{ServerBuilder, middleware::ApiKeyMiddleware};
|
||||
use http_body_util::Full;
|
||||
use hyper::{body::Bytes, Request, Response};
|
||||
|
||||
let server = ServerBuilder::new()
|
||||
.address("127.0.0.1", 8080)
|
||||
.add_api_key_middleware("your-secret-key")
|
||||
.build();
|
||||
async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
|
||||
// Tu lógica aquí
|
||||
Responder::ok("Respuesta")
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Authentication
|
||||
### Responder
|
||||
|
||||
```rust
|
||||
use servme::{ServerBuilder, middleware::JwtMiddleware};
|
||||
|
||||
let server = ServerBuilder::new()
|
||||
.address("127.0.0.1", 8080)
|
||||
.add_jwt_middleware("your-secret-key")
|
||||
.build();
|
||||
```
|
||||
|
||||
### IP Filtering
|
||||
|
||||
```rust
|
||||
use servme::{ServerBuilder, middleware::IpFilterMiddleware};
|
||||
|
||||
let server = ServerBuilder::new()
|
||||
.address("127.0.0.1", 8080)
|
||||
.add_ip_filter_middleware(
|
||||
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
||||
true // allow private IPs
|
||||
)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Constantes Disponibles
|
||||
|
||||
```rust
|
||||
use servme::constants::{
|
||||
DEFAULT_HOST, // "127.0.0.1"
|
||||
DEFAULT_PORT, // 8080
|
||||
JWT_COOKIE_NAME, // "access_token"
|
||||
BEARER_PREFIX, // "Bearer "
|
||||
FILE_EXTENSIONS, // [".json", ".html", ".css", ".js"]
|
||||
MAX_ALLOWED_IPS, // 1000
|
||||
};
|
||||
```
|
||||
|
||||
## Responder Helpers
|
||||
`Responder` proporciona métodos auxiliares para crear respuestas HTTP comunes:
|
||||
|
||||
```rust
|
||||
use servme::Responder;
|
||||
|
||||
// JSON response
|
||||
Responder::json(&data)?;
|
||||
// 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
|
||||
|
||||
// Redirect
|
||||
Responder::redirect("/new-location")?;
|
||||
|
||||
// Status codes
|
||||
Responder::not_found()?;
|
||||
Responder::unauthorized()?;
|
||||
Responder::forbidden()?;
|
||||
Responder::bad_request("error message")?;
|
||||
Responder::internal_error("error message")?;
|
||||
// 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
|
||||
```
|
||||
|
||||
## Construcción y Tests
|
||||
### Extraer datos del Request
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
```rust
|
||||
use servme::Requester;
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
// Extraer body JSON
|
||||
let data: MyStruct = Requester::extract_body(req).await?;
|
||||
|
||||
# Run with debug logging
|
||||
RUST_LOG=debug cargo run
|
||||
// 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
|
||||
```
|
||||
|
||||
## Estructura del Proyecto
|
||||
---
|
||||
|
||||
## Middleware
|
||||
|
||||
Los middleware se ejecutan antes del handler y pueden authnticar, filtrar o modificar requests.
|
||||
|
||||
### API Key
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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()`:
|
||||
|
||||
```rust
|
||||
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
|
||||
├── config.rs # ServerConfig
|
||||
├── builder.rs # ServerBuilder - configuración del servidor
|
||||
├── config.rs # ServerConfig - estructura de configuración
|
||||
├── server.rs # Servidor HTTP con graceful shutdown
|
||||
├── error.rs # ServerError enum
|
||||
├── constants.rs # Constantes configurables
|
||||
├── responder.rs # Helper para construir respuestas
|
||||
├── requester.rs # Helper para extraer request info
|
||||
├── url_extract.rs # URL parsing y query params
|
||||
├── 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 # API Key authentication
|
||||
├── jwt.rs # JWT authentication
|
||||
├── ip_filter.rs # IP filtering
|
||||
└── auth_types.rs # Tipos de autenticación
|
||||
├── 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
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
MIT
|
||||
Reference in New Issue
Block a user