UNPKG

@ne1410s/xprest

Version:
421 lines (404 loc) 17.2 kB
'use strict'; var cors = require('cors'); var xp = require('express'); var bodyParser = require('body-parser'); var ejs = require('ejs'); var path = require('path'); var fs = require('fs'); var jws = require('jws'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var ejs__namespace = /*#__PURE__*/_interopNamespaceDefault(ejs); var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path); var jws__namespace = /*#__PURE__*/_interopNamespaceDefault(jws); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var PipelineError = /** @class */ (function (_super) { __extends(PipelineError, _super); function PipelineError(status, data, message) { var _this = _super.call(this, message) || this; _this.status = status; _this.data = data; return _this; } return PipelineError; }(Error)); var Xprest = /** @class */ (function () { function Xprest() { this.api = xp(); this.api.use(cors()); this.api.use(bodyParser.json); this.api.engine('html', ejs__namespace.renderFile); this.api.engine('js', ejs__namespace.renderFile); } /** * Applies global middleware. Example uses include providing default headers, * logging, etc. * @param handlers */ Xprest.prototype.global = function () { var _this = this; var handlers = []; for (var _i = 0; _i < arguments.length; _i++) { handlers[_i] = arguments[_i]; } var procs = handlers.map(function (p) { return _this.convert(p); }); this.api.use(procs); }; /** * Specifies a static file resource. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. */ Xprest.prototype.resource = function (apiRoute, localPath, mdwPre, mdwPost) { var _a; var _this = this; var inner = function (req, res, next) { res.sendFile(path__namespace.resolve(process.cwd(), localPath)); }; var procs = [inner]; if (mdwPre) procs.unshift.apply(procs, mdwPre.map(function (p) { return _this.convert(p); })); if (mdwPost) procs.push.apply(procs, mdwPost.map(function (p) { return _this.convert(p); })); (_a = this.api).get.apply(_a, __spreadArray([apiRoute], procs, false)); }; /** * Specifies a dynamic file resource. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. * @param variables Exposed in the rendering of the file. For example: * <%= new Date().getTime() * myVar.myProp %> */ Xprest.prototype.render = function (apiRoute, localPath, variables, mdwPre, mdwPost) { var _a; var _this = this; var inner = function (req, res, next) { res.render(path__namespace.resolve(process.cwd(), localPath), variables); }; var procs = [inner]; if (mdwPre) procs.unshift.apply(procs, mdwPre.map(function (p) { return _this.convert(p); })); if (mdwPost) procs.push.apply(procs, mdwPost.map(function (p) { return _this.convert(p); })); (_a = this.api).get.apply(_a, __spreadArray([apiRoute], procs, false)); }; /** * Specifies a streamable resource; e.g. video. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. * @param mime The mime type. */ Xprest.prototype.stream = function (apiRoute, localPath, mime, mdwPre) { var _a; var _this = this; var inner = this.xp_Stream(localPath, mime); var procs = [inner]; if (mdwPre) procs.push.apply(procs, mdwPre.map(function (p) { return _this.convert(p); })); (_a = this.api).get.apply(_a, __spreadArray([apiRoute], procs, false)); }; /** * Specifies a restful api endpoint. * @param apiRoute The api route. * @param verb The verb. * @param handler Handles requests. */ Xprest.prototype.endpoint = function (apiRoute, verb) { var _a; var _this = this; var handlers = []; for (var _i = 2; _i < arguments.length; _i++) { handlers[_i - 2] = arguments[_i]; } var procs = handlers.map(function (p) { return _this.convert(p); }); (_a = this.api)[verb].apply(_a, __spreadArray([apiRoute], procs, false)); }; /** * Starts listening for the specified requests. * @param port The port. * @param onready Called once listening is in place. */ Xprest.prototype.start = function (port, onready) { this.api.listen(port, onready); }; /** Provides express pipeline handler for media streaming. */ Xprest.prototype.xp_Stream = function (localPath, mime) { var filePath = path__namespace.resolve(process.cwd(), localPath); return function (req, res) { fs.stat(filePath, function (err, status) { var range = req.headers.range; if (range) { var parts = range.replace(/bytes=/, '').split('-'); var start = parseInt(parts[0], 10); var end = parts[1] ? parseInt(parts[1], 10) : status.size - 1; var chunksize = end - start + 1; var file = fs.createReadStream(filePath, { start: start, end: end }); res.writeHead(206, { 'Content-Range': "bytes ".concat(start, "-").concat(end, "/").concat(status.size), 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': mime, }); file.pipe(res); } else { res.writeHead(200, { 'Content-Length': status.size, 'Content-Type': mime, }); fs.createReadStream(filePath).pipe(res); } }); }; }; /** Converts middleware functions to express-style handlers. */ Xprest.prototype.convert = function (mdw) { return function (hReq, hRes, next) { var _a, _b, _c; var dataOut; var subIn = { data: __assign(__assign(__assign({}, hReq.body), hReq.query), hReq.params), requestHeaders: hReq.headers, responseHeaders: hRes.getHeaders(), status: hRes.statusCode, }; try { dataOut = mdw(subIn); } catch (ex) { var e = typeof ex === 'string' ? ex : ((_b = (_a = ex.data) !== null && _a !== void 0 ? _a : ex.message) !== null && _b !== void 0 ? _b : 'Internal Server Error'); hRes.statusCode = (_c = ex.status) !== null && _c !== void 0 ? _c : 500; if (!hRes.headersSent) hRes.send({ error: e }); else console.warn('Unable to set error - already sent', ex); return; // ensure no further actions } var updateKeys = Object.keys(subIn.responseHeaders).filter(function (k) { return !hRes.hasHeader(k) || hRes.getHeader(k) !== subIn.responseHeaders[k]; }); var deleteKeys = Object.keys(hRes.getHeaders()).filter(function (k) { return !subIn.responseHeaders[k]; }); if (!hRes.headersSent) { updateKeys.forEach(function (k) { return hRes.setHeader(k, subIn.responseHeaders[k]); }); deleteKeys.forEach(function (k) { return hRes.removeHeader(k); }); if (subIn.status !== hRes.statusCode) hRes.statusCode = subIn.status; if (dataOut !== undefined) hRes.send(dataOut); } else { var delta = updateKeys.concat(deleteKeys); if (delta.length > 0) console.warn('Unable to edit headers - already sent', delta); if (subIn.status !== hRes.statusCode) console.warn('Unable to set status - already sent'); if (dataOut !== undefined) console.warn('Unable to send data - already sent', dataOut); } next(); }; }; return Xprest; }()); var XprestSecure = /** @class */ (function (_super) { __extends(XprestSecure, _super); function XprestSecure(jwtIssuer) { var _this = _super.call(this) || this; _this.jwtIssuer = jwtIssuer; return _this; } /** * Specifies a (secure) static file resource. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. */ XprestSecure.prototype.resourceSecure = function (apiRoute, localPath, roles, mdwPre, mdwPost) { mdwPre.unshift(this.mdw_CheckRole(roles)); _super.prototype.resource.call(this, apiRoute, localPath, mdwPre, mdwPost); }; /** * Specifies a (secure) dynamic file resource. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. * @param variables Exposed in the rendering of the file. For example: * <%= new Date().getTime() * myVar.myProp %> */ XprestSecure.prototype.renderSecure = function (apiRoute, localPath, variables, roles, mdwPre, mdwPost) { mdwPre.unshift(this.mdw_CheckRole(roles)); _super.prototype.render.call(this, apiRoute, localPath, variables, mdwPre, mdwPost); }; /** * Specifies a (secure) streamable resource; e.g. video. * @param apiRoute The api route. * @param localPath The file path, relative to the cwd. * @param mime The mime type. */ XprestSecure.prototype.streamSecure = function (apiRoute, localPath, mime, roles, mdwPre) { mdwPre.unshift(this.mdw_CheckRole(roles)); _super.prototype.stream.call(this, apiRoute, localPath, mime, mdwPre); }; /** * Specifies a (secure) restful api endpoint. * @param apiRoute The api route. * @param verb The verb. * @param handler Handles requests. */ XprestSecure.prototype.endpointSecure = function (apiRoute, verb, roles) { var handlers = []; for (var _i = 3; _i < arguments.length; _i++) { handlers[_i - 3] = arguments[_i]; } handlers.unshift(this.mdw_CheckRole(roles)); _super.prototype.endpoint.apply(this, __spreadArray([apiRoute, verb], handlers, false)); }; /** * Specifies a route at which to listen for POST login requests and a function * to dictate behaviour. For valid login attempts, return a payload object - * in accordance with the user. Subject (sub) and roles (rol) are suggested. * For invalid attempts, a null payload must be returned. */ XprestSecure.prototype.authenticate = function (apiRoute, payloadFn, mdwPre, mdwPost) { var handlers = [this.mdw_IssueToken(payloadFn)]; if (mdwPre) handlers.unshift.apply(handlers, mdwPre); if (mdwPost) handlers.push.apply(handlers, mdwPost); this.endpoint.apply(this, __spreadArray([apiRoute, 'post'], handlers, false)); }; XprestSecure.prototype.mdw_IssueToken = function (fn) { var _this = this; return function (sub) { var payload = fn(sub.data); if (!payload) throw new PipelineError(401, 'User not recognised'); return { token: _this.jwtIssuer.issue(payload) }; }; }; XprestSecure.prototype.mdw_CheckRole = function (roles) { var _this = this; var tokenRegex = /^[Bb]earer ([\w-]*\.[\w-]*\.[\w-]*)$/; return function (sub) { var _a; var authz = sub.requestHeaders['authorization']; var token = ((authz || '').match(tokenRegex) || [])[1] || ''; var jwt = _this.jwtIssuer.parseValid(token); if (!jwt) throw new PipelineError(401, 'User not recognised'); if (roles.length && !((_a = jwt.rol) !== null && _a !== void 0 ? _a : []).some(function (r) { return roles.includes(r); })) { throw new PipelineError(403, 'User not authorized'); } }; }; return XprestSecure; }(Xprest)); /** A host for issuing json web tokens. */ var JwtIssuer = /** @class */ (function () { /** Initialises a new instance. */ function JwtIssuer(issuer, secret, minutes) { if (minutes === void 0) { minutes = 15; } this.issuer = issuer; this.secret = secret; this.minutes = minutes; } /** Issues a token containing the supplied payload. */ JwtIssuer.prototype.issue = function (payload) { this.preparePayload(payload); return jws__namespace.sign({ header: { alg: 'HS256' }, payload: payload, secret: this.secret, }); }; /** Parses the contents of a (valid) token. */ JwtIssuer.prototype.parseValid = function (token) { var now = new Date().getTime() / 1000; var payload = this.parseRaw(token); return payload && (!payload.exp || now < payload.exp) && (!payload.nbf || now >= payload.nbf) && jws__namespace.verify(token, 'HS256', this.secret) ? payload : null; }; /** Parses the contents of a token. */ JwtIssuer.prototype.parseRaw = function (token) { var decoded = jws__namespace.decode(token); return decoded ? JSON.parse(decoded.payload) : null; }; /** Prepares payload according to current time and instance config. */ JwtIssuer.prototype.preparePayload = function (payload) { payload.iss = this.issuer; var date = new Date(); payload.iat = date.getTime() / 1000; payload.nbf = payload.nbf || payload.iat; if (!payload.exp) { date.setMinutes(date.getMinutes() + this.minutes); payload.exp = date.getTime() / 1000; } }; return JwtIssuer; }()); exports.JwtIssuer = JwtIssuer; exports.PipelineError = PipelineError; exports.Xprest = Xprest; exports.XprestSecure = XprestSecure;