节点服务器关闭然后重新启动时奇怪的socket.io行为

时间:2018-08-19 18:31:09

标签: javascript node.js security socket.io ddos

我为我的网站实现了一个简单的聊天,用户可以在其中使用ExpressJS和Socket.io相互交谈。我添加了一个简单的保护措施,以防止ddos攻击,这种攻击可能是由一个人向窗口发送垃圾邮件引起的,如下所示:

if (RedisClient.get(user).lastMessageDate > currentTime - 1 second) {

   return error("Only one message per second is allowed")

} else {

   io.emit('message', ...)     
   RedisClient.set(user).lastMessageDate = new Date()

}

我正在使用以下代码对此进行测试:

setInterval(function() {
    $('input').val('message ' + Math.random());
    $('form').submit();
}, 1);

当节点服务器始终处于启动状态时,它可以正常工作。

但是,如果我关闭Node服务器,然后运行上面的代码,然后在几秒钟内再次启动Node服务器,事情就会变得非常奇怪。然后突然,成百上千条消息被插入窗口,浏览器崩溃。我认为这是因为节点服务器关闭时,socket.io保存了所有客户端发出的消息,并且一旦检测到节点服务器再次处于联机状态,它将立即异步推送所有这些消息。

我该如何防范?而这里到底发生了什么?

编辑:如果我在内存中使用Node而不是Redis,则不会发生这种情况。我猜是因为服务器上充斥着READ,并且在RedisClient.set(user).lastMessageDate = new Date()完成之前发生了许多READ。我想我需要的是原子READ / SET?我正在使用以下模块:https://github.com/NodeRedis/node_redis用于从Node连接到Redis。

2 个答案:

答案 0 :(得分:6)

您是正确的,这是由于客户端上的消息排队和服务器上的洪水泛滥所致。

当服务器接收消息时,它会一次接收所有消息,并且所有这些消息都不同步。因此,每个socket.on("message:...事件都是分别执行的,即一个socket.on("message...与另一个事件无关,而是分别执行的。

即使您的Redis-Server的延迟时间为几毫秒,这些消息也会一次全部收到,并且一切都将变为else状态。

您有以下几种选择。

  1. 使用速率限制器库like this library。这很容易配置,并且具有多个配置选项。

  2. 如果您想自己做所有事情,请在服务器上使用队列。这将占用服务器上的内存,但是您将实现所需的功能。而不是将所有消息写入服务器,而是将其放入队列。为每个新客户端创建一个新队列,并在处理队列中的最后一项时删除此队列。

  3. (更新)使用multi + watch创建锁,以便除当前命令外的所有其他命令都将失败。

伪代码将是这样的。

let queue = {};

let queueHandler = user => {
  while(queue.user.length > 0){
    // your redis push logic here
  }
  delete queue.user
}


let pushToQueue = (messageObject) => {
  let user = messageObject.user;

  if(queue.messageObject.user){
    queue.user = [messageObject];
  } else {
    queue.user.push(messageObject);
  }

  queueHandler(user);
}

socket.on("message", pushToQueue(message));

更新

Redis支持与WATCH一起使用的锁定。使用此功能,您可以锁定密钥,并且其他尝试在该时间内访问该密钥的命令都会失败。

来自redis client README

  

使用multi可以确保您的修改作为事务运行,   但您不能确定首先到达那里。如果另一个客户怎么办   在处理数据时修改了密钥?

     

为解决此问题,Redis支持WATCH命令,该命令旨在   与MULTI一起使用:       var redis = require(“ redis”),       client = redis.createClient({...});

client.watch("foo", function( err ){
if(err) throw err;

client.get("foo", function(err, result) {
    if(err) throw err;

    // Process result
    // Heavy and time consuming operation here

    client.multi()
        .set("foo", "some heavy computation")
        .exec(function(err, results) {

            /**
             * If err is null, it means Redis successfully attempted 
             * the operation.
             */ 
            if(err) throw err;

            /**
             * If results === null, it means that a concurrent client
             * changed the key while we were processing it and thus 
             * the execution of the MULTI command was not performed.
             * 
             * NOTICE: Failing an execution of MULTI is not considered
             * an error. So you will have err === null and results === null
             */

        });
}); });

答案 1 :(得分:4)

也许您可以扩展客户端代码,以防止在套接字断开连接时发送数据?这样,可以防止在套接字断开连接(即服务器处于脱机状态)时库不对消息进行排队。

这可以通过检查socket.connected是否为真来实现:

// Only allow data to be sent to server when socket is connected
function sendToServer(socket, message, data) {

    if(socket.connected) {
        socket.send(message, data)
    }
}

有关更多信息,请参见文档https://socket.io/docs/client-api/#socket-connected

这种方法将防止在套接字断开连接的所有情况下的内置排队行为,这可能不是所希望的,但是,如果可以防止出现您所关注的问题,则可以避免这种情况。

更新

或者,您可以在服务器上使用自定义中间件来通过socket.io的服务器API实现节流行为:

/*
Server side code
*/
io.on("connection", function (socket) {

    // Add custom throttle middleware to the socket when connected
    socket.use(function (packet, next) {

        var currentTime = Date.now();

        // If socket has previous timestamp, check that enough time has
        // lapsed since last message processed
        if(socket.lastMessageTimestamp) {
            var deltaTime = currentTime - socket.lastMessageTimestamp;

            // If not enough time has lapsed, throw an error back to the
            // client
            if (deltaTime < 1000) {
                next(new Error("Only one message per second is allowed"))
                return
            }
        }

        // Update the timestamp on the socket, and allow this message to
        // be processed
        socket.lastMessageTimestamp = currentTime
        next()
    });
});