新浪微博Web版中的私信聊天功能的几个技术细节
蒋彪@南京
1. Pull而不是push
一般在网页上要取得实时聊天数据,无外乎两种技术实现
a. 用ajax定时发起轮训, 主动的pull服务器端数据
b. 基于Flash,ActiveX,Html5等富客户端技术, 和server建立长连接,server端实时的push到客户端
新浪的实现是长轮训, 观察它的http请求,每隔一段时间网页就会发起如下的一个GET request(时间间隔不是固定的,怀疑server端做了指数推移)
(基于iframe的流pull?)
http://4.136.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback313&message=%5B%7B%22channel%22%3A%22%2Fmeta%2Fconnect%22%2C%22connectionType%22%3A%22callback-polling%22%2C%22id%22%3A271%2C%22clientId%22%3A%22gt2psmdubshz54tl6j%22%7D%5D&1361434136747
将参数unescape之后,这个url就是
http://4.136.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback313&message=[{"channel":"/meta/connect","connectionType":"callback-polling","id":271,"clientId":"gt2psmdubshz54tl6j"}]&1361434136747
请求的头文件Connection设置为keep-alive
,使用
http1.1
中的长连接,
并且服务器端
在没有新消息的情况下不返回
response
,造成事实上的长连接。
如果
server
端没有收到任何新消息,时间到之后,返回如下
response
try{parent.org.cometd.script._callback44([{"id":"46","successful":true,"channel":"/meta/connect"}])}catch (ex) {}
浏览器收到之后,再次发起一次请求,如此轮训
如果server端收到新消息之后,会立即返回如下格式的response
try{parent.org.cometd.script._callback28([{"id":"30","successful":true,"channel":"/meta/connect"}, {"data":{"type":"msg","items":[[1759237047,"ccc",1361497541084,"web_v4","3548440233536175"]]},"id":"c7y9s3","channel":"/im/j8vamp_1647676027"}])} catch (ex) {}
这段response会被浏览器按js解析执行,将ccc
这个
message
动态刷到聊天框中
2. 如何Send Message
同理,在聊天框中发送消息的话,也是用ajax发送如下get request。
http://0.136.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback44&message=%5B%7B%22channel%22%3A%22%2Fim%2Freq%22%2C%22data%22%3A%7B%22uid%22%3A%221759237047%22%2C%22seq%22%3A%221759237047%22%2C%22msg%22%3A%22testFromTony%22%2C%22cmd%22%3A%22msg%22%7D%2C%22id%22%3A46%2C%22clientId%22%3A%221uq7v70nfjl0whi0gd4%22%7D%5D&1361497717707
同样,转义之后,有意义的request如下
http://0.136.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback44&message=[{"channel":"/im/req","data":{"uid":"1759237047","seq":"1759237047","msg":"testFromTony","cmd":"msg"},"id":46,"clientId":"1uq7v70nfjl0whi0gd4"}]&1361497717707
其中的testFromTony就是message
server端成功接收到request之后的返回如下
try{parent.org.cometd.script._callback44([{"id":"46","successful":true,"channel":"/im/req"}, {"data":{"ret":0},"id":"46","channel":"/im/j8vamp_1647676027"}])}catch (ex) {}
3. 好友聊天的安全
新浪微博的聊天机制依赖于Cookie和ServerTokenID双重保证。
你可以登录新浪微博之后,在浏览器中打开另一个tab,跑以下的html。下面的页面因为和新浪微博共享内存cookie,因此可以自动发message
<html> <head> </head> <body> <img src='http://0.136.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback44&message=[{"channel":"/im/req", "data":{"uid":"1759237046","seq":"1759237046","msg":"testFromTonyaaaadddd1","cmd":"msg"}, "id":46,"clientId":"1uw3tes1u5xjjv0vpou"}]1361502107092'></img> </body> </html>
关键在于发送的url中,有一个参数很重要,是clientId。这个ID是每次登录时,由服务器端分配出来的唯一标识客户端的ID,这个ID不正确,发送会出错。
更要命的是,不同的客户端login到微博上面的时候,服务器集群会把聊天信息定向到不同的聊天APIServer上去,比如
http://1.126.web0.im.weibo.com
http://0.136.web0.im.weibo.com
幸好,微博自己的页面里把这些数据当作Dom元素记下来了,其中就有clientId, API server domain等重要数据。如下:
<iframe id="cometd_uc" style="position: absolute; left: -100px; top: -100px; height: 1px; width: 1px; visibility: hidden; display: none;"> <html> <head> <script type="text/javascript" src="http://1.126.web0.im.weibo.com/im?jsonp=parent.org.cometd.script._callback16&message=%5B%7B%22channel%22%3A%22%2Fmeta%2Fconnect%22%2C%22connectionType%22%3A%22callback-polling%22%2C%22id%22%3A18%2C%22clientId%22%3A%223y0wflmlof3hccq3u5%22%7D%5D&1361512732973" charset="utf-8"> try{parent.org.cometd.script._callback16([{"id":"18","successful":true,"channel":"/meta/connect"}])}catch (ex) {} </script> </head> <body></body> </html> </iframe>
可惜的是,尝试用各种各样的XSS想拿到这个dom元素都失败了。微博在所有的输入框中都做了encode。不知道有哪位大神能发现一个可以入侵的窗口。
版本归蒋彪所有, 转载请注明出自《南湖边上的小木屋》
#以上#
- 1楼guangboo昨天 14:53
- 应该是使用了类似BOSH的传输协议,可以在WEB上模拟TCP连接。只是服务器端要做一些处理,在连接超时前如果没有状态更新就不返回(response),否则返回空等信息。客户端收到返回再重新发送请求,这样就实现了类似于TCP的常连接效果。
- Re: nanjingjiangbiao昨天 15:00
- 回复guangboon原来如此,受教受教。用长轮训模拟长连接,原来这个技术名叫BOSH。我孤陋寡闻了