Skip to content
Snippets Groups Projects
Commit f5e5942e authored by Atanamo's avatar Atanamo
Browse files

Rethought and integrated flooding protection helper

* Refactored the class again to use weights instead of sizes for rating entries
* Simplified sub routines to be more efficient
* Initializing helper object and doing checks in client event listeners

TODO: Move settings for flooding protection to config
parent 55b2333e
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -148,6 +148,7 @@ class BotChannel extends Channel
 
# @override
_handleClientMessage: (clientSocket, messageText) =>
return unless clientSocket.rating.checkForFlooding(8)
log.debug "Client message to IRC (#{@ircChannelName}):", messageText
botID = clientSocket.identity.getGameID() or -1
targetBot = @botList[botID]
Loading
Loading
Loading
Loading
@@ -304,16 +304,21 @@ class Channel
# May be overridden
# @protected
_handleClientMessage: (clientSocket, messageText) ->
return unless clientSocket.rating.checkForFlooding(8)
log.debug "Client message to '#{@name}':", messageText
messageText = messageText?.trim() or ''
return if messageText is ''
@_sendMessageToRoom(clientSocket.identity, messageText)
 
_handleClientHistoryRequest: (clientSocket) =>
flagName = 'hasHistory_' + @name
return unless clientSocket.rating.checkForFlooding((if clientSocket[flagName] then 10 else 1))
log.debug "Client requests chat history for '#{@name}'"
clientSocket[flagName] = true # Flag socket to have requested history at least once
@_sendHistoryToSocket(clientSocket)
 
_handleClientLeave: (clientSocket, isClose=false) ->
return unless clientSocket.rating.checkForFlooding(3)
if not isClose and clientSocket.identity.getUserID() is @creatorID
# Disallow permanent leaving on channels created by the client
@_sendToSocket(clientSocket, 'leave_fail', 'Cannot leave own channels')
Loading
Loading
@@ -330,6 +335,7 @@ class Channel
delay_promise.done()
 
_handleClientDeleteRequest: (clientSocket) ->
return unless clientSocket.rating.checkForFlooding(4)
if clientSocket.identity.getUserID() isnt @creatorID
@_sendToSocket(clientSocket, 'delete_fail', 'Can only delete own channels')
else if @getNumberOfClients() > 1
Loading
Loading
Loading
Loading
@@ -2,63 +2,54 @@
## Abstraction to store and recognize flooding by a client.
##
class ClientFloodingRating
clientSocket: null
ratingEntries: null # Array of: [*{'timestamp', 'size'}]
floodingRecognizedCallback: null
ratingEntries: null # Array of: [*{'timestamp', 'weight'}]
 
# Describes a rate limit of 1mb/s:
LIMIT_SIZE = 1048576 # Maximum number of bytes/characters
TIME_INTERVAL = 1000 # Interval in milliseconds
LIMIT_WEIGHT = 33 # Maximum total weight in interval
TIME_INTERVAL = 3000 # Interval in milliseconds
 
constructor: (@clientSocket) ->
constructor: (@floodingRecognizedCallback) ->
@ratingEntries = []
 
_addRatingEntry: (size) ->
newEntry =
_addRatingEntry: (newWeight) ->
@ratingEntries.push
timestamp: Date.now()
size: size
@ratingEntries.push(newEntry)
return newEntry
weight: newWeight
return
 
_getEntriesWithinInterval: ->
# Collect entries created within interval
_calculateTotalWeightOfLatestEntries: ->
intervalEntries = []
nowTimestamp = Date.now()
totalWeight = 0
 
for currEntry in @ratingEntries by -1
if nowTimestamp - currEntry.timestamp < TIME_INTERVAL # Must be younger than interval time
intervalEntries.push(currEntry)
if nowTimestamp - currEntry.timestamp <= TIME_INTERVAL # Must be younger than interval time
intervalEntries.unshift(currEntry) # Collect entries created within interval in chronological
totalWeight += currEntry.weight # Sum up weight of entries in interval
else
# Break at first entry outside interval
break
 
return intervalEntries
# Update ratings array
@ratingEntries = intervalEntries
 
_getCalculatedTotalSize: (entries) ->
totalSize = 0
for currEntry in entries by 1
totalSize += currEntry.size
return totalSize
return totalWeight
 
checkForFlooding: (eventWeight) ->
@_addRatingEntry(eventWeight)
 
checkForFlooding: (chunk) ->
@_addRatingEntry(chunk.length)
# Remove outdated entries / update array
@ratingEntries = @_getEntriesWithinInterval()
# Sum up size of entries in interval
totalSize = @_getCalculatedTotalSize(@ratingEntries)
# Collect entries in interval and sum up their weight
totalWeight = @_calculateTotalWeightOfLatestEntries()
 
# Check limit
if totalSize > LIMIT_SIZE
clientSocket.disconnect() # TODO: Disconnect due to flooding.
if totalWeight > LIMIT_WEIGHT
@floodingRecognizedCallback?()
return false
 
return true
 
destroy: ->
@clientSocket = null
@ratingEntries = null
 
 
## Export class
module.exports = ClientFloodingProtection
module.exports = ClientFloodingRating
 
