API de Subida de Documentos
Endpoint único para depositar albaranes y documentación de transporte en PDF, organizados por carpeta de cliente. Autenticación por token Bearer. Todas las respuestas son JSON UTF-8.
| Base URL | https://deca.infonetconsultores.com |
| Protocolo | HTTPS · JSON |
| Auth | Bearer token en cabecera Authorization |
| Versión | 1.0 |
Autenticación
Toda petición debe incluir la cabecera Authorization con un token Bearer estático. La comparación se hace con hash_equals(), resistente a ataques de temporización.
Authorization: Bearer <TU_TOKEN>
| Situación | Respuesta |
|---|---|
| Cabecera ausente o mal formada | 401 + WWW-Authenticate: Bearer |
| Token presente pero incorrecto | 403 (queda registrado en el log con la IP) |
Apache con CGI/FastCGI: la cabecera Authorization se pierde antes de llegar a PHP salvo que el .htaccess la reinyecte. Si recibes 401 con un token válido, revisa que AllowOverride All esté activo en el vhost.
En Nginx + PHP-FPM el .htaccess no aplica: añade fastcgi_param HTTP_AUTHORIZATION $http_authorization; al bloque del server.
Endpoint
Crea la carpeta del cliente si no existe y guarda el PDF con el nombre indicado. La escritura es atómica: se escribe primero en un fichero temporal y luego se renombra, de modo que nunca queda un PDF a medias en disco.
Cabeceras
| Cabecera | Tipo | Valor | |
|---|---|---|---|
| Authorization | string | obligatoria | Bearer <token> |
| Content-Type | string | recomendada | application/json — si se omite o es distinta, el cuerpo se lee como formulario ($_POST) |
Cuerpo de la petición
Se acepta application/json (recomendado) o application/x-www-form-urlencoded / multipart/form-data con los mismos nombres de campo.
| Campo | Tipo | Descripción | |
|---|---|---|---|
| cliente | string | obligatorio | Nombre del cliente. Se convierte a un slug que da nombre a la carpeta: se quitan acentos, se pasa a minúsculas y se sustituye todo lo no alfanumérico por guiones. Máximo 100 caracteres tras el saneado."Transportes Pérez, S.L." → carpeta transportes-perez-s-l |
| nombre_fichero | string | obligatorio | Nombre con el que se guardará el PDF. Se toma solo el basename, se filtran los caracteres a [A-Za-z0-9._-] y se fuerza la extensión .pdf. Máximo 150 caracteres. |
| pdf_base64 | string | obligatorio | Contenido del PDF codificado en Base64. Se admiten saltos de línea y espacios (se eliminan) y el formato Data URI data:application/pdf;base64,…. Máximo 20 MB ya decodificado. |
Esquema
{
"cliente": "Transportes Perez SL",
"nombre_fichero": "ALB-2026-0001.pdf",
"pdf_base64": "JVBERi0xLjQKMSAwIG9iajw8L1R5cGUv..."
}
Ejemplos de petición
# El PDF se codifica al vuelo y se inserta en el JSON curl -X POST https://deca.infonetconsultores.com/subir.php \ -H "Authorization: Bearer TU_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"cliente\":\"Transportes Perez SL\", \"nombre_fichero\":\"ALB-2026-0001.pdf\", \"pdf_base64\":\"$(base64 -w0 albaran.pdf)\"}"
Respuesta correcta
documentos/<cliente>/<fichero> con permisos 0644.{
"ok": true,
"cliente": "transportes-perez-sl",
"fichero": "ALB-2026-0001.pdf",
"ruta": "documentos/transportes-perez-sl/ALB-2026-0001.pdf",
"bytes": 152340
}
| Campo | Tipo | Descripción |
|---|---|---|
| ok | boolean | Siempre true en la respuesta 201. |
| cliente | string | Slug realmente usado como carpeta, ya saneado. |
| fichero | string | Nombre final tras el saneado y el forzado de extensión. |
| ruta | string | Ruta relativa del fichero dentro del directorio de subidas. |
| bytes | integer | Tamaño del PDF decodificado, en bytes. |
Errores
Todos los errores devuelven {"ok": false, "error": "<mensaje>"} con el código HTTP correspondiente.
{
"ok": false,
"error": "El contenido decodificado no es un PDF."
}
Catálogo completo
error los distingue."Falta el parámetro \"cliente\"." · "Falta el parámetro \"nombre_fichero\"." · "Falta el parámetro \"pdf_base64\"."
"Data URI mal formado en \"pdf_base64\"."
"El campo \"pdf_base64\" no contiene base64 válido."
"El nombre de cliente no contiene caracteres válidos." · "El nombre de fichero no contiene caracteres válidos."
"Ruta de destino no permitida."
Authorization, o no sigue el formato Bearer <token>. Se devuelve WWW-Authenticate: Bearer.subidas.log junto con la IP de origen.Allow: POST.ALLOW_OVERWRITE = false).post_max_size debe ser mayor (32 MB en el .htaccess).%PDF-. Evita que se suban ejecutables o scripts renombrados."No se pudo escribir el fichero en disco." · "No se pudo mover el fichero a su destino final."
Saneado de nombres
Ningún dato del cliente llega en crudo al sistema de ficheros. Estas son las transformaciones exactas:
Carpeta de cliente
| Entrada | Carpeta resultante |
|---|---|
| Transportes Pérez, S.L. | transportes-perez-s-l |
| LOGÍSTICA DEL SUR | logistica-del-sur |
| ../../etc | etc |
| !!! | 400 — no quedan caracteres válidos |
Nombre de fichero
| Entrada | Fichero resultante |
|---|---|
| ALB-2026-0001.pdf | ALB-2026-0001.pdf sin cambios |
| albarán nº5.pdf | albar_n_5.pdf |
| factura | factura.pdf extensión forzada |
| ../../../shell.php | shell.php.pdf |
| .htaccess | htaccess.pdf |
Además del filtrado, tras crear la carpeta se comprueba con realpath() que la ruta resuelta siga estando dentro del directorio base. Si no, se responde 400. Es la red de seguridad final contra el path traversal.
Límites y validación
| Parámetro | Valor | Dónde se cambia |
|---|---|---|
| Tamaño máximo | 20 MB (20971520 bytes) decodificado | MAX_FILE_BYTES · subir.php:36 |
| Sobrescritura | Desactivada → 409 | ALLOW_OVERWRITE · subir.php:39 |
| Directorio base | ./documentos | BASE_UPLOAD_DIR · subir.php:33 |
| Log | ./subidas.log | LOG_FILE · subir.php:42 |
| Longitud slug cliente | 100 caracteres | slugCliente() |
| Longitud nombre fichero | 150 caracteres | nombreFicheroSeguro() |
| Cuerpo HTTP máximo | 32 MB | post_max_size · .htaccess |
Orden de validación
El endpoint corta en el primer fallo, en este orden:
1. Método POST ................... 405 2. Cabecera Authorization ........ 401 3. Token válido (hash_equals) .... 403 4. JSON parseable ................ 400 5. Los tres campos presentes ..... 400 6. Base64 decodificable .......... 400 7. Tamaño <= 20 MB ............... 413 8. Firma %PDF- ................... 415 9. Nombres saneables ............. 400 10. mkdir carpeta cliente ......... 500 11. realpath dentro de la base .... 400 12. No existe ya el fichero ....... 409 13. Escritura + rename atómico .... 500 ↓ 201 Created
Seguridad
| Medida | Implementación |
|---|---|
| Timing attack | hash_equals() en la comparación del token |
| Path traversal | basename() + whitelist [A-Za-z0-9._-] + verificación con realpath() |
| Ficheros ocultos | ltrim($nombre, '.') impide crear .htaccess y similares |
| Byte nulo | Se eliminan los \0 antes de tocar el sistema de ficheros |
| Tipo de contenido | Verificación de la firma %PDF-, no de la extensión |
| Escritura parcial | Temporal + rename() atómico; se limpia el temporal si algo falla |
| Auditoría | subidas.log registra subidas correctas y fallos de autenticación con IP |
Pendiente de decidir: tal como está desplegado, la carpeta documentos/ cuelga del directorio web. Cualquiera que adivine https://deca.infonetconsultores.com/documentos/cliente/fichero.pdf puede descargar el PDF sin token.
Si los albaranes son confidenciales: mueve BASE_UPLOAD_DIR fuera del docroot (p. ej. dirname(__DIR__).'/documentos_privados') y sirve las descargas desde un PHP que valide el Bearer. Lo mismo aplica a subidas.log.
Conversor PDF → Base64
El fichero no sale de tu navegador: la conversión es local con FileReader. Útil para preparar el cuerpo JSON de Postman.
Probar el endpoint
Lanza una petición real contra subir.php. El token no se guarda en ningún sitio: vive solo en este campo mientras la pestaña esté abierta.