Servidores HTTP de node para el servicio de archivos estáticos

    Uno de los usos más fundamentales de un servidor HTTP es servir archivos estáticos al navegador de un usuario, como CSS, JavaScript o archivos de imagen. Más allá del uso normal del navegador, existen miles de otras razones por las que necesitaría entregar archivos estáticos, como para descargar música o datos científicos. De cualquier manera, deberá encontrar una forma sencilla de permitir que el usuario descargue estos archivos de su servidor.

    Una forma sencilla de hacer esto es crear un servidor HTTP de node. Como probablemente sepa, Node.js sobresale en el manejo de tareas intensivas de E / S, lo que lo convierte en una opción natural aquí. Puede optar por crear su propio servidor HTTP simple desde la base http módulo que se envía con Node, o puede usar el popular servir-estático paquete, que proporciona muchas características comunes de un servidor de archivos estáticos.

    El objetivo final de nuestro servidor estático es permitir que el usuario especifique una ruta de archivo en la URL y que ese archivo se devuelva como contenido de la página. Sin embargo, el usuario no debería poder especificar cualquier ruta en nuestro servidor, de lo contrario, un usuario malintencionado podría intentar aprovecharse de un sistema mal configurado y robar información confidencial. Un ataque simple podría verse así: localhost:8080/etc/shadow. Aquí el atacante estaría solicitando el /etc/shadow archivo. Para evitar este tipo de ataques, deberíamos poder decirle al servidor que solo permita al usuario descargar ciertos archivos, o solo archivos de ciertos directorios (como /var/www/my-website/public).

    Creando el tuyo

    Esta sección está destinada a aquellos de ustedes que necesitan una opción más personalizada o para aquellos que desean aprender cómo funcionan los servidores estáticos (o simplemente los servidores en general). Si tiene un caso de uso bastante común, será mejor que pase a la siguiente sección y comience a trabajar directamente con el serve-static módulo.

    Mientras crea su propio servidor desde el http El módulo requiere un poco de trabajo, puede ser muy gratificante mostrarle cómo funcionan los servidores debajo, lo que incluye compensaciones por problemas de rendimiento y seguridad que deben tenerse en cuenta. Aunque, es bastante fácil crear su propio servidor estático de node personalizado utilizando solo el http módulo, por lo que no tenemos que profundizar demasiado en los aspectos internos de un servidor HTTP.

    Obviamente el http El módulo no será tan fácil de usar como algo como Express, pero es un gran punto de partida como servidor HTTP. Aquí le mostraré cómo crear un servidor HTTP estático simple, que luego puede agregar y personalizar a su gusto.

    Comencemos simplemente inicializando y ejecutando nuestro servidor HTTP:

    "use strict";
    
    var http = require('http');
    
    var staticServe = function(req, res) {
        res.statusCode = 200;
        res.write('ok');
        return res.end();
    };
    
    var httpServer = http.createServer(staticServe);
    
    httpServer.listen(8080);
    

    Si ejecuta este código y navega a localhost:8080 en su navegador, todo lo que verá es “ok” en la pantalla. Este código maneja todas las solicitudes a la dirección localhost:8080. Incluso para rutas no root como localhost:8080/some/url/path seguirá recibiendo la misma respuesta. Así que cada solicitud recibida por el servidor es manejada por el staticServe función, que es donde estará la mayor parte de la lógica de nuestro servidor estático.

    El siguiente paso es obtener una ruta de archivo del usuario, que adquirimos utilizando la ruta URL. Probablemente sería una mala idea dejar que el usuario especifique una ruta absoluta en nuestro sistema por algunas razones:

    • El servidor no debe revelar detalles del sistema operativo subyacente.
    • El usuario debe tener limitaciones en los archivos que puede descargar para que no pueda intentar acceder a archivos confidenciales, como /etc/shadow.
    • La URL no debería requerir partes redundantes de la ruta del archivo (como la raíz del directorio: /var/www/my-website/public/...)

    Dados estos requisitos, necesitamos especificar una ruta base para el servidor y luego usar la URL dada como una ruta relativa fuera de la base. Para lograr esto, podemos utilizar el .resolve() y .join() funciones de Node camino módulo:

    "use strict";
    
    var path = require('path');
    var http = require('http');
    
    var staticBasePath="./static";
    
    var staticServe = function(req, res) {
        var resolvedBase = path.resolve(staticBasePath);
        var safeSuffix = path.normalize(req.url).replace(/^(..[/\])+/, '');
        var fileLoc = path.join(resolvedBase, safeSuffix);
        
        res.statusCode = 200;
    
        res.write(fileLoc);
        return res.end();
    };
    
    var httpServer = http.createServer(staticServe);
    
    httpServer.listen(8080);
    

    Aquí construimos la ruta completa del archivo usando una ruta base, staticBasePathy la URL proporcionada, que luego le imprimimos al usuario.

    Ahora, si navegas hacia el mismo localhost:8080/some/url/path URL, debería ver el siguiente texto impreso en el navegador:

    /Users/scott/Projects/static-server/static/some/url/path
    

    Tenga en cuenta que la ruta de su archivo probablemente será diferente a la mía, dependiendo de su sistema operativo, nombre de usuario y ruta del proyecto. La conclusión más importante son los últimos directorios que se muestran (static/some/url/path).

    Mediante la eliminación ‘.’ y de req.url, y luego usando el .resolve(), .normalize()y .join() métodos, podemos restringir al usuario para que solo acceda a archivos dentro del ./static directorio. Incluso si intenta hacer referencia a un directorio principal usando .. no podrá acceder a ningún directorio principal fuera de “estático”, por lo que nuestros otros datos están seguros.

    Nota: Nuestro código de unión de rutas no se ha probado a fondo y debe considerarse inseguro sin las pruebas adecuadas por su cuenta.

    Ahora que hemos restringido la ruta para devolver solo archivos en el directorio dado, podemos comenzar a entregar los archivos reales. Para hacer esto, simplemente usaremos el fs.readFile() método para cargar el contenido del archivo.

    Para servir mejor los contenidos, todo lo que tenemos que hacer es enviar el archivo al usuario utilizando el res.write(content) método, al igual que hicimos con la ruta del archivo anteriormente. Si no podemos encontrar el archivo solicitado, devolveremos un error 404.

    "use strict";
    
    var fs = require('fs');
    var path = require('path');
    var http = require('http');
    
    var staticBasePath="./static";
    
    var staticServe = function(req, res) {
        var resolvedBase = path.resolve(staticBasePath);
        var safeSuffix = path.normalize(req.url).replace(/^(..[/\])+/, '');
        var fileLoc = path.join(resolvedBase, safeSuffix);
        
        fs.readFile(fileLoc, function(err, data) {
            if (err) {
                res.writeHead(404, 'Not Found');
                res.write('404: File Not Found!');
                return res.end();
            }
            
            res.statusCode = 200;
    
            res.write(data);
            return res.end();
        });
    };
    
    var httpServer = http.createServer(staticServe);
    
    httpServer.listen(8080);
    

    ¡Excelente! Ahora tenemos un servidor de archivos estático primitivo.

    Todavía hay bastantes mejoras que podemos hacer en este código, como el almacenamiento en caché de archivos, agregar más encabezados HTTP y controles de seguridad. Veremos brevemente algunos de ellos en las próximas subsecciones.

    Almacenamiento en caché

    La técnica de almacenamiento en caché más simple es simplemente usar almacenamiento en memoria caché ilimitado. Este es un buen punto de partida, pero no debe usarse en producción (no siempre se puede almacenar todo en la memoria caché). Todo lo que tenemos que hacer aquí es crear un objeto JavaScript simple para contener el contenido de los archivos que hemos cargado previamente. Luego, en las solicitudes de archivos posteriores, podemos verificar si el archivo ya se ha cargado utilizando la ruta del archivo como clave de búsqueda. Si existen datos en el objeto de caché para la clave dada, entonces devolvemos el contenido guardado; de lo contrario, abrimos el archivo como antes:

    "use strict";
    
    var fs = require('fs');
    var path = require('path');
    var http = require('http');
    
    var staticBasePath="./static";
    
    var cache = {};
    
    var staticServe = function(req, res) {
        var resolvedBase = path.resolve(staticBasePath);
        var safeSuffix = path.normalize(req.url).replace(/^(..[/\])+/, '');
        var fileLoc = path.join(resolvedBase, safeSuffix);
    
        // Check the cache first...
        if (cache[fileLoc] !== undefined) {
            res.statusCode = 200;
    
            res.write(cache[fileLoc]);
            return res.end();
        }
        
        // ...otherwise load the file
        fs.readFile(fileLoc, function(err, data) {
            if (err) {
                res.writeHead(404, 'Not Found');
                res.write('404: File Not Found!');
                return res.end();
            }
    
            // Save to the cache
            cache[fileLoc] = data;
            
            res.statusCode = 200;
    
            res.write(data);
            return res.end();
        });
    };
    
    var httpServer = http.createServer(staticServe);
    
    httpServer.listen(8080);
    

    Como mencioné, probablemente no deberíamos dejar que la caché sea ilimitada, de lo contrario, podríamos ocupar toda la memoria del sistema. Un mejor enfoque sería utilizar un algoritmo de caché más inteligente, como lru-cache, que implementa el menos usado recientemente concepto de caché. De esta forma, si no se solicita un archivo durante un tiempo, se elimina de la caché y se conserva la memoria.

    Corrientes

    Otra gran mejora que podemos hacer es cargar el contenido del archivo usando arroyos en vez de fs.readFile(). El problema con fs.readFile() es que necesita cargar y almacenar en búfer todo el contenido del archivo antes de poder enviarlo al usuario.

    Usando una secuencia, por otro lado, podemos enviar el contenido del archivo al usuario mientras se carga desde el disco, byte a byte. Como no tenemos que esperar a que se cargue todo el archivo, esto reduce tanto el tiempo que se tarda en responder a la solicitud del usuario como la memoria que se necesita para manejar la solicitud, ya que no es necesario cargar todo el archivo a la vez. .

    Usando un enfoque sin flujo como fs.readFile() puede resultar especialmente costoso para nosotros si el usuario tiene una conexión lenta, lo que significaría que tendríamos que mantener el contenido del archivo en la memoria por más tiempo. Con las transmisiones, no tenemos este problema ya que los datos solo se cargan y envían desde el sistema de archivos tan rápido como la conexión del usuario puede aceptarlos. Este concepto se llama contrapresión.

    Aquí se ofrece un ejemplo sencillo que implementa la transmisión:

    "use strict";
    
    var fs = require('fs');
    var path = require('path');
    var http = require('http');
    
    var staticBasePath="./static";
    
    var cache = {};
    
    var staticServe = function(req, res) {
        var resolvedBase = path.resolve(staticBasePath);
        var safeSuffix = path.normalize(req.url).replace(/^(..[/\])+/, '');
        var fileLoc = path.join(resolvedBase, safeSuffix);
        
            var stream = fs.createReadStream(fileLoc);
    
            // Handle non-existent file
            stream.on('error', function(error) {
                res.writeHead(404, 'Not Found');
                res.write('404: File Not Found!');
                res.end();
            });
    
            // File exists, stream it to user
            res.statusCode = 200;
            stream.pipe(res);
    };
    
    var httpServer = http.createServer(staticServe);
    
    httpServer.listen(8080);
    

    Tenga en cuenta que esto no agrega ningún almacenamiento en caché como mostramos anteriormente. Si desea incluirlo, todo lo que necesita hacer es agregar un oyente a la transmisión data evento y guardar incrementalmente los fragmentos en la caché. Dejaré esto en tus manos para que lo implementes por tu cuenta 🙂

    servir-estático

    Si necesita un servidor de archivos estático para uso en producción, hay algunas otras opciones que puede considerar en lugar de escribir el suyo desde cero. Nginx es una de las mejores opciones que existen, pero si su caso de uso requiere que use Node por cualquier motivo, o si tiene algo en contra de Nginx, entonces el servir-estático El módulo también funciona muy bien.

    En mi opinión, lo mejor de este módulo es que también se puede utilizar como middleware para el popular framework web Express. Este parece ser el caso de uso para la mayoría de las personas, ya que de todos modos, por lo general, necesitan entregar contenido dinámico junto con sus archivos estáticos.

    Primero, si desea usarlo como un servidor independiente, puede usar el http módulo con serve-static y finalhandler en unas pocas líneas como esta:

    var http = require('http');
    var finalhandler = require('finalhandler');
    var serveStatic = require('serve-static');
    
    var staticBasePath="./static";
    
    var serve = serveStatic(staticBasePath, {'index': false});
    
    var server = http.createServer(function(req, res){
        var done = finalhandler(req, res);
        serve(req, res, done);
    })
    
    server.listen(8080);
    

    De lo contrario, si está utilizando Express, todo lo que necesita hacer es agregarlo como middleware:

    var express = require('express')
    var serveStatic = require('serve-static')
    
    var staticBasePath="./static";
     
    var app = express()
     
    app.use(serveStatic(staticBasePath, {'index': false}))
    app.listen(8080)
    

    Conclusión

    En este artículo, he presentado algunas opciones para ejecutar un servidor de archivos estático con Node.js. Tenga en cuenta que todavía hay más opciones que las que he mencionado aquí.

    Por ejemplo, hay algunos otros módulos similares, como node-estático y servidor http. Simplemente no los usé aquí desde serve-static es mucho más utilizado y, por tanto, probablemente más estable. Solo sepa que hay otras opciones que vale la pena revisar.

    Si tiene otras mejoras para hacer que los servidores de archivos estáticos sean más rápidos, ¡no dude en publicarlas en los comentarios!

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *