encharm/http-master

View on GitHub
src/DispatchTable.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';
var XRegExp = require('xregexp');
var assert = require('assert');

// globStringToRegex from: http://stackoverflow.com/a/13818704/403571
function regexpQuote(str, delimiter) {
  // http://kevin.vanzonneveld.net
  // +   original by: booeyOH
  // +   improved by: Ates Goral (http://magnetiq.com)
  // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  // +   bugfixed by: Onno Marsman
  // +   improved by: Brett Zamir (http://brett-zamir.me)
  // *     example 1: preg_quote("$40");
  // *     returns 1: '\$40'
  // *     example 2: preg_quote("*RRRING* Hello?");
  // *     returns 2: '\*RRRING\* Hello\?'
  // *     example 3: preg_quote("\\.+*?[^]$(){}=!<>|:");
  // *     returns 3: '\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:'
  return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
}

function splitFirst(str) {
  var m = str.match(/^(\^?[^\/]*)\$?(?:(\/)(\^?)(.*))?$/);
  if(m.length > 2) {
    // make ^/path from /^path
    return [m[1], m[3] + m[2]+m[4]]; 
  }
  return [m[1]];
}

function globStringToRegex(str, specialCh, optionalEnding) {
  if(!specialCh)
    specialCh = '.';
  var inside = regexpQuote(str);
  if(specialCh == '.') {
    inside = inside.replace(/^\\\*$/g, '(?:(?<host>.*))');
    inside = inside.replace(/^\\\*\\\?\\\./g, '(?:(.+)\\.)?');
    inside = inside.replace(/^\\\*\\\./g, '(?:(.+)\\.)');
    inside = inside.replace(/\\\.\\\*\\\?/g, '(?:\\.([^'+specialCh+']+))?');    
  }
  else
    inside = inside.replace(/\/\\\*$/g, '(?:\/(?<rest>.*|)|)');
  inside = inside.replace(/\\\*/g, '([^'+specialCh+']+)');

  var regexp = new XRegExp('^' + inside + (optionalEnding?('(?:'+optionalEnding+')?'):'') +  '$');
  return regexp;
}

function getRegexpIfNeeded(str, specialCh, optionalEnding) {
  if (typeof str == 'string') {
    var m = str.match(/^\^(.*)\$?$/);
    if (m) {
      return new XRegExp('^' + m[1] + (optionalEnding?('(?:'+optionalEnding+')?'):'') +  '$');
    } else if (str.match(/[*?]/)) {
      return globStringToRegex(str, specialCh, optionalEnding);
    }
  }
  return undefined;
}

function postParseKey(entryKey, entry) {
  var regexp = getRegexpIfNeeded(entryKey, '.', ':' + entry.port);
  if (regexp)
    entry.regexp = regexp;
  return entryKey;
}

function DispatchTable(port, params) {
  var parseEntry = params.entryParser;
  var config = params.config;

  var self = this;
  this.requestHandler = params.requestHandler;
  this.upgradeHandler = params.upgradeHandler;
  this.table = {};
  this.regexpEntries = [];
  this.failedEntries = {};
  Object.keys(config || {}).forEach(function(entryKey) {
    var entry = config[entryKey];


    // split entry 192.168.0.0/host to
    // ['192.168.0.0', '/']
    var entryKeyData = splitFirst(entryKey);
    entryKey = entryKeyData[0];
    var entryPath = entryKeyData[1];

    if(entryPath) {
      entryPath = decodeURIComponent(entryPath);
    }
    if (parseEntry) {
      var parsedEntry = parseEntry(entry);
      assert(typeof parsedEntry !== 'undefined', 'entryParser should have returned something');
      entry = parsedEntry;
    }
    entry = {
      target: entry,
      port: port
    };
    if (entryPath) {
      entry.path = entryPath;
      var pathRegexp = getRegexpIfNeeded(entryPath, '\/');
      if (pathRegexp)
        entry.pathRegexp = pathRegexp;
    }
    entryKey = postParseKey(entryKey, entry);
    port = port || 80;

    if (entry.regexp) {
      self.regexpEntries.push(entry);
    } else {
      if (self.table[entryKey]) {
        if (self.table[entryKey] instanceof Array) {
          self.table[entryKey].push(entry);
          self.table[entryKey + ':' + port].push(entry);
        } else {
          var oldEntry = self.table[entryKey];
          self.table[entryKey] = [oldEntry, entry];
          self.table[entryKey + ':' + port] = [oldEntry, entry];
        }
      } else {
        self.table[entryKey + ':' + port] = entry;
        self.table[entryKey] = entry;
      }
    }
  });
}

DispatchTable.prototype.checkPathForReq = function(req, entry) {
  if(!entry.path)
    return true;
  var m;

  var parsedUrl = req.parsedUrl;
  var pathname = parsedUrl.pathname || '';

  try {
    pathname = decodeURIComponent(pathname);
  } catch(err) {}

  if(entry.pathRegexp) {
    m = pathname.match(entry.pathRegexp);
    if (m) {
      if(!req.match)
        req.match = [];
      for(var i = 1;i < m.length;++i) {
        req.match.push(m[i]);
      }
      return true;
    } 
  }
  else if(pathname == entry.path) {
    return true;
  }
  return false;
};

DispatchTable.prototype.getTargetForReq = function(req) {
  var i, m;
  var host = req.unicodeHost || req.headers.host || ''; // host can be undefined

  var self = this;
  var target;

  // look for specific host match first
  // and generic path-only match then
  [host, ''].some(function(host) {
    var entry = self.table[host];
    if (entry) {
      if (entry.target) {
        if(self.checkPathForReq(req, entry)) {
          target = entry.target
          return true;
        }
      }
      else { // multiple entries, check pathnames
        var targetEntries = entry;
        for (i = 0; i < targetEntries.length; ++i) {
          if(self.checkPathForReq(req, targetEntries[i])) {
            target = targetEntries[i].target;
            return true;
          }
        }
      }
    }
  });
  if(target) {
    return target;
  }
  // if host-only matches failed, look for path matches
  if (this.regexpEntries.length) {
    var regexpEntries = this.regexpEntries;
    for (i = 0; i < regexpEntries.length; ++i) {
      var entry = regexpEntries[i];
      if(!entry.regexp) {
        // TODO: research this
        continue;
      }
      m = host.match(entry.regexp);
      if (m) {
        if(!req.match)
          req.match = [];
        for(var i = 1;i < m.length;++i)
          req.match.push(m[i]);
        if(this.checkPathForReq(req, entry)) {
          return entry.target;
        }
      }
    }
  }
};

DispatchTable.prototype.dispatchRequest = function(req, res, next) {
  var target = this.getTargetForReq(req);
  if(target && this.requestHandler) {
    return this.requestHandler(req, res, next, target);
  }
  next();
};

DispatchTable.prototype.handleRequest = DispatchTable.prototype.dispatchRequest;

module.exports = DispatchTable;

module.exports.regexpQuote = regexpQuote;