@batolye/bdk-core
Version:
Module to provide core utilities for BulusAtolyesi applications and services
510 lines (391 loc) • 18.5 kB
JavaScript
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");
}
}
});
}
});
};
}
;