socket-anti-spam
Version:
This module prevents socket.emit spams by clients via ip bans
298 lines (244 loc) • 7.63 kB
JavaScript
const moment = require('moment')
const events = require('events')
class SocketAntiSpam {
constructor(sets) {
this.debug('constructor', 'Initializing')
this.defaultOptions = {
banTime: 60,
kickThreshold: 10,
kickTimesBeforeBan: 3,
banning: true,
}
this.users = {}
this.options = this.defaultOptions
this.event = new events.EventEmitter()
if (this.not(sets.banTime)) {
sets.banTime = this.defaultOptions.banTime
}
if (this.not(sets.kickThreshold)) {
sets.kickThreshold = this.defaultOptions.kickThreshold
}
if (this.not(sets.kickTimesBeforeBan)) {
sets.kickTimesBeforeBan = this.defaultOptions.kickTimesBeforeBan
}
if (this.not(sets.banning)) {
sets.banning = this.defaultOptions.banning
}
if (this.not(sets.redis)) {
sets.redis = false
this.debug('constructor', 'Not using redis..')
}
if (sets.io) {
this.debug('constructor', 'Socket-io variable given, binding to onevent\'s')
sets.io.on('connection', (socket) => {
const _onevent = socket.onevent
socket.onevent = packet => {
const args = packet.data || []
this.addSpam(socket).then(() => {
_onevent.call(socket, packet)
}).catch(e => {
this.event.emit('error', new Error(e))
})
}
this.authenticate(socket)
})
}
this.options = sets
this.redis = this.options.redis
if (this.redis) {
this.debug('constructor', 'Using redis store..')
setInterval(() => {
this.redisCommit()
}, 4000)
}
this.redisRead()
}
debug(place, text) {
let printText = ''
for (var i = 1; i < arguments.length; i++) {
printText += arguments[i] + ' '
}
require('debug')(`socketantispam:${place}`)(`${printText}`)
}
not(val) {
return (val == null || val === false || val == undefined)
}
redisCommit() {
if (!this.redis) {
return false
}
this.debug('redis', 'Commiting Data')
return new Promise((resolve, reject) => {
this.redis.set('socketantispam_users', JSON.stringify(this.users))
resolve()
})
}
redisRead() {
if (!this.redis) {
return false
}
this.debug('redis', 'Reading Data')
return new Promise((resolve, reject) => {
this.redis.get('socketantispam_users', (err, reply) => {
if (err) {
return reject(err)
}
this.users = JSON.parse(reply)
return resolve()
})
})
}
authenticate(socket) {
this.debug('authenticate', 'Authenticating socket', socket.id)
return new Promise((resolve, reject) => {
if (this.not(socket.ip)) {
socket.ip = socket.client.request.headers['x-forwarded-for'] || socket.client.conn.remoteAddress
}
this.event.emit('authenticate', socket)
if (typeof(this.users[socket.ip]) == 'undefined') {
this.users[socket.ip] = {
score: 0,
banned: false,
kickCount: 0,
bannedUntil: 0,
lastInteraction: moment(),
lastLowerKick: moment(),
}
}
const data = this.users[socket.ip]
if (data.banned) {
data.banned = false
if (data.bannedUntil.diff === undefined) {
data.bannedUntil = moment(data.bannedUntil)
}
if (data.bannedUntil.diff(moment(), 'seconds') >= 1) {
data.banned = true
socket.banned = true
this.debug('authenticate', 'Banned socket on authentication', socket.id, 'for', data.bannedUntil.diff(moment(), 'seconds'), 'seconds')
socket.disconnect()
}
}
return resolve(data)
})
}
addSpam(socket) {
return new Promise((resolve, reject) => {
if (this.not(socket)) {
return reject(new Error('socket variable is not defined'))
}
this.debug('addspam', 'Adding spamscore to', socket.id)
this.authenticate(socket).then(data => {
if (data.banned) {
return reject(new Error('socket is banned'))
}
const lastInteraction = moment.duration(moment().diff(data.lastInteraction)).asSeconds()
data.lastInteraction = moment()
if (lastInteraction < 1) {
data.score++
}
if (lastInteraction >= 1) {
const newScore = data.score - Math.round(lastInteraction)
data.score = newScore
if (newScore <= 0) {
data.score = 0
}
}
const lastLowerKick = moment.duration(moment().diff(data.lastLowerKick)).asSeconds()
if (lastLowerKick >= 1800 && data.kickCount >= 1) {
data.lastLowerKick = moment()
data.kickCount--
}
this.event.emit('spamscore', socket, data)
if (data.score >= this.options.kickThreshold) {
this.event.emit('kick', socket, data)
data.score = 0
data.kickCount = data.kickCount + 1
if (data.kickCount >= this.options.kickTimesBeforeBan && this.options.banning) {
this.event.emit('ban', socket, data)
data.kickCount = 0
data.banned = true
data.lastLowerKick = moment()
data.bannedUntil = moment().add(this.options.banTime, 'minutes')
}
socket.disconnect()
}
this.debug('addspam', 'Current spamscore of', socket.id, 'is', data.score)
return resolve(data)
}).catch(e => {
return reject(e)
})
})
}
ban(data, min) {
this.debug('ban', 'Banning', data, min)
return new Promise((resolve, reject) => {
if (this.not(data)) {
throw new Error('No options defined')
}
if (this.not(min)) {
min = this.options.banTime
}
let ip = false
if (typeof(this.users[data]) !='undefined') {
ip = data
}
if (typeof(this.users[data.ip]) != 'undefined') {
ip = data.ip
}
if (ip) {
return this.banUser(true, ip).then(resolve).catch(reject)
}
return reject(new Error('ip is not defined'))
})
}
unBan(data) {
this.debug('unban', 'Unbanning', data)
return new Promise((resolve, reject) => {
if (this.not(data)) {
return reject(new Error('No options defined'))
}
let ip = false
if (typeof(this.users[data]) != 'undefined') {
ip = data
}
if (typeof(this.users[data.ip]) != 'undefined') {
ip = data.ip
}
if (ip) {
return this.banUser(false, ip).then(resolve).catch(reject)
}
return reject(new Error('ip is not defined'))
})
}
banUser(ban, data, min) {
this.debug('banUser', ban, data, min)
return new Promise((resolve, reject) => {
this.users[data].kickCount = 0
this.users[data].score = 0
if (ban) {
this.users[data].banned = true
this.users[data].lastLowerKick = moment()
this.users[data].bannedUntil = moment().add(min, 'minutes')
} else {
this.users[data].banned = false
this.users[data].lastLowerKick = moment()
this.users[data].bannedUntil = 0
}
return resolve()
})
}
getBans() {
return new Promise((resolve, reject) => {
const banned = []
for (let user in this.users) {
if (this.users[user].banned)
banned.push({
ip: user,
until: this.users[user].bannedUntil,
})
}
return resolve(banned)
})
}
}
module.exports = SocketAntiSpam