Back
Featured image of post Bilibili 直播 WebSocket 流逆向

Bilibili 直播 WebSocket 流逆向

B 站直播消息流 WebSocket 封包及命令解析。

B 站直播流协议整体上比较简单,只是收集命令比较耗费精力。本文将介绍 B 站 WebSocket 协议使用了的包体和结构定义。

通讯过程

准备连接

获取自身 UID (可选)

略。获取方法太多,最常见的为 https://api.bilibili.com/x/web-interface/nav,带 Cookie 访问即可。

获取真实房间号

连接前需要调取 https://api.live.bilibili.com/room/v1/Room/room_init,以获得真实房间号。

最小示例请求,无需鉴权:

curl -G 'https://api.live.bilibili.com/room/v1/Room/room_init?id=7777' --compressed

返回示例:

{
  "code": 0,                 // 状态码, 0 为成功
  "msg": "ok",               // 错误信息
  "message": "ok",           // 错误信息
  "data": {                  // 数据
    "room_id": 545068,       // 真实房间号
    "short_id": 7777,        // 短房间号, 可能为 0, 代表无
    "uid": 8739477,          // 主播 UID
    "need_p2p": 0,
    "is_hidden": false,
    "is_locked": false,
    "is_portrait": false,
    "live_status": 1,        // 暂停 = 0, 直播 = 1, 轮播 = 2
    "hidden_till": 0,
    "lock_till": 0,
    "encrypted": false,
    "pwd_verified": false,
    "live_time": 1677038397,
    "room_shield": 0,
    "is_sp": 0,
    "special_type": 0        // 普通 = 0, 付费 = 1, 拜年纪 = 2
  }
}

获取 WSS 服务器及密钥

访问 https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo 可获取消息流服务器及认证包所需的密钥。

最小示例请求,无需鉴权,id 应为真实房间号,否则可能连接到错误的直播间:

curl 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=545068' --compressed

返回示例:

{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "group": "live",             // <\
    "business_id": 0,            // <
    "refresh_row_factor": 0.125, // < 这五个参数没有多大意义
    "refresh_rate": 100,         // <
    "max_delay": 5000,           // </
    "token": "nDI********I0ll-ryrd-", // 密钥, 认证包的时候会用到
    "host_list": [
      {
        "host": "live.chat.bilibili.com", // 域名
        "port": 2243,                     // 端口
        "wss_port": 2245,                 // WSS 端口
        "ws_port": 2244                   // WS  端口
      }
      // 剩下为其他分流, 略
    ]
  }
}

域名应任选一 $.data.host_list[] 中的,拼接为 wss://$host:$wss_port/sub

WebSoket 消息流包体结构

见图:

WebSoket 消息流包体结构

其中 HeaderProtocolType 常数为:

enum Protocol {
  Command       = 0,
  Special       = 1,
  CommandZlib   = 2,
  CommandBrotli = 3,
}

enum Type {
  Heartbeat       = 2,
  HeartbeatResp   = 3,
  Command         = 5,
  Certificate     = 7,
  CertificateResp = 8,
}

Sequence 参数可一直为 1HeadSize 恒为 16TotalSize 即为 16 + 包体长度。

客户端仅可能发 Special 协议,且为 HeartbeatCertificate 类型的包。

建立连接

使用 WSS 协议向之前获取到的域名建立连接。所有通讯都使用 WebSocket 的 Binary Frame。

在连接后,应在几秒之内首先发送 Certificate 包,否则服务器会中断连接。

包头应为 Special 协议和 Certificate 类型。

JSON 包体定义为:

struct Certificate {
  uid      : u64, // 此处 UID 或许可以填 0, B 站游客可看到弹幕流
  roomid   : u64,
  key      : String,
  protover : u8,
}

其中 key 为之前获取到的密钥,roomid 为要连接的房间号,protover 可选值为:

enum Protover {
  Normal = 1,
  Zlib   = 2,
  Brotli = 3,
}

该值影响了之后命令会收到的压缩类型。无压缩协议似乎已被禁用,只可使用 Zlib 及 Brotli 压缩协议。

此时,服务器会返回 CertificateResp 类型的包,其 JSON 定义为 { code: number }code 为 0 即代表鉴权成功。

此时服务器也会开始发送 Command 类型的业务包。

客户端还需要每隔一段时间向服务器发送心跳包,以保持连接。心跳包头为 Special 协议和 Heartbeat 类型。包体可为 [Object object] 字符串。

服务器在收到心跳包后,会返回 HeartbeatResp。包体或许为 UInt32 的人气值。不过该属性可能已被弃用,往往为 1

Command 类型存在几种不同的压缩方式,需要通过包头的 Protocol 来判断包体如何读取。

ProtocolCommand 时,包体为单个 JSON,直接解码即可。

ProtocolCommandZlibCommandBrotli 时,包体会嵌套一层连续的包(参见上文配图)。需要先解压包体后读取。即嵌套的包头和包体均被压缩。

业务包 JSON 结构一般为:

{
  "cmd": "CMD_ID",
  "data": {
    "payload": "payload1",
  }
}

不过有时也会在顶层有其他属性,如 DANMU_MSG

{
  "cmd": "DANMU_MSG",
  "dm_v2": "略",
  "info": [ /* 略 */ ]
}

业务包解析

Plutus

comments powered by Disqus