Last active
June 4, 2018 03:41
-
-
Save claustres/5e5a707b063f086baafe7b0f0e73d466 to your computer and use it in GitHub Desktop.
Rate limiting with FeathersJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { RateLimiter as SocketLimiter } from 'limiter' | |
import HttpLimiter from 'express-rate-limit' | |
import { TooManyRequests } from 'feathers-errors' | |
import makeDebug from 'debug' | |
... | |
const debug = makeDebug('debug') | |
function setupSockets (app) { | |
const apiLimiter = app.get('apiLimiter') | |
const authConfig = app.get('authentication') | |
const authLimiter = authConfig.limiter | |
return io => { | |
io.on('connection', socket => { | |
debug('New socket connection', socket.id, socket.conn.remoteAddress) | |
socket.on('disconnect', () => { | |
debug('Socket disconnection', socket.id, socket.conn.remoteAddress) | |
}) | |
if (apiLimiter && apiLimiter.websocket) { | |
const { tokensPerInterval, interval } = apiLimiter.websocket | |
socket.socketLimiter = new SocketLimiter(tokensPerInterval, interval) | |
socket.use((packet, next) => { | |
// Message are formatted like this 'service_path::service_method' | |
let [ path, method ] = packet[0].split('::') | |
debug(socket.socketLimiter.getTokensRemaining() + ' remaining API token for socket', socket.id, socket.conn.remoteAddress) | |
if (!socket.socketLimiter.tryRemoveTokens(1)) { // if exceeded | |
const message = 'Too many requests in a given amount of time (rate limiting)' | |
debug(message) | |
socket.emit('rate-limit', new TooManyRequests(message)) | |
// Add a timeout so that error message is correctly handled | |
setTimeout(() => socket.disconnect(true), 3000) | |
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 | |
const message = 'Too many authentication requests in a given amount of time (rate limiting)' | |
debug(message) | |
socket.emit('rate-limit', new TooManyRequests(message)) | |
// Add a timeout so that error message is correctly handled | |
setTimeout(() => socket.disconnect(true), 3000) | |
} | |
} | |
}) | |
} | |
}) | |
} | |
} | |
// On the server side | |
// ------------------ | |
let app = feathers() | |
// Load app configuration first | |
app.configure(configuration()) | |
// REST limiters before any built-in service initialization | |
const apiLimiter = app.get('apiLimiter') | |
if (apiLimiter && apiLimiter.http) { | |
app.use('/', new HttpLimiter(apiLimiter.http)) | |
} | |
const authConfig = app.get('authentication') | |
const authLimiter = authConfig.limiter | |
if (authLimiter && authLimiter.http) { | |
app.use(authConfig.path, new HttpLimiter(authLimiter.http)) | |
} | |
// Set up providers | |
app.configure(rest()) | |
app.configure(socketio(setupSockets(app))) | |
// On the client side | |
// ------------------ | |
let api = feathers() | |
api.socket = io(url) | |
api.configure(feathers.socketio(api.socket)) | |
// Retrieve our specific errors on rate-limiting and alert user | |
api.socket.on('rate-limit', (error) => ...) | |
api.socket.on('disconnect', () => ...) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment