Преглед изворни кода

Massive rewrite - v1.0.0 (#7)

see release notes in the readme
Dave Eddy пре 5 година
родитељ
комит
586bfea910
9 измењених фајлова са 3285 додато и 293 уклоњено
  1. 2 0
      .gitignore
  2. 159 11
      README.md
  3. 571 276
      fs-caching-server.js
  4. 2067 0
      package-lock.json
  5. 10 6
      package.json
  6. 1 0
      smf/manifest.xml
  7. 449 0
      tests/all.js
  8. 10 0
      tests/dist-config.json
  9. 16 0
      tests/lib/index.js

+ 2 - 0
.gitignore

@@ -1,2 +1,4 @@
 node_modules
 cache
+tests/config.json
+tests/tmp

+ 159 - 11
README.md

@@ -6,24 +6,38 @@ A caching HTTP server/proxy that stores data on the local filesystem
 Installation
 ------------
 
-    [sudo] npm install -g fs-caching-server
+    [sudo] npm install [-g] fs-caching-server
 
-Description
------------
+`v1.0.0` Release Notes
+----------------------
+
+`v1.0.0` adds the following changes as well as bug fixes.
+
+- Fixes HEAD before GET caching - old behavior would cache 0-byte files
+- Handles redirects (or more accurately, doesn't handle - just proxies them)
+- Can retrieve from an HTTPS backend URL
+- Tests! there were none before - now a lot of tests have been added to ensure functionality
+- Can be used as a module (this added mostly for testing)
+- Cache dir can be specified as an argument/env variable - CWD not required anymore
+- Access logs now contain debug UUID if debug is specified
+
+CLI
+---
+
+### Description
 
 The `fs-caching-server` program installed can be used to spin up an HTTP server
 that acts a proxy to any other HTTP(s) server - with the added ability to
 cache GET and HEAD requests that match a given regex.
 
-Example
--------
+### Example
 
 This will create a caching proxy that fronts Joyent's pkgsrc servers
 
     $ mkdir cache
-    $ fs-caching-server -c cache/ -d -U http://pkgsrc.joyent.com
+    $ fs-caching-server -c cache/ -d -U https://pkgsrc.joyent.com
     listening on http://0.0.0.0:8080
-    proxying requests to http://pkgsrc.joyent.com
+    proxying requests to https://pkgsrc.joyent.com
     caching matches of /\.(png|jpg|jpeg|css|html|js|tar|tgz|tar\.gz)$/
     caching to /home/dave/dev/fs-caching-server/cache
 
@@ -49,23 +63,157 @@ and the local filesystem.  The second request shows the file was already
 present so it was streamed to the client without ever reaching out to
 pkgsrc.joyent.com.
 
-Usage
------
+### Usage
 
     $ fs-caching-server -h
     usage: fs-caching-server [options]
 
     options
-      -c, --cache-dir <dir>     directory to use for caching data, defaults to CWD
+      -c, --cache-dir <dir>     [env FS_CACHE_DIR] directory to use for caching data, defaults to CWD
       -d, --debug               enable debug logging to stderr
       -H, --host <host>         [env FS_CACHE_HOST] the host on which to listen, defaults to 0.0.0.0
       -h, --help                print this message and exit
       -p, --port <port>         [env FS_CACHE_PORT] the port on which to listen, defaults to 8080
-      -r, --regex <regex>       [env FS_CACHE_REGEX] regex to match to cache files, defaults to \.(png|jpg|jpeg|css|html|js|tar|tgz|tar\.gz)$
+      -r, --regex <regex>       [env FS_CACHE_REGEX] regex to match to cache files, defaults to /\.(png|jpg|jpeg|css|html|js|tar|tgz|tar\.gz)$/
       -U, --url <url>           [env FS_CACHE_URL] URL to proxy to
       -u, --updates             check npm for available updates
       -v, --version             print the version number and exit
 
+Module
+------
+
+### Description
+
+This module can also be used as a JavaScript module.
+
+### Example
+
+``` js
+var FsCachingServer = require('fs-caching-server').FsCachingServer;
+
+// proxy to Joyent's pkgsrc
+var opts = {
+    cacheDir: '/home/dave/cache-dir',
+    host: '0.0.0.0',
+    port: 8080,
+    backendUrl: 'https://pkgsrc.joyent.com'
+};
+
+var cachingServer = new FsCachingServer(opts);
+
+cachingServer.once('start', function () {
+    console.log('server started');
+});
+
+if (process.env.NODE_DEBUG) {
+    // debug messages go to stderr
+    cachingServer.on('log', console.error);
+}
+
+// access log messages to stdout
+cachingServer.on('access-log', console.log);
+
+cachingServer.start();
+```
+
+### Usage
+
+``` js
+/*
+ * FsCachingServer
+ *
+ * Create an instance of an FS Caching Server
+ *
+ * Aurguments
+ *  opts                  Object
+ *    opts.host           String (Required) Host to bind to. ex: '0.0.0.0',
+ *                                          '127.0.0.1', etc.
+ *    opts.port           Number (Required) Port to bind to. ex: 80, 8080, etc.
+ *    opts.backendUrl     String (Required) URL of the backend to proxy
+ *                                          requests to. ex:
+ *                                          'http://1.2.3.4:5678'
+ *    opts.cacheDir       String (Required) Directory for the cached items. ex:
+ *                                          '/tmp/fs-caching-server'
+ *    opts.regex          RegExp (Optional) Regex to match to enable caching,
+ *                                          defaults to REGEX above.
+ *    opts.noProxyHeaders Array  (Optional) An array of headers to not proxy to
+ *                                          the backend, default is [date,
+ *                                          server, host].
+ *    opts.cacheMethods   Array  (Optional) An array of methods to proxy,
+ *                                          default is [GET, HEAD].
+ *
+ * Methods
+ *
+ * .start()
+ *  - Start the server.
+ *
+ * .stop()
+ *  - Stop the server.
+ *
+ * .onIdle(cb)
+ *  - Call the callback when the caching server is "idle" (see events below).
+ *
+ * Events
+ *
+ * 'start'
+ *  - Called when the listener is started.
+ *
+ * 'stop'
+ *  - Called when the listener is stopped.
+ *
+ * 'access-log'
+ *  - Called per-request with a CLF-formatted apache log style string.
+ *
+ * 'log'
+ *  - Called with debug logs from the server - useful for debugging.
+ *
+ * 'idle'
+ *  - Called when the server is idle.  "idle" does not mean there are not
+ *  pending web requests, but instead means there are no pending filesystem
+ *  actions remaining.  This is useful for writing automated tests.
+ */
+ ```
+
+Testing
+-------
+
+```
+$ NODE_DEBUG=1 npm test
+
+
+> fs-caching-server@0.0.3 test /home/dave/dev/node-fs-caching-server
+> ./node_modules/tape/bin/tape tests/*.js
+
+TAP version 13
+# start cachingServer
+ok 1 tmp dir "/home/dave/dev/node-fs-caching-server/tests/tmp" cleared
+starting server
+listening on http://127.0.0.1:8081
+proxying requests to http://127.0.0.1:8080
+caching matches of /\.(png|jpg|jpeg|css|html|js|tar|tgz|tar\.gz)$/
+caching to /home/dave/dev/node-fs-caching-server/tests/tmp
+ok 2 cachingServer started
+# start backendServer
+ok 3 backendServer started on http://127.0.0.1:8080
+# simple cached request
+[63e44996-ac28-4a0f-b306-61aeeb88b53c] INCOMING REQUEST - GET /hello.png
+[63e44996-ac28-4a0f-b306-61aeeb88b53c] proxying GET to http://127.0.0.1:8080/hello.png
+[63e44996-ac28-4a0f-b306-61aeeb88b53c] saving local file to /home/dave/dev/node-fs-caching-server/tests/tmp/hello.png.in-progress
+[63e44996-ac28-4a0f-b306-61aeeb88b53c] 127.0.0.1 - - [13/Mar/2021:20:58:52 -0500] "GET /hello.png HTTP/1.1" 200 48 "-" "-"
+...
+...
+...
+ok 50 backendServer closed
+# stop cachingServer
+ok 51 cachingServer stopped
+
+1..51
+# tests 51
+# pass  51
+
+# ok
+```
+
 License
 -------
 

+ 571 - 276
fs-caching-server.js

@@ -7,323 +7,618 @@
  * License: MIT
  */
 
+var events = require('events');
 var fs = require('fs');
 var http = require('http');
+var https = require('https');
+var path = require('path');
 var url = require('url');
 var util = require('util');
 
 var accesslog = require('access-log');
-var getopt = require('posix-getopt');
+var assert = require('assert-plus');
 var mime = require('mime');
 var mkdirp = require('mkdirp');
-var path = require('path-platform');
-var uuid = require('node-uuid');
-var clone = require("readable-stream-clone");
+var uuid = require('uuid');
+var Clone = require('readable-stream-clone');
 
-var package = require('./package.json');
+/*
+ * default headers to ignore when proxying request (not copied to backend
+ * server).
+ */
+var NO_PROXY_HEADERS = ['date', 'server', 'host'];
+
+/*
+ * default methods that will be considered for caching - all others will be
+ * proxied directly.
+ */
+var CACHE_METHODS = ['GET', 'HEAD'];
 
+// default regex to match for caching.
+var REGEX = /\.(png|jpg|jpeg|css|html|js|tar|tgz|tar\.gz)$/;
+
+// safe hasOwnProperty
 function hap(o, p) {
-  return ({}).hasOwnProperty.call(o, p);
+    return ({}).hasOwnProperty.call(o, p);
 }
 
-// don't copy these headers when proxying request
-var NO_PROXY_HEADERS = ['date', 'server', 'host'];
+/*
+ * FsCachingServer
+ *
+ * Create an instance of an FS Caching Server
+ *
+ * Aurguments
+ *  opts                  Object
+ *    opts.host           String (Required) Host to bind to. ex: '0.0.0.0',
+ *                                          '127.0.0.1', etc.
+ *    opts.port           Number (Required) Port to bind to. ex: 80, 8080, etc.
+ *    opts.backendUrl     String (Required) URL of the backend to proxy
+ *                                          requests to. ex:
+ *                                          'http://1.2.3.4:5678'
+ *    opts.cacheDir       String (Required) Directory for the cached items. ex:
+ *                                          '/tmp/fs-caching-server'
+ *    opts.regex          RegExp (Optional) Regex to match to enable caching,
+ *                                          defaults to REGEX above.
+ *    opts.noProxyHeaders Array  (Optional) An array of headers to not proxy to
+ *                                          the backend, default is [date,
+ *                                          server, host].
+ *    opts.cacheMethods   Array  (Optional) An array of methods to proxy,
+ *                                          default is [GET, HEAD].
+ *
+ * Methods
+ *
+ * .start()
+ *  - Start the server.
+ *
+ * .stop()
+ *  - Stop the server.
+ *
+ * .onIdle(cb)
+ *  - Call the callback when the caching server is "idle" (see events below).
+ *
+ * Events
+ *
+ * 'start'
+ *  - Called when the listener is started.
+ *
+ * 'stop'
+ *  - Called when the listener is stopped.
+ *
+ * 'access-log'
+ *  - Called per-request with a CLF-formatted apache log style string.
+ *
+ * 'log'
+ *  - Called with debug logs from the server - useful for debugging.
+ *
+ * 'idle'
+ *  - Called when the server is idle.  "idle" does not mean there are not
+ *  pending web requests, but instead means there are no pending filesystem
+ *  actions remaining.  This is useful for writing automated tests.
+ */
+function FsCachingServer(opts) {
+    var self = this;
 
-// these methods use the cache, everything is proxied
-var CACHE_METHODS = ['GET', 'HEAD'];
+    assert.object(opts, 'opts');
+    assert.string(opts.host, 'opts.host');
+    assert.number(opts.port, 'opts.port');
+    assert.string(opts.backendUrl, 'opts.backendUrl');
+    assert.string(opts.cacheDir, 'opts.cacheDir');
+    assert.optionalRegexp(opts.regex, 'opts.regex');
+    assert.optionalArrayOfString(opts.noProxyHeaders, 'opts.noProxyHeaders');
+    assert.optionalArrayOfString(opts.cacheMethods, 'opts.cacheMethods');
 
-// command line arguments
-var opts = {
-  host: process.env.FS_CACHE_HOST || '0.0.0.0',
-  port: process.env.FS_CACHE_PORT || 8080,
-  url: process.env.FS_CACHE_URL,
-  regex: process.env.FS_CACHE_REGEX || '\\.(png|jpg|jpeg|css|html|js|tar|tgz|tar\\.gz)$',
-  debug: process.env.FS_CACHE_DEBUG,
-};
+    events.EventEmitter.call(self);
 
-var usage = [
-  'usage: fs-caching-server [options]',
-  '',
-  'options',
-  '  -c, --cache-dir <dir>     directory to use for caching data, defaults to CWD',
-  '  -d, --debug               enable debug logging to stderr',
-  '  -H, --host <host>         [env FS_CACHE_HOST] the host on which to listen, defaults to ' + opts.host,
-  '  -h, --help                print this message and exit',
-  '  -p, --port <port>         [env FS_CACHE_PORT] the port on which to listen, defaults to ' + opts.port,
-  '  -r, --regex <regex>       [env FS_CACHE_REGEX] regex to match to cache files, defaults to ' + opts.regex,
-  '  -U, --url <url>           [env FS_CACHE_URL] URL to proxy to',
-  '  -u, --updates             check npm for available updates',
-  '  -v, --version             print the version number and exit',
-].join('\n');
-
-var options = [
-  'c:(cache-dir)',
-  'd(debug)',
-  'H:(host)',
-  'h(help)',
-  'p:(port)',
-  'r:(regex)',
-  'U:(url)',
-  'u(updates)',
-  'v(version)'
-].join('');
-var parser = new getopt.BasicParser(options, process.argv);
-var option;
-while ((option = parser.getopt()) !== undefined) {
-  switch (option.option) {
-    case 'c': process.chdir(option.optarg); break;
-    case 'd': opts.debug = true; break;
-    case 'H': opts.host = option.optarg; break;
-    case 'h': console.log(usage); process.exit(0); break;
-    case 'p': opts.port = parseInt(option.optarg, 10); break;
-    case 'r': opts.regex = option.optarg; break;
-    case 'U': opts.url = option.optarg; break;
-    case 'u': // check for updates
-      require('latest').checkupdate(package, function(ret, msg) {
-        console.log(msg);
-        process.exit(ret);
-      });
-      return;
-    case 'v': console.log(package.version); process.exit(0); break;
-    default: console.error(usage); process.exit(1);
-  }
-}
+    self.host = opts.host;
+    self.port = opts.port;
+    self.backendUrl = opts.backendUrl;
+    self.cacheDir = opts.cacheDir;
+    self.regex = opts.regex || REGEX;
+    self.noProxyHeaders = opts.noProxyHeaders || NO_PROXY_HEADERS;
+    self.cacheMethods = opts.cacheMethods || CACHE_METHODS;
+    self.server = null;
+    self.idle = true;
+    self.backendHttps = !!self.backendUrl.match(/^https:/);
 
-if (!opts.url) {
-  console.error('url must be specified with `-U <url>` or as FS_CACHE_URL');
-  process.exit(1);
+    self._opts = opts;
 }
+util.inherits(FsCachingServer, events.EventEmitter);
 
+/*
+ * Start the server
+ *
+ * emits "listening" when the server starts
+ */
+FsCachingServer.prototype.start = function start() {
+    var self = this;
 
-// remove trailing slash
-opts.url = opts.url.replace(/\/*$/, '');
+    assert(!self.server, 'server already exists');
+    assert(!self.inProgress, 'requests in progress');
 
-// create the regex option - this may throw
-opts.regex = new RegExp(opts.regex);
+    self._log('starting server');
 
-// start the server
-http.createServer(onrequest).listen(opts.port, opts.host, listening);
+    self.server = http.createServer(onRequest);
+    self.server.listen(self.port, self.host, onListen);
+    self.inProgress = {};
+    self.idle = true;
 
-function listening() {
-  console.log('listening on http://%s:%d', opts.host, opts.port);
-  console.log('proxying requests to %s', opts.url);
-  console.log('caching matches of %s', opts.regex);
-  console.log('caching to %s', process.cwd());
-}
+    function onListen() {
+        self._log('listening on http://%s:%d', self.host, self.port);
+        self._log('proxying requests to %s', self.backendUrl);
+        self._log('caching matches of %s', self.regex);
+        self._log('caching to %s', self.cacheDir);
 
-// store files that are currently in progress -
-// if multiple requests are made for the same file, this will ensure that
-// only 1 connection is made to the server, and all subsequent requests will
-// be queued and then handled after the initial transfer is finished
-var inprogress = {};
-function onrequest(req, res) {
-  accesslog(req, res);
-
-  var _id = uuid.v4();
-  function log() {
-    if (!opts.debug)
-      return;
-    var s = util.format.apply(util, arguments);
-    return console.error('[%s] %s', _id, s);
-  }
-  log('INCOMING REQUEST - %s %s', req.method, req.url);
-
-  // parse the URL and determine the filename
-  var parsed = url.parse(req.url);
-  var file;
-  try {
-    file = '.' + path.posix.normalize(decodeURIComponent(parsed.pathname));
-  } catch (e) {
-    log('failed to parse pathname - sending 400 to client -', e.message);
-    res.statusCode = 400;
-    res.end();
-    return;
-  }
-
-  // If the request is not a HEAD or GET request, or if it does not match the
-  // regex supplied, we simply proxy it without a cache.
-  if (CACHE_METHODS.indexOf(req.method) < 0 || ! opts.regex.test(file)) {
-    log('request will be proxied with no caching');
-    var uristring = opts.url + parsed.path;
-    var uri = url.parse(uristring);
-    uri.method = req.method;
-    uri.headers = {};
-    Object.keys(req.headers || {}).forEach(function(header) {
-      if (NO_PROXY_HEADERS.indexOf(header) === -1)
-        uri.headers[header] = req.headers[header];
-    });
-    uri.headers.host = uri.host;
-    var oreq = http.request(uri, function(ores) {
-      res.statusCode = ores.statusCode;
-      Object.keys(ores.headers || {}).forEach(function(header) {
-        if (NO_PROXY_HEADERS.indexOf(header) === -1)
-          res.setHeader(header, ores.headers[header]);
-      });
-      ores.pipe(res);
-    });
-    oreq.on('error', function(e) {
-      res.statusCode = 500;
-      res.end();
-    });
-    req.pipe(oreq);
-    return;
-  }
-
-  // check to see if the file exists
-  fs.stat(file, function(err, stats) {
-    // directory, give up
-    if (stats && stats.isDirectory()) {
-      log('%s is a directory - sending 400 to client', file);
-      res.statusCode = 400;
-      res.end();
-      return;
+        self.emit('start');
     }
 
-    // file exists, stream it locally
-    if (stats) {
-      log('%s is a file (cached) - streaming to client', file);
-      streamfile(file, stats, req, res);
-      return;
+    function onRequest(req, res) {
+        self._onRequest(req, res);
     }
+};
+
+/*
+ * Stop the server
+ *
+ * emits "stop" when the server stops
+ */
+FsCachingServer.prototype.stop = function stop() {
+    var self = this;
+
+    assert(self.server, 'server does not exist');
+
+    self.server.once('close', function () {
+        self.idle = true;
+        self.server = null;
+        self.emit('stop');
+    });
+    self.server.close();
+};
 
-    // another request is already proxying for this file, we wait
-    if (hap(inprogress, file)) {
-      log('%s download in progress - response queued', file);
-      inprogress[file].push([req, res]);
-      return;
+/*
+ * A convience method for calling the given 'cb' when the server is idle.  The
+ * callback will be invoked immediately if the server is idle, or will be
+ * scheduled to run when the server becomes idle.
+ */
+FsCachingServer.prototype.onIdle = function onIdle(cb) {
+    var self = this;
+
+    assert.func(cb, 'cb');
+
+    if (self.idle) {
+        cb();
+    } else {
+        self.once('idle', cb);
+    }
+};
+
+/*
+ * Called internally when a new request is received
+ */
+FsCachingServer.prototype._onRequest = function _onRequest(req, res) {
+    var self = this;
+
+    var _id = uuid.v4();
+
+    function log() {
+        var s = util.format.apply(util, arguments);
+        self._log('[%s] %s', _id, s);
     }
 
-    // error with stat, proxy it
-    inprogress[file] = [];
-    var uristring = opts.url + parsed.path;
-    var uri = url.parse(uristring);
-    uri.method = req.method;
-    uri.headers = {};
-    Object.keys(req.headers || {}).forEach(function(header) {
-      if (NO_PROXY_HEADERS.indexOf(header) === -1)
-        uri.headers[header] = req.headers[header];
+    accesslog(req, res, undefined, function (s) {
+        self.emit('access-log', s);
+        log(s);
     });
-    uri.headers.host = uri.host;
-    log('proxying %s to %s', uri.method, uristring);
-
-    // proxy it
-    var oreq = http.request(uri, function(ores) {
-      res.statusCode = ores.statusCode;
-      Object.keys(ores.headers || {}).forEach(function(header) {
-        if (NO_PROXY_HEADERS.indexOf(header) === -1)
-          res.setHeader(header, ores.headers[header]);
-      });
-
-      if (res.statusCode !== 200) {
-        ores.pipe(res);
-        finish();
+
+    log('INCOMING REQUEST - %s %s', req.method, req.url);
+
+    // parse the URL and determine the filename
+    var parsed = url.parse(req.url);
+    var file;
+    try {
+        file = path.posix.normalize(decodeURIComponent(parsed.pathname));
+    } catch (e) {
+        log('failed to parse pathname - sending 400 to client -', e.message);
+        res.statusCode = 400;
+        res.end();
         return;
-      }
-
-      mkdirp(path.dirname(file), function(err) {
-        var tmp = file + '.in-progress';
-        log('saving local file to %s', tmp);
-        var ws = fs.createWriteStream(tmp);
-        ws.on('finish', function() {
-          fs.rename(tmp, file, function(err) {
-            if (err) {
-              log('failed to rename %s to %s', tmp, file);
-              finish();
-            } else {
-              log('renamed %s to %s', tmp, file);
-              finish(file, ores);
+    }
+
+    /*
+     * Any request that isn't in the list of methods to cache, or any request
+     * to a file that doesn't match the regex, gets proxied directly.
+     */
+    if (self.cacheMethods.indexOf(req.method) < 0 || ! self.regex.test(file)) {
+        proxyRequest();
+        return;
+    }
+
+    // make the filename relative to the cache dir
+    file = path.join(self.cacheDir, file);
+
+    // check to see if the file exists
+    fs.stat(file, function (err, stats) {
+        // directory, give up
+        if (stats && stats.isDirectory()) {
+            log('%s is a directory - sending 400 to client', file);
+            res.statusCode = 400;
+            res.end();
+            return;
+        }
+
+        // file exists, stream it locally
+        if (stats) {
+            log('%s is a file (cached) - streaming to client', file);
+            streamFile(file, stats, req, res);
+            return;
+        }
+
+        // another request is already proxying for this file, we wait
+        if (hap(self.inProgress, file)) {
+            log('%s download in progress - response queued', file);
+            self.inProgress[file].push({
+                id: _id,
+                req: req,
+                res: res,
+            });
+            return;
+        }
+
+        /*
+         * If we are here the file matches the caching requirements based on
+         * method and regex, and is also not found on the local filesystem.
+         *
+         * The final step before caching the request is to ensure it is *not* a
+         * HEAD requests.  HEAD requests should only ever be cached if the data
+         * was retrieved and cached first by another type of request.  In this
+         * specific case the HEAD request should just be proxied directly.
+         */
+        if (req.method === 'HEAD') {
+            proxyRequest();
+            return;
+        }
+
+        // error with stat, proxy it
+        self.inProgress[file] = [];
+        self.idle = false;
+
+        var uristring = self.backendUrl + parsed.path;
+        var uri = url.parse(uristring);
+        uri.method = req.method;
+        uri.headers = {};
+        Object.keys(req.headers || {}).forEach(function (header) {
+            if (self.noProxyHeaders.indexOf(header) === -1) {
+                uri.headers[header] = req.headers[header];
+            }
+        });
+        uri.headers.host = uri.host;
+
+        log('proxying %s to %s', uri.method, uristring);
+
+        // proxy it
+        var oreq = self._request(uri, function (ores) {
+            res.statusCode = ores.statusCode;
+
+            Object.keys(ores.headers || {}).forEach(function (header) {
+                if (self.noProxyHeaders.indexOf(header) === -1) {
+                    res.setHeader(header, ores.headers[header]);
+                }
+            });
+
+            if (res.statusCode < 200 || res.statusCode >= 300) {
+                //ores.pipe(res);
+                log('statusCode %d from backend not in 200 range - proxying ' +
+                    'back to caller', res.statusCode);
+                finish({
+                    statusCode: res.statusCode,
+                });
+                res.end();
+                return;
             }
-          });
+
+            mkdirp(path.dirname(file), function (err) {
+                var tmp = file + '.in-progress';
+
+                log('saving local file to %s', tmp);
+
+                var ws = fs.createWriteStream(tmp);
+
+                ws.once('finish', function () {
+                    fs.rename(tmp, file, function (err) {
+                        if (err) {
+                            log('failed to rename %s to %s', tmp, file);
+                            finish({
+                                statusCode: 500
+                            });
+                            return;
+                        }
+
+                        // everything worked! proxy all with success
+                        log('renamed %s to %s', tmp, file);
+                        finish({
+                            ores: ores
+                        });
+                    });
+                });
+
+                ws.once('error', function (e) {
+                    log('failed to save local file %s', e.message);
+                    ores.unpipe(ws);
+                    finish({
+                        statusCode: 500,
+                    });
+                });
+
+                var ores_ws = new Clone(ores);
+                var ores_res = new Clone(ores);
+                ores_ws.pipe(ws);
+                ores_res.pipe(res);
+            });
         });
-        ws.on('error', function(e) {
-          log('failed to save local file %s', e.message);
-          ores.unpipe(ws);
-          finish();
+
+        oreq.on('error', function (e) {
+            log('error with proxy request %s', e.message);
+            res.statusCode = 500;
+            res.end();
+            finish({
+                statusCode: 500
+            });
         });
-        ores_ws = new clone(ores);
-        ores_res = new clone(ores);
-        ores_ws.pipe(ws);
-        ores_res.pipe(res);
-      });
-    });
-    oreq.on('error', function(e) {
-      log('error with proxy request %s', e.message);
-      finish();
-      res.statusCode = 500;
-      res.end();
-    });
-    oreq.end();
-  });
-}
 
-// finish queued up requests
-function finish(file, ores) {
-  if (!file || !ores) {
-    inprogress[file].forEach(function(o) {
-      var res = o[1];
-      res.statusCode = 400;
-      res.end();
+        oreq.end();
     });
-    delete inprogress[file];
-    return;
-  }
-  fs.stat(file, function(err, stats) {
-    if (stats && stats.isDirectory()) {
-      // directory, give up
-      inprogress[file].forEach(function(o) {
-        var res = o[1];
-        res.statusCode = 400;
-        res.end();
-      });
-    } else if (stats) {
-      // file exists, stream it locally
-      inprogress[file].forEach(function(o) {
-        var req = o[0];
-        var res = o[1];
-        res.statusCode = ores.statusCode;
-        Object.keys(ores.headers || {}).forEach(function(header) {
-          if (NO_PROXY_HEADERS.indexOf(header) === -1)
-            res.setHeader(header, ores.headers[header]);
+
+    /*
+     * Proxy file directly with no caching
+     */
+    function proxyRequest() {
+        log('request will be proxied with no caching');
+
+        var uristring = self.backendUrl + parsed.path;
+        var uri = url.parse(uristring);
+        uri.method = req.method;
+        uri.headers = {};
+
+        Object.keys(req.headers || {}).forEach(function (header) {
+            if (self.noProxyHeaders.indexOf(header) === -1) {
+                uri.headers[header] = req.headers[header];
+            }
+        });
+
+        uri.headers.host = uri.host;
+
+        var oreq = self._request(uri, function (ores) {
+            res.statusCode = ores.statusCode;
+            Object.keys(ores.headers || {}).forEach(function (header) {
+                if (self.noProxyHeaders.indexOf(header) === -1) {
+                    res.setHeader(header, ores.headers[header]);
+                }
+            });
+            ores.pipe(res);
+        });
+
+        oreq.once('error', function (e) {
+            res.statusCode = 500;
+            res.end();
+        });
+
+        req.pipe(oreq);
+        return;
+    }
+
+    /*
+     * Process requests that may be blocked on the current file to be cached.
+     */
+    function finish(opts) {
+        assert.object(opts, 'opts');
+        assert.optionalNumber(opts.statusCode, 'opts.statusCode');
+        assert.optionalObject(opts.ores, 'opts.ores');
+
+        if (hap(opts, 'statusCode')) {
+            self.inProgress[file].forEach(function (o) {
+                o.res.statusCode = opts.statusCode;
+                o.res.end();
+            });
+
+            delete self.inProgress[file];
+            checkIdle();
+            return;
+        }
+
+        assert.object(opts.ores, 'opts.ores');
+        fs.stat(file, function (err, stats) {
+            if (stats && stats.isDirectory()) {
+                // directory, give up
+                self.inProgress[file].forEach(function (o) {
+                    o.res.statusCode = 400;
+                    o.res.end();
+                });
+            } else if (stats) {
+                // file exists, stream it locally
+                self.inProgress[file].forEach(function (o) {
+                    o.res.statusCode = opts.ores.statusCode;
+
+                    Object.keys(opts.ores.headers || {}).forEach(function (header) {
+                        if (self.noProxyHeaders.indexOf(header) === -1) {
+                            o.res.setHeader(header, opts.ores.headers[header]);
+                        }
+                    });
+
+                    streamFile(file, stats, o.req, o.res);
+                });
+            } else {
+                // not found
+                self.inProgress[file].forEach(function (o) {
+                    o.res.statusCode = 500;
+                    o.res.end();
+                });
+            }
+
+            delete self.inProgress[file];
+            checkIdle();
         });
-        streamfile(file, stats, req, res);
-      });
+    }
+
+    /*
+     * Check if the server is idle and emit an event if it is
+     */
+    function checkIdle() {
+        if (Object.keys(self.inProgress).length === 0) {
+            self.idle = true;
+            self.emit('idle');
+        }
+    }
+};
+
+/*
+ * Emit a "log" event with the given arguments (formatted via util.format)
+ */
+FsCachingServer.prototype._log = function _log() {
+    var self = this;
+
+    var s = util.format.apply(util, arguments);
+
+    self.emit('log', s);
+};
+
+/*
+ * Create an outgoing http/https request based on the backend URL
+ */
+FsCachingServer.prototype._request = function _request(uri, cb) {
+    var self = this;
+
+    if (self.backendHttps) {
+        return https.request(uri, cb);
     } else {
-      // not found
-      inprogress[file].forEach(function(o) {
-        var res = o[1];
-        res.statusCode = 500;
+        return http.request(uri, cb);
+    }
+};
+
+/*
+ * Given a filename and its stats object (and req and res)
+ * stream it to the caller.
+ */
+function streamFile(file, stats, req, res) {
+    var etag = util.format('"%d-%d"', stats.size, stats.mtime.getTime());
+
+    res.setHeader('Last-Modified', stats.mtime.toUTCString());
+    res.setHeader('Content-Type', mime.lookup(file));
+    res.setHeader('ETag', etag);
+
+    if (req.headers['if-none-match'] === etag) {
+        // etag matched, end the request
+        res.statusCode = 304;
         res.end();
-      });
+        return;
     }
-    delete inprogress[file];
-  });
+
+    res.setHeader('Content-Length', stats.size);
+    if (req.method === 'HEAD') {
+        res.end();
+        return;
+    }
+
+    var rs = fs.createReadStream(file);
+    rs.pipe(res);
+    rs.once('error', function (e) {
+        res.statusCode = e.code === 'ENOENT' ? 404 : 500;
+        res.end();
+    });
+    res.once('close', function () {
+        rs.destroy();
+    });
+}
+
+/*
+ * Main method (invoked from CLI)
+ */
+function main() {
+    var getopt = require('posix-getopt');
+
+    var package = require('./package.json');
+
+    // command line arguments
+    var opts = {
+        host: process.env.FS_CACHE_HOST || '0.0.0.0',
+        port: process.env.FS_CACHE_PORT || 8080,
+        backendUrl: process.env.FS_CACHE_URL,
+        cacheDir: process.env.FS_CACHE_DIR || process.cwd(),
+        regex: process.env.FS_CACHE_REGEX,
+    };
+    var debug = !!process.env.FS_CACHE_DEBUG;
+
+    var usage = [
+        'usage: fs-caching-server [options]',
+        '',
+        'options',
+        '  -c, --cache-dir <dir>     [env FS_CACHE_DIR] directory to use for caching data, defaults to CWD',
+        '  -d, --debug               enable debug logging to stderr',
+        '  -H, --host <host>         [env FS_CACHE_HOST] the host on which to listen, defaults to ' + opts.host,
+        '  -h, --help                print this message and exit',
+        '  -p, --port <port>         [env FS_CACHE_PORT] the port on which to listen, defaults to ' + opts.port,
+        '  -r, --regex <regex>       [env FS_CACHE_REGEX] regex to match to cache files, defaults to ' + REGEX,
+        '  -U, --url <url>           [env FS_CACHE_URL] URL to proxy to',
+        '  -u, --updates             check npm for available updates',
+        '  -v, --version             print the version number and exit',
+    ].join('\n');
+
+    var options = [
+        'c:(cache-dir)',
+        'd(debug)',
+        'H:(host)',
+        'h(help)',
+        'p:(port)',
+        'r:(regex)',
+        'U:(url)',
+        'u(updates)',
+        'v(version)'
+    ].join('');
+    var parser = new getopt.BasicParser(options, process.argv);
+    var option;
+    while ((option = parser.getopt()) !== undefined) {
+        switch (option.option) {
+        case 'c': opts.cacheDir = option.optarg; break;
+        case 'd': debug = true; break;
+        case 'H': opts.host = option.optarg; break;
+        case 'h': console.log(usage); process.exit(0); break;
+        case 'p': opts.port = parseInt(option.optarg, 10); break;
+        case 'r': opts.regex = option.optarg; break;
+        case 'U': opts.backendUrl = option.optarg; break;
+        case 'u': // check for updates
+            require('latest').checkupdate(package, function (ret, msg) {
+                console.log(msg);
+                process.exit(ret);
+            });
+            return;
+        case 'v': console.log(package.version); process.exit(0); break;
+        default: console.error(usage); process.exit(1);
+        }
+    }
+
+    if (!opts.backendUrl) {
+        console.error('url must be specified with `-U <url>` or as FS_CACHE_URL');
+        process.exit(1);
+    }
+
+    if (opts.regex) {
+        opts.regex = new RegExp(opts.regex);
+    }
+
+    // remove trailing slash
+    opts.backendUrl = opts.backendUrl.replace(/\/{0,}$/, '');
+
+    var fsCachingServer = new FsCachingServer(opts);
+
+    fsCachingServer.on('access-log', console.log);
+    if (debug) {
+        fsCachingServer.on('log', console.error);
+    }
+
+    fsCachingServer.start();
 }
 
-// given a filename and its stats object (and req and res)
-// stream it
-function streamfile(file, stats, req, res) {
-  var etag = util.format('"%d-%d"', stats.size, stats.mtime.getTime());
-  res.setHeader('Last-Modified', stats.mtime.toUTCString());
-  res.setHeader('Content-Type', mime.lookup(file));
-  res.setHeader('ETag', etag);
-  if (req.headers['if-none-match'] === etag) {
-    // etag matched, end the request
-    res.statusCode = 304;
-    res.end();
-    return;
-  }
-
-  res.setHeader('Content-Length', stats.size);
-  if (req.method === 'HEAD') {
-    res.end();
-    return;
-  }
-
-  var rs = fs.createReadStream(file);
-  rs.pipe(res);
-  rs.on('error', function(e) {
-    res.statusCode = e.code === 'ENOENT' ? 404 : 500;
-    res.end();
-  });
-  res.on('close', rs.destroy.bind(rs));
+if (require.main === module) {
+    main();
+} else {
+    module.exports = FsCachingServer;
+    module.exports.FsCachingServer = FsCachingServer;
 }

+ 2067 - 0
package-lock.json

@@ -0,0 +1,2067 @@
+{
+  "name": "fs-caching-server",
+  "version": "0.0.3",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "access-log": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/access-log/-/access-log-0.4.1.tgz",
+      "integrity": "sha1-6ISitdIqlUcn0lk+UWh6DQibYbg=",
+      "requires": {
+        "strftime": "0.6.2"
+      }
+    },
+    "array-filter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz",
+      "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=",
+      "dev": true
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+    },
+    "available-typed-arrays": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
+      "integrity": "sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==",
+      "dev": true,
+      "requires": {
+        "array-filter": "1.0.0"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "call-bind": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1",
+        "get-intrinsic": "1.1.1"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "deep-equal": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz",
+      "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "es-get-iterator": "1.1.2",
+        "get-intrinsic": "1.1.1",
+        "is-arguments": "1.1.0",
+        "is-date-object": "1.0.2",
+        "is-regex": "1.1.2",
+        "isarray": "2.0.5",
+        "object-is": "1.1.5",
+        "object-keys": "1.1.1",
+        "object.assign": "4.1.2",
+        "regexp.prototype.flags": "1.3.1",
+        "side-channel": "1.0.4",
+        "which-boxed-primitive": "1.0.2",
+        "which-collection": "1.0.1",
+        "which-typed-array": "1.1.4"
+      }
+    },
+    "define-properties": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "dev": true,
+      "requires": {
+        "object-keys": "1.1.1"
+      }
+    },
+    "defined": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+      "dev": true
+    },
+    "dotignore": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz",
+      "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==",
+      "dev": true,
+      "requires": {
+        "minimatch": "3.0.4"
+      }
+    },
+    "es-abstract": {
+      "version": "1.18.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
+      "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "es-to-primitive": "1.2.1",
+        "function-bind": "1.1.1",
+        "get-intrinsic": "1.1.1",
+        "has": "1.0.3",
+        "has-symbols": "1.0.2",
+        "is-callable": "1.2.3",
+        "is-negative-zero": "2.0.1",
+        "is-regex": "1.1.2",
+        "is-string": "1.0.5",
+        "object-inspect": "1.9.0",
+        "object-keys": "1.1.1",
+        "object.assign": "4.1.2",
+        "string.prototype.trimend": "1.0.4",
+        "string.prototype.trimstart": "1.0.4",
+        "unbox-primitive": "1.0.0"
+      }
+    },
+    "es-get-iterator": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz",
+      "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "get-intrinsic": "1.1.1",
+        "has-symbols": "1.0.2",
+        "is-arguments": "1.1.0",
+        "is-map": "2.0.2",
+        "is-set": "2.0.2",
+        "is-string": "1.0.5",
+        "isarray": "2.0.5"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+      "dev": true,
+      "requires": {
+        "is-callable": "1.2.3",
+        "is-date-object": "1.0.2",
+        "is-symbol": "1.0.3"
+      }
+    },
+    "for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "dev": true,
+      "requires": {
+        "is-callable": "1.2.3"
+      }
+    },
+    "foreach": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+      "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
+      "dev": true
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "get-intrinsic": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+      "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1",
+        "has": "1.0.3",
+        "has-symbols": "1.0.2"
+      }
+    },
+    "glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "1.0.0",
+        "inflight": "1.0.6",
+        "inherits": "2.0.4",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
+      }
+    },
+    "has": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1"
+      }
+    },
+    "has-bigints": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+      "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+      "dev": true
+    },
+    "has-symbols": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+      "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "is-arguments": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz",
+      "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2"
+      }
+    },
+    "is-bigint": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz",
+      "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==",
+      "dev": true
+    },
+    "is-boolean-object": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
+      "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2"
+      }
+    },
+    "is-callable": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
+      "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==",
+      "dev": true
+    },
+    "is-core-module": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
+      "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
+      "dev": true,
+      "requires": {
+        "has": "1.0.3"
+      }
+    },
+    "is-date-object": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
+      "dev": true
+    },
+    "is-map": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
+      "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+      "dev": true
+    },
+    "is-negative-zero": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+      "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+      "dev": true
+    },
+    "is-number-object": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
+      "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
+      "dev": true
+    },
+    "is-regex": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
+      "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "has-symbols": "1.0.2"
+      }
+    },
+    "is-set": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
+      "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+      "dev": true
+    },
+    "is-string": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
+      "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
+      "dev": true
+    },
+    "is-symbol": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
+      "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
+      "dev": true,
+      "requires": {
+        "has-symbols": "1.0.2"
+      }
+    },
+    "is-typed-array": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.5.tgz",
+      "integrity": "sha512-S+GRDgJlR3PyEbsX/Fobd9cqpZBuvUS+8asRqYDMLCb2qMzt1oz5m5oxQCxOgUDxiWsOVNi4yaF+/uvdlHlYug==",
+      "dev": true,
+      "requires": {
+        "available-typed-arrays": "1.0.2",
+        "call-bind": "1.0.2",
+        "es-abstract": "1.18.0",
+        "foreach": "2.0.5",
+        "has-symbols": "1.0.2"
+      }
+    },
+    "is-weakmap": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
+      "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+      "dev": true
+    },
+    "is-weakset": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz",
+      "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==",
+      "dev": true
+    },
+    "isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+      "dev": true
+    },
+    "latest": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/latest/-/latest-0.2.0.tgz",
+      "integrity": "sha1-6kfrj0srsM+RcW76qJbC4WI3WHs=",
+      "requires": {
+        "npm": "2.15.12"
+      }
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "1.1.11"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "requires": {
+        "minimist": "1.2.5"
+      }
+    },
+    "npm": {
+      "version": "2.15.12",
+      "resolved": "https://registry.npmjs.org/npm/-/npm-2.15.12.tgz",
+      "integrity": "sha1-33w+1aJ3w/nUtdgZsFMR0QogCuY=",
+      "requires": {
+        "abbrev": "1.0.9",
+        "ansi": "0.3.1",
+        "ansi-regex": "2.0.0",
+        "ansicolors": "0.3.2",
+        "ansistyles": "0.1.3",
+        "archy": "1.0.0",
+        "async-some": "1.0.2",
+        "block-stream": "0.0.9",
+        "char-spinner": "1.0.1",
+        "chmodr": "1.0.2",
+        "chownr": "1.0.1",
+        "cmd-shim": "2.0.2",
+        "columnify": "1.5.4",
+        "config-chain": "1.1.10",
+        "dezalgo": "1.0.3",
+        "editor": "1.0.0",
+        "fs-vacuum": "1.2.9",
+        "fs-write-stream-atomic": "1.0.8",
+        "fstream": "1.0.10",
+        "fstream-npm": "1.1.1",
+        "github-url-from-git": "1.4.0",
+        "github-url-from-username-repo": "1.0.2",
+        "glob": "7.0.6",
+        "graceful-fs": "4.1.6",
+        "hosted-git-info": "2.1.5",
+        "imurmurhash": "0.1.4",
+        "inflight": "1.0.5",
+        "inherits": "2.0.3",
+        "ini": "1.3.4",
+        "init-package-json": "1.9.4",
+        "lockfile": "1.0.1",
+        "lru-cache": "4.0.1",
+        "minimatch": "3.0.3",
+        "mkdirp": "0.5.1",
+        "node-gyp": "3.6.0",
+        "nopt": "3.0.6",
+        "normalize-git-url": "3.0.2",
+        "normalize-package-data": "2.3.5",
+        "npm-cache-filename": "1.0.2",
+        "npm-install-checks": "1.0.7",
+        "npm-package-arg": "4.1.0",
+        "npm-registry-client": "7.2.1",
+        "npm-user-validate": "0.1.5",
+        "npmlog": "2.0.4",
+        "once": "1.4.0",
+        "opener": "1.4.1",
+        "osenv": "0.1.3",
+        "path-is-inside": "1.0.1",
+        "read": "1.0.7",
+        "read-installed": "4.0.3",
+        "read-package-json": "2.0.4",
+        "readable-stream": "2.1.5",
+        "realize-package-specifier": "3.0.1",
+        "request": "2.74.0",
+        "retry": "0.10.0",
+        "rimraf": "2.5.4",
+        "semver": "5.1.0",
+        "sha": "2.0.1",
+        "slide": "1.1.6",
+        "sorted-object": "2.0.0",
+        "spdx-license-ids": "1.2.2",
+        "strip-ansi": "3.0.1",
+        "tar": "2.2.1",
+        "text-table": "0.2.0",
+        "uid-number": "0.0.6",
+        "umask": "1.1.0",
+        "validate-npm-package-license": "3.0.1",
+        "validate-npm-package-name": "2.2.2",
+        "which": "1.2.11",
+        "wrappy": "1.0.2",
+        "write-file-atomic": "1.1.4"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.0.9",
+          "bundled": true
+        },
+        "ansi": {
+          "version": "0.3.1",
+          "bundled": true
+        },
+        "ansi-regex": {
+          "version": "2.0.0",
+          "bundled": true
+        },
+        "ansicolors": {
+          "version": "0.3.2",
+          "bundled": true
+        },
+        "ansistyles": {
+          "version": "0.1.3",
+          "bundled": true
+        },
+        "archy": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "async-some": {
+          "version": "1.0.2",
+          "bundled": true,
+          "requires": {
+            "dezalgo": "1.0.3"
+          }
+        },
+        "block-stream": {
+          "version": "0.0.9",
+          "bundled": true,
+          "requires": {
+            "inherits": "2.0.3"
+          }
+        },
+        "char-spinner": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "chmodr": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "chownr": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "cmd-shim": {
+          "version": "2.0.2",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "mkdirp": "0.5.1"
+          }
+        },
+        "columnify": {
+          "version": "1.5.4",
+          "bundled": true,
+          "requires": {
+            "strip-ansi": "3.0.1",
+            "wcwidth": "1.0.0"
+          },
+          "dependencies": {
+            "wcwidth": {
+              "version": "1.0.0",
+              "bundled": true,
+              "requires": {
+                "defaults": "1.0.3"
+              },
+              "dependencies": {
+                "defaults": {
+                  "version": "1.0.3",
+                  "bundled": true,
+                  "requires": {
+                    "clone": "1.0.2"
+                  },
+                  "dependencies": {
+                    "clone": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "config-chain": {
+          "version": "1.1.10",
+          "bundled": true,
+          "requires": {
+            "ini": "1.3.4",
+            "proto-list": "1.2.4"
+          },
+          "dependencies": {
+            "proto-list": {
+              "version": "1.2.4",
+              "bundled": true
+            }
+          }
+        },
+        "dezalgo": {
+          "version": "1.0.3",
+          "bundled": true,
+          "requires": {
+            "asap": "2.0.3",
+            "wrappy": "1.0.2"
+          },
+          "dependencies": {
+            "asap": {
+              "version": "2.0.3",
+              "bundled": true
+            }
+          }
+        },
+        "editor": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "fs-vacuum": {
+          "version": "1.2.9",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "path-is-inside": "1.0.1",
+            "rimraf": "2.5.4"
+          }
+        },
+        "fs-write-stream-atomic": {
+          "version": "1.0.8",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "iferr": "0.1.5",
+            "imurmurhash": "0.1.4",
+            "readable-stream": "2.1.5"
+          },
+          "dependencies": {
+            "iferr": {
+              "version": "0.1.5",
+              "bundled": true
+            }
+          }
+        },
+        "fstream": {
+          "version": "1.0.10",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "inherits": "2.0.3",
+            "mkdirp": "0.5.1",
+            "rimraf": "2.5.4"
+          }
+        },
+        "fstream-npm": {
+          "version": "1.1.1",
+          "bundled": true,
+          "requires": {
+            "fstream-ignore": "1.0.5",
+            "inherits": "2.0.3"
+          },
+          "dependencies": {
+            "fstream-ignore": {
+              "version": "1.0.5",
+              "bundled": true,
+              "requires": {
+                "fstream": "1.0.10",
+                "inherits": "2.0.3",
+                "minimatch": "3.0.3"
+              }
+            }
+          }
+        },
+        "github-url-from-git": {
+          "version": "1.4.0",
+          "bundled": true
+        },
+        "github-url-from-username-repo": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "glob": {
+          "version": "7.0.6",
+          "bundled": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.5",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.3",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.0"
+          },
+          "dependencies": {
+            "fs.realpath": {
+              "version": "1.0.0",
+              "bundled": true
+            },
+            "path-is-absolute": {
+              "version": "1.0.0",
+              "bundled": true
+            }
+          }
+        },
+        "graceful-fs": {
+          "version": "4.1.6",
+          "bundled": true
+        },
+        "hosted-git-info": {
+          "version": "2.1.5",
+          "bundled": true
+        },
+        "imurmurhash": {
+          "version": "0.1.4",
+          "bundled": true
+        },
+        "inflight": {
+          "version": "1.0.5",
+          "bundled": true,
+          "requires": {
+            "once": "1.4.0",
+            "wrappy": "1.0.2"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true
+        },
+        "ini": {
+          "version": "1.3.4",
+          "bundled": true
+        },
+        "init-package-json": {
+          "version": "1.9.4",
+          "bundled": true,
+          "requires": {
+            "glob": "6.0.4",
+            "npm-package-arg": "4.1.0",
+            "promzard": "0.3.0",
+            "read": "1.0.7",
+            "read-package-json": "2.0.4",
+            "semver": "5.1.0",
+            "validate-npm-package-license": "3.0.1",
+            "validate-npm-package-name": "2.2.2"
+          },
+          "dependencies": {
+            "glob": {
+              "version": "6.0.4",
+              "bundled": true,
+              "requires": {
+                "inflight": "1.0.5",
+                "inherits": "2.0.3",
+                "minimatch": "3.0.3",
+                "once": "1.4.0",
+                "path-is-absolute": "1.0.0"
+              },
+              "dependencies": {
+                "path-is-absolute": {
+                  "version": "1.0.0",
+                  "bundled": true
+                }
+              }
+            },
+            "promzard": {
+              "version": "0.3.0",
+              "bundled": true,
+              "requires": {
+                "read": "1.0.7"
+              }
+            }
+          }
+        },
+        "lockfile": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "lru-cache": {
+          "version": "4.0.1",
+          "bundled": true,
+          "requires": {
+            "pseudomap": "1.0.2",
+            "yallist": "2.0.0"
+          },
+          "dependencies": {
+            "pseudomap": {
+              "version": "1.0.2",
+              "bundled": true
+            },
+            "yallist": {
+              "version": "2.0.0",
+              "bundled": true
+            }
+          }
+        },
+        "minimatch": {
+          "version": "3.0.3",
+          "bundled": true,
+          "requires": {
+            "brace-expansion": "1.1.6"
+          },
+          "dependencies": {
+            "brace-expansion": {
+              "version": "1.1.6",
+              "bundled": true,
+              "requires": {
+                "balanced-match": "0.4.2",
+                "concat-map": "0.0.1"
+              },
+              "dependencies": {
+                "balanced-match": {
+                  "version": "0.4.2",
+                  "bundled": true
+                },
+                "concat-map": {
+                  "version": "0.0.1",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "requires": {
+            "minimist": "0.0.8"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "0.0.8",
+              "bundled": true
+            }
+          }
+        },
+        "node-gyp": {
+          "version": "3.6.0",
+          "bundled": true,
+          "requires": {
+            "fstream": "1.0.10",
+            "glob": "7.0.6",
+            "graceful-fs": "4.1.6",
+            "minimatch": "3.0.3",
+            "mkdirp": "0.5.1",
+            "nopt": "3.0.6",
+            "npmlog": "2.0.4",
+            "osenv": "0.1.3",
+            "request": "2.74.0",
+            "rimraf": "2.5.4",
+            "semver": "5.3.0",
+            "tar": "2.2.1",
+            "which": "1.2.11"
+          },
+          "dependencies": {
+            "semver": {
+              "version": "5.3.0",
+              "bundled": true
+            }
+          }
+        },
+        "nopt": {
+          "version": "3.0.6",
+          "bundled": true,
+          "requires": {
+            "abbrev": "1.0.9"
+          }
+        },
+        "normalize-git-url": {
+          "version": "3.0.2",
+          "bundled": true
+        },
+        "normalize-package-data": {
+          "version": "2.3.5",
+          "bundled": true,
+          "requires": {
+            "hosted-git-info": "2.1.5",
+            "is-builtin-module": "1.0.0",
+            "semver": "5.1.0",
+            "validate-npm-package-license": "3.0.1"
+          },
+          "dependencies": {
+            "is-builtin-module": {
+              "version": "1.0.0",
+              "bundled": true,
+              "requires": {
+                "builtin-modules": "1.1.0"
+              },
+              "dependencies": {
+                "builtin-modules": {
+                  "version": "1.1.0",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "npm-cache-filename": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "npm-install-checks": {
+          "version": "1.0.7",
+          "bundled": true,
+          "requires": {
+            "npmlog": "2.0.4",
+            "semver": "5.1.0"
+          }
+        },
+        "npm-package-arg": {
+          "version": "4.1.0",
+          "bundled": true,
+          "requires": {
+            "hosted-git-info": "2.1.5",
+            "semver": "5.1.0"
+          }
+        },
+        "npm-registry-client": {
+          "version": "7.2.1",
+          "bundled": true,
+          "requires": {
+            "concat-stream": "1.5.2",
+            "graceful-fs": "4.1.6",
+            "normalize-package-data": "2.3.5",
+            "npm-package-arg": "4.1.0",
+            "npmlog": "2.0.4",
+            "once": "1.4.0",
+            "request": "2.74.0",
+            "retry": "0.10.0",
+            "semver": "5.1.0",
+            "slide": "1.1.6"
+          },
+          "dependencies": {
+            "concat-stream": {
+              "version": "1.5.2",
+              "bundled": true,
+              "requires": {
+                "inherits": "2.0.3",
+                "readable-stream": "2.0.6",
+                "typedarray": "0.0.6"
+              },
+              "dependencies": {
+                "readable-stream": {
+                  "version": "2.0.6",
+                  "bundled": true,
+                  "requires": {
+                    "core-util-is": "1.0.2",
+                    "inherits": "2.0.3",
+                    "isarray": "1.0.0",
+                    "process-nextick-args": "1.0.7",
+                    "string_decoder": "0.10.31",
+                    "util-deprecate": "1.0.2"
+                  },
+                  "dependencies": {
+                    "core-util-is": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    },
+                    "isarray": {
+                      "version": "1.0.0",
+                      "bundled": true
+                    },
+                    "process-nextick-args": {
+                      "version": "1.0.7",
+                      "bundled": true
+                    },
+                    "string_decoder": {
+                      "version": "0.10.31",
+                      "bundled": true
+                    },
+                    "util-deprecate": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    }
+                  }
+                },
+                "typedarray": {
+                  "version": "0.0.6",
+                  "bundled": true
+                }
+              }
+            },
+            "retry": {
+              "version": "0.10.0",
+              "bundled": true
+            }
+          }
+        },
+        "npm-user-validate": {
+          "version": "0.1.5",
+          "bundled": true
+        },
+        "npmlog": {
+          "version": "2.0.4",
+          "bundled": true,
+          "requires": {
+            "ansi": "0.3.1",
+            "are-we-there-yet": "1.1.2",
+            "gauge": "1.2.7"
+          },
+          "dependencies": {
+            "are-we-there-yet": {
+              "version": "1.1.2",
+              "bundled": true,
+              "requires": {
+                "delegates": "1.0.0",
+                "readable-stream": "2.1.5"
+              },
+              "dependencies": {
+                "delegates": {
+                  "version": "1.0.0",
+                  "bundled": true
+                }
+              }
+            },
+            "gauge": {
+              "version": "1.2.7",
+              "bundled": true,
+              "requires": {
+                "ansi": "0.3.1",
+                "has-unicode": "2.0.0",
+                "lodash.pad": "4.4.0",
+                "lodash.padend": "4.5.0",
+                "lodash.padstart": "4.5.0"
+              },
+              "dependencies": {
+                "has-unicode": {
+                  "version": "2.0.0",
+                  "bundled": true
+                },
+                "lodash._baseslice": {
+                  "version": "4.0.0",
+                  "bundled": true
+                },
+                "lodash._basetostring": {
+                  "version": "4.12.0",
+                  "bundled": true
+                },
+                "lodash.pad": {
+                  "version": "4.4.0",
+                  "bundled": true,
+                  "requires": {
+                    "lodash._baseslice": "4.0.0",
+                    "lodash._basetostring": "4.12.0",
+                    "lodash.tostring": "4.1.4"
+                  }
+                },
+                "lodash.padend": {
+                  "version": "4.5.0",
+                  "bundled": true,
+                  "requires": {
+                    "lodash._baseslice": "4.0.0",
+                    "lodash._basetostring": "4.12.0",
+                    "lodash.tostring": "4.1.4"
+                  }
+                },
+                "lodash.padstart": {
+                  "version": "4.5.0",
+                  "bundled": true,
+                  "requires": {
+                    "lodash._baseslice": "4.0.0",
+                    "lodash._basetostring": "4.12.0",
+                    "lodash.tostring": "4.1.4"
+                  }
+                },
+                "lodash.tostring": {
+                  "version": "4.1.4",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "requires": {
+            "wrappy": "1.0.2"
+          }
+        },
+        "opener": {
+          "version": "1.4.1",
+          "bundled": true
+        },
+        "osenv": {
+          "version": "0.1.3",
+          "bundled": true,
+          "requires": {
+            "os-homedir": "1.0.0",
+            "os-tmpdir": "1.0.1"
+          },
+          "dependencies": {
+            "os-homedir": {
+              "version": "1.0.0",
+              "bundled": true
+            },
+            "os-tmpdir": {
+              "version": "1.0.1",
+              "bundled": true
+            }
+          }
+        },
+        "path-is-inside": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "read": {
+          "version": "1.0.7",
+          "bundled": true,
+          "requires": {
+            "mute-stream": "0.0.5"
+          },
+          "dependencies": {
+            "mute-stream": {
+              "version": "0.0.5",
+              "bundled": true
+            }
+          }
+        },
+        "read-installed": {
+          "version": "4.0.3",
+          "bundled": true,
+          "requires": {
+            "debuglog": "1.0.1",
+            "graceful-fs": "4.1.6",
+            "read-package-json": "2.0.4",
+            "readdir-scoped-modules": "1.0.2",
+            "semver": "5.1.0",
+            "slide": "1.1.6",
+            "util-extend": "1.0.1"
+          },
+          "dependencies": {
+            "debuglog": {
+              "version": "1.0.1",
+              "bundled": true
+            },
+            "readdir-scoped-modules": {
+              "version": "1.0.2",
+              "bundled": true,
+              "requires": {
+                "debuglog": "1.0.1",
+                "dezalgo": "1.0.3",
+                "graceful-fs": "4.1.6",
+                "once": "1.4.0"
+              }
+            },
+            "util-extend": {
+              "version": "1.0.1",
+              "bundled": true
+            }
+          }
+        },
+        "read-package-json": {
+          "version": "2.0.4",
+          "bundled": true,
+          "requires": {
+            "glob": "6.0.4",
+            "graceful-fs": "4.1.6",
+            "json-parse-helpfulerror": "1.0.3",
+            "normalize-package-data": "2.3.5"
+          },
+          "dependencies": {
+            "glob": {
+              "version": "6.0.4",
+              "bundled": true,
+              "requires": {
+                "inflight": "1.0.5",
+                "inherits": "2.0.3",
+                "minimatch": "3.0.3",
+                "once": "1.4.0",
+                "path-is-absolute": "1.0.0"
+              },
+              "dependencies": {
+                "path-is-absolute": {
+                  "version": "1.0.0",
+                  "bundled": true
+                }
+              }
+            },
+            "json-parse-helpfulerror": {
+              "version": "1.0.3",
+              "bundled": true,
+              "requires": {
+                "jju": "1.3.0"
+              },
+              "dependencies": {
+                "jju": {
+                  "version": "1.3.0",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.1.5",
+          "bundled": true,
+          "requires": {
+            "buffer-shims": "1.0.0",
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "1.0.7",
+            "string_decoder": "0.10.31",
+            "util-deprecate": "1.0.2"
+          },
+          "dependencies": {
+            "buffer-shims": {
+              "version": "1.0.0",
+              "bundled": true
+            },
+            "core-util-is": {
+              "version": "1.0.2",
+              "bundled": true
+            },
+            "isarray": {
+              "version": "1.0.0",
+              "bundled": true
+            },
+            "process-nextick-args": {
+              "version": "1.0.7",
+              "bundled": true
+            },
+            "string_decoder": {
+              "version": "0.10.31",
+              "bundled": true
+            },
+            "util-deprecate": {
+              "version": "1.0.2",
+              "bundled": true
+            }
+          }
+        },
+        "realize-package-specifier": {
+          "version": "3.0.1",
+          "bundled": true,
+          "requires": {
+            "dezalgo": "1.0.3",
+            "npm-package-arg": "4.1.0"
+          }
+        },
+        "request": {
+          "version": "2.74.0",
+          "bundled": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.4.1",
+            "bl": "1.1.2",
+            "caseless": "0.11.0",
+            "combined-stream": "1.0.5",
+            "extend": "3.0.0",
+            "forever-agent": "0.6.1",
+            "form-data": "1.0.0-rc4",
+            "har-validator": "2.0.6",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.11",
+            "node-uuid": "1.4.7",
+            "oauth-sign": "0.8.2",
+            "qs": "6.2.1",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.1",
+            "tunnel-agent": "0.4.3"
+          },
+          "dependencies": {
+            "aws-sign2": {
+              "version": "0.6.0",
+              "bundled": true
+            },
+            "aws4": {
+              "version": "1.4.1",
+              "bundled": true
+            },
+            "bl": {
+              "version": "1.1.2",
+              "bundled": true,
+              "requires": {
+                "readable-stream": "2.0.6"
+              },
+              "dependencies": {
+                "readable-stream": {
+                  "version": "2.0.6",
+                  "bundled": true,
+                  "requires": {
+                    "core-util-is": "1.0.2",
+                    "inherits": "2.0.3",
+                    "isarray": "1.0.0",
+                    "process-nextick-args": "1.0.7",
+                    "string_decoder": "0.10.31",
+                    "util-deprecate": "1.0.2"
+                  },
+                  "dependencies": {
+                    "core-util-is": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    },
+                    "isarray": {
+                      "version": "1.0.0",
+                      "bundled": true
+                    },
+                    "process-nextick-args": {
+                      "version": "1.0.7",
+                      "bundled": true
+                    },
+                    "string_decoder": {
+                      "version": "0.10.31",
+                      "bundled": true
+                    },
+                    "util-deprecate": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    }
+                  }
+                }
+              }
+            },
+            "caseless": {
+              "version": "0.11.0",
+              "bundled": true
+            },
+            "combined-stream": {
+              "version": "1.0.5",
+              "bundled": true,
+              "requires": {
+                "delayed-stream": "1.0.0"
+              },
+              "dependencies": {
+                "delayed-stream": {
+                  "version": "1.0.0",
+                  "bundled": true
+                }
+              }
+            },
+            "extend": {
+              "version": "3.0.0",
+              "bundled": true
+            },
+            "forever-agent": {
+              "version": "0.6.1",
+              "bundled": true
+            },
+            "form-data": {
+              "version": "1.0.0-rc4",
+              "bundled": true,
+              "requires": {
+                "async": "1.5.2",
+                "combined-stream": "1.0.5",
+                "mime-types": "2.1.11"
+              },
+              "dependencies": {
+                "async": {
+                  "version": "1.5.2",
+                  "bundled": true
+                }
+              }
+            },
+            "har-validator": {
+              "version": "2.0.6",
+              "bundled": true,
+              "requires": {
+                "chalk": "1.1.3",
+                "commander": "2.9.0",
+                "is-my-json-valid": "2.13.1",
+                "pinkie-promise": "2.0.1"
+              },
+              "dependencies": {
+                "chalk": {
+                  "version": "1.1.3",
+                  "bundled": true,
+                  "requires": {
+                    "ansi-styles": "2.2.1",
+                    "escape-string-regexp": "1.0.5",
+                    "has-ansi": "2.0.0",
+                    "strip-ansi": "3.0.1",
+                    "supports-color": "2.0.0"
+                  },
+                  "dependencies": {
+                    "ansi-styles": {
+                      "version": "2.2.1",
+                      "bundled": true
+                    },
+                    "escape-string-regexp": {
+                      "version": "1.0.5",
+                      "bundled": true
+                    },
+                    "has-ansi": {
+                      "version": "2.0.0",
+                      "bundled": true,
+                      "requires": {
+                        "ansi-regex": "2.0.0"
+                      }
+                    },
+                    "supports-color": {
+                      "version": "2.0.0",
+                      "bundled": true
+                    }
+                  }
+                },
+                "commander": {
+                  "version": "2.9.0",
+                  "bundled": true,
+                  "requires": {
+                    "graceful-readlink": "1.0.1"
+                  },
+                  "dependencies": {
+                    "graceful-readlink": {
+                      "version": "1.0.1",
+                      "bundled": true
+                    }
+                  }
+                },
+                "is-my-json-valid": {
+                  "version": "2.13.1",
+                  "bundled": true,
+                  "requires": {
+                    "generate-function": "2.0.0",
+                    "generate-object-property": "1.2.0",
+                    "jsonpointer": "2.0.0",
+                    "xtend": "4.0.1"
+                  },
+                  "dependencies": {
+                    "generate-function": {
+                      "version": "2.0.0",
+                      "bundled": true
+                    },
+                    "generate-object-property": {
+                      "version": "1.2.0",
+                      "bundled": true,
+                      "requires": {
+                        "is-property": "1.0.2"
+                      },
+                      "dependencies": {
+                        "is-property": {
+                          "version": "1.0.2",
+                          "bundled": true
+                        }
+                      }
+                    },
+                    "jsonpointer": {
+                      "version": "2.0.0",
+                      "bundled": true
+                    },
+                    "xtend": {
+                      "version": "4.0.1",
+                      "bundled": true
+                    }
+                  }
+                },
+                "pinkie-promise": {
+                  "version": "2.0.1",
+                  "bundled": true,
+                  "requires": {
+                    "pinkie": "2.0.4"
+                  },
+                  "dependencies": {
+                    "pinkie": {
+                      "version": "2.0.4",
+                      "bundled": true
+                    }
+                  }
+                }
+              }
+            },
+            "hawk": {
+              "version": "3.1.3",
+              "bundled": true,
+              "requires": {
+                "boom": "2.10.1",
+                "cryptiles": "2.0.5",
+                "hoek": "2.16.3",
+                "sntp": "1.0.9"
+              },
+              "dependencies": {
+                "boom": {
+                  "version": "2.10.1",
+                  "bundled": true,
+                  "requires": {
+                    "hoek": "2.16.3"
+                  }
+                },
+                "cryptiles": {
+                  "version": "2.0.5",
+                  "bundled": true,
+                  "requires": {
+                    "boom": "2.10.1"
+                  }
+                },
+                "hoek": {
+                  "version": "2.16.3",
+                  "bundled": true
+                },
+                "sntp": {
+                  "version": "1.0.9",
+                  "bundled": true,
+                  "requires": {
+                    "hoek": "2.16.3"
+                  }
+                }
+              }
+            },
+            "http-signature": {
+              "version": "1.1.1",
+              "bundled": true,
+              "requires": {
+                "assert-plus": "0.2.0",
+                "jsprim": "1.3.0",
+                "sshpk": "1.9.2"
+              },
+              "dependencies": {
+                "assert-plus": {
+                  "version": "0.2.0",
+                  "bundled": true
+                },
+                "jsprim": {
+                  "version": "1.3.0",
+                  "bundled": true,
+                  "requires": {
+                    "extsprintf": "1.0.2",
+                    "json-schema": "0.2.2",
+                    "verror": "1.3.6"
+                  },
+                  "dependencies": {
+                    "extsprintf": {
+                      "version": "1.0.2",
+                      "bundled": true
+                    },
+                    "json-schema": {
+                      "version": "0.2.2",
+                      "bundled": true
+                    },
+                    "verror": {
+                      "version": "1.3.6",
+                      "bundled": true,
+                      "requires": {
+                        "extsprintf": "1.0.2"
+                      }
+                    }
+                  }
+                },
+                "sshpk": {
+                  "version": "1.9.2",
+                  "bundled": true,
+                  "requires": {
+                    "asn1": "0.2.3",
+                    "assert-plus": "1.0.0",
+                    "dashdash": "1.14.0",
+                    "ecc-jsbn": "0.1.1",
+                    "getpass": "0.1.6",
+                    "jodid25519": "1.0.2",
+                    "jsbn": "0.1.0",
+                    "tweetnacl": "0.13.3"
+                  },
+                  "dependencies": {
+                    "asn1": {
+                      "version": "0.2.3",
+                      "bundled": true
+                    },
+                    "assert-plus": {
+                      "version": "1.0.0",
+                      "bundled": true
+                    },
+                    "dashdash": {
+                      "version": "1.14.0",
+                      "bundled": true,
+                      "requires": {
+                        "assert-plus": "1.0.0"
+                      }
+                    },
+                    "ecc-jsbn": {
+                      "version": "0.1.1",
+                      "bundled": true,
+                      "optional": true,
+                      "requires": {
+                        "jsbn": "0.1.0"
+                      }
+                    },
+                    "getpass": {
+                      "version": "0.1.6",
+                      "bundled": true,
+                      "requires": {
+                        "assert-plus": "1.0.0"
+                      }
+                    },
+                    "jodid25519": {
+                      "version": "1.0.2",
+                      "bundled": true,
+                      "optional": true,
+                      "requires": {
+                        "jsbn": "0.1.0"
+                      }
+                    },
+                    "jsbn": {
+                      "version": "0.1.0",
+                      "bundled": true,
+                      "optional": true
+                    },
+                    "tweetnacl": {
+                      "version": "0.13.3",
+                      "bundled": true,
+                      "optional": true
+                    }
+                  }
+                }
+              }
+            },
+            "is-typedarray": {
+              "version": "1.0.0",
+              "bundled": true
+            },
+            "isstream": {
+              "version": "0.1.2",
+              "bundled": true
+            },
+            "json-stringify-safe": {
+              "version": "5.0.1",
+              "bundled": true
+            },
+            "mime-types": {
+              "version": "2.1.11",
+              "bundled": true,
+              "requires": {
+                "mime-db": "1.23.0"
+              },
+              "dependencies": {
+                "mime-db": {
+                  "version": "1.23.0",
+                  "bundled": true
+                }
+              }
+            },
+            "node-uuid": {
+              "version": "1.4.7",
+              "bundled": true
+            },
+            "oauth-sign": {
+              "version": "0.8.2",
+              "bundled": true
+            },
+            "qs": {
+              "version": "6.2.1",
+              "bundled": true
+            },
+            "stringstream": {
+              "version": "0.0.5",
+              "bundled": true
+            },
+            "tough-cookie": {
+              "version": "2.3.1",
+              "bundled": true
+            },
+            "tunnel-agent": {
+              "version": "0.4.3",
+              "bundled": true
+            }
+          }
+        },
+        "retry": {
+          "version": "0.10.0",
+          "bundled": true
+        },
+        "rimraf": {
+          "version": "2.5.4",
+          "bundled": true,
+          "requires": {
+            "glob": "7.0.6"
+          }
+        },
+        "semver": {
+          "version": "5.1.0",
+          "bundled": true
+        },
+        "sha": {
+          "version": "2.0.1",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "readable-stream": "2.0.2"
+          },
+          "dependencies": {
+            "readable-stream": {
+              "version": "2.0.2",
+              "bundled": true,
+              "requires": {
+                "core-util-is": "1.0.1",
+                "inherits": "2.0.3",
+                "isarray": "0.0.1",
+                "process-nextick-args": "1.0.3",
+                "string_decoder": "0.10.31",
+                "util-deprecate": "1.0.1"
+              },
+              "dependencies": {
+                "core-util-is": {
+                  "version": "1.0.1",
+                  "bundled": true
+                },
+                "isarray": {
+                  "version": "0.0.1",
+                  "bundled": true
+                },
+                "process-nextick-args": {
+                  "version": "1.0.3",
+                  "bundled": true
+                },
+                "string_decoder": {
+                  "version": "0.10.31",
+                  "bundled": true
+                },
+                "util-deprecate": {
+                  "version": "1.0.1",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "slide": {
+          "version": "1.1.6",
+          "bundled": true
+        },
+        "sorted-object": {
+          "version": "2.0.0",
+          "bundled": true
+        },
+        "spdx-license-ids": {
+          "version": "1.2.2",
+          "bundled": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "requires": {
+            "ansi-regex": "2.0.0"
+          }
+        },
+        "tar": {
+          "version": "2.2.1",
+          "bundled": true,
+          "requires": {
+            "block-stream": "0.0.9",
+            "fstream": "1.0.10",
+            "inherits": "2.0.3"
+          }
+        },
+        "text-table": {
+          "version": "0.2.0",
+          "bundled": true
+        },
+        "uid-number": {
+          "version": "0.0.6",
+          "bundled": true
+        },
+        "umask": {
+          "version": "1.1.0",
+          "bundled": true
+        },
+        "validate-npm-package-license": {
+          "version": "3.0.1",
+          "bundled": true,
+          "requires": {
+            "spdx-correct": "1.0.2",
+            "spdx-expression-parse": "1.0.2"
+          },
+          "dependencies": {
+            "spdx-correct": {
+              "version": "1.0.2",
+              "bundled": true,
+              "requires": {
+                "spdx-license-ids": "1.2.2"
+              }
+            },
+            "spdx-expression-parse": {
+              "version": "1.0.2",
+              "bundled": true,
+              "requires": {
+                "spdx-exceptions": "1.0.4",
+                "spdx-license-ids": "1.2.2"
+              },
+              "dependencies": {
+                "spdx-exceptions": {
+                  "version": "1.0.4",
+                  "bundled": true
+                }
+              }
+            }
+          }
+        },
+        "validate-npm-package-name": {
+          "version": "2.2.2",
+          "bundled": true,
+          "requires": {
+            "builtins": "0.0.7"
+          },
+          "dependencies": {
+            "builtins": {
+              "version": "0.0.7",
+              "bundled": true
+            }
+          }
+        },
+        "which": {
+          "version": "1.2.11",
+          "bundled": true,
+          "requires": {
+            "isexe": "1.1.2"
+          },
+          "dependencies": {
+            "isexe": {
+              "version": "1.1.2",
+              "bundled": true
+            }
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "write-file-atomic": {
+          "version": "1.1.4",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.6",
+            "imurmurhash": "0.1.4",
+            "slide": "1.1.6"
+          }
+        }
+      }
+    },
+    "object-inspect": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
+      "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
+      "dev": true
+    },
+    "object-is": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
+      "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3"
+      }
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true
+    },
+    "object.assign": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+      "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3",
+        "has-symbols": "1.0.2",
+        "object-keys": "1.1.1"
+      }
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1.0.2"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+      "dev": true
+    },
+    "posix-getopt": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/posix-getopt/-/posix-getopt-1.2.0.tgz",
+      "integrity": "sha1-Su7rfa3mb8qKk2XdqfawBXQctiE="
+    },
+    "readable-stream-clone": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/readable-stream-clone/-/readable-stream-clone-0.0.7.tgz",
+      "integrity": "sha512-mdkQtdg5elliOR64xcHVMkghSHQGjpsdCFAs6fSe28wTaVypR0GywmK5ndKpOSQzzuNpSrzC12Rv9UEkDLX73A=="
+    },
+    "regexp.prototype.flags": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz",
+      "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3"
+      }
+    },
+    "resolve": {
+      "version": "2.0.0-next.3",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz",
+      "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==",
+      "dev": true,
+      "requires": {
+        "is-core-module": "2.2.0",
+        "path-parse": "1.0.6"
+      }
+    },
+    "resumer": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz",
+      "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=",
+      "dev": true,
+      "requires": {
+        "through": "2.3.8"
+      }
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.6"
+      }
+    },
+    "side-channel": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "get-intrinsic": "1.1.1",
+        "object-inspect": "1.9.0"
+      }
+    },
+    "strftime": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.6.2.tgz",
+      "integrity": "sha1-2kwSBzzr7jzWD0ghlAzCexnTSKE="
+    },
+    "string.prototype.trim": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz",
+      "integrity": "sha512-hWCk/iqf7lp0/AgTF7/ddO1IWtSNPASjlzCicV5irAVdE1grjsneK26YG6xACMBEdCvO8fUST0UzDMh/2Qy+9Q==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3",
+        "es-abstract": "1.18.0"
+      }
+    },
+    "string.prototype.trimend": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+      "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3"
+      }
+    },
+    "string.prototype.trimstart": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+      "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "define-properties": "1.1.3"
+      }
+    },
+    "tape": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/tape/-/tape-5.2.2.tgz",
+      "integrity": "sha512-grXrzPC1ly2kyTMKdqxh5GiLpb0BpNctCuecTB0psHX4Gu0nc+uxWR4xKjTh/4CfQlH4zhvTM2/EXmHXp6v/uA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "1.0.2",
+        "deep-equal": "2.0.5",
+        "defined": "1.0.0",
+        "dotignore": "0.1.2",
+        "for-each": "0.3.3",
+        "glob": "7.1.6",
+        "has": "1.0.3",
+        "inherits": "2.0.4",
+        "is-regex": "1.1.2",
+        "minimist": "1.2.5",
+        "object-inspect": "1.9.0",
+        "object-is": "1.1.5",
+        "object.assign": "4.1.2",
+        "resolve": "2.0.0-next.3",
+        "resumer": "0.0.0",
+        "string.prototype.trim": "1.2.4",
+        "through": "2.3.8"
+      }
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "unbox-primitive": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz",
+      "integrity": "sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1",
+        "has-bigints": "1.0.1",
+        "has-symbols": "1.0.2",
+        "which-boxed-primitive": "1.0.2"
+      }
+    },
+    "uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
+    },
+    "which-boxed-primitive": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+      "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+      "dev": true,
+      "requires": {
+        "is-bigint": "1.0.1",
+        "is-boolean-object": "1.1.0",
+        "is-number-object": "1.0.4",
+        "is-string": "1.0.5",
+        "is-symbol": "1.0.3"
+      }
+    },
+    "which-collection": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
+      "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+      "dev": true,
+      "requires": {
+        "is-map": "2.0.2",
+        "is-set": "2.0.2",
+        "is-weakmap": "2.0.1",
+        "is-weakset": "2.0.1"
+      }
+    },
+    "which-typed-array": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz",
+      "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==",
+      "dev": true,
+      "requires": {
+        "available-typed-arrays": "1.0.2",
+        "call-bind": "1.0.2",
+        "es-abstract": "1.18.0",
+        "foreach": "2.0.5",
+        "function-bind": "1.1.1",
+        "has-symbols": "1.0.2",
+        "is-typed-array": "1.1.5"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    }
+  }
+}

