src/DispatchTable.js
'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;