| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478 | 1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
2x
2x
2x
2x
2x
2x
2x
2x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
1x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
4x
4x
2x
4x
4x
1x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
4x
4x
4x
2x
2x
30x
24x
22x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
| import path from 'path'
import makeDebug from 'debug'
import logger from 'winston'
import _ from 'lodash'
import 'winston-daily-rotate-file'
import compress from 'compression'
import cors from 'cors'
import helmet from 'helmet'
import bodyParser from 'body-parser'
import { RateLimiter as SocketLimiter } from 'limiter'
import HttpLimiter from 'express-rate-limit'
import feathers from '@feathersjs/feathers'
import configuration from '@feathersjs/configuration'
import { TooManyRequests } from '@feathersjs/errors'
import express from '@feathersjs/express'
import rest from '@feathersjs/express/rest'
import socketio from '@feathersjs/socketio'
import authentication from '@feathersjs/authentication'
import jwt from '@feathersjs/authentication-jwt'
import local from '@feathersjs/authentication-local'
import oauth2 from '@feathersjs/authentication-oauth2'
import GithubStrategy from 'passport-github'
import GoogleStrategy from 'passport-google-oauth20'
import OAuth2Verifier from './verifier'
import PasswordValidator from 'password-validator'
import { ObjectID } from 'mongodb'
import { Database } from './db'
const debug = makeDebug('kalisio:kCore:application')
const debugLimiter = makeDebug('kalisio:kCore:application:limiter')
function auth () {
const app = this
const config = app.get('authentication')
Iif (!config) return
const limiter = config.limiter
Iif (limiter && limiter.http) {
app.use(config.path, new HttpLimiter(limiter.http))
}
// Store availalbe OAuth2 providers
app.authenticationProviders = []
// Get access to password validator if a policy is defined
Eif (config.passwordPolicy) {
let validator
app.getPasswordPolicy = function () {
// Create on first access, should not be done outside a function because the app has not yet been correctly initialized
Iif (validator) return validator
let { minLength, maxLength, uppercase, lowercase, digits, symbols, noSpaces, prohibited } = config.passwordPolicy
validator = new PasswordValidator()
Eif (minLength) validator.is().min(minLength)
Eif (maxLength) validator.is().max(maxLength)
Eif (uppercase) validator.has().uppercase()
Eif (lowercase) validator.has().lowercase()
Eif (digits) validator.has().digits()
Eif (symbols) validator.has().symbols()
Iif (noSpaces) validator.not().spaces()
Eif (prohibited) validator.is().not().oneOf(prohibited)
// Add util functions/options to compare with previous passwords stored in history when required
const verifier = new local.Verifier(app, _.merge({ usernameField: 'email', passwordField: 'password' },
_.pick(config, ['service']), config.local))
validator.comparePassword = verifier._comparePassword
validator.options = config.passwordPolicy
return validator
}
}
// Set up authentication with the secret
app.configure(authentication(config))
app.configure(jwt())
app.configure(local())
Iif (config.github) {
app.configure(oauth2({
name: 'github',
Strategy: GithubStrategy,
Verifier: OAuth2Verifier
}))
app.authenticationProviders.push('github')
}
Iif (config.google) {
app.configure(oauth2({
name: 'google',
Strategy: GoogleStrategy,
Verifier: OAuth2Verifier
}))
app.authenticationProviders.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.getService('authentication').hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies)
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
})
}
export 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)
app.use.apply(app, args)
debug('Service declared on path ' + feathersPath)
// Return the Feathers service, ie base service + Feathers' internals
return app.service(feathersPath)
}
export function configureService (name, service, servicesPath) {
try {
const hooks = require(path.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.join(servicesPath, name, name + '.channels'))
_.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
}
export 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 = _.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))) }
}
}
export function createService (name, app, options = {}) {
const createFeathersService = require('feathers-' + app.db.adapter)
const paginate = app.get('paginate')
const serviceOptions = Object.assign({
name: name,
paginate
}, options)
// For DB services a model has to be provided
let fileName = options.fileName || name
let dbService = false
try {
if (options.modelsPath) {
const configureModel = require(path.join(options.modelsPath, fileName + '.model.' + app.db.adapter))
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)
} else if (options.proxy) {
service = createProxyService(options.proxy)
} else {
// Otherwise we expect the service to be provided as a Feathers service interface
service = require(path.join(options.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, serviceOptions)
}
// Need to set this manually for services not using class inheritance or default adapters
if (options.events) service.events = options.events
}
// Get our initialized service so that we can register hooks and filters
let servicePath = options.path || name
let contextId
if (options.context) {
contextId = (typeof options.context === 'object'
? (ObjectID.isValid(options.context) ? options.context.toString() : options.context._id.toString()) : options.context)
servicePath = contextId + '/' + servicePath
}
service = declareService(servicePath, app, service, options.middlewares)
// Register hooks and event filters
service = configureService(fileName, service, options.servicesPath)
// Optionnally a specific service mixin can be provided, apply it
if (dbService && options.servicesPath) {
try {
let serviceMixin = require(path.join(options.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, options)
}
service.mixin(serviceMixin)
} catch (error) {
debug('No ' + fileName + ' service mixin configured on path ' + options.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.name = name
service.app = app
service.options = options
service.path = servicePath
service.context = options.context
// Add some utility functions
service.getPath = function (withApiPrefix) {
let path = service.path
if (withApiPrefix) {
path = app.get('apiPath') + '/' + path
}
return path
}
service.getContextId = function () {
return contextId // As string
}
debug(service.name + ' service registration completed')
app.emit('service', service)
return service
}
function setupLogger (logsConfig) {
// Remove winston defaults
try {
logger.remove(logger.transports.Console)
} catch (error) {
// Logger might be down, use console
console.error('Could not remove default logger transport', error)
}
// We have one entry per log type
let logsTypes = logsConfig ? Object.getOwnPropertyNames(logsConfig) : []
// Create corresponding winston transports with options
logsTypes.forEach(logType => {
let options = logsConfig[logType]
// Setup default log level if not defined
if (!options.level) {
options.level = (process.env.NODE_ENV === 'development' ? 'debug' : 'info')
}
try {
logger.add(logger.transports[logType], options)
} catch (error) {
// Logger might be down, use console
console.error('Could not setup default log levels', error)
}
})
}
function tooManyRequests (socket, message, key) {
debug(message)
const error = new TooManyRequests(message, { translation: { key } })
socket.emit('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 EventEmitters 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 = _.get(apiLimiter, 'websocket.maxConcurrency', 0)
const maxIpConnections = _.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 = _.get(connections, socket.conn.remoteAddress) - 1
debug('Total number of connections for', socket.id, socket.conn.remoteAddress, nbIpConnections)
_.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 (_.has(connections, socket.conn.remoteAddress)) {
const nbIpConnections = _.get(connections, socket.conn.remoteAddress) + 1
debug('Total number of connections for', socket.id, socket.conn.remoteAddress, nbConnections)
_.set(connections, socket.conn.remoteAddress, nbIpConnections)
if (nbIpConnections > maxIpConnections) {
tooManyRequests(socket, 'Too many concurrent connections (rate limiting)', 'RATE_LIMITING_CONCURRENCY')
return
}
} else {
_.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 SocketLimiter(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.emit(`${servicePath} error`, result)
// socket.emit(result)
return
}
}
}
next()
})
}
if (authLimiter && authLimiter.websocket) {
const { tokensPerInterval, interval } = authLimiter.websocket
socket.authSocketLimiter = new SocketLimiter(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')
}
}
})
}
})
}
}
export function kalisio () {
let app = express(feathers())
// By default EventEmitters 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.
app.setMaxListeners(0)
// Load app configuration first
app.configure(configuration())
// Then setup logger
setupLogger(app.get('logs'))
// This avoid managing the API path before each service name
app.getService = function (path, context) {
// Context is given as string ID
Iif (context && typeof context === 'string') {
return app.service(app.get('apiPath') + '/' + context + '/' + path)
} else Iif (context && typeof context === 'object') {
// Could be Object ID or raw object
if (ObjectID.isValid(context)) return app.service(app.get('apiPath') + '/' + context.toString() + '/' + path)
else return app.service(app.get('apiPath') + '/' + context._id.toString() + '/' + path)
} else {
return app.service(app.get('apiPath') + '/' + path)
}
}
// This is used to add hooks/filters to services
app.configureService = function (name, service, servicesPath) {
return configureService(name, service, servicesPath)
}
// This is used to create standard services
app.createService = function (name, options) {
return createService(name, app, options)
}
// Override Feathers configure that do not manage async operations,
// here we also simply call the function given as parameter but await for it
app.configure = async function (fn) {
await fn.call(this, this)
return this
}
const apiLimiter = app.get('apiLimiter')
Iif (apiLimiter && apiLimiter.http) {
app.use(app.get('apiPath'), new HttpLimiter(apiLimiter.http))
}
// Enable CORS, security, compression, and body parsing
app.use(cors(app.get('cors')))
app.use(helmet(app.get('helmet')))
app.use(compress(app.get('compression')))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
// Set up plugins and providers
app.configure(rest())
app.configure(socketio({ path: app.get('apiPath') + 'ws' }, setupSockets(app)))
app.configure(auth)
// Initialize DB
app.db = Database.create(app)
return app
}
|