+ 10 - 6
package.json

@@ -5,7 +5,7 @@
   "main": "fs-caching-server.js",
   "preferGlobal": true,
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "./node_modules/tape/bin/tape tests/*.js"
   },
   "bin": {
     "fs-caching-server": "./fs-caching-server.js"
@@ -26,13 +26,17 @@
   },
   "homepage": "https://github.com/bahamas10/node-fs-caching-server",
   "dependencies": {
-    "access-log": "^0.3.9",
+    "access-log": "^0.4.1",
+    "assert-plus": "^1.0.0",
     "latest": "^0.2.0",
     "mime": "^1.3.4",
     "mkdirp": "^0.5.0",
-    "node-uuid": "^1.4.3",
-    "path-platform": "^0.11.15",
-    "posix-getopt": "^1.1.0",
-    "readable-stream-clone": "^0.0.7"
+    "posix-getopt": "^1.2.0",
+    "readable-stream-clone": "^0.0.7",
+    "uuid": "^8.3.2"
+  },
+  "devDependencies": {
+    "rimraf": "^3.0.2",
+    "tape": "^5.2.2"
   }
 }

+ 1 - 0
smf/manifest.xml

@@ -9,6 +9,7 @@
 		<method_context working_directory='/tmp'>
 			<method_credential user='nobody' group='other'/>
 			<method_environment>
