UNPKG

backend-js

Version:

Backend-js is a layer built above expressjs to enable behaviours framework for nodejs applications.

627 lines (533 loc) 16.4 kB
/*jslint node: true */ /*jshint esversion: 6 */ "use strict"; var fs = require("fs"); var { createSecureContext } = require("tls"); var { URLSearchParams } = require("url"); var bodyParser = require("body-parser"); var logger = require("morgan"); var HttpStatus = require("http-status-codes"); var rateLimit = require("express-rate-limit"); var session = require("express-session"); var memorystore = require("memorystore"); var debug = require("debug"); var cors = require("cors"); var dotenv = require("dotenv"); var { Server } = require("socket.io"); var { BehavioursServer, compare, resolve, serve, app, routes, behaviour } = require("./src/behaviour.js"); var { ModelEntity, QueryExpression, setComparisonOperators, setLogicalOperators, AggregateExpression, setComputationOperators, setModelController, getModelController, model } = require("./src/model.js"); var { ServiceParameter, ServiceParameterType, service } = require("./src/service.js"); var { setResourceController, getResourceController } = require("./src/resource.js"); var { setCorsOptions, respond } = require("./src/utils.js"); var LIMIT = 5; var HITS = 30; var TIMEOUT = 1000; var WINDOW = HITS * TIMEOUT; var MAX = LIMIT * HITS; var limited = {}; var limiter = rateLimit({ windowMs: WINDOW, max: MAX, delayMs: 0, headers: false, keyGenerator(req, res) { let key; var { ip, socket } = req; if (ip) { key = ip.replace(/:\d+[^:]*$/, ''); } else { key = socket.remoteAddress; } if (!limited[key]) { limited[key] = { count: 0, time: new Date().getTime() }; } var onClose = function () { var ending = !res.writableEnded; if (ending && limited[key]) { limited[key].count += HITS; } }; if (!req.socket.__onClose) { req.socket.__onClose = true; req.socket.on("close", onClose); } return key; }, handler(req, res, next) { let key = this.keyGenerator(req); var time = new Date().getTime(); time -= limited[key].time; var resetting = time > WINDOW; var count = ++limited[key].count; if (resetting) { limited[key] = undefined; delete limited[key]; } var timeout = count / MAX * TIMEOUT; var limitable = count > MAX; if (limitable || resetting) { var limiting = !!limitable; limiting &= time <= WINDOW; if (!limiting) { limiting |= timeout > WINDOW; } if (limiting) { return res.status(...[ this.statusCode ]).send(this.message); } } setTimeout(function () { let not_ended = !req.aborted; not_ended &= !res.headersSent; if (not_ended) next(); }, timeout); } }); debug.enable("backend:*"); var inform = debug("backend:index:info"); inform.log = console.log.bind(console); debug = debug("backend:index"); var server; module.exports = { ModelEntity, QueryExpression, setComparisonOperators, setLogicalOperators, AggregateExpression, setComputationOperators, setModelController, getModelController, ServiceParameter, ServiceParameterType, setResourceController, getResourceController, model, service, behaviour, server(paths, options) { if (server) return server; if (options.env) { var { env } = options; var envOpts = env if (typeof env !== "object") { envOpts = undefined; } dotenv.config(envOpts); } app.disable("x-powered-by"); if (options.proxy) { app.set(...[ "trust proxy", options.proxy ]); } app.use(logger("dev")); app.use(limiter); var corsDelegate = function () { let [ req, callback ] = arguments; var corsOptions = { origin: false, credentials: true }; var maxAge = options.maxAge; var keys = Object.keys(routes); let length = keys.length; for (var i = 0; i < length; i++) { var routeOptions = routes[ keys[i] ]; var { prefix } = routeOptions; if (!prefix) { prefix = options.path; } var method; var { method: rM } = routeOptions; let _ = typeof rM; var valid = _ === "string"; if (valid) { rM = rM.toLowerCase(); _ = typeof app[rM]; valid &= _ === "function"; } if (valid) method = rM; var { origins } = routeOptions; if (origins === undefined) { ({ origins } = options); } _ = typeof origins; var allow = _ !== "string"; if (!allow) { allow |= origins.length === 0; } if (allow) { origins = origins === true; } var query, path = req.path; if (!path) { path = req.originalUrl; } if (!path) path = req.url; ([ path, query ] = path.split("?")); var events_path = false; let eventful = !!query; eventful &= !!routeOptions.events; rM = req.method.toLowerCase(); if (eventful && compare({ path: resolve(...[ prefix, "/events", path ]) }, { path }) && rM === "get") { query = new URLSearchParams(...[ query ]).toString(); if (keys[i] == query.behaviour) { events_path = true; } } var cors_ready = !!origins; if (cors_ready) { cors_ready = events_path; if (!cors_ready) { cors_ready = compare({ path: resolve(...[ prefix, routeOptions.path, path ]) }, { path }); cors_ready &= [ method, "options" ].indexOf(rM) > -1; } } if (cors_ready) { setCorsOptions(...[ corsOptions, origins, routeOptions, req ]); let { maxAge: mA } = routeOptions if (mA != undefined) { maxAge = mA; } break; } } if (!isNaN(parseInt(maxAge))) { corsOptions.maxAge = maxAge; } callback(null, corsOptions); }; app.all("/*", cors(corsDelegate)); var { parser, format } = options; let __ = typeof format; var parsing = __ === "string"; if (!parsing) { __ = typeof parser; parsing = __ === "string"; if (parsing) format = parser; } if (!parsing) { __ = typeof parser; parsing = __ === "object"; if (parsing) { ({ format } = parser); __ = typeof format; parsing = __ === "string"; } } if (!parsing) format = undefined; var { upgrade, validate, connect } = new BehavioursServer(...[ options.path, format, paths, options.operations, options.tenants, { schedule: options.schedule } ]); var proxied = typeof paths === "object"; if (proxied) { let _ = typeof paths.proxy; proxied &= _ === "string"; if (proxied) { proxied &= paths.proxy.length > 0; } } if (proxied) require(paths.proxy); if (typeof options.static === "object") { let _ = typeof options.static.route; if (_ === "string") { app.use(...[ options.static.route, serve(...[ options.static.path, options.static ]) ]); } else app.use(serve(...[ options.static.path, options.static ])); } app.use(session = session(function () { var { cookie } = options; if (typeof cookie !== "object") { cookie = {}; } var store; if (!cookie || !cookie.store) { var MemoryStore = memorystore(...[ session ]); store = new MemoryStore(); } return Object.assign({ name: "behaviours.sid", secret: "" + new Date().getTime(), resave: false, saveUninitialized: true, store }, cookie); }())); var parserOptions = parser; __ = typeof parserOptions; if (__ !== "object") { ({ parserOptions } = options); } __ = typeof parserOptions; if (__ !== "object") { parserOptions = undefined; } if (!parserOptions) { parserOptions = undefined; } if (parsing) { __ = typeof bodyParser[format]; if (__ === "function") { parser = bodyParser[format](...[ parserOptions ]); } } else { __ = typeof parser; if (__ !== "function") { parser = bodyParser.json(...[ parserOptions ]); } } __ = typeof parser; if (__ === "function") { app.use(parser); } __ = typeof paths; var requiring = __ === "string"; if (requiring) { requiring &= paths.length > 0; } if (requiring) require(paths); else { requiring = __ === "object"; if (requiring) { __ = typeof paths.local; requiring &= __ === "string"; if (requiring) { let { length } = paths.local; requiring &= length > 0; } } if (requiring) { require(paths.local); } } app.use(function (req, res, next) { var err = new Error("Not found"); if (/[A-Z]/.test(req.path)) { err = new Error("Not " + "found, maybe the " + "case-sensitivity of " + "the path"); } err.code = 404; next(err); }); app.use(function (err, req, res, next) { debug(err); if (res.headersSent) { return next(err); } respond(res.status(...[ HttpStatus.getStatus(...[ err.code ]) || 500 ]), { behaviour: err.behaviour, name: err.name, version: err.version, message: err.message }, format); }); __ = typeof options.https; var https = __ === "object"; var port = options.port; if (!port) port = process.env.PORT; if (!port) port = https ? 443 : 80; app.set("port", port); var protocol = https ? "https" : "http"; server = require(...[ protocol ]).createServer(https ? function read() { ([https] = arguments); var createSC = function (domain) { return createSecureContext(...[ read(domains[domain]) ]); }; __ = typeof https.domains; var domains = __ === "object"; if (domains) { ({ domains } = https); Object.keys(domains).forEach(...[ function (domain) { domains[ domain ] = createSC(domain); } ]); https.SNICallback = function () { var [ domain, cb ] = arguments; var ctx = domains[domain]; if (cb) cb(null, ctx); else { return ctx; } }; } return [ "key", "cert", "ca" ].reduce(function (opts, opt) { var path = https[opt]; __ = typeof path; var existed = __ === "string"; existed &= fs.existsSync(path); if (existed) { opts[ opt ] = fs.readFileSync(...[ path ]).toString(); } return opts; }, https); }(options.https) : app, function () { return https ? app : undefined; }()); var io = new Server(server, function () { var { websocket } = options; if (typeof websocket !== "object") { websocket = {}; } return Object.assign({ cors: corsDelegate, allowEIO3: true }, websocket); }()); io.of(function (path, query, next) { var err = validate(path, query); next(err, !err); }).on("connect", function (socket) { socket.once(...[ "disconnect", function () { inform("backend " + "socket:" + socket.id + " disconnected on port " + app.get("port")); } ]); connect(socket); }).use(function (socket, next) { session(socket.handshake, {}, next); }); server.removeAllListeners("upgrade"); server.on("upgrade", function () { let [ req, socket, head ] = arguments; if (!upgrade(req, socket, head)) { io.engine.handleUpgrade(...[ req, socket, head ]); } }); server.listen(...[ app.get("port"), function () { inform("backend listening on " + "port " + app.get("port")); } ]); return server; }, app(paths, options) { if (server) return app; this.server(paths, options); return app; } };