UNPKG

@batolye/bdk-core

Version:

Module to provide core utilities for BulusAtolyesi applications and services

510 lines (391 loc) 18.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.configureAuth = configureAuth; exports.configureService = configureService; exports.createService = createService; exports.setupLogger = setupLogger; exports.setupSockets = setupSockets; var _path = _interopRequireDefault(require("path")); var _debug = _interopRequireDefault(require("debug")); var _lodash = _interopRequireDefault(require("lodash")); var _limiter = require("limiter"); var _expressRateLimit = _interopRequireDefault(require("express-rate-limit")); var _errors = require("@feathersjs/errors"); var _authentication = _interopRequireDefault(require("@feathersjs/authentication")); var _authenticationJwt = _interopRequireDefault(require("@feathersjs/authentication-jwt")); var _authenticationLocal = _interopRequireDefault(require("@feathersjs/authentication-local")); var _authenticationOauth = _interopRequireDefault(require("@feathersjs/authentication-oauth2")); var _passportGithub = _interopRequireDefault(require("passport-github")); var _passportGoogleOauth = _interopRequireDefault(require("passport-google-oauth20")); var _verifier = _interopRequireDefault(require("./verifier")); var _passwordValidator = _interopRequireDefault(require("password-validator")); var _winston = _interopRequireDefault(require("winston")); require("winston-daily-rotate-file"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const debug = (0, _debug.default)("batolye:bdk-core:application-helper"); const debugLimiter = (0, _debug.default)("batolye:bdk-core:application:limiter"); function configureAuth() { const app = this; const config = app.get("authentication"); if (!config) return; const limiter = config.limiter; if (limiter && limiter.http) { app.use(config.path, new _expressRateLimit.default(limiter.http)); } // Store availalbe OAuth2 providers app.core_OAuthProviders = []; // Get access to password validator if a policy is defined if (config.passwordPolicy) { let validator; app.core_getPasswordPolicy = function () { // Create on first access, should not be done outside a function because the app has not yet been correctly initialized if (validator) return validator; let { minLength, maxLength, uppercase, lowercase, digits, symbols, noSpaces, prohibited } = config.passwordPolicy; validator = new _passwordValidator.default(); if (minLength) validator.is().min(minLength); if (maxLength) validator.is().max(maxLength); if (uppercase) validator.has().uppercase(); if (lowercase) validator.has().lowercase(); if (digits) validator.has().digits(); if (symbols) validator.has().symbols(); if (noSpaces) validator.not().spaces(); if (prohibited) validator.is().not().oneOf(prohibited); // Add util functions/options to compare with previous passwords stored in history when required const verifier = new _authenticationLocal.default.Verifier(app, _lodash.default.merge({ usernameField: "email", passwordField: "password" }, _lodash.default.pick(config, ["service"]), config.local)); validator.comparePassword = verifier._comparePassword; validator.options = config.passwordPolicy; return validator; }; } // Set up authentication with the secret app.configure((0, _authentication.default)(config)); app.configure((0, _authenticationJwt.default)()); app.configure((0, _authenticationLocal.default)()); if (config.github) { app.configure((0, _authenticationOauth.default)({ name: "github", Strategy: _passportGithub.default, Verifier: _verifier.default })); app.core_OAuthProviders.push("github"); } if (config.google) { app.configure((0, _authenticationOauth.default)({ name: "google", Strategy: _passportGoogleOauth.default, Verifier: _verifier.default })); app.core_OAuthProviders.push("google"); } // The `authentication` service is used to create a JWT. // The before `create` hook registers strategies that can be used // to create a new valid JWT (e.g. local or oauth2) app.core_getService("authentication").hooks({ before: { create: [_authentication.default.hooks.authenticate(config.strategies)], remove: [_authentication.default.hooks.authenticate("jwt")] } }); } function declareService(path, app, service, middlewares = {}) { const feathersPath = app.get("apiPath") + "/" + path; let feathersService = app.service(feathersPath); // Some internal Feathers service might internally declare the service if (feathersService) { return feathersService; } // Initialize our service by providing any middleware as well let args = [feathersPath]; if (middlewares.before) args = args.concat(middlewares.before); args.push(service); if (middlewares.after) args = args.concat(middlewares.after); if (args.length) app.use.apply(app, args); debug("Service declared on path " + feathersPath); // Return the Feathers service, ie base service + Feathers' internals feathersService = app.service(feathersPath); return feathersService; } function configureService(name, service, servicesPath) { try { const hooks = require(_path.default.join(servicesPath, name, name + ".hooks")); service.hooks(hooks); debug(name + " service hooks configured on path " + servicesPath); } catch (error) { debug("No " + name + " service hooks configured on path " + servicesPath); if (error.code !== "MODULE_NOT_FOUND") { // Log error in this case as this might be linked to a syntax error in required file debug(error); } // As this is optionnal this require has to fail silently } try { const channels = require(_path.default.join(servicesPath, name, name + ".channels")); _lodash.default.forOwn(channels, (publisher, event) => { if (event === "all") service.publish(publisher);else service.publish(event, publisher); }); debug(name + " service channels configured on path " + servicesPath); } catch (error) { debug("No " + name + " service channels configured on path " + servicesPath); if (error.code !== "MODULE_NOT_FOUND") { // Log error in this case as this might be linked to a syntax error in required file debug(error); } // As this is optionnal this require has to fail silently } return service; } function createProxyService(options) { const targetService = options.service; function proxyParams(params) { if (options.params) { let proxiedParams; if (options.params === "function") { proxiedParams = options.params(params); } else { proxiedParams = _lodash.default.merge(params, options.params); } return proxiedParams; } else return params; } function proxyId(id) { if (options.id) return options.id(id);else return id; } function proxyData(data) { if (options.data) return options.data(data);else return data; } function proxyResult(data) { if (options.result) return options.result(data);else return data; } return { async find(params) { return proxyResult((await targetService.find(proxyParams(params)))); }, async get(id, params) { return proxyResult((await targetService.get(proxyId(id), proxyParams(params)))); }, async create(data, params) { return proxyResult((await targetService.create(proxyData(data), proxyParams(params)))); }, async update(id, data, params) { return proxyResult((await targetService.update(proxyId(id), proxyData(data), proxyParams(params)))); }, async patch(id, data, params) { return proxyResult((await targetService.patch(proxyId(id), proxyData(data), proxyParams(params)))); }, async remove(id, params) { return proxyResult((await targetService.remove(proxyId(id), proxyParams(params)))); } }; } function createService(name, app, options = {}) { const createFeathersService = require("feathers-" + app.db.adapter); const paginate = app.get("paginate"); let serviceOptions = Object.assign({ name, paginate }, options); // For DB services a model has to be provided let fileName = options.fileName || name; let dbService = false; try { if (serviceOptions.modelsPath) { const configureModel = require(_path.default.join(serviceOptions.modelsPath, fileName + ".model." + app.db.adapter)); serviceOptions.Model = configureModel(app, serviceOptions); dbService = true; } } catch (error) { debug("No " + fileName + " service model configured on path " + options.modelsPath); if (error.code !== "MODULE_NOT_FOUND") { // Log error in this case as this might be linked to a syntax error in required file debug(error); } // As this is optionnal this require has to fail silently } // Initialize our service with any options it requires let service; if (dbService) { service = createFeathersService(serviceOptions); // service.core_options içerisinde bulunan id değeri atanır. mongo için id: '_id' // https://github.com/feathersjs-ecosystem/feathers-mongodb/blob/master/lib/index.js // https://github.com/feathersjs/feathers/blob/master/packages/adapter-commons/lib/service.js serviceOptions = service.options; } else if (serviceOptions.proxy) { service = createProxyService(serviceOptions.proxy); } else { // Otherwise we expect the service to be provided as a Feathers service interface service = require(_path.default.join(serviceOptions.servicesPath, fileName, fileName + ".service")); // If we get a function try to call it assuming it will return the service object if (typeof service === "function") { service = service(name, app, Object.assign({}, serviceOptions)); } else if (typeof service === "object" && service.default) { if (typeof service.default === "function") service = service.default(name, app, Object.assign({}, serviceOptions));else service = service.default; } // Need to set this manually for services not using class inheritance or default adapters if (serviceOptions.events) service.events = serviceOptions.events; } // Get our initialized service so that we can register hooks and filters let servicePath = serviceOptions.path || name; let zoneId; if (serviceOptions.zone) { zoneId = typeof serviceOptions.zone === "object" ? ObjectID.isValid(serviceOptions.zone) ? serviceOptions.zone.toString() : serviceOptions.zone._id.toString() : serviceOptions.zone; servicePath = zoneId + "/" + servicePath; } service = declareService(servicePath, app, service, serviceOptions.middlewares); // Register hooks and event filters service = configureService(fileName, service, serviceOptions.servicesPath); // Optionnally a specific service mixin can be provided, apply it if (dbService && serviceOptions.servicesPath) { try { let serviceMixin = require(_path.default.join(serviceOptions.servicesPath, fileName, fileName + ".service")); // If we get a function try to call it assuming it will return the mixin object if (typeof serviceMixin === "function") { serviceMixin = serviceMixin(fileName, app, Object.assign({}, serviceOptions)); } else if (typeof serviceMixin === "object" && serviceMixin.default) { if (typeof serviceMixin.default === "function") serviceMixin = serviceMixin.default(fileName, app, Object.assign({}, serviceOptions));else serviceMixin = serviceMixin.default; } service.mixin(serviceMixin); } catch (error) { debug("No " + fileName + " service mixin configured on path " + serviceOptions.servicesPath); if (error.code !== "MODULE_NOT_FOUND") { // Log error in this case as this might be linked to a syntax error in required file debug(error); } // As this is optionnal this require has to fail silently } } // Then configuration service.core_name = name; service.core_options = serviceOptions; service.core_path = servicePath; service.core_zone = options.zone; // Add some utility functions service.core_getPath = function (withApiPrefix) { let path = service.core_path; if (withApiPrefix) { path = app.get("apiPath") + "/" + path; } return path; }; service.core_getZoneId = function () { return zoneId; // As string }; debug(service.core_name + " service registration completed"); app.emit("service", service); return service; } function setupLogger(logsConfig) { // Create corresponding winston transports with options const transports = logsConfig ? Object.keys(logsConfig).map(key => { const options = logsConfig[key]; // Setup default log level if not defined if (!options.level) { options.level = process.env.NODE_ENV === "development" ? "debug" : "info"; } return new _winston.default.transports[key](options); }) : []; const logger = _winston.default.createLogger({ format: _winston.default.format.json(), transports }); return logger; } function tooManyRequests(socket, message, key) { debug(message); const error = new _errors.TooManyRequests(message, { translation: { key } }); socket.Emitter("rate-limit", error); // Add a timeout so that error message is correctly handled setTimeout(() => socket.disconnect(true), 3000); } function setupSockets(app) { const apiLimiter = app.get("apiLimiter"); const authConfig = app.get("authentication"); const authLimiter = authConfig ? authConfig.limiter : null; let connections = {}; let nbConnections = 0; return io => { // By default EventEmitterters will print a warning if more than 10 listeners are added for a particular event. // The value can be set to Infinity (or 0) to indicate an unlimited number of listeners. io.sockets.setMaxListeners(0); const maxConnections = _lodash.default.get(apiLimiter, "websocket.maxConcurrency", 0); const maxIpConnections = _lodash.default.get(apiLimiter, "websocket.concurrency", 0); io.on("connection", socket => { nbConnections++; debug(`New socket connection on server with pid ${process.pid}`, socket.id, socket.conn.remoteAddress, nbConnections); // Setup disconnect handler first socket.on("disconnect", () => { nbConnections--; debug(`Socket disconnection on server with pid ${process.pid}`, socket.id, socket.conn.remoteAddress, nbConnections); if (maxIpConnections > 0) { const nbIpConnections = _lodash.default.get(connections, socket.conn.remoteAddress) - 1; debug("Total number of connections for", socket.id, socket.conn.remoteAddress, nbIpConnections); _lodash.default.set(connections, socket.conn.remoteAddress, nbIpConnections); } }); if (maxConnections > 0) { if (nbConnections > maxConnections) { tooManyRequests(socket, "Too many concurrent connections (rate limiting)", "RATE_LIMITING_CONCURRENCY"); return; } } if (maxIpConnections > 0) { if (_lodash.default.has(connections, socket.conn.remoteAddress)) { const nbIpConnections = _lodash.default.get(connections, socket.conn.remoteAddress) + 1; debug("Total number of connections for", socket.id, socket.conn.remoteAddress, nbConnections); _lodash.default.set(connections, socket.conn.remoteAddress, nbIpConnections); if (nbIpConnections > maxIpConnections) { tooManyRequests(socket, "Too many concurrent connections (rate limiting)", "RATE_LIMITING_CONCURRENCY"); return; } } else { _lodash.default.set(connections, socket.conn.remoteAddress, 1); } } /* For debug purpose: trace all data received socket.use((packet, next) => { console.log(packet) next() }) */ if (apiLimiter && apiLimiter.websocket) { const { tokensPerInterval, interval } = apiLimiter.websocket; socket.socketLimiter = new _limiter.RateLimiter(tokensPerInterval, interval); socket.use((packet, next) => { if (packet.length > 0) { // Message are formatted like this 'service_path::service_method' let pathAndMethod = packet[0].split("::"); if (pathAndMethod.length > 0) { // const servicePath = pathAndMethod[0] debugLimiter(socket.socketLimiter.getTokensRemaining() + " remaining API token for socket", socket.id, socket.conn.remoteAddress); if (!socket.socketLimiter.tryRemoveTokens(1)) { // if exceeded tooManyRequests(socket, "Too many requests in a given amount of time (rate limiting)", "RATE_LIMITING"); // FIXME: calling this causes a client timeout // next(error) // Need to normalize the error object as JSON // let result = {} // Object.getOwnPropertyNames(error).forEach(key => (result[key] = error[key])) // Trying to send error like in https://github.com/feathersjs/transport-commons/blob/auk/src/events.js#L103 // does not work either (also generates a client timeout) // socket.Emitter(`${servicePath} error`, result) // socket.Emitter(result) return; } } } next(); }); } if (authLimiter && authLimiter.websocket) { const { tokensPerInterval, interval } = authLimiter.websocket; socket.authSocketLimiter = new _limiter.RateLimiter(tokensPerInterval, interval); socket.on("authenticate", data => { // We only limit password guessing if (data.strategy === "local") { debugLimiter(socket.authSocketLimiter.getTokensRemaining() + " remaining authentication token for socket", socket.id, socket.conn.remoteAddress); if (!socket.authSocketLimiter.tryRemoveTokens(1)) { // if exceeded tooManyRequests(socket, "Too many authentication requests in a given amount of time (rate limiting)", "RATE_LIMITING_AUTHENTICATION"); } } }); } }); }; }