http: make http base server safer.

This commit is contained in:
Christopher Jeffrey 2017-01-11 18:42:08 -08:00
parent 428e2a1301
commit 1cda7cb702
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
2 changed files with 320 additions and 163 deletions

View File

@ -3,9 +3,15 @@
var HTTPBase = require('../lib/http/base'); var HTTPBase = require('../lib/http/base');
var WSProxy = require('./wsproxy'); var WSProxy = require('./wsproxy');
var fs = require('fs'); var fs = require('fs');
var server, proxy;
var server = new HTTPBase(); var index = fs.readFileSync(__dirname + '/index.html');
var proxy = new WSProxy({ var indexjs = fs.readFileSync(__dirname + '/index.js');
var bcoin = fs.readFileSync(__dirname + '/bcoin.js');
var master = fs.readFileSync(__dirname + '/bcoin-master.js');
var worker = fs.readFileSync(__dirname + '/bcoin-worker.js');
proxy = new WSProxy({
pow: process.argv.indexOf('--pow') !== -1, pow: process.argv.indexOf('--pow') !== -1,
ports: [8333, 18333, 18444, 28333, 28901] ports: [8333, 18333, 18444, 28333, 28901]
}); });
@ -14,14 +20,10 @@ proxy.on('error', function(err) {
console.error(err.stack + ''); console.error(err.stack + '');
}); });
var index = fs.readFileSync(__dirname + '/index.html'); server = new HTTPBase();
var indexjs = fs.readFileSync(__dirname + '/index.js');
var bcoin = fs.readFileSync(__dirname + '/bcoin.js');
var master = fs.readFileSync(__dirname + '/bcoin-master.js');
var worker = fs.readFileSync(__dirname + '/bcoin-worker.js');
server.get('/favicon.ico', function(req, res, send, next) { server.get('/favicon.ico', function(req, res, send, next) {
send(404, '', 'text'); send(404, '', 'txt');
}); });
server.get('/', function(req, res, send, next) { server.get('/', function(req, res, send, next) {

View File

@ -7,10 +7,10 @@
'use strict'; 'use strict';
var assert = require('assert');
var AsyncObject = require('../utils/async'); var AsyncObject = require('../utils/async');
var util = require('../utils/util'); var util = require('../utils/util');
var assert = require('assert'); var URL = require('url');
var url = require('url');
/** /**
* HTTPBase * HTTPBase
@ -32,13 +32,7 @@ function HTTPBase(options) {
this.options = options; this.options = options;
this.io = null; this.io = null;
this.routes = { this.routes = new Routes();
get: [],
post: [],
put: [],
del: []
};
this.stack = []; this.stack = [];
this.server = options.key this.server = options.key
@ -97,47 +91,46 @@ HTTPBase.prototype._initRouter = function _initRouter() {
var self = this; var self = this;
this.server.on('request', function(req, res) { this.server.on('request', function(req, res) {
function _send(code, msg, type) { var i, j, routes, route, match, item;
send(res, code, msg, type);
function send(code, msg, type) {
sendResponse(res, code, msg, type);
} }
function done(err) { function done(err) {
if (err) { if (err) {
send(res, err.statusCode || 400, { error: err.message + '' }); send(err.statusCode || 400, { error: err.message + '' });
try { try {
req.destroy(); req.destroy();
req.socket.destroy(); req.socket.destroy();
} catch (e) { } catch (e) {
; ;
} }
self.emit('error', err); self.emit('error', err);
} }
} }
try { try {
parsePath(req); parsePath(req, 100);
} catch (e) { } catch (e) {
return done(e); return done(e);
} }
self.emit('request', req, res); self.emit('request', req, res);
parseBody(req, function(err) { i = 0;
var method, routes, i; routes = self.routes.getHandlers(req.method);
if (!routes)
return done(new Error('No routes found.'));
parseBody(req, function(err) {
if (err) if (err)
return done(err); return done(err);
method = (req.method || 'GET').toLowerCase();
routes = self.routes[method];
i = 0;
if (!routes)
return done(new Error('No routes found.'));
(function next(err) { (function next(err) {
var route, path, callback, compiled, matched;
if (err) if (err)
return done(err); return done(err);
@ -145,35 +138,28 @@ HTTPBase.prototype._initRouter = function _initRouter() {
return done(new Error('Route not found.')); return done(new Error('Route not found.'));
route = routes[i++]; route = routes[i++];
path = route.path; match = route.match(req.pathname);
callback = route.callback;
if (!route.regex) { if (!match)
compiled = compilePath(path);
route.regex = compiled.regex;
route.map = compiled.map;
}
matched = route.regex.exec(req.pathname);
if (!matched)
return next(); return next();
req.params = {}; req.params = {};
matched.slice(1).forEach(function(item, i) {
if (route.map[i])
req.params[route.map[i]] = item;
req.params[i] = item;
});
self._handle(req, res, _send, function(err) { for (j = 0; j < match.length; j++) {
item = match[j];
if (route.map[j])
req.params[route.map[j]] = item;
req.params[j] = item;
}
self.handleStack(req, res, send, function(err) {
if (err) if (err)
return done(err); return done(err);
// Avoid stack overflows // Avoid stack overflows
util.nextTick(function() { util.nextTick(function() {
try { try {
callback.call(route.ctx, req, res, _send, next); route.call(req, res, send, next);
} catch (e) { } catch (e) {
done(e); done(e);
} }
@ -255,15 +241,15 @@ HTTPBase.prototype._close = function close(callback) {
* Handle middleware stack. * Handle middleware stack.
* @param {HTTPRequest} req * @param {HTTPRequest} req
* @param {HTTPResponse} res * @param {HTTPResponse} res
* @param {Function} _send * @param {Function} send
* @returns {Promise} * @returns {Promise}
* @private * @private
*/ */
HTTPBase.prototype._handle = function _handle(req, res, _send, callback) { HTTPBase.prototype.handleStack = function handleStack(req, res, send, callback) {
var self = this; var self = this;
var i = 0; var i = 0;
var handler; var route;
(function next(err) { (function next(err) {
if (err) if (err)
@ -272,14 +258,14 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
if (i === self.stack.length) if (i === self.stack.length)
return callback(); return callback();
handler = self.stack[i++]; route = self.stack[i++];
util.nextTick(function() { util.nextTick(function() {
if (handler.path && req.pathname.indexOf(handler.path) !== 0) if (!route.hasPrefix(req.pathname))
return next(); return next();
try { try {
handler.callback.call(handler.ctx, req, res, _send, next); route.call(req, res, send, next);
} catch (e) { } catch (e) {
next(e); next(e);
} }
@ -287,15 +273,6 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
})(); })();
}; };
/**
* Middleware and route callback.
* @callback RouteCallback
* @param {HTTPRequest} req
* @param {HTTPResponse} res
* @param {Function} next
* @param {Function} send
*/
/** /**
* Add a middleware to the stack. * Add a middleware to the stack.
* @param {String?} path * @param {String?} path
@ -303,19 +280,20 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
*/ */
HTTPBase.prototype.use = function use(path, callback, ctx) { HTTPBase.prototype.use = function use(path, callback, ctx) {
var i;
if (!callback) { if (!callback) {
callback = path; callback = path;
path = null; path = null;
} }
if (Array.isArray(path)) { if (Array.isArray(path)) {
path.forEach(function(path) { for (i = 0; i < path.length; i++)
this.use(path, callback, ctx); this.use(path[i], callback, ctx);
}, this);
return; return;
} }
this.stack.push({ ctx: ctx, path: path, callback: callback }); this.stack.push(new Route(ctx, path, callback));
}; };
/** /**
@ -325,13 +303,18 @@ HTTPBase.prototype.use = function use(path, callback, ctx) {
*/ */
HTTPBase.prototype.get = function get(path, callback, ctx) { HTTPBase.prototype.get = function get(path, callback, ctx) {
var i;
if (Array.isArray(path)) { if (Array.isArray(path)) {
path.forEach(function(path) { for (i = 0; i < path.length; i++)
this.get(path, callback, ctx); this.get(path[i], callback, ctx);
}, this);
return; return;
} }
this.routes.get.push({ ctx: ctx, path: path, callback: callback });
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.get.push(new Route(ctx, path, callback));
}; };
/** /**
@ -341,13 +324,18 @@ HTTPBase.prototype.get = function get(path, callback, ctx) {
*/ */
HTTPBase.prototype.post = function post(path, callback, ctx) { HTTPBase.prototype.post = function post(path, callback, ctx) {
var i;
if (Array.isArray(path)) { if (Array.isArray(path)) {
path.forEach(function(path) { for (i = 0; i < path.length; i++)
this.post(path, callback, ctx); this.post(path[i], callback, ctx);
}, this);
return; return;
} }
this.routes.post.push({ ctx: ctx, path: path, callback: callback });
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.post.push(new Route(ctx, path, callback));
}; };
/** /**
@ -357,13 +345,18 @@ HTTPBase.prototype.post = function post(path, callback, ctx) {
*/ */
HTTPBase.prototype.put = function put(path, callback, ctx) { HTTPBase.prototype.put = function put(path, callback, ctx) {
var i;
if (Array.isArray(path)) { if (Array.isArray(path)) {
path.forEach(function(path) { for (i = 0; i < path.length; i++)
this.put(path, callback, ctx); this.put(path[i], callback, ctx);
}, this);
return; return;
} }
this.routes.put.push({ ctx: ctx, path: path, callback: callback });
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.put.push(new Route(ctx, path, callback));
}; };
/** /**
@ -373,13 +366,18 @@ HTTPBase.prototype.put = function put(path, callback, ctx) {
*/ */
HTTPBase.prototype.del = function del(path, callback, ctx) { HTTPBase.prototype.del = function del(path, callback, ctx) {
var i;
if (Array.isArray(path)) { if (Array.isArray(path)) {
path.forEach(function(path) { for (i = 0; i < path.length; i++)
this.del(path, callback, ctx); this.del(path[i], callback, ctx);
}, this);
return; return;
} }
this.routes.del.push({ ctx: ctx, path: path, callback: callback });
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.del.push(new Route(ctx, path, callback));
}; };
/** /**
@ -416,70 +414,177 @@ HTTPBase.prototype.listen = function listen(port, host) {
}); });
}; };
/**
* Route
* @constructor
*/
function Route(ctx, path, callback) {
if (!(this instanceof Route))
return new Route(ctx, path, callback);
this.ctx = null;
this.path = null;
this.callback = null;
this.regex = /^/;
this.map = [];
this.compiled = false;
if (ctx) {
assert(typeof ctx === 'object');
this.ctx = ctx;
}
if (path) {
if (path instanceof RegExp) {
this.regex = path;
} else {
assert(typeof path === 'string');
this.path = path;
}
}
assert(typeof callback === 'function');
this.callback = callback;
}
Route.prototype.compile = function compile() {
var path = this.path;
var map = this.map;
var regex;
if (this.compiled)
return;
this.compiled = true;
if (!path)
return;
path = path.replace(/(\/[^\/]+)\?/g, '(?:$1)?');
path = path.replace(/\.(?!\+)/g, '\\.');
path = path.replace(/\*/g, '.*?');
path = path.replace(/%/g, '\\');
path = path.replace(/:(\w+)/g, function(str, name) {
map.push(name);
return '([^/]+)';
});
this.regex = new RegExp('^' + path + '$');
};
Route.prototype.match = function match(pathname) {
var match;
assert(this.path);
this.compile();
match = this.regex.exec(pathname);
if (!match)
return;
return match.slice(1);
};
Route.prototype.hasPrefix = function hasPrefix(pathname) {
if (!this.path)
return true;
return pathname.indexOf(this.path) === 0;
};
Route.prototype.call = function call(req, res, send, next) {
this.callback.call(this.ctx, req, res, send, next);
};
/**
* Routes
* @constructor
*/
function Routes() {
if (!(this instanceof Routes))
return new Routes();
this.get = [];
this.post = [];
this.put = [];
this.del = [];
}
Routes.prototype.getHandlers = function getHandlers(method) {
if (!method)
return;
method = method.toUpperCase();
switch (method) {
case 'GET':
return this.get;
case 'POST':
return this.post;
case 'PUT':
return this.put;
case 'DEL':
return this.del;
default:
return;
}
};
/* /*
* Helpers * Helpers
*/ */
function send(res, code, msg, type) { function sendResponse(res, code, msg, type) {
var len; var len;
if (!msg) assert(typeof code === 'number', 'Code must be a number.');
if (msg == null)
msg = { error: 'No message.' }; msg = { error: 'No message.' };
try { if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) {
res.statusCode = code; msg = JSON.stringify(msg, null, 2) + '\n';
if (!type)
if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) {
msg = JSON.stringify(msg, null, 2) + '\n';
type = 'json'; type = 'json';
} assert(type === 'json', 'Bad type passed with json object.');
if (type === 'html')
res.setHeader('Content-Type', 'text/html; charset=utf-8');
else if (type === 'text')
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
else if (type === 'json')
res.setHeader('Content-Type', 'application/json');
else if (type === 'js')
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
else if (type === 'binary')
res.setHeader('Content-Type', 'application/octet-stream');
len = typeof msg === 'string'
? Buffer.byteLength(msg, 'utf8')
: msg.length;
res.setHeader('Content-Length', len + '');
res.write(msg);
res.end();
} catch (e) {
;
} }
}
function compilePath(path) { if (!type)
var map = []; type = typeof msg === 'string' ? 'txt' : 'bin';
if (path instanceof RegExp) res.statusCode = code;
return { regex: path, map: map }; res.setHeader('Content-Type', getType(type));
var regex = path if (typeof msg === 'string') {
.replace(/(\/[^\/]+)\?/g, '(?:$1)?') len = Buffer.byteLength(msg, 'utf8');
.replace(/\.(?!\+)/g, '\\.') res.setHeader('Content-Length', len + '');
.replace(/\*/g, '.*?') try {
.replace(/%/g, '\\') res.write(msg, 'utf8');
.replace(/:(\w+)/g, function(__, name) { res.end();
map.push(name); } catch (e) {
return '([^/]+)'; ;
} }
); return;
}
regex = new RegExp('^' + regex + '$'); if (Buffer.isBuffer(msg)) {
res.setHeader('Content-Length', msg.length + '');
try {
res.write(msg);
res.end();
} catch (e) {
;
}
return;
}
return { assert(false, 'Bad object passed to send.');
map: map,
regex: regex
};
} }
function parseBody(req, callback) { function parseBody(req, callback) {
@ -523,11 +628,13 @@ function parseBody(req, callback) {
}); });
} }
function parsePairs(str) { function parsePairs(str, limit) {
var parts = str.split('&'); var parts = str.split('&');
var data = {}; var data = {};
var i, index, pair, key, value; var i, index, pair, key, value;
assert(!limit || parts.length <= limit, 'Too many keys in querystring.');
for (i = 0; i < parts.length; i++) { for (i = 0; i < parts.length; i++) {
pair = parts[i]; pair = parts[i];
index = pair.indexOf('='); index = pair.indexOf('=');
@ -556,45 +663,93 @@ function parsePairs(str) {
return data; return data;
} }
function parsePath(req) { function parsePath(req, limit) {
var uri = url.parse(req.url); var uri = URL.parse(req.url);
var pathname = uri.pathname || '/'; var pathname = uri.pathname;
var query = {};
var path, parts, url;
if (pathname[pathname.length - 1] === '/') if (pathname) {
pathname = pathname.slice(0, -1); pathname = pathname.replace(/\/{2,}/g, '/');
pathname = unescape(pathname); if (pathname[0] !== '/')
pathname = '/' + pathname;
req.path = pathname; if (pathname.length > 1) {
if (pathname[pathname.length - 1] === '/')
pathname = pathname.slice(0, -1);
}
if (req.path[0] === '/') pathname = unescape(pathname);
req.path = req.path.substring(1); } else {
pathname = '/';
req.path = req.path.split('/');
if (!req.path[0])
req.path = [];
req.pathname = pathname || '/';
if (req.url.indexOf('//') !== -1) {
req.url = req.url.replace(/^([^:\/]+)?\/\/[^\/]+/, '');
if (!req.url)
req.url = '/';
} }
if (!req.query) { assert(pathname.length > 0);
req.query = uri.query assert(pathname[0] === '/');
? parsePairs(uri.query, '&')
: {}; if (pathname.length > 1)
assert(pathname[pathname.length - 1] !== '/');
path = pathname;
if (path[0] === '/')
path = path.substring(1);
parts = path.split('/');
if (parts.length === 1) {
if (parts[0].length === 0)
parts = [];
} }
url = pathname;
if (uri.search && uri.search.length > 1) {
assert(uri.search[0] === '?');
url += uri.search;
}
if (uri.hash && uri.hash.length > 1) {
assert(uri.hash[0] === '#');
url += uri.hash;
}
if (uri.query)
query = parsePairs(uri.query, limit);
req.url = url;
req.pathname = pathname;
req.path = parts;
req.query = query;
req.params = {};
} }
function unescape(str) { function unescape(str) {
try { str = decodeURIComponent(str);
str = decodeURIComponent(str).replace(/\+/g, ' '); str = str.replace(/\+/g, ' ');
} finally { str = str.replace(/\0/g, '');
return str.replace(/\0/g, ''); return str;
}
function getType(type) {
switch (type) {
case 'json':
return 'application/json';
case 'form':
return 'application/x-www-form-urlencoded; charset=utf-8';
case 'html':
return 'text/html; charset=utf-8';
case 'js':
return 'application/javascript; charset=utf-8';
case 'css':
return 'text/css; charset=utf-8';
case 'txt':
return 'text/plain; charset=utf-8';
case 'bin':
return 'application/octet-stream';
default:
throw new Error('Unknown type: ' + type);
} }
} }