socket.io的一个“坑”

栏目: Html5 · 发布时间: 6年前

内容简介:厂里有一个推送服务,负责网页推送和数据同步,基于socket.io。网页推送通过rabbitmq监听队列实现组织成员变化和对应socket.io房间用户的同步。新建一个组织后,立即邀请一个用户B,则当前用户A(不是被邀请的,是邀请别人的)用户也会收到目标用户的邀请通知推送,但是由于A并不是这个通知的接收人,所以点开会丢出403。

0x00 背景

厂里有一个推送服务,负责网页推送和数据同步,基于socket.io。

网页推送通过rabbitmq监听队列实现组织成员变化和对应socket.io房间用户的同步。

0x01 表现

新建一个组织后,立即邀请一个用户B,则当前用户A(不是被邀请的,是邀请别人的)用户也会收到目标用户的邀请通知推送,但是由于A并不是这个通知的接收人,所以点开会丢出403。

正常情况下用户A根本不应该收到这条通知。

并且仅限创建组织后立即邀请,无论是刷新过后还是再邀请第二个用户,就不会收到这条邀请推送了。

0x02 初步研究

第一反应当然是发送推送的时候是不是多带了一个用户id,可是无论打断点还是console.log,均只有对应的用户B的id。

到推送服务那边添加日志,也只看到用户B的id。

if (authIds) {
  for (const authId of authIds) { //authIds只有B用户的uid
    nsp.to(userRoom(authId))
      .emit(event, {
        messageId,
        message: data.message,
        icon: data.icon,
        payload: data.payload,
      })
  }
}

0x03 我们需要更深入些

我将代码改成了

if (authIds) {
  for (const authId of authIds) {
    const room = nsp.to(userRoom(authId))
    console.log(userRoom(authId))
    console.log(room.sockets)
    room.emit(event, {
        messageId,
        message: data.message,
        icon: data.icon,
        payload: data.payload,
      })
  }
}

发现第二个console.log输出的sockets总是带有用户A的socket

于是跟踪一下emit方法的实现

// socket.io/lib/namespace.js:204
/**
 * Emits to all clients.
 *
 * @return {Namespace} self
 * @api public
 */

Namespace.prototype.emit = function(ev){
  if (~exports.events.indexOf(ev)) {
    emit.apply(this, arguments);
    return this;
  }
  // set up packet object
  var args = Array.prototype.slice.call(arguments);
  var packet = {
    type: (this.flags.binary !== undefined ? this.flags.binary : hasBin(args)) ? parser.BINARY_EVENT : parser.EVENT,
    data: args
  };

  if ('function' == typeof args[args.length - 1]) {
    throw new Error('Callbacks are not supported when broadcasting');
  }

  var rooms = this.rooms.slice(0);
  var flags = Object.assign({}, this.flags);

  // reset flags
  this.rooms = [];
  this.flags = {};

  this.adapter.broadcast(packet, {
    rooms: rooms,
    flags: flags
  });

  return this;
};
// socket.io-adapter/index.js:110
/**
 * Broadcasts a packet.
 *
 * Options:
 *  - `flags` {Object} flags for this packet
 *  - `except` {Array} sids that should be excluded
 *  - `rooms` {Array} list of rooms to broadcast to
 *
 * @param {Object} packet object
 * @api public
 */

Adapter.prototype.broadcast = function(packet, opts){
  var rooms = opts.rooms || [];
  var except = opts.except || [];
  var flags = opts.flags || {};
  var packetOpts = {
    preEncoded: true,
    volatile: flags.volatile,
    compress: flags.compress
  };
  var ids = {};
  var self = this;
  var socket;

  packet.nsp = this.nsp.name;
  this.encoder.encode(packet, function(encodedPackets) {
    if (rooms.length) {
      for (var i = 0; i < rooms.length; i++) {
        var room = self.rooms[rooms[i]];
        if (!room) continue;
        var sockets = room.sockets;
        for (var id in sockets) {
          if (sockets.hasOwnProperty(id)) {
            if (ids[id] || ~except.indexOf(id)) continue;
            socket = self.nsp.connected[id];
            if (socket) {
              socket.packet(encodedPackets, packetOpts);
              ids[id] = true;
            }
          }
        }
      }
    } else {
      for (var id in self.sids) {
        if (self.sids.hasOwnProperty(id)) {
          if (~except.indexOf(id)) continue;
          socket = self.nsp.connected[id];
          if (socket) socket.packet(encodedPackets, packetOpts);
        }
      }
    }
  });
};

顺着这里的代码不难发现,socket.io是在发送的时候才去对应的room里遍历对应的socket,也就是说nsp.sockets属性永远保存的是所有sockets,而不受to方法的影响。

为了查找原因,我们需要看一下to方法的实现

// socket.io/lib/namespace.js:139
/**
 * Targets a room when emitting.
 *
 * @param {String} name
 * @return {Namespace} self
 * @api public
 */

Namespace.prototype.to =
Namespace.prototype.in = function(name){
  if (!~this.rooms.indexOf(name)) this.rooms.push(name);
  return this;
};

to在这里只是push了一下this.rooms数组。于是就有查找的方向了,一定是有某个地方多调用了一次to。

0x04 真相永远只有一个

还好在距离上面代码不远的地方,我找到了

const {orgId} = data
switch (ctx.fields.routingKey) {
  case 'create':
  case 'members.add': 
    const {userId} = data
    const sockets = 
    Object.values(nsp.to(userRoom(userId)).connected)
    for (const socket of sockets) {
      socket.join(orgRoom(orgId))
    }

按照to方法的逻辑,其实这里是有问题的,connected属性和sockets属性一样,to只是push了一下数组,并不会影响这两个属性的值,这里的代码负责的也正好是同步组织创建和组织房间的socket。

于是这个bug的逻辑就很清晰了

  1. 用户A创建组织Z,监听组织变化的这里调用了一次nsp.to(userARoom),nsp.rooms被push了一个用户A的房间,但是没有调用emit方法,所以rooms数组没有被清空
  2. 用户A立即邀请用户B,发送邀请通知时nsp.to(userBRoom),又push一次用户B的room,调用emit,所以发送给了用户A和用户B
  3. 用户A再邀请用户C,发送邀请通知时nsp.to(userCRoom),但是没有步骤一带入的userARoom,所以表现正常
    最终只要修改一下所有求房间内用户的方法就可以了
const sockets = Object.values(nsp.connected)
          .filter((socket) => Object.keys(socket.rooms).includes(userRoom(userId)))

0x05 后话

本来以为nsp.to方法返回的是一个独立的namespace实例,没想到只是push了一下room数组。

不过socket.io这样做其实有隐患,如果在to和emit之间有异步调用,可能会出现to,to,emit,emit这样的调用顺序,造成推送给错误的客户端。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Host Your Web Site In The Cloud

Host Your Web Site In The Cloud

Jeff Barr / SitePoint / 2010-9-28 / USD 39.95

Host Your Web Site On The Cloud is the OFFICIAL step-by-step guide to this revolutionary approach to hosting and managing your websites and applications, authored by Amazon's very own Jeffrey Barr. "H......一起来看看 《Host Your Web Site In The Cloud》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具