Hace algún tiempo, un cliente me reportó que estaba teniendo un problema a la hora de subir una imagen para una publicación del blog de su web. Dicha web está desarrollada haciendo uso de OctoberCMS.
Tras llevar a cabo una exploración inicial, me di cuenta de que el problema afectaba a todos los widgets de tipo fileupload. Esta circunstáncia, que se producía de un día para otro sin que la parte del código que se encarga de gestionar las subidas de ficheros hubiera sido modificada y el hecho de que el servidor estuviera respondiendo con un código de estado HTTP 400, me indujeron a intuir que realmente el problema se encontraba más en el lado del servidor (a nivel de sistema operativo) que en el código fuente de la web en si.
Por lo que decidí realizar una depuración más exhaustiva, tomando como punto de partida la subida de un archivo en el widget e iterando sobre cada paso del proceso resultante.
Para poner un poco en contexto, cabe decir que a grandes rasgos, cuando se sube un attachment desde el CMS, el proceso pasa por las siguientes etapas:
- Recopilación de datos del fichero recibido desde la petición POST (nombre, tamaño, extensión, contenido...) [Es OK]
- Copia del contenido del archivo recibido al directorio /tmp del servidor, con el fin de que este persista mientras se culmina el proceso [Es OK]
- Validación de datos y construcción de la ruta de destino (donde irá el archivo). [Es OK]
- Movida del archivo ubicado en el directorio /tmp a la ruta de destino (en el caso que nos ocupa, se trata de /var/www/html/storage/app/uploads/...). Si esta no existe, el CMS la intenta crear. [AQUÍ FALLA]
Tal y como indico, el fallo se produce en la última etapa. El CMS genera la ruta de destino, en base a lo que determina su algoritmo. Por ejemplo, para el fichero con el que realizé la prueba, me generó la siguiente:
/var/www/html/storage/app/uploads/public/61a/743/7a3/
Tras generar la ruta de destino, el CMS procede a comprobar si la ruta generada se trata de un directorio válido. Si dicha comprobación no resulta exitosa (por que el directorio y los que le preceden todavía no existe), procede a intentar crearlo de manera recursiva (en caso de que tampoco exista alguno de los directorios que preceden al de destino, comportamiento idéntico al del comando de UNIX mkdir -p
).
...
/*
* Verify the directory exists, if not try to create it. If creation fails
* because the directory was created by a concurrent process then proceed,
* otherwise trigger the error.
*/
if (
!FileHelper::isDirectory($destinationPath) &&
!FileHelper::makeDirectory($destinationPath, 0777, true, true) &&
!FileHelper::isDirectory($destinationPath)
) {
if (($lastErr = error_get_last()) !== null) {
trigger_error($lastErr['message'], E_USER_WARNING);
}
}
return FileHelper::copy($sourcePath, $destinationPath . $destinationFileName);
...
En nuestro caso, dicha condición no se cumplía, por lo cual, tanto el método isDirectory()
como el método makeDirectory()
estaban devolviendo false
.
Así que procedamos a analizar más a fondo como OctoberCMS trata de crear este directorio y por qué esta fallando. Para ello, el CMS se sirve de una función nativa de PHP llamada mkdir() (que emula al comando mkdir
de UNIX).
public function makeDirectory(
string $path,
int $mode = 0755,
bool $recursive = false,
bool $force = false
) : bool {
if ($force) {
return @mkdir($path, $mode, $recursive);
}
return mkdir($path, $mode, $recursive);
}
En el caso que nos ocupa, dicha función está devolviendo el siguiente error:
mkdir(): Permission denied.
Nota: al estar el parámetro $force
establecido en true
, se están silenciando los errores de la función mkdir()
con el operador @
, lo cual provoca que el procedimiento error_get_last()
no devuelva ningún error (es por ello, que la respuesta del servidor viene completamente vacía). Tuve que establcer el parametro $force
a false
para poder continuar depurando.
El error obtenido significa que el usuario asociado al proceso (en este caso, apache), carece de permisos suficientes en el sistema de archivos para realizar la operación requerida, en este caso, de escritura.
Con toda esta información, y la causa potencial bastante acotada, procedí a conectarme al servidor mediante SSH para comprobar que permisos / usuarios y grupos propietarios tiene el directorio sobre el cual se estaba tratando de operar, en nuestro caso /var/www/html/storage/uploads/public.
Me encontré con que la mayoría estaban configurados de manera correcta, con el usuario y grupo apache como propietarios, y la palabra de permisos 775.
Pero en algunos casos parecía ser que el propietario era el usuario root cuando debía ser el usuario apache. Y ahí es donde residía el problema.
La ruta de destino construída para el fichero de prueba que estaba tratando de subir, contenía el directorio 61a (ya existente). Este directorio era uno de los que tenían a root como propietario, lo cual ya impedía que apache operase sobre él.
Prosiguiendo con el análisis de la captura, me llamó la atención que estos directorios que misteriosamente se estaban creando con (más bien, siendo creados por) el usuario root, lo hicieran a las 02:00 horas de la madrugada, es decir, la hora en la que se ejecuta una tarea programada relativa a la importación y sincronización periódica de ciertos datos que aparecen en la web. Uno de los procesos definidos por este algoritmo de importación es el de volcar una serie de fotografías proporcionadas por cierto webservice externo, en caso de que estas no existan previamente. Dichos recursos son subidos al servidor y asociados a modelos de datos como attachments, por lo que se rigen por exactamente el mismo proceso que el problema que nos ocupa.
Por tanto, se puede deducir que el comando definido en dicha tarea programada estaba siendo ejecutado por el usuario root, en lugar de por el usuario apache, afectando negativamente a los permisos de la estructura de directorios vista hasta ahora.
Por lo que propuse la siguiente solución al departamento de sistemas encargado de gestionar el sistema operativo del servidor:
- Ejecutar el comando chown -R sobre /var/www/html/storage/uploads/public, para así establecer el usuario apache como propietario en todos los directorios que no lo tengan.
- Comprobar el crontab que define la tarea programada a ejecutar cada dia al as 02:00 horas de la madrugada e indicarle que la ejecute con el usuario apache. Proporciono un hilo del foro serverfault que aborda el tema cron - crontab running as a specific user - Server Fault
Dicha propuesta fue un éxito, ya que logró solventar el problema de manera permanente.
¡Punto para Wifft! =D