nymi-roaming-authenticator
Version:
An illustration of how one might write a roaming authenticator for Nymi Bands
173 lines (155 loc) • 5.58 kB
JavaScript
'use strict';
const
http = require('http'),
stringify = require('fast-safe-stringify'),
HttpServer = http.Server,
wayfarer = require('wayfarer'),
methods = ['get', 'put', 'post', 'delete'];
class Server extends HttpServer
{
/**
* Create Server instance
* @param {object} [opts]
* @param {function} [listener]
*/
constructor (opts, listener)
{
super(listener);
this._opts = opts || {};
this._opts.maxSize = this._opts.maxSize || 512 * 1024;
this._routers = new Map();
this._middleware = [];
this.use((req, res, next) => {
if (req.method === 'POST' || req.method === 'PUT') {
if (req.headers['content-type'] !== 'application/json') {
return res.err(`POST requests must be application/json not ${req.headers['content-type']}`, 415);
} else if (req.headers['content-length'] > this._opts.maxSize) {
return res.err(`${req.headers['content-length']} exceeds maximum size for requests`, 413);
}
}
next();
});
this.use((req, res, next) => {
let body = [];
if (~~req.headers['content-length'] === 0) {
return next();
}
if ((req.method === 'POST' || req.method === 'PUT') && !req.body) {
req.on('data', chunk => {
if (chunk.length > this._opts.maxSize || Buffer.byteLength(body.join('')) > this._opts.maxSize) {
return res.err('Payload size exceeds maxmium body length', 413)
}
body.push(chunk.toString());
});
req.on('end', () => {
if (body.length) {
try {
req.body = JSON.parse(body.join(''));
} catch (err) {
return res.err('Payload is not valid JSON', 400);
}
}
next();
});
return;
}
next();
});
this.on('request', (req, res) => this._handleReq(req, this._augmentRes(res), this._middleware, this._routers));
}
/**
* Use
* @param {function|function[]} functions
* @return {void}
*/
use (functions)
{
this._middleware.push.apply(this._middleware, Array.isArray(functions) ? functions : [functions]);
}
/**
* Router
* @param {string} namespace
* @typedef {object} Router
* @property {function} get
* @property {function} post
* @property {function} put
* @property {function} delete
* @returns {Router}
*/
router (namespace)
{
return methods.reduce((r, m) => Object.assign({[m]: (ma, f) => this._addRoute(m, [namespace, ma], f)}, r), {});
}
/**
* Add route
* @param {string} method
* @param {string[]} matcher
* @param {function|function[]} funcs
* @private
* @return {void}
*/
_addRoute (method, matcher, funcs)
{
if (!this._routers.has(method)) {
this._routers.set(method, wayfarer('/_'));
}
let route = matcher.join('/').replace(/^\//, '').replace(/\/+/g, '/');
this._routers.get(method).on(route, (params, req, res) => this._handleReq(req, res, funcs, false, params));
}
/**
* Handle requests
* @param {object} req
* @param {object} res
* @param {function[]} routeList
* @param {Map|boolean} routers
* @param {object} [params]
* @private
* @return {void}
*/
_handleReq (req, res, routeList, routers, params) {
let len, count = 0, lcm = req.method.toLowerCase(),
next = err => {
if(err) {
return res.err(500);
}
if(++count < len) {
return setImmediate(() => routeList[count](req, res, next));
} else if(routers) {
try {
routers.get(lcm)(req.url.split('?')[0], req, res, next);
} catch (err) {
return res.err(404);
}
} else {
!res.finished && res.end();
}
};
routeList = Array.isArray(routeList) ? routeList : [routeList];
len = routeList.length;
req.urlParams = params;
setImmediate(() => routeList[count](req, res, next));
}
/**
* Augment response
* @param {object} res
* @return {object}
* @private
*/
_augmentRes (res)
{
res.setHeader('content-type', 'application/json');
//noinspection JSUnusedGlobalSymbols
return Object.assign(res, {
send: (content, code) => {
res.statusCode = ~~code || 200;
res.end(content ? stringify(content) : '', 'utf8');
},
err: (content, code) => {
res.statusCode = ~~code || ~~content || 500;
res.statusMessage = http.STATUS_CODES[res.statusCode];
res.end(content && typeof content !== 'number' ? stringify(content) : '', 'utf8');
}
});
}
}
module.exports = Server;