内容简介:厂里有一个推送服务,负责网页推送和数据同步,基于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的逻辑就很清晰了
- 用户A创建组织Z,监听组织变化的这里调用了一次nsp.to(userARoom),nsp.rooms被push了一个用户A的房间,但是没有调用emit方法,所以rooms数组没有被清空
- 用户A立即邀请用户B,发送邀请通知时nsp.to(userBRoom),又push一次用户B的room,调用emit,所以发送给了用户A和用户B
-
用户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这样的调用顺序,造成推送给错误的客户端。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。