Stage 4
放弃了一条路,当然要找到其他的方案。对于网页,我们有一个基础操作叫F12。在这里的演示将使用Chrome而不是我平常使用的Firefox - 因为Chrome对娱乐(视频、Flash等)的支持比较好…
首先打开直播,等一会儿看连接。鼠标在时间轴上后来的地方随便拖一下即可选中。如果没问题的话,你应该能看到7个请求。当然也有可能不是,我这七个当中有一个是登陆以后才有的。
这里有三个?EIO=3&transport=websocket
的请求,一个主播号+随机串.flv(十分显然就是直播的视频内容),一个unread?type=all
,一个user?anchor_uid=主播号&psize=5&type=week
,一个anchor?anchor_uid=主播号&psize=5&type=week
。感觉前三个其中就有答案,但是十分悲伤,我对WebSocket毫无了解 - 也无法测试是否正确。
我扫了一眼,发现后面的三个看起来像是登陆后的啥操作,于是开了个新的小窗抓。这下只剩三个了:视频和两个WebSocket。点开就可以发现,其中有一个的地址和之前Aardio的非常相似:wss://cht-1.ws.kingkong.com.tw/chat_nsp/
和wss://ctl.ws.kingkong.com.tw/control_nsp/
感谢伟大的开发者工具,WebSocket的内容变得十分简单。ctl的那个内容全部都是42/control_nsp开头的玩意儿,目测没有也能正常用,所以先不管。认真分析42/chat_nsp开头的。看着名字,很明显这就是聊天弹幕!
仔细看看收到的内容。
第一行是服务器发来的:
0{sid: "长度20的base64 binary", upgrades: [], pingInterval: 50000, pingTimeout: 60000}
这个神奇的sid我没看出来到底是啥,但是反正是服务器发给我的。
接下来就是中规中矩的普通交流:
发40/chat_nsp,
收40
收40/chat_nsp
发42/chat_nsp,["authentication",{"live_id":"主播号+随机串","anchor_pfid":主播号,"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ.tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ.tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s","from":"WEB","client_type":"web","r":0}]
收42/chat_nsp,["authenticated",true]
然后就是弹幕了。
Stage 5
很明显,关键就是分析这段东西了。主播号和live_id很明显,不管。这个access_token和token一模一样,就是我们要分析的东西。根据Aardio的代码,这段东西是通过eval一段js获得的 - 看起来eval很不靠谱啊…
我把截图放到了dalao群里,然后有个大佬貌似认出了这是啥(emmmm 不懂,幸好我当时没有头铁去分析那个压缩的js… 那tm是个加密库)
那么说,现在可以靠谱地分析一下了。这是base64编码的字符串 - 并且里面有两个点。很明显,点肯定不是base64里的一部分 - 那么暂且当成分隔符分成三部分。
第一部分:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分:eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ
第三部分:tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s
很稳,直接上.jpg
头铁又莽的我直接把这东西丢到了在线解码工具里。这些工具好用得一笔,十分方便地给出了我想要的结果… 唯一的问题是,省略了一点儿细节。第一部分的内容是
{"alg":"HS256","typ":"JWT"}
第二部分则为
{"live_id":"主播号+随机串","pfid":"76f8ffc2e5e44b0dabed1fe5ef3fc3d1","name":"訪客fc3d1","access_token":null,"lv":1,"from":1,"from_seq":1,"channel_id":1,"client_type":"web"}
目测发现,pfid的后五位就是访客的具体编号。虽然不知道是怎么生成的,但是暂时还行吧。
那么问题来了,第三部分。
首先 这里面有个下划线… 我完全不知道这下划线是啥操作,但是假装它是+或/吧。看起来区别不是很大,基本上就只是一个字节。
b5 1f 32 96 ba 34 58 c5 c8 7e 62 76 9d 6d d1 a0 5c 0e 9b 1c 04 54 01 f9 52 e9 b0 19 68 33 13 fb
(如果是+的话,最后一个字节改为eb)
这玩意儿解完是binary没法看,我也猜不出来,那自然就又要换条路了。
根据第一部分解出来的东西,我们可以找到一个关键词:HS256。我相信这玩意儿不是凭空出现的,也不是711那样来的,所以就去搜了一下。
Stage 6
在Kingkong/js/template/live.js里,我们成功搜到了HS256。它大概长这样:
DETAIL@ph
var o = function() {
function t(e) {
var i = e.liveId
, a = e.liveKey
, o = e.relColor
, s = e.pfid
, r = e.name
, l = e.accessToken
, c = e.gradeId
, d = e.gradeLv
, u = e.limit
, h = e.lv
, f = e.from
, g = e.fromSeq
, m = e.channelId
, v = e.anchor
, p = e.follow
, y = e.sex
, _ = e.balance
, b = e.sun
, k = e.balanceNoble
, w = e.mute
, $ = e.relation
, I = e.avatar
, T = e.signed
, C = e.nobleLevel
, S = n(e, ["liveId", "liveKey", "relColor", "pfid", "name", "accessToken", "gradeId", "gradeLv", "limit", "lv", "from", "fromSeq", "channelId", "anchor", "follow", "sex", "balance", "sun", "balanceNoble", "mute", "relation", "avatar", "signed", "nobleLevel"]);
!function(t, e) {
if (!(t instanceof e))
throw new TypeError("Cannot call a class as a function")
}(this, t),
this.liveId = i,
this.liveKey = a,
this.pfidName = "localPFID",
this.accessToken = l,
this.pfid = s || S.uid || this._getPFID(),
this.name = r || S.nickname || this._getName(this.pfid),
this.relColor = o || "#ffffff",
this.gradeId = c || S.grade_id || 1,
this.gradeLv = d || S.grade_lvl || 1,
this.lv = h || 1,
this.from = f || 1,
this.fromSeq = g || 1,
this.channelId = m || 1,
this.anchor = v || !1,
this.admin = function(t, e) {
return (t & 1 << e) == 1 << e
}($, 1),
this.follow = p || 0,
this.sex = y || 0,
this.balance = _ || 0,
this.sun = b || 0,
this.balanceNoble = k || S.balance_noble || 0,
this.mute = w || 0,
this.relation = $,
this.avatar = I || 0,
this.limit = u,
this.signed = T,
this.nobleLevel = C || S.nlv || 0,
this.nobleHornNum = S.noble_horn_cnt || 0,
this.nobleState = S.nst || 0,
this.token = this.getToken()
}
return function(t, e, i) {
e && a(t.prototype, e),
i && a(t, i)
}(t, [{
key: "_getPFID",
value: function() {
return localStorage && localStorage[this.pfidName] ? localStorage[this.pfidName] : this._makePFID()
}
}, {
key: "_makePFID",
value: function() {
var t = UUID.generate().toString().replace(/-/g, "");
return t = t.substring(0, t.length),
localStorage && (localStorage[this.pfidName] = t),
t
}
}, {
key: "_getName",
value: function(t) {
var e;
return t && (e = "訪客" + t.toString().substring(t.length - 5, t.length)),
e
}
}, {
key: "getToken",
value: function() {
var t = {};
t.live_id = this.liveId,
t.pfid = this.pfid,
t.name = this.name,
t.access_token = this.accessToken,
t.lv = this.lv,
t.from = this.from,
t.from_seq = this.fromSeq,
t.channel_id = this.channelId,
t.client_type = "web";
function e(t) {
var e = CryptoJS.enc.Base64.stringify(t);
return e = (e = (e = e.replace(/=+$/, "")).replace(/\+/g, "-")).replace(/\//g, "_")
}
var i = e(CryptoJS.enc.Utf8.parse(JSON.stringify({
alg: "HS256",
typ: "JWT"
}))) + "." + e(CryptoJS.enc.Utf8.parse(JSON.stringify(t)))
, n = this.liveKey
, a = CryptoJS.HmacSHA256(i, n);
return i + "." + (a = e(a))
}
}]),
t
}()
DETAILEND@ph
好的,十分成功地找到了整套相关代码。那么逻辑暂时也明确了:
第一行是base64(utf8({"alg":"HS256","typ":"JWT"}))
第二行是base64(utf8({live_id,pfid,name,access_token,lv,from,from_seq,channel_id,client_type}))
pfid的值只是个客户端随机生成的值UUID.generate().toString().replace(/-/g, "")
而第三部分是base64(HMACSHA256(一二行, key=liveKey))
上面调用的base64会把所有的+替换成-,把/替换成_
那么,暂时先这样吧… 我去找个WebSocket库试试先…