fcoin/lib/http/base.js
2017-01-11 18:42:08 -08:00

761 lines
14 KiB
JavaScript

/*!
* http.js - http server for bcoin
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
* Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
var assert = require('assert');
var AsyncObject = require('../utils/async');
var util = require('../utils/util');
var URL = require('url');
/**
* HTTPBase
* @exports HTTPBase
* @constructor
* @param {Object?} options
* @emits HTTPBase#websocket
*/
function HTTPBase(options) {
if (!(this instanceof HTTPBase))
return new HTTPBase(options);
if (!options)
options = {};
AsyncObject.call(this);
this.options = options;
this.io = null;
this.routes = new Routes();
this.stack = [];
this.server = options.key
? require('https').createServer(options)
: require('http').createServer();
this._init();
}
util.inherits(HTTPBase, AsyncObject);
/**
* Initialize server.
* @private
*/
HTTPBase.prototype._init = function _init() {
var self = this;
this._initRouter();
this._initIO();
this.server.on('connection', function(socket) {
socket.on('error', function(err) {
var str;
if (err.message === 'Parse Error') {
str = 'http_parser.execute failure (';
str += 'parsed=' + (err.bytesParsed || -1);
str += ' code=' + err.code;
str += ')';
err = new Error(str);
}
self.emit('error', err);
try {
socket.destroy();
} catch (e) {
;
}
});
});
this.server.on('error', function(err) {
self.emit('error', err);
});
};
/**
* Initialize router.
* @private
*/
HTTPBase.prototype._initRouter = function _initRouter() {
var self = this;
this.server.on('request', function(req, res) {
var i, j, routes, route, match, item;
function send(code, msg, type) {
sendResponse(res, code, msg, type);
}
function done(err) {
if (err) {
send(err.statusCode || 400, { error: err.message + '' });
try {
req.destroy();
req.socket.destroy();
} catch (e) {
;
}
self.emit('error', err);
}
}
try {
parsePath(req, 100);
} catch (e) {
return done(e);
}
self.emit('request', req, res);
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);
(function next(err) {
if (err)
return done(err);
if (i === routes.length)
return done(new Error('Route not found.'));
route = routes[i++];
match = route.match(req.pathname);
if (!match)
return next();
req.params = {};
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 {
route.call(req, res, send, next);
} catch (e) {
done(e);
}
});
});
})();
});
});
};
/**
* Initialize websockets.
* @private
*/
HTTPBase.prototype._initIO = function _initIO() {
var self = this;
var IOServer;
if (!this.options.sockets)
return;
try {
IOServer = require('socket.io');
} catch (e) {
;
}
if (!IOServer)
return;
this.io = new IOServer({
transports: ['websocket']
});
this.io.attach(this.server);
this.io.on('connection', function(socket) {
self.emit('websocket', socket);
});
};
/**
* Open the server.
* @alias HTTPBase#open
* @returns {Promise}
*/
HTTPBase.prototype._open = function open() {
assert(typeof this.options.port === 'number', 'Port required.');
return this.listen(this.options.port, this.options.host);
};
/**
* Close the server.
* @alias HTTPBase#close
* @returns {Promise}
*/
HTTPBase.prototype._close = function close(callback) {
var self = this;
return new Promise(function(resolve, reject) {
if (self.io) {
self.server.once('close', resolve);
self.io.close();
return;
}
self.server.close(function(err) {
if (err)
return reject(err);
resolve();
});
});
};
/**
* Handle middleware stack.
* @param {HTTPRequest} req
* @param {HTTPResponse} res
* @param {Function} send
* @returns {Promise}
* @private
*/
HTTPBase.prototype.handleStack = function handleStack(req, res, send, callback) {
var self = this;
var i = 0;
var route;
(function next(err) {
if (err)
return callback(err);
if (i === self.stack.length)
return callback();
route = self.stack[i++];
util.nextTick(function() {
if (!route.hasPrefix(req.pathname))
return next();
try {
route.call(req, res, send, next);
} catch (e) {
next(e);
}
});
})();
};
/**
* Add a middleware to the stack.
* @param {String?} path
* @param {RouteCallback} callback
*/
HTTPBase.prototype.use = function use(path, callback, ctx) {
var i;
if (!callback) {
callback = path;
path = null;
}
if (Array.isArray(path)) {
for (i = 0; i < path.length; i++)
this.use(path[i], callback, ctx);
return;
}
this.stack.push(new Route(ctx, path, callback));
};
/**
* Add a GET route.
* @param {String?} path
* @param {RouteCallback} callback
*/
HTTPBase.prototype.get = function get(path, callback, ctx) {
var i;
if (Array.isArray(path)) {
for (i = 0; i < path.length; i++)
this.get(path[i], callback, ctx);
return;
}
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.get.push(new Route(ctx, path, callback));
};
/**
* Add a POST route.
* @param {String?} path
* @param {RouteCallback} callback
*/
HTTPBase.prototype.post = function post(path, callback, ctx) {
var i;
if (Array.isArray(path)) {
for (i = 0; i < path.length; i++)
this.post(path[i], callback, ctx);
return;
}
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.post.push(new Route(ctx, path, callback));
};
/**
* Add a PUT route.
* @param {String?} path
* @param {RouteCallback} callback
*/
HTTPBase.prototype.put = function put(path, callback, ctx) {
var i;
if (Array.isArray(path)) {
for (i = 0; i < path.length; i++)
this.put(path[i], callback, ctx);
return;
}
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.put.push(new Route(ctx, path, callback));
};
/**
* Add a DELETE route.
* @param {String?} path
* @param {RouteCallback} callback
*/
HTTPBase.prototype.del = function del(path, callback, ctx) {
var i;
if (Array.isArray(path)) {
for (i = 0; i < path.length; i++)
this.del(path[i], callback, ctx);
return;
}
assert(typeof path === 'string');
assert(path.length > 0);
this.routes.del.push(new Route(ctx, path, callback));
};
/**
* Get server address.
* @returns {Object}
*/
HTTPBase.prototype.address = function address() {
return this.server.address();
};
/**
* Listen on port and host.
* @param {Number} port
* @param {String?} host
* @returns {Promise}
*/
HTTPBase.prototype.listen = function listen(port, host) {
var self = this;
return new Promise(function(resolve, reject) {
var addr;
self.server.listen(port, host, function(err) {
if (err)
return reject(err);
addr = self.address();
self.emit('listening', addr);
resolve(addr);
});
});
};
/**
* 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 sendResponse(res, code, msg, type) {
var len;
assert(typeof code === 'number', 'Code must be a number.');
if (msg == null)
msg = { error: 'No message.' };
if (msg && typeof msg === 'object' && !Buffer.isBuffer(msg)) {
msg = JSON.stringify(msg, null, 2) + '\n';
if (!type)
type = 'json';
assert(type === 'json', 'Bad type passed with json object.');
}
if (!type)
type = typeof msg === 'string' ? 'txt' : 'bin';
res.statusCode = code;
res.setHeader('Content-Type', getType(type));
if (typeof msg === 'string') {
len = Buffer.byteLength(msg, 'utf8');
res.setHeader('Content-Length', len + '');
try {
res.write(msg, 'utf8');
res.end();
} catch (e) {
;
}
return;
}
if (Buffer.isBuffer(msg)) {
res.setHeader('Content-Length', msg.length + '');
try {
res.write(msg);
res.end();
} catch (e) {
;
}
return;
}
assert(false, 'Bad object passed to send.');
}
function parseBody(req, callback) {
var StringDecoder = require('string_decoder').StringDecoder;
var decode = new StringDecoder('utf8');
var total = 0;
var body = '';
req.body = {};
if (req.method === 'GET')
return callback();
req.on('data', function(data) {
total += data.length;
if (total > 20 * 1024 * 1024)
return callback(new Error('Overflow.'));
body += decode.write(data);
});
req.on('error', function(err) {
try {
req.destroy();
req.socket.destroy();
} catch (e) {
;
}
callback(err);
});
req.on('end', function() {
try {
if (body)
req.body = JSON.parse(body);
} catch (e) {
return callback(e);
}
callback();
});
}
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('=');
if (index === -1) {
key = pair;
value = '';
} else {
key = pair.substring(0, index);
value = pair.substring(index + 1);
}
key = unescape(key);
if (key.length === 0)
continue;
value = unescape(value);
if (value.length === 0)
continue;
data[key] = value;
}
return data;
}
function parsePath(req, limit) {
var uri = URL.parse(req.url);
var pathname = uri.pathname;
var query = {};
var path, parts, url;
if (pathname) {
pathname = pathname.replace(/\/{2,}/g, '/');
if (pathname[0] !== '/')
pathname = '/' + pathname;
if (pathname.length > 1) {
if (pathname[pathname.length - 1] === '/')
pathname = pathname.slice(0, -1);
}
pathname = unescape(pathname);
} else {
pathname = '/';
}
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) {
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);
}
}
/*
* Expose
*/
module.exports = HTTPBase;