+				<envvar name='FS_CACHE_DIR' value='/var/tmp/cache'/> <!-- the cache dir -->
 				<envvar name='FS_CACHE_URL' value='http://pkgsrc.joyent.com'/> <!-- the URL you want to proxy -->
 				<envvar name='FS_CACHE_HOST' value='0.0.0.0'/>
 				<envvar name='FS_CACHE_PORT' value='8080'/>

+ 449 - 0
tests/all.js

@@ -0,0 +1,449 @@
+/*
+ * Basic caching tests
+ */
+
+var fs = require('fs');
+var http = require('http');
+var path = require('path');
+var url = require('url');
+var util = require('util');
+
+var assert = require('assert-plus');
+var mkdirp = require('mkdirp');
+var rimraf = require('rimraf');
+var test = require('tape');
+
+var lib = require('./lib');
+var FsCachingServer = require('../').FsCachingServer;
+
+var f = util.format;
+var config = lib.readConfig();
+
+var cachingServerURL = f('http://%s:%d', config.cachingServer.host,
+    config.cachingServer.port);
+var backendServerURL = f('http://%s:%d', config.backendServer.host,
+    config.backendServer.port);
+
+var cachingServer;
+var backendServer;
+
+var dir = path.join(__dirname, 'tmp');
+
+/*
+ * wrapper for making web requests to the caching server
+ */
+function cacheRequest(p, opts, cb) {
+    assert.string(p, 'p');
+    assert.object(opts, 'opts');
+    assert.string(opts.method, 'opts.method');
+    assert.func(cb, 'cb');
+
+    var uri = f('%s%s', cachingServerURL, p);
+
+    var o = url.parse(uri);
+    Object.keys(opts).forEach(function (key) {
+        var val = opts[key];
+        o[key] = val;
+    });
+
+    var req = http.request(o, function (res) {
+        var data = '';
+
+        res.setEncoding('utf-8');
+
+        res.on('data', function (d) {
+            data += d;
+        });
+
+        res.on('end', function () {
+            cb(null, data, res);
+        });
+
+        res.once('error', function (err) {
+            cb(err);
+        });
+    });
+
+    req.end();
+}
+
+/*
+ * start the caching frontend
+ */
+test('start cachingServer', function (t) {
+    var opts = {
+        cacheDir: dir,
+        host: config.cachingServer.host,
+        port: config.cachingServer.port,
+        backendUrl: backendServerURL
+    };
+
+    mkdirp.sync(dir);
+    rimraf.sync(dir + '/*');
+    t.pass(f('tmp dir "%s" cleared', dir));
+
+    cachingServer = new FsCachingServer(opts);
+
+    cachingServer.once('start', function () {
+        t.pass('cachingServer started');
+        t.end();
+    });
+
+    if (process.env.NODE_DEBUG) {
+        cachingServer.on('log', console.log);
+    }
+
+    cachingServer.start();
+});
+
+/*
+ * start the web server backend
+ */
+test('start backendServer', function (t) {
+    backendServer = http.createServer(onRequest);
+    backendServer.listen(config.backendServer.port, config.backendServer.host,
+        onListen);
+
+    function onRequest(req, res) {
+        var p = url.parse(req.url).pathname;
+        var s = f('%s request by pid %d at %s\n', p, process.pid, Date.now());
+
+        // handle: /statusCode/<num>
+        var matches;
+        if ((matches = p.match(/^\/statusCode\/([0-9]+)/))) {
+            res.statusCode = parseInt(matches[1], 10);
+            res.end(s);
+            return;
+        }
+
+        switch (p) {
+        case '/301.png':
+            res.statusCode = 301;
+            res.setHeader('Location', '/foo.png');
+            res.end();
+            break;
+        case '/header.png':
+            res.setHeader('x-fun-header', 'woo');
+            res.end();
+        default:
+            res.end(s);
+            break;
+        }
+    }
+
+    function onListen() {
+        t.pass(f('backendServer started on %s', backendServerURL));
+        t.end();
+    }
+});
+
+/*
+ * Basic request that should be cached.
+ */
+test('simple cached request', function (t) {
+    var f = '/hello.png';
+
+    cacheRequest(f, {method: 'GET'}, function (err, webData) {
+        t.error(err, 'GET ' + f);
+
+        cachingServer.onIdle(function () {
+            // check to make sure the cache has this data
+            var fileData = fs.readFileSync(path.join(dir, f), 'utf-8');
+
+            t.equal(webData, fileData, 'file data in sync with web data');
+
+            // request it again to ensure it's correct
+            cacheRequest(f, {method: 'GET'}, function (err, webData2) {
+                t.error(err, 'GET ' + f);
+
+                t.equal(webData, webData2, 'both web requests same data');
+
+                cachingServer.onIdle(function () {
+                    t.end();
+                });
+            });
+        });
+    });
+});
+
+/*
+ * Basic request that should not be cached.
+ */
+test('simple non-cached request', function (t) {
+    var f = '/hello.txt';
+
+    cacheRequest(f, {method: 'GET'}, function (err, webData) {
+        t.error(err, 'GET ' + f);
+
+        cachingServer.onIdle(function () {
+            // check to make sure the cache DOES NOT have this data
+            var file = path.join(dir, f);
+
+            t.throws(function () {
+                fs.statSync(file);
+            }, file + ' should not exist');
+
+            // request it again to ensure the data is different (time difference)
+            cacheRequest(f, {method: 'GET'}, function (err, webData2) {
+                t.error(err, 'GET ' + f);
+
+                t.notEqual(webData, webData2, 'both web requests different data');
+
+                cachingServer.onIdle(function () {
+                    t.end();
+                });
+            });
+        });
+    });
+});
+
+/*
+ * Codes that *should not* be proxied.
+ */
+test('statusCodes without proxy', function (t) {
+    var codes = [301, 302, 403, 404, 500, 501, 502];
+    var idx = 0;
+
+    function go() {
+        var code = codes[idx++];
+        if (!code) {
+            t.end();
+            return;
+        }
+        var uri = f('/statusCode/%d/foo.png', code);
+
+        cacheRequest(uri, {method: 'GET'}, function (err, webData) {
+            t.error(err, 'GET ' + uri);
+            t.equal(webData, '', 'webData should be empty from caching server');
+
+            cachingServer.onIdle(function () {
+                // ensure the file does NOT exist in the cache dir
+                var file = path.join(dir, uri);
+                t.throws(function () {
+                    fs.statSync(file);
+                }, file + ' should not exist');
+
+                go();
+            });
+        });
+    }
+
+    go();
+});
+
+/*
+ * Codes that *should* be proxied.
+ */
+test('statusCodes with proxy', function (t) {
+    var codes = [200];
+    var idx = 0;
+
+    function go() {
+        var code = codes[idx++];
+        if (!code) {
+            t.end();
+            return;
+        }
+        var uri = f('/statusCode/%d/foo.png', code);
+
+        cacheRequest(uri, {method: 'GET'}, function (err, webData) {
+            t.error(err, 'GET ' + uri);
+            t.ok(webData, 'webData should have data from server');
+
+            cachingServer.onIdle(function () {
+                // ensure the file exists with the corect data
+                var file = path.join(dir, uri);
+                var fileData = fs.readFileSync(file, 'utf-8');
+                t.equal(webData, fileData, 'file data in sync with web data');
+
+                go();
+            });
+        });
+    }
+
+    go();
+});
+
+/*
+ * The first request to an item (cache-miss) will result in the request being
+ * proxied directly to the backendServer.  Subsequent requests will not be
+ * proxied and instead will just be handed the cached file without any of the
+ * original headers.
+ */
+test('headers proxied only on first request', function (t) {
+    var uri = '/header.png';
+    var serverHeader = 'x-fun-header';
+
+    // initial request (cache miss) should proxy headers from server
+    cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
+        t.error(err, 'GET ' + uri);
+
+        var headers = res.headers;
+        var customHeader = headers[serverHeader];
+
+        t.equal(customHeader, 'woo', f('custom header %s seen', serverHeader));
+
+        cachingServer.onIdle(function () {
+            // second request (cache hit) won't remember headers from server
+            cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
+                t.error(err, 'GET ' + uri);
+
+                var headers = res.headers;
+                var customHeader = headers[serverHeader];
+
+                t.ok(!customHeader, f('custom header %s not seen', serverHeader));
+
+                cachingServer.onIdle(function () {
+                    t.end();
+                });
+            });
+        });
+    });
+});
+
+/*
+ * FsCachingServer handles redirects by specifically choosing to not handle
+ * them.  Instead, the statusCodes and headers from the backendServer will be
+ * sent directly to the caller, and it is up to that caller if they'd like to
+ * follow the redirect.  If the redirects eventually hit a GET or HEAD request
+ * that falls within the 200 range, then it will be cached as normal.
+ */
+test('301 redirect', function (t) {
+    var uri = '/301.png';
+    var redirect = '/foo.png';
+
+    cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
+        t.error(err, 'GET ' + uri);
+        t.ok(!webData, 'body empty');
+        t.equal(res.statusCode, 301, '301 seen');
+        var headers = res.headers;
+        var loc = headers.location;
+
+        t.equal(loc, redirect, 'location is correct');
+
+        cachingServer.onIdle(function () {
+            t.end();
+        });
+    });
+});
+
+/*
+ * Requesting a directory that exists in the cache should result in a 400.
+ */
+test('GET directory in cache', function (t) {
+    var uri = '/directory.png';
+
+    fs.mkdirSync(path.join(dir, uri));
+
+    cacheRequest(uri, {method: 'GET'}, function (err, webData, res) {
+        t.error(err, 'GET ' + uri);
+        t.ok(!webData, 'body empty');
+        t.equal(res.statusCode, 400, '400 seen');
+
+        cachingServer.onIdle(function () {
+            t.end();
+        });
+    });
+});
+
+/*
+ * Two simulataneous requests for a cache-miss.  This will result in one of the
+ * requests being responsible for downloading the file and getting it streamed
+ * to them live, and the other request being paused until the data is fully
+ * downloaded.
+ *
+ * To simulate this fs.stat will be artifically slown down so both requests
+ * will block before the cache download begins.
+ */
+test('Two simultaneous requests', function (t) {
+    var originalStat = fs.stat.bind(fs);
+
+    fs.stat = function slowStat(f, cb) {
+        setTimeout(function () {
+            originalStat(f, cb);
+        }, 100);
+    };
+
+    var uri = '/simultaneous.png';
+    var todo = 2;
+
+    cacheRequest(uri, {method: 'GET'}, requestOne);
+    setTimeout(function () {
+        cacheRequest(uri, {method: 'GET'}, requestTwo);
+    }, 30);
+
+    var data1;
+    var data2;
+
+    function requestOne(err, data, res) {
+        t.error(err, '1. GET ' + uri);
+        data1 = data;
+
+        finish();
+    }
+
+    function requestTwo(err, data, res) {
+        t.error(err, '2. GET ' + uri);
+        data2 = data;
+
+        finish();
+    }
+
+    function finish() {
+        if (--todo > 0) {
+            return;
+        }
+
+        fs.stat = originalStat;
+        cachingServer.onIdle(function () {
+            t.equal(data1, data2, 'data the same');
+            t.end();
+        });
+    }
+});
+
+/*
+ * HEAD requests should only server cached results and not cache them itself.
+ * i.e. if a HEAD request is seen first it should just be proxied to the
+ * backend directly with no caching.
+ */
+test('HEAD request', function (t) {
+    var uri = '/head-cache-test.png';
+
+    cacheRequest(uri, {method: 'HEAD'}, function (err, data, res) {
+        t.error(err, 'HEAD ' + uri);
+
+        cachingServer.onIdle(function () {
+            // check to make sure the cache DOES NOT have this data
+            var file = path.join(dir, uri);
+
+            t.throws(function () {
+                fs.statSync(file);
+            }, file + ' should not exist');
+
+            t.end();
+        });
+    });
+});
+
+/*
+ * Close the backend HTTP server
+ */
+test('close backendServer', function (t) {
+    backendServer.once('close', function () {
+        t.pass('backendServer closed');
+        t.end();
+    });
+    backendServer.close();
+});
+
+/*
+ * Stop the caching server
+ */
+test('stop cachingServer', function (t) {
+    cachingServer.once('stop', function () {
+        t.pass('cachingServer stopped');
+        t.end();
+    });
+    cachingServer.stop();
+});

+ 10 - 0
tests/dist-config.json

@@ -0,0 +1,10 @@
+{
+    "backendServer": {
+        "host": "127.0.0.1",
+        "port": 8080
+    },
+    "cachingServer": {
+        "host": "127.0.0.1",
+        "port": 8081
+    }
+}

+ 16 - 0
tests/lib/index.js

@@ -0,0 +1,16 @@
+module.exports.readConfig = readConfig;
+
+function readConfig() {
+    var config;
+
+    try {
+        config = require('../config.json');
+    } catch (e) {
+        console.error('failed to read/parse config');
+        console.error('ensure that you cp dist-config.json config.json');
+        console.error(e);
+        process.exit(1);
+    }
+
+    return config;
+}