Hola comunidad. Luego de una espera interminable y de innumerables obstáculos para tratar de sacar al aire el blog BlackHat, al final me he dado por vencido y me he incorporado a apoyar este magnifico blog, espero mis entradas sean del agrado de todos.

Para mi primer artículo me gustaría compartir con ustedes un poco acerca de NodeJS, una tecnología que en estos momentos está bastante de moda y que se define a si misma como una biblioteca y entorno de ejecución de E/S dirigida por eventos y por lo tanto asíncrona que se ejecuta sobre el intérprete de JavaScript creado por Google V8, y para ello les explicaré como crear un servidor web, similar a Apache o a cualquier otro en sus funciones básicas.

NodeJS

Bueno, manos a la obra. Para ello es necesario preguntarnos qué características vamos a incorporar a nuestro servidor web. Por ser un servidor sencillo y de ejemplo solo voy a explicar como servir ficheros estáticos (HTML, JS, PNG, GIF, JPEG, etc), como satisfacer la solicitud del favicon y como lograr una configuración flexible del servidor.

Para comenzar creemos un archivo basicServer.js  con el siguiente código:

var http = require('http');
var url = require('url');

exports.createServer = function() {
  var htserver = http.createServer(function(req, res) {
    req.basicServer = {
      urlparsed: url.parse(req.url, true)
    };
    processHeaders(req, res);
    dispatchToContainer(htserver, req, res);
  });

  htserver.basicServer = {
    containers: []
  };

  htserver.addContainer = function(host, path, module, options) {
    if (lookupContainer(htserver, host, path) !== undefined) {
      throw new Error("Already mapped " + host + "/" + path);
    }
    htserver.basicServer.containers.push({
      host: host,
      path: path,
      module: module,
      options: options
    });
    return this;
  }

  htserver.useFavIcon = function(host, path) {
    return this.addContainer(host, "/favicon.ico", require('./faviconHandler'), {
      iconPath: path
    });
  }

  htserver.docroot = function(host, path, rootPath) {
    return this.addContainer(host, path, require('./staticHandler'), {
      docroot: rootPath
    });
  }
  return htserver;
}

var lookupContainer = function(htserver, host, path) {
  for (var i = 0; i < htserver.basicServer.containers.length; i++) {
    var container = htserver.basicServer.containers[i];
    var hostMatches = host.toLowerCase().match(container.host);
    var pathMatches = path.match(container.path);
    if (hostMatches !== null && pathMatches !== null) {
      return {
        container: container,
        host: hostMatches,
        path: pathMatches
      };
    }
  }
  return undefined;
}

Lo que tenemos acá es el núcleo de nuestro servidor, que básicamente crea y retorna un objeto HTTP Server, luego de añadirle algunas funcionalidades extras. La estrategia que se persigue es, primero, añadir información utilitaria al objeto request (en la función proccessHeaders que detallaré más adelante) y luego enviar la petición al manejador adecuado (en la función dispatchToContainer que estaremos viendo más adelante). La otra cosa que hacemos aquí es añadir tres funciones para gestionar una lista de contenedores, la primera agrega un contenedor al servidor (addContainer), la segunda se encarga de  configurar el Favicon (useFavIcon) y la última se utiliza para manejar los archivos estáticos (docroot).

Hay varias funciones que usamos antes, pero no hemos mirado todavía. La primera de ellas, lookupContainer, mira en la matriz contenedores buscando un contenedor que coincida con el el host y la ruta de la petición HTTP. Ésta es una exploración bastante sencilla en la que si se encuentra uno es devuelto o de lo contrario se devuelve indefinido.

Otra de las funciones de la que hemos hablado es la que procesa las cabeceras, proccessHeaders, la cual escanea el arreglo contenido en req.headers buscando encabezados de las cookies y de host, ya que ambos son útiles para el despacho de las peticiones. Esta función se llama en cada petición. Acá les dejo el código de esta función, el cual debe incorporarse al final del archivo basicServer.js creado al principio.

var processHeaders = function(req, res) {
  req.basicServer.cookies = [];
  var keys = Object.keys(req.headers);
  for (var i = 0, l = keys.length; i < l; i++) {
    var hname = keys[i];
    var hval = req.headers[hname];
    if (hname.toLowerCase() === "host") {
      req.basicServer.host = hval;
    }
    if (hname.toLowerCase() === "cookie") {
      req.basicServer.cookies.push(hval);
    }
  }
}

