Servidores HTTP de nodo 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 navegador normal usa...

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, hay miles de otras razones por las que necesitaría servir archivos estáticos, como para descargar música o datos científicos. De cualquier manera, deberá encontrar una manera simple de permitir que el usuario descargue estos archivos de su servidor.

Una forma sencilla de hacer esto es crear un servidor HTTP de nodo. Como probablemente sepa, Node.js se destaca 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 el módulo base http que se envía con Node, o puede usar el popular servir-estático , que proporciona muchas características comunes de un servidor de archivos estático.

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 el contenido de la página. Sin embargo, el usuario no debería poder especificar solo cualquier ruta en nuestro servidor, de lo contrario, un usuario malintencionado podría intentar aprovechar 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 archivo /etc/shadow. 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 tu propio

Esta sección está pensada para 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 solo 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 módulo serve-static.

Si bien crear su propio servidor desde el módulo http requiere un poco de trabajo, puede ser muy gratificante mostrarle cómo funcionan los servidores por debajo, lo que incluye compensaciones por rendimiento y seguridad que deben tenerse en cuenta. Aunque, es bastante fácil crear su propio servidor estático de Nodo personalizado usando solo el módulo http incorporado, por lo que no tenemos que profundizar demasiado en las partes internas de un servidor HTTP.

Obviamente, el módulo http no será tan fácil de usar como algo como Express, pero es un excelente 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"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 raíz como localhost:8080/some/url/path obtendrás la misma respuesta. Entonces, cada solicitud recibida por el servidor es manejada por la función staticServe, 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 mediante la ruta URL. Probablemente sería una mala idea dejar que el usuario especifique una ruta absoluta en nuestro sistema por varias razones:

  • El servidor no debe revelar detalles del sistema operativo subyacente
  • El usuario debe estar limitado 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 usar las funciones .resolve() y .join() del módulo sendero de Node:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
"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, staticBasePath, y la URL dada, que luego imprimimos al usuario.

Ahora, si navega a la misma URL localhost:8080/some/url/path, debería ver el siguiente texto impreso en el navegador:

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

Tenga en cuenta que la ruta de su archivo probablemente sea diferente a la mía, según 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).

Al eliminar '.' y '..' de req.url, y luego usar los métodos .resolve(), .normalize() y .join(), podemos\ Podemos restringir al usuario para que solo acceda a los archivos dentro del directorio ./static. Incluso si intenta referirse a un directorio principal usando .., no podrá acceder a ningún directorio principal fuera de 'static', por lo que nuestros otros datos están seguros.

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

Ahora que hemos restringido la ruta para que solo devuelva archivos en el directorio dado, podemos comenzar a entregar los archivos reales. Para hacer esto, simplemente usaremos el método fs.readFile() 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 método res.write(content), como hicimos con la ruta del archivo anteriormente. Si no podemos encontrar el archivo solicitado, devolveremos un error 404.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"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. Echaremos un breve vistazo a algunos de estos en las próximas subsecciones.

Almacenamiento en caché

La técnica de almacenamiento en caché más simple es usar el almacenamiento en caché ilimitado en memoria. Este es un buen punto de partida, pero no debe usarse en producción (no siempre se puede almacenar en caché todo en la memoria). 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, devolvemos el contenido guardado; de lo contrario, abrimos el archivo como antes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"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 simplemente dejar que el caché no esté limitado, de lo contrario, podemos ocupar toda la memoria del sistema. Un mejor enfoque sería usar un algoritmo de caché más inteligente, como lru-caché, que implementa [menos usados ​​recientemente](https://en .wikipedia.org/wiki/Cache_algorithms#Examples) concepto de caché. De esta forma, si un archivo no se solicita durante un tiempo, se elimina de la memoria caché y se conserva la memoria.

Corrientes

Otra gran mejora que podemos hacer es cargar el contenido del archivo usando arroyos en lugar 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 un flujo, por otro lado, podemos enviar el contenido del archivo al usuario mientras se carga desde el disco, byte por byte. Dado que 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 necesitamos para cargar todo el archivo a la vez.

El uso de un enfoque sin transmisión 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 secuencias, 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 proporciona un ejemplo simple que implementa la transmisión:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"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 al evento data de la transmisión y guardar gradualmente los fragmentos en el caché. Te dejaré esto a ti para que lo implementes por tu cuenta :)

servicio estático {#servicio 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 uno propio 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 servir -estático también funciona muy bien.

Lo bueno de este módulo, en mi opinión, es que también se puede usar como software intermedio 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, generalmente necesitan servir contenido dinámico junto con sus archivos estáticos.

Primero, si desea usarlo como un servidor independiente, puede usar el módulo http con serve-static y manejador final en solo unas lineas como esta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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:

1
2
3
4
5
6
7
8
9
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 nodo estático y [servidor http](https://www.npmjs.com/package /servidor-http). Simplemente no los usé aquí ya que serve-static se usa mucho más y, por lo tanto, probablemente sea 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!