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 WSProxy = require('./wsproxy');
var fs = require('fs');
var server, proxy;
var server = new HTTPBase();
var proxy = new WSProxy({
var index = fs.readFileSync(__dirname + '/index.html');
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,
ports: [8333, 18333, 18444, 28333, 28901]
});
@ -14,14 +20,10 @@ proxy.on('error', function(err) {
console.error(err.stack + '');
});
var index = fs.readFileSync(__dirname + '/index.html');
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 = new HTTPBase();
server.get('/favicon.ico', function(req, res, send, next) {
send(404, '', 'text');
send(404, '', 'txt');
});
server.get('/', function(req, res, send, next) {

View File

@ -7,10 +7,10 @@
'use strict';
var assert = require('assert');
var AsyncObject = require('../utils/async');
var util = require('../utils/util');
var assert = require('assert');
var url = require('url');
var URL = require('url');
/**
* HTTPBase
@ -32,13 +32,7 @@ function HTTPBase(options) {
this.options = options;
this.io = null;
this.routes = {
get: [],
post: [],
put: [],
del: []
};
this.routes = new Routes();
this.stack = [];
this.server = options.key
@ -97,47 +91,46 @@ HTTPBase.prototype._initRouter = function _initRouter() {
var self = this;
this.server.on('request', function(req, res) {
function _send(code, msg, type) {
send(res, code, msg, type);
var i, j, routes, route, match, item;
function send(code, msg, type) {
sendResponse(res, code, msg, type);
}
function done(err) {
if (err) {
send(res, err.statusCode || 400, { error: err.message + '' });
send(err.statusCode || 400, { error: err.message + '' });
try {
req.destroy();
req.socket.destroy();
} catch (e) {
;
}
self.emit('error', err);
}
}
try {
parsePath(req);
parsePath(req, 100);
} catch (e) {
return done(e);
}
self.emit('request', req, res);
parseBody(req, function(err) {
var method, routes, i;
i = 0;
routes = self.routes.getHandlers(req.method);
if (!routes)
return done(new Error('No routes found.'));
parseBody(req, function(err) {
if (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) {
var route, path, callback, compiled, matched;
if (err)
return done(err);
@ -145,35 +138,28 @@ HTTPBase.prototype._initRouter = function _initRouter() {
return done(new Error('Route not found.'));
route = routes[i++];
path = route.path;
callback = route.callback;
match = route.match(req.pathname);
if (!route.regex) {
compiled = compilePath(path);
route.regex = compiled.regex;
route.map = compiled.map;
}
matched = route.regex.exec(req.pathname);
if (!matched)
if (!match)
return next();
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)
return done(err);
// Avoid stack overflows
util.nextTick(function() {
try {
callback.call(route.ctx, req, res, _send, next);
route.call(req, res, send, next);
} catch (e) {
done(e);
}
@ -255,15 +241,15 @@ HTTPBase.prototype._close = function close(callback) {
* Handle middleware stack.
* @param {HTTPRequest} req
* @param {HTTPResponse} res
* @param {Function} _send
* @param {Function} send
* @returns {Promise}
* @private
*/
HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
HTTPBase.prototype.handleStack = function handleStack(req, res, send, callback) {
var self = this;
var i = 0;
var handler;
var route;
(function next(err) {
if (err)
@ -272,14 +258,14 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
if (i === self.stack.length)
return callback();
handler = self.stack[i++];
route = self.stack[i++];
util.nextTick(function() {
if (handler.path && req.pathname.indexOf(handler.path) !== 0)
if (!route.hasPrefix(req.pathname))
return next();
try {
handler.callback.call(handler.ctx, req, res, _send, next);
route.call(req, res, send, next);
} catch (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.
* @param {String?} path
@ -303,19 +280,20 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) {
*/
HTTPBase.prototype.use = function use(path, callback, ctx) {
var i;
if (!callback) {
callback = path;
path = null;
}
if (Array.isArray(path)) {
path.forEach(function(path) {
this.use(path, callback, ctx);
}, this);
for (i = 0; i < path.length; i++)
this.use(path[i], callback, ctx);
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) {
var i;
if (Array.isArray(path)) {
path.forEach(function(path) {
this.get(path, callback, ctx);
}, this);
for (i = 0; i < path.length; i++)
this.get(path[i], callback, ctx);
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) {
var i;
if (Array.isArray(path)) {
path.forEach(function(path) {
this.post(path, callback, ctx);
}, this);
for (i = 0; i < path.length; i++)
this.post(path[i], callback, ctx);
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) {
var i;
if (Array.isArray(path)) {
path.forEach(function(path) {
this.put(path, callback, ctx);
}, this);
for (i = 0; i < path.length; i++)
this.put(path[i], callback, ctx);
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) {
var i;
if (Array.isArray(path)) {
path.forEach(function(path) {
this.del(path, callback, ctx);
}, this);
for (i = 0; i < path.length; i++)
this.del(path[i], callback, ctx);
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
*/
function send(res, code, msg, type) {
function sendResponse(res, code, msg, type) {
var len;
if (!msg)
assert(typeof code === 'number', 'Code must be a number.');
if (msg == null)
msg = { error: 'No message.' };
try {
res.statusCode = code;
if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) {
msg = JSON.stringify(msg, null, 2) + '\n';
if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) {
msg = JSON.stringify(msg, null, 2) + '\n';
if (!type)
type = 'json';
}
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) {
;
assert(type === 'json', 'Bad type passed with json object.');
}
}
function compilePath(path) {
var map = [];
if (!type)
type = typeof msg === 'string' ? 'txt' : 'bin';
if (path instanceof RegExp)
return { regex: path, map: map };
res.statusCode = code;
res.setHeader('Content-Type', getType(type));
var regex = path
.replace(/(\/[^\/]+)\?/g, '(?:$1)?')
.replace(/\.(?!\+)/g, '\\.')
.replace(/\*/g, '.*?')
.replace(/%/g, '\\')
.replace(/:(\w+)/g, function(__, name) {
map.push(name);
return '([^/]+)';
if (typeof msg === 'string') {
len = Buffer.byteLength(msg, 'utf8');
res.setHeader('Content-Length', len + '');
try {
res.write(msg, 'utf8');
res.end();
} catch (e) {
;
}
);
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 {
map: map,
regex: regex
};
assert(false, 'Bad object passed to send.');
}
function parseBody(req, callback) {
@ -523,11 +628,13 @@ function parseBody(req, callback) {
});
}
function parsePairs(str) {
function parsePairs(str, limit) {
var parts = str.split('&');
var data = {};
var i, index, pair, key, value;
assert(!limit || parts.length <= limit, 'Too many keys in querystring.');
for (i = 0; i < parts.length; i++) {
pair = parts[i];
index = pair.indexOf('=');
@ -556,45 +663,93 @@ function parsePairs(str) {
return data;
}
function parsePath(req) {
var uri = url.parse(req.url);
var pathname = uri.pathname || '/';
function parsePath(req, limit) {
var uri = URL.parse(req.url);
var pathname = uri.pathname;
var query = {};
var path, parts, url;
if (pathname[pathname.length - 1] === '/')
pathname = pathname.slice(0, -1);
if (pathname) {
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] === '/')
req.path = req.path.substring(1);
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 = '/';
pathname = unescape(pathname);
} else {
pathname = '/';
}
if (!req.query) {
req.query = uri.query
? parsePairs(uri.query, '&')
: {};
assert(pathname.length > 0);
assert(pathname[0] === '/');
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) {
try {
str = decodeURIComponent(str).replace(/\+/g, ' ');
} finally {
return str.replace(/\0/g, '');
str = decodeURIComponent(str);
str = str.replace(/\+/g, ' ');
str = 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);
}
}