La última de las funciones es dispatchToContainer, la cual hace lo que su nombre indica, envía las peticiones al contenedor correspondiente si este existe, sino devuelve una página 404. Es en esta función donde el servidor llama al manejador que debe procesar la petición. Esta función, al igual que la anterior, se llama en cada petición. Al igual que en el caso anterior deben agregar el código de esta función al archivo basicServer.js.

var dispatchToContainer = function(htserver, req, res) {
  var container = lookupContainer(htserver, req.basicServer.host, req.basicServer.urlparsed.pathname);
  if (container !== undefined) {
    req.basicServer.hostMatches = container.host;
    req.basicServer.pathMatches = container.path;
    req.basicServer.container = container.container;
    container.container.module.handle(req, res);
  } else {
    res.writeHead(404, {
      'Content-Type': 'text/plain'
    });
    res.end("no handler found for " + req.host + "/" + req.urlparsed.path);
  }
}

Ahora pasemos a ver en detalle los dos controladores que utilizamos en la confección de nuestro servidor. El primero de ellos, faviconHandler.js, es el encargado de responder a las peticiones favicon, y se incorpora a nuestro servidor cuando se llama a la función useFavIcon, acá el código:

var fs = require('fs');
var mime = require('mime');
exports.handle = function(req, res) {
  if (req.method !== "GET") {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.end("invalid method " + req.method);
  } else if (req.basicServer.container.options.iconPath !== undefined) {
    fs.readFile(req.basicServer.container.options.iconPath,
    function(err, buf) {
      if (err) {
        res.writeHead(500, {'Content-Type': 'text/plain'});
        res.end(req.basicServer.container.options.iconPath + " not found");
      } else {
        res.writeHead(200, {
          'Content-Type': mime.lookup(req.basicServer.container.options.iconPath),
          'Content-Length': buf.length
        });
        res.end(buf);
      }
    });
  } else {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.end("no favicon");
  }
}

El módulo MIME se utiliza en este controlador para determinar el tipo del archivo de icono suministrado. Un favicon puede ser cualquier tipo de imagen y hay que informar al navegador web del tipo de imagen que se envía. El otro controlador es el encargado de responder cuando se solicitan ficheros estáticos, a este le llamaremos staticHandler.js y contiene el siguiente código:

var fs = require('fs');
var mime = require('mime');
var sys = require('sys');
exports.handle = function(req, res) {
  if (req.method !== "GET") {
    res.writeHead(404, {'Content-Type': 'text/plain'});
    res.end("invalid method " + req.method);
  } else {
    var fname = req.basicServer.container.options.docroot + req.basicServer.urlparsed.pathname;
    if (fname.match(/\/$/)) fname += "index.html";
    fs.stat(fname, function(err, stats) {
      if (err) {
        res.writeHead(500, {'Content-Type': 'text/plain'});
        res.end("file " + fname + " not found " + err);
      } else {
        fs.readFile(fname, function(err, buf) {
          if (err) {
            res.writeHead(500, {'Content-Type': 'text/plain'});
            res.end("file " + fname + " not readable " + err);
          } else {
            res.writeHead(200, {
              'Content-Type': mime.lookup(fname),
              'Content-Length': buf.length
            });
            res.end(buf);
          }
        });
      }
    });
  }
}

Acá también se utiliza el módulo MIME para determinar la cabecera Content-Type correcta. Se requiere MIME para que el navegador web puede interpretar los datos correctamente. Un caso especial es cuando la URL solicitada termina en /, entonces el manejador cambia la petición añadiendo index.html.

Bueno, luego de la confección de nuestro servidor web básico solo nos resta utilizarlo, para ello creemos un archivo server.js e incorporemos el siguiente código:

var port = 8888;
var server = require('./basicserver.js').createServer();
server.useFavIcon("localhost", "./www/favicon.png");
server.docroot("localhost", "/", "./www");
server.listen(port);

En este archivo se crea el servidor, se le configura la ruta desde donde debe cargar el favicon, y se le notifica cual es el directorio que debe publicar, en mi caso cree una carpeta en el directorio de mi servidor llamada www en la cual pondré los archivos estáticos que deseo publicar. Por último llamamos a la función listen del servidor y le pasamos el puerto de escucha. Solo nos resta levantar el servidor con el comando node server.js e ir en el navegador a la dirección http://localhost:8888/, para poder disfrutar de nuestro servidor en funcionamiento.

Al final de la entrada les dejo un comprimido con los archivos que hemos visto hoy. Espero les sea útil e interesante este artículo, en otras entradas seguiré adicionando funcionalidades al servidor para con ello seguir aprendiendo de NodeJS y sus potencialidades. Hasta la próxima !!!

 

BasicServer (291 descargas)