Paquete: io.github.blacknoize404.miniJWS.HttpServer
Archivo: miniJWS-core/src/main/java/.../HttpServer.java
El orquestador central. Gestiona el ServerSocket, el pool de hilos, la tabla de rutas, la cadena de middleware y el ciclo de vida de las conexiones.
| Campo | Tipo | Propósito |
|---|---|---|
routes |
Map<String, RequestRunner> |
ConcurrentHashMap — clave = MÉTODO:/ruta |
middlewares |
List<Middleware> |
CopyOnWriteArrayList — seguro para cargas de lectura intensiva |
socket |
ServerSocket |
Aceptador TCP |
threadPool |
ExecutorService |
Pool de hilos fijo (por defecto: 2 × núcleos CPU) |
running |
AtomicBoolean |
Controla el bucle de aceptación |
shutdownLatch |
CountDownLatch |
Bloquea idle() hasta que stop() se dispara |
- Constructor: Abre
ServerSocket, crea el pool de hilos - Configuración:
addRoute(),addStaticRoute(),use()— todos devuelventhis(fluido) run(): Establecerunning=true, registra el hook SIGINT, entra en el bucle de aceptación. Cada socket aceptado se envía ahandleConnection()mediante el pool de hilos.stop(): Establecerunning=false, decrementa el latch, cierra el socketidle(): Bloquea esperando el latch (para modo demonio/servidor)- Apagado:
run()llama ashutdown()después de que el bucle de aceptación termina, drenando el pool de hilos
accept → for(hasta 100 peticiones):
decode(request) → cadena de middleware → encode(response)
if Connection:close o timeout → break
cerrar socket
El método buildChain() crea una cadena de lambdas anidadas. El último middleware registrado se ejecuta primero (envuelve el terminal). El middleware terminal hace el emparejamiento de rutas:
- Coincidencia exacta (
routes.get(key)) - Coincidencia por parámetros de ruta (
findRouteWithParams()— escaneo lineal de todas las rutas) - 404
static Map<String, String> matchPath(String pattern, String path)Comparación segmento por segmento:
- Segmento estático: debe coincidir exactamente
:param: captura el segmento en el mapa de parámetros*: coincide con cualquier segmento individual (sin captura)**: coincide con todos los segmentos restantes, retorna inmediatamente
Paquete: io.github.blacknoize404.miniJWS.requests.HttpRequest
Archivo: miniJWS-core/src/main/java/.../requests/HttpRequest.java
Modelo de petición inmutable construido por la clase interna Builder.
| Campo | Tipo | Descripción |
|---|---|---|
httpMethod |
HttpMethod |
Enum: GET, POST, etc. |
uri |
URI |
URI parseada (solo ruta, query separada) |
protocolVersion |
String |
p.ej. HTTP/1.1 |
headers |
Map<String, List<String>> |
Cabeceras multivalor (claves con su formato original) |
parameters |
Map<String, String> |
Parámetros de query + ruta combinados (los de ruta tienen prioridad) |
cookies |
Map<String, String> |
Parseadas de la cabecera Cookie en tiempo de construcción |
body |
Optional<byte[]> |
Bytes del cuerpo en bruto |
parseCookies() se ejecuta durante la construcción. Divide la cabecera Cookie por ;, luego cada par por =:
Cookie: session=abc123; token=xyz
→ {session: "abc123", token: "xyz"}
| Método | Implementación |
|---|---|
bodyAsString() |
new String(body, UTF_8) |
bodyAsForm() |
Divide por &, luego =, decodifica URL cada parte |
bodyAsJson() |
Parser JSON plano escrito a mano (sin librería externa) — pares clave:valor separados por ,, maneja cadenas escapadas y entrecomilladas |
El parser JSON es intencionadamente plano — solo maneja un nivel de {"clave": "valor", ...}. Los objetos o arrays anidados se saltan (el seguimiento de profundidad omite contenido dentro de {} y []).
Paquete: io.github.blacknoize404.miniJWS.responses.HttpResponse
Archivo: miniJWS-core/src/main/java/.../responses/HttpResponse.java
Modelo de respuesta inmutable construido por la clase interna Builder.
Toda respuesta incluye automáticamente:
Server: MiniJWS(deHttpServer.SERVER_NAME)Date: <marca temporal RFC 1123>(generada en tiempo de construcción)
HttpResponse.redirect("/login"); // 302 Found
HttpResponse.redirect("/new-url", 301); // 301 Moved PermanentlyAmbos construyen una respuesta con cabecera Location y sin cuerpo.
builder.setCookie("name", "value"); // simple
builder.setCookie("name", "value", 3600, "/", true); // con Max-Age, Path, HttpOnlyInternamente añade una cabecera Set-Cookie. La variante completa construye name=value; Max-Age=3600; Path=/; HttpOnly.
new HttpResponse.Builder()
.setStatusCode(200)
.setContentType(ContentType.JSON)
.addHeader("Cache-Control", "no-cache")
.setBody("{\"ok\":true}")
.build();Paquete: io.github.blacknoize404.miniJWS.requests.HttpDecoder
Archivo: miniJWS-core/src/main/java/.../requests/HttpDecoder.java
Clase utilitaria estática que parsea InputStream → Optional<HttpRequest>.
- Línea de petición:
readLine()→ dividir por espacio →HttpMethod,URI, protocolo - Cabeceras: Bucle
readLine()hasta línea vacía. Cada línea se divide por:.Content-LengthyTransfer-Encodingse rastrean durante el parseo de cabeceras. - Obs-fold: Las líneas que empiezan con espacio/tabulador son continuación del valor de la cabecera anterior.
- Cuerpo:
- Si
Transfer-Encoding: chunked→readChunkedBody()(parsear tamaño hex, quitar extensiones, leer chunk, saltar CRLF) - Si
Content-Length > 0→readExactBody()(lectura bloqueante de bytes exactos)
- Si
- URI: la porción de query se parsea por separado →
parseQueryParams()conURLDecoder.decode(UTF_8)
Bucle byte a byte acumulando bytes hasta encontrar \r\n:
while ((b = in.read()) != -1) {
if (b == CR) { crFound = true; }
else if (b == LF) { return buf.toString(US_ASCII); }
else { if (crFound) buf.write(CR); buf.write(b); }
}Longitud máxima de línea: 8_192 bytes (devuelve null si se excede).
MAX_CHUNK_SIZE: 10 MiB por chunkMAX_CONTENT_LENGTH: 50 MiB total del cuerpoContent-Lengthduplicado → rechazar (devolver vacío)
Paquete: io.github.blacknoize404.miniJWS.responses.HttpEncoder
Archivo: miniJWS-core/src/main/java/.../responses/HttpEncoder.java
Clase utilitaria estática que serializa HttpResponse → OutputStream.
- Línea de estado:
HTTP/1.1 200 OK\r\n - Todas las cabeceras:
Clave: valor\r\n - Cabecera Content-Length (si hay cuerpo)
- Línea vacía
\r\n - Bytes del cuerpo (escritos directamente al
OutputStream, no a través del escritor de texto)
Detalle crítico: Las cabeceras se escriben mediante un BufferedWriter(OutputStreamWriter(outputStream, US_ASCII)) y se vacían antes de que el cuerpo se escriba directamente vía outputStream.write(data). Esto evita que el escritor US-ASCII corrompa contenido binario (gzip, imágenes, secuencias UTF-8 multibyte).
Paquete: io.github.blacknoize404.miniJWS.handlers.StaticFileHandler
Archivo: miniJWS-core/src/main/java/.../handlers/StaticFileHandler.java
Implementa RequestRunner para servir archivos desde un directorio.
Protección de tres capas contra path traversal:
- Verificación de cadena:
rawPath.contains("..")→ 400 - Verificación de normalización:
baseDir.resolve(relative).normalize().startsWith(baseDir)→ 403 - Condición de carrera:
NoSuchFileExceptioncapturada → 404 en lugar de 500
- Archivos de índice de directorio (por defecto:
index.html, configurable) - Detección MIME mediante
ContentType.fromExtension() - Soporte de archivos binarios (bytes sin procesar)
Usa ContentType.fromExtension(ext) que mapea la extensión del archivo al tipo MIME mediante el EXT_MAP en ContentType. Las extensiones desconocidas por defecto usan application/octet-stream.