From 1cda7cb7020ef92a3bd9256acf0f9898fdd6cbdd Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 11 Jan 2017 18:42:08 -0800 Subject: [PATCH] http: make http base server safer. --- browser/server.js | 18 +- lib/http/base.js | 465 ++++++++++++++++++++++++++++++---------------- 2 files changed, 320 insertions(+), 163 deletions(-) diff --git a/browser/server.js b/browser/server.js index 2cfaaa2a..616c6ccc 100644 --- a/browser/server.js +++ b/browser/server.js @@ -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) { diff --git a/lib/http/base.js b/lib/http/base.js index ca84e255..d6e84be6 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -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); } }