Loading
Loading
@@ -4,9 +4,10 @@ socketio = require 'socket.io'
 
## Include app modules
Config = require './config'
ClientIdentity = require './clientidentity'
Channel = require './channel'
BotChannel = require './botchannel'
ClientIdentity = require './clientidentity'
ClientFloodingRating = require './clientrating'
 
 
## Abstraction of a handler for common client request on a socket.io socket.
Loading
Loading
@@ -40,10 +41,16 @@ class SocketHandler
 
_handleClientConnect: (clientSocket) =>
log.debug 'Client connected...'
# Add flooding rating object
floodingCallback = =>
@_handleClientFlooding(clientSocket)
clientSocket.rating = new ClientFloodingRating(floodingCallback)
clientSocket.rating.checkForFlooding(2)
# Bind socket events to new client
@_bindSocketClientEvents(clientSocket)
 
_handleClientDisconnect: (clientSocket) =>
return if clientSocket.isDisconnected
log.debug 'Client disconnected...'
clientSocket.isDisconnected = true
# Deregister listeners
Loading
Loading
@@ -51,10 +58,22 @@ class SocketHandler
clientSocket.removeAllListeners 'auth'
clientSocket.removeAllListeners 'join'
 
_handleClientFlooding: (clientSocket) =>
return if clientSocket.isDisconnected
log.info "Disconnecting client '#{clientSocket.identity?.getName()}' due to flooding!"
clientSocket.emit 'forced_disconnect', 'Recognized flooding attack'
clientSocket.emit 'disconnect'
clientSocket.disconnect(false) # Disconnect without closing connection (avoids automatic reconnects)
_handleClientAuthRequest: (clientSocket, authData) =>
log.debug 'Client requests auth...'
return unless clientSocket.rating.checkForFlooding((if clientSocket.hasTriedAuth then 14 else 1))
return if clientSocket.identity?
log.debug 'Client requests auth...'
# Flag socket to have tried auth at least once
clientSocket.hasTriedAuth = true
 
# Set start values
userID = authData.userID
gameID = authData.gameID
securityToken = authData.token
Loading
Loading
@@ -112,6 +131,7 @@ class SocketHandler
 
_handleClientChannelJoin: (clientSocket, channelData) =>
return unless clientSocket.identity?
return unless clientSocket.rating.checkForFlooding(7)
 
gameID = clientSocket.identity.getGameID()
requestedChannelTitle = channelData?.title or null
Loading
Loading
Loading
Loading
@@ -10,6 +10,7 @@ class this.SocketClient
instanceData: null
identityData: null
lastMessageSentStamp: 0
isDisonnected: true
 
constructor: (@chatController, @serverIP, @serverPort, @instanceData) ->
 
Loading
Loading
@@ -21,6 +22,7 @@ class this.SocketClient
@socket.on 'connect', @_handleServerConnect # Build-in event
@socket.on 'disconnect', @_handleServerDisconnect # Build-in event
@socket.on 'error', @_handleServerDisconnect # Build-in event
@socket.on 'forced_disconnect', @_handleServerDisconnect
 
@socket.on 'auth_ack', @_handleServerAuthAck
@socket.on 'auth_fail', @_handleServerAuthFail
Loading
Loading
@@ -49,11 +51,14 @@ class this.SocketClient
#
 
_handleServerConnect: =>
@isDisonnected = false
@chatController.handleServerMessage(Translation.get('manage_msg.connect_success'))
@chatController.handleServerMessage(Translation.get('manage_msg.auth_start'))
@_sendAuthRequest()
 
_handleServerDisconnect: (errorMsg) =>
return if @isDisonnected
@isDisonnected = true
@identityData = null
if errorMsg?
serverText = Translation.getForServerMessage(errorMsg)
Loading
Loading
Loading
Loading
@@ -15,6 +15,7 @@ class this.Translation
'server_msg.cannot_leave_own_channel': 'Self-created channels can not be leaved!'
'server_msg.can_only_delete_own_channels': 'A channel can only be deleted, if it has been created by yourself!'
'server_msg.can_only_delete_empty_channels': 'A channel can only be deleted, if no other users are joined to it (even if offline)!'
'server_msg.recognized_flooding_attack': 'You were kicked from the server because of spamming suspicion!'
 
'manage_msg.loading_start': 'Loading...'
'manage_msg.connect_success': 'Connection established!'
Loading
Loading
@@ -79,6 +80,7 @@ class this.Translation
'server_msg.cannot_leave_own_channels': 'Selbst erstellte Channels k&ouml;nnen nicht verlassen werden!'
'server_msg.can_only_delete_own_channels': 'Channels k&ouml;nnen nur gel&ouml;scht werden, wenn sie selbst erstellt wurden!'
'server_msg.can_only_delete_empty_channels': 'Channels k&ouml;nnen nur gel&ouml;scht werden, wenn keine anderen User beigetreten sind (selbst wenn offline)!'
'server_msg.recognized_flooding_attack': 'Du wurdest wegen Spamming-Verdacht vom Server geworfen!'
 
'manage_msg.loading_start': 'Initialisierung l&auml;uft...'
'manage_msg.connect_success': 'Verbindung zum Server hergestellt!'
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment