修复 wss 部分错误
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
虎牙活动积分兑换/抢购的 Go 复刻版。当前支持两种传输层:
|
||||
|
||||
- `http`:走 `https://cdnws.api.huya.com/`,兼容原有命令。
|
||||
- `wss`:走浏览器同源 WebSocket 协议,更贴近手动抢购抓包里的链路。
|
||||
- `wss`:实验实现。浏览器手动抓包确认兑换链路会走 WSS,但当前 Go WSS 外层协议仍需按 TAF `WebSocketCommand` 重做。
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -57,19 +57,19 @@ CLI 示例:
|
||||
直接兑换指定 PID:
|
||||
|
||||
```bash
|
||||
./huya-go -cmd exchange -sid 2203 -pid 12889 -cookie-file cookie.txt -transport wss
|
||||
./huya-go -cmd exchange -sid 2203 -pid 12889 -cookie-file cookie.txt -transport http
|
||||
```
|
||||
|
||||
按奖品名轮询并抢购:
|
||||
|
||||
```bash
|
||||
./huya-go -cmd monitor -sid 2203 -name 小黄鸭 -cookie-file cookie.txt -transport wss -loops 20 -delay 120 -concurrency 5
|
||||
./huya-go -cmd monitor -sid 2203 -name 小黄鸭 -cookie-file cookie.txt -transport http -loops 20 -delay 120 -concurrency 5
|
||||
```
|
||||
|
||||
定时开始:
|
||||
|
||||
```bash
|
||||
./huya-go -cmd monitor -sid 2203 -pid 12889 -cookie-file cookie.txt -transport wss -at "2026-07-06 20:00:00"
|
||||
./huya-go -cmd monitor -sid 2203 -pid 12889 -cookie-file cookie.txt -transport http -at "2026-07-06 20:00:00"
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
@@ -79,7 +79,7 @@ CLI 示例:
|
||||
- `-pid`:奖品 PID。`exchange` 必填,`monitor` 可用 `-pid` 或 `-name`。
|
||||
- `-cookie`:单账号 Cookie。
|
||||
- `-cookie-file`:多账号 Cookie 文件。
|
||||
- `-transport`:`http` 或 `wss`,默认 `http`;抢购建议使用 `wss`。
|
||||
- `-transport`:`http` 或 `wss`,默认 `http`;当前实际使用建议保持 `http`。
|
||||
- `-wss-host`:WebSocket host。默认值来自 2026-07-06 手动抓包,失效时应从浏览器抓包或 `launch.queryHttpDns` 结果更新。
|
||||
- `-timeout`:单次调用超时秒数。
|
||||
- `-loops`、`-delay`、`-concurrency`:轮询次数、轮询间隔、并发账号数。
|
||||
|
||||
+75
-55
@@ -1,92 +1,110 @@
|
||||
# WSS 协议要点
|
||||
|
||||
本文件记录 Go 版本当前实现依据,来源包括浏览器手动抢购 HAR、软件运行 HAR/日志、逆向导出的分析文本,以及 Python 参考项目 `/Users/yml/codes/douyu_login_py/core/huya`。
|
||||
本文件记录 2026-07-06 手动浏览器 HAR、页面 JS 和 Go 版本当前实现的复核结论。
|
||||
|
||||
## 抓包结论
|
||||
|
||||
手动浏览器链路是 HTTP + WSS 混合:
|
||||
|
||||
- 页面初始化、商品信息、HttpDNS 等有多条 `https://cdnws.api.huya.com/` POST。
|
||||
- 浏览器同时建立了 3 条 `wss://77bc035c-ws.va.huya.com/` 连接。
|
||||
- `scoreExchangePrize` 出现在 WSS 连接数据和静态 JS 中,没有出现在 `cdnws` POST 请求中。
|
||||
- 当前 HTTP 版本可用,是因为 `webActUI` 的 WUP over HTTP 编码已经对齐。
|
||||
- 当前 WSS 版本会超时,是因为 Go 里的 WSS 外层协议模型不符合浏览器 JS。
|
||||
|
||||
## 连接
|
||||
|
||||
- WebSocket URL:`wss://<host>/?baseinfo=<base64 taf>`
|
||||
- 默认 host:`77bc035c-ws.va.huya.com`
|
||||
- 手动 HAR host:`77bc035c-ws.va.huya.com`
|
||||
- host 来自 `launch.queryHttpDns` 或浏览器抓包,不应长期写死。
|
||||
- 握手启用 `permessage-deflate`。
|
||||
- Go 版本使用 `github.com/gorilla/websocket` 处理握手、mask 与压缩。
|
||||
- 浏览器请求扩展:`permessage-deflate; client_max_window_bits`
|
||||
- 服务端返回扩展:`permessage-deflate;server_no_context_takeover;client_no_context_takeover`
|
||||
|
||||
`baseinfo` 是 `WSConnectParaInfo` 的 TAF 编码后 base64,再 URL escape。WSS 模式会把 Cookie 中的 `huya_ua` 归一为:
|
||||
## 正确外层结构
|
||||
|
||||
浏览器 JS 不是 `1B version + 1B command + 4B sequence + body`。
|
||||
|
||||
页面实际构造 TAF/JCE `WebSocketCommand`:
|
||||
|
||||
```text
|
||||
webh5&0.0.1&websocket&&diypc_52775
|
||||
WebSocketCommand {
|
||||
0: iCmdType int32
|
||||
1: vData bytes
|
||||
2: lRequestId int64
|
||||
3: traceId string
|
||||
4: iEncryptType int32
|
||||
5: lTime int64
|
||||
6: sMD5 string
|
||||
}
|
||||
```
|
||||
|
||||
## 外层消息
|
||||
|
||||
Python 参考中对应 `WssMessage/WssCommand`,Go 版本实现为 `internal/huya/wss_message.go`。
|
||||
WUP 请求发送逻辑:
|
||||
|
||||
```text
|
||||
1B version(0) + 1B command + 4B sequence(big-endian) + body
|
||||
Wup.setServant(servant)
|
||||
Wup.setFunc(func)
|
||||
Wup.setRequestId(requestId)
|
||||
Wup.writeStruct("tReq", req)
|
||||
|
||||
WebSocketCommand.iCmdType = EWSCmd_WupReq(3)
|
||||
WebSocketCommand.vData = Wup.encode()
|
||||
WebSocketCommand.traceId = "<trace>:<trace>:0:<enableTrace>"
|
||||
ws.send(WebSocketCommand.writeTo(JceOutputStream))
|
||||
```
|
||||
|
||||
已知命令:
|
||||
服务端返回 `EWSCmd_WupRsp(4)`,内部 `vData` 才是 WUP 响应。
|
||||
|
||||
- `0x03`:RPC request
|
||||
- `0x04`:RPC response
|
||||
- `0x10`:heartbeat send
|
||||
- `0x11`:heartbeat recv
|
||||
- `0x17`:confirm
|
||||
- `0x21`:register
|
||||
## 命令枚举
|
||||
|
||||
已知 sequence:
|
||||
页面 JS 中可见的关键枚举:
|
||||
|
||||
- 心跳:`0x1d00000d`
|
||||
- `launch.wsLaunch`:`0x1d000078`
|
||||
- `mobileui.getConfig`:`0x1d000076`
|
||||
- register:`0x1d00003c`
|
||||
- confirm:`0x1d000002`
|
||||
- 业务 RPC:`0x1d000106`
|
||||
- `3`:`EWSCmd_WupReq`
|
||||
- `4`:`EWSCmd_WupRsp`
|
||||
- `5`:`EWSCmdC2S_HeartBeat`
|
||||
- `6`:`EWSCmdS2C_HeartBeatAck`
|
||||
- `10`:`EWSCmdC2S_VerifyCookieReq`
|
||||
- `11`:`EWSCmdS2C_VerifyCookieRsp`
|
||||
- `20`:`EWSCmdC2S_HeartBeatReq`
|
||||
- `21`:`EWSCmdS2C_HeartBeatRsp`
|
||||
- `23`:`EWSCmdC2S_UpdateUserExpsReq`
|
||||
- `24`:`EWSCmdS2C_UpdateUserExpsRsp`
|
||||
- `33`:`EWSCmdC2S_UpdateUserInfoReq`
|
||||
- `34`:`EWSCmdS2C_UpdateUserInfoRsp`
|
||||
|
||||
## RPC body
|
||||
手动 HAR 解出的服务端 WSS 回包包括:
|
||||
|
||||
WUP 请求由 `internal/wup` 编码,输出自带 4 字节大端总长度。
|
||||
|
||||
小包格式:
|
||||
|
||||
```text
|
||||
[4B WUP 总长度][WUP body] + 2c 36 00 4c 5c 66 00
|
||||
```
|
||||
|
||||
大包格式:
|
||||
|
||||
```text
|
||||
[1B 长度低字节][4B WUP 总长度][裸 WUP body] + trace 尾部
|
||||
```
|
||||
|
||||
其中 trace 尾部形如:
|
||||
|
||||
```text
|
||||
,6%<hex16>:<hex16>:0:3 + 4c 5c 66 00
|
||||
```
|
||||
|
||||
响应解析时,Go 版本会从 WSS body 中切出内部 WUP 包,再交给 `wup.Response.Decode`。
|
||||
- `EWSCmdS2C_HeartBeatRsp`
|
||||
- `EWSCmd_WupRsp launch.wsLaunch`
|
||||
- `EWSCmd_WupRsp mobileui.getConfig`
|
||||
- `EWSCmd_WupRsp webActUI.getActPrizeList`
|
||||
- `EWSCmd_WupRsp webActUI.getUserScore`
|
||||
- `EWSCmd_WupRsp webActUI.scoreExchangePrize`
|
||||
|
||||
## 初始化流程
|
||||
|
||||
WSS session 建立后按以下顺序初始化:
|
||||
浏览器 WSS session 的关键流程:
|
||||
|
||||
1. 发送心跳。
|
||||
2. 调用 `launch.wsLaunch`,读取 `guid` 和 `clientIp`。
|
||||
3. 调用 `mobileui.getConfig`,失败只记录日志,继续后续流程。
|
||||
4. 使用 `wsLaunch` 返回的 `guid` 发送 register。
|
||||
5. 发送 confirm。
|
||||
1. 连接 `wss://<host>/?baseinfo=<base64 taf>`。
|
||||
2. 发送 `EWSCmdC2S_HeartBeatReq`。
|
||||
3. 通过 WSS 调用 `launch.wsLaunch`。
|
||||
4. `wsLaunch` 后触发 `sendWSUpdateUserInfoReq`。
|
||||
5. `launch.queryHttpDns` 仍通过 HTTP `cdnws` 调用。
|
||||
6. 后续 WUP 业务在连接可用时走 `signalSdk.sendWup`,连接不可用时可 fallback 到 `sendWupHttp`。
|
||||
|
||||
之后业务接口复用同一个连接,并用 FIFO 队列匹配 `0x04` RPC response。
|
||||
响应应按 WUP `iRequestId` 匹配回调,不能只用 FIFO。
|
||||
|
||||
## 活动接口
|
||||
|
||||
当前 Go 版本的 HTTP 与 WSS 都实现 `huya.ActivityClient`:
|
||||
当前 Go HTTP 版本已实现:
|
||||
|
||||
- `webActUI.getActPrizeList`
|
||||
- `webActUI.getUserScore`
|
||||
- `webActUI.getUserPrizeRecords`
|
||||
- `webActUI.scoreExchangePrize`
|
||||
|
||||
WSS 版本仍是实验状态,需要按 `WebSocketCommand` 结构重写。
|
||||
|
||||
抢购兑换请求结构:
|
||||
|
||||
```text
|
||||
@@ -112,6 +130,8 @@ tReq.isRole = 1
|
||||
|
||||
## 待完善
|
||||
|
||||
- 实现 TAF `WebSocketCommand` 编解码。
|
||||
- WSS WUP response 按 `iRequestId` 匹配请求。
|
||||
- 实现 `WSUpdateUserInfoReq`、心跳、verify cookie 等浏览器初始化步骤。
|
||||
- 自动调用 `launch.queryHttpDns` 获取最新 WSS host。
|
||||
- 对 WSS RPC response 做 request id 或业务包级匹配,当前与 Python 参考一致使用 FIFO。
|
||||
- 增加基于真实抓包样本的 WSS body 编解码单测。
|
||||
- 增加基于真实抓包样本的 WSS 编解码单测。
|
||||
|
||||
Binary file not shown.
@@ -181,7 +181,7 @@ function statusTag(status?: number, error?: string): ReactNode {
|
||||
|
||||
function pickConfig(values: Partial<UiConfig>): UiConfig {
|
||||
return {
|
||||
transport: values.transport || 'wss',
|
||||
transport: values.transport || 'http',
|
||||
wssHost: values.wssHost || '',
|
||||
sid: Number(values.sid || 0),
|
||||
pid: Number(values.pid || 0),
|
||||
@@ -418,8 +418,8 @@ export default function Dashboard() {
|
||||
<Form.Item name="transport" label="传输">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'wss', label: 'WSS' },
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'wss', label: 'WSS 实验' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ func (a *App) Shutdown(ctx context.Context) {
|
||||
|
||||
func (a *App) DefaultConfig() Config {
|
||||
return Config{
|
||||
Transport: "wss",
|
||||
Transport: "http",
|
||||
WSSHost: huya.DefaultWSSHost,
|
||||
SID: 2203,
|
||||
PID: 12889,
|
||||
|
||||
+185
-189
@@ -48,7 +48,8 @@ type WSSSession struct {
|
||||
|
||||
writeMu sync.Mutex
|
||||
pendingMu sync.Mutex
|
||||
pending []chan rpcResult
|
||||
pending map[int32]chan rpcResult
|
||||
requestMu sync.Mutex
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
@@ -59,7 +60,7 @@ type WSSSession struct {
|
||||
}
|
||||
|
||||
type rpcResult struct {
|
||||
body []byte
|
||||
resp *wup.Response
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -173,7 +174,8 @@ func (c *WSSClient) dialSession(ctx context.Context, uid int64, cookie string) (
|
||||
guid: guid,
|
||||
conn: conn,
|
||||
closed: make(chan struct{}),
|
||||
nextWUPRequestID: 8,
|
||||
pending: map[int32]chan rpcResult{},
|
||||
nextWUPRequestID: 1,
|
||||
}
|
||||
go session.readLoop()
|
||||
go session.heartbeatLoop()
|
||||
@@ -214,18 +216,25 @@ func (s *WSSSession) readLoop() {
|
||||
if messageType != websocket.BinaryMessage {
|
||||
continue
|
||||
}
|
||||
msg, err := DecodeWSSMessage(raw)
|
||||
cmd, err := DecodeWSSCommand(raw)
|
||||
if err != nil {
|
||||
s.client.logf("[WSS] 解码消息失败: %v", err)
|
||||
continue
|
||||
}
|
||||
switch msg.Command {
|
||||
case WSSCommandRPCResponse:
|
||||
s.resolveNext(rpcResult{body: msg.Body})
|
||||
case WSSCommandHeartbeatRecv:
|
||||
switch cmd.CmdType {
|
||||
case WSSCmdWupRsp:
|
||||
var resp wup.Response
|
||||
if err := resp.Decode(cmd.Data); err != nil {
|
||||
s.client.logf("[WSS] 解码 WUP 响应失败: %v", err)
|
||||
continue
|
||||
}
|
||||
s.resolveRequest(resp.RequestID, rpcResult{resp: &resp})
|
||||
case WSSCmdHeartBeatAck, WSSCmdHeartBeatRsp:
|
||||
// 心跳响应无需处理。
|
||||
case WSSCmdVerifyCookieRsp, WSSCmdUpdateUserInfoRsp, WSSCmdUpdateUserExpsRsp:
|
||||
s.client.logf("[WSS] 收到控制响应 cmd=%d data=%dB", cmd.CmdType, len(cmd.Data))
|
||||
default:
|
||||
s.client.logf("[WSS] 收到 cmd=0x%02x seq=0x%08x body=%dB", msg.Command, msg.Sequence, len(msg.Body))
|
||||
s.client.logf("[WSS] 收到 cmd=%d data=%dB requestID=%d", cmd.CmdType, len(cmd.Data), cmd.RequestID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,70 +266,40 @@ func (s *WSSSession) closeWithError(err error) {
|
||||
func (s *WSSSession) failPending(err error) {
|
||||
s.pendingMu.Lock()
|
||||
pending := s.pending
|
||||
s.pending = nil
|
||||
s.pending = map[int32]chan rpcResult{}
|
||||
s.pendingMu.Unlock()
|
||||
for _, ch := range pending {
|
||||
ch <- rpcResult{err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WSSSession) resolveNext(result rpcResult) {
|
||||
func (s *WSSSession) resolveRequest(requestID int32, result rpcResult) {
|
||||
s.pendingMu.Lock()
|
||||
if len(s.pending) == 0 {
|
||||
ch := s.pending[requestID]
|
||||
if ch == nil {
|
||||
s.pendingMu.Unlock()
|
||||
s.client.logf("[WSS] 未匹配响应 requestID=%d", requestID)
|
||||
return
|
||||
}
|
||||
ch := s.pending[0]
|
||||
copy(s.pending, s.pending[1:])
|
||||
s.pending = s.pending[:len(s.pending)-1]
|
||||
delete(s.pending, requestID)
|
||||
s.pendingMu.Unlock()
|
||||
ch <- result
|
||||
}
|
||||
|
||||
func (s *WSSSession) removePending(target chan rpcResult) {
|
||||
func (s *WSSSession) removePending(requestID int32) {
|
||||
s.pendingMu.Lock()
|
||||
defer s.pendingMu.Unlock()
|
||||
for i, ch := range s.pending {
|
||||
if ch == target {
|
||||
copy(s.pending[i:], s.pending[i+1:])
|
||||
s.pending = s.pending[:len(s.pending)-1]
|
||||
return
|
||||
}
|
||||
}
|
||||
delete(s.pending, requestID)
|
||||
}
|
||||
|
||||
func (s *WSSSession) writeMessage(ctx context.Context, msg WSSMessage) error {
|
||||
func (s *WSSSession) writePayload(ctx context.Context, payload []byte) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = s.conn.SetWriteDeadline(deadline)
|
||||
defer s.conn.SetWriteDeadline(time.Time{})
|
||||
}
|
||||
return s.conn.WriteMessage(websocket.BinaryMessage, msg.Encode())
|
||||
}
|
||||
|
||||
func (s *WSSSession) callRaw(ctx context.Context, msg WSSMessage) ([]byte, error) {
|
||||
ch := make(chan rpcResult, 1)
|
||||
s.pendingMu.Lock()
|
||||
s.pending = append(s.pending, ch)
|
||||
s.pendingMu.Unlock()
|
||||
|
||||
if err := s.writeMessage(ctx, msg); err != nil {
|
||||
s.removePending(ch)
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case result := <-ch:
|
||||
return result.body, result.err
|
||||
case <-ctx.Done():
|
||||
s.removePending(ch)
|
||||
return nil, ctx.Err()
|
||||
case <-s.closed:
|
||||
if s.closeErr != nil {
|
||||
return nil, s.closeErr
|
||||
}
|
||||
return nil, errors.New("wss closed")
|
||||
}
|
||||
return s.conn.WriteMessage(websocket.BinaryMessage, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) initialize(ctx context.Context) error {
|
||||
@@ -330,15 +309,18 @@ func (s *WSSSession) initialize(ctx context.Context) error {
|
||||
if err := s.callWSLaunch(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.callGetConfig(ctx); err != nil {
|
||||
s.client.logf("[WSS] getConfig 失败但继续: %v", err)
|
||||
}
|
||||
if s.launchGUID != "" {
|
||||
if err := s.sendRegister(ctx, s.launchGUID); err != nil {
|
||||
if err := s.sendUpdateUserInfo(ctx, s.launchGUID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.sendConfirm(ctx)
|
||||
if err := s.sendVerifyCookie(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.callGetConfig(ctx); err != nil {
|
||||
s.client.logf("[WSS] getConfig 失败但继续: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WSSSession) withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
@@ -349,48 +331,19 @@ func (s *WSSSession) withTimeout(ctx context.Context) (context.Context, context.
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendHeartbeat(ctx context.Context) error {
|
||||
w := taf.NewWriter()
|
||||
if err := w.WriteHead(0, taf.TypeList); err != nil {
|
||||
payload, err := EncodeWSSCommand(WSSCmdHeartBeatReq, nil, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(0, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(0, "live:0"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
body := append(append([]byte{}, w.Bytes()...), wssTailBytes...)
|
||||
return s.writeMessage(ctx, WSSMessage{Command: WSSCommandHeartbeatSend, Sequence: wssSeqHeartbeat, Body: body})
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) callWSLaunch(ctx context.Context) error {
|
||||
req := &wsLaunchReq{}
|
||||
wupReq := wup.NewRequest("launch", "wsLaunch", -1)
|
||||
wupReq.Timeout = 0
|
||||
if err := wupReq.WriteStruct("tReq", req); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := wupReq.Encode()
|
||||
req := &wsLaunchReq{UID: s.uid, GUID: s.guid, UA: WSSInitUA}
|
||||
resp, err := s.callWUP(ctx, "launch", "wsLaunch", req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := s.withTimeout(ctx)
|
||||
defer cancel()
|
||||
body, err := s.callRaw(ctx, WSSMessage{
|
||||
Command: WSSCommandRPCRequest,
|
||||
Sequence: wssSeqWSLaunch,
|
||||
Body: encodeWSSRPCBody(data),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resp wup.Response
|
||||
if err := resp.Decode(extractWUPFromWSSBody(body)); err != nil {
|
||||
return err
|
||||
}
|
||||
var launch wsLaunchResp
|
||||
ok, err := resp.ReadStruct("tRsp", &launch)
|
||||
if err != nil {
|
||||
@@ -406,82 +359,51 @@ func (s *WSSSession) callWSLaunch(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *WSSSession) callGetConfig(ctx context.Context) error {
|
||||
req := &getConfigReq{}
|
||||
wupReq := wup.NewRequest("mobileui", "getConfig", 7)
|
||||
wupReq.Timeout = 0
|
||||
if err := wupReq.WriteStruct("tReq", req); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := wupReq.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := s.withTimeout(ctx)
|
||||
defer cancel()
|
||||
_, err = s.callRaw(ctx, WSSMessage{
|
||||
Command: WSSCommandRPCRequest,
|
||||
Sequence: wssSeqGetConfig,
|
||||
Body: encodeWSSRPCBody(data),
|
||||
})
|
||||
req := &getConfigReq{UID: s.uid, GUID: s.launchGUID, Cookie: s.cookie}
|
||||
_, err := s.callWUP(ctx, "mobileui", "getConfig", req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendRegister(ctx context.Context, launchGUID string) error {
|
||||
w := taf.NewWriter()
|
||||
if err := w.WriteString(0, "HUYA&ZH&2052"); err != nil {
|
||||
func (s *WSSSession) sendUpdateUserInfo(ctx context.Context, launchGUID string) error {
|
||||
req := &wsUpdateUserInfoReq{AppSrc: "HUYA&ZH&2052", GUID: launchGUID}
|
||||
payload, err := encodeWSSStructCommand(WSSCmdUpdateUserInfoReq, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, launchGUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(2, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(3, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructBegin(6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(0, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(1, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(3, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructEnd(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(7, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(8, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
body := append(append([]byte{}, w.Bytes()...), wssTailBytes...)
|
||||
return s.writeMessage(ctx, WSSMessage{Command: WSSCommandRegister, Sequence: wssSeqRegister, Body: body})
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendConfirm(ctx context.Context) error {
|
||||
w := taf.NewWriter()
|
||||
if err := w.WriteMapStringString(0, nil); err != nil {
|
||||
func (s *WSSSession) sendVerifyCookie(ctx context.Context) error {
|
||||
guid := s.launchGUID
|
||||
if guid == "" {
|
||||
guid = s.guid
|
||||
}
|
||||
req := &wsVerifyCookieReq{
|
||||
UID: s.uid,
|
||||
UA: WSSInitUA,
|
||||
Cookie: NormalizeWSSCookie(s.cookie),
|
||||
GUID: guid,
|
||||
AutoRegisterUID: 1,
|
||||
AppSrc: "HUYA&ZH&2052",
|
||||
}
|
||||
payload, err := encodeWSSStructCommand(WSSCmdVerifyCookieReq, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := append(append([]byte{}, w.Bytes()...), wssTailBytes...)
|
||||
return s.writeMessage(ctx, WSSMessage{Command: WSSCommandConfirmSend, Sequence: wssSeqConfirm, Body: body})
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) nextRequestID() int32 {
|
||||
s.requestMu.Lock()
|
||||
defer s.requestMu.Unlock()
|
||||
id := s.nextWUPRequestID
|
||||
s.nextWUPRequestID++
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *WSSSession) callRPC(ctx context.Context, service, method string, reqStruct taf.Marshaler, respStruct taf.Unmarshaler) (*wup.Response, error) {
|
||||
wupReq := wup.NewRequest(service, method, s.nextRequestID())
|
||||
func (s *WSSSession) callWUP(ctx context.Context, service, method string, reqStruct taf.Marshaler) (*wup.Response, error) {
|
||||
requestID := s.nextRequestID()
|
||||
wupReq := wup.NewRequest(service, method, requestID)
|
||||
wupReq.Timeout = 0
|
||||
if err := wupReq.WriteStruct("tReq", reqStruct); err != nil {
|
||||
return nil, err
|
||||
@@ -492,17 +414,37 @@ func (s *WSSSession) callRPC(ctx context.Context, service, method string, reqStr
|
||||
}
|
||||
ctx, cancel := s.withTimeout(ctx)
|
||||
defer cancel()
|
||||
s.client.logf("[WSS] RPC %s.%s body=%dB", service, method, len(data))
|
||||
body, err := s.callRaw(ctx, WSSMessage{
|
||||
Command: WSSCommandRPCRequest,
|
||||
Sequence: wssSeqBusiness,
|
||||
Body: encodeWSSRPCBody(data),
|
||||
})
|
||||
payload, err := EncodeWSSCommand(WSSCmdWupReq, data, NewWSSCommandTrace())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp wup.Response
|
||||
if err := resp.Decode(extractWUPFromWSSBody(body)); err != nil {
|
||||
ch := make(chan rpcResult, 1)
|
||||
s.pendingMu.Lock()
|
||||
s.pending[requestID] = ch
|
||||
s.pendingMu.Unlock()
|
||||
s.client.logf("[WSS] WUP %s.%s requestID=%d body=%dB", service, method, requestID, len(data))
|
||||
if err := s.writePayload(ctx, payload); err != nil {
|
||||
s.removePending(requestID)
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case result := <-ch:
|
||||
return result.resp, result.err
|
||||
case <-ctx.Done():
|
||||
s.removePending(requestID)
|
||||
return nil, ctx.Err()
|
||||
case <-s.closed:
|
||||
s.removePending(requestID)
|
||||
if s.closeErr != nil {
|
||||
return nil, s.closeErr
|
||||
}
|
||||
return nil, errors.New("wss closed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WSSSession) callRPC(ctx context.Context, service, method string, reqStruct taf.Marshaler, respStruct taf.Unmarshaler) (*wup.Response, error) {
|
||||
resp, err := s.callWUP(ctx, service, method, reqStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if respStruct != nil {
|
||||
@@ -514,7 +456,7 @@ func (s *WSSSession) callRPC(ctx context.Context, service, method string, reqStr
|
||||
return nil, fmt.Errorf("%s.%s 响应缺少 tRsp/tResp", service, method)
|
||||
}
|
||||
}
|
||||
return &resp, nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *WSSClient) buildUser(uid int64, cookie string) ActivityUserID {
|
||||
@@ -588,16 +530,24 @@ func NormalizeWSSCookie(cookie string) string {
|
||||
return strings.Join(out, "; ")
|
||||
}
|
||||
|
||||
type wsLaunchReq struct{}
|
||||
type wsLaunchReq struct {
|
||||
UID int64
|
||||
GUID string
|
||||
UA string
|
||||
}
|
||||
|
||||
func (r *wsLaunchReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteInt64(0, 0); err != nil {
|
||||
if err := w.WriteInt64(0, r.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, ""); err != nil {
|
||||
if err := w.WriteString(1, r.GUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(2, WSSInitUA); err != nil {
|
||||
ua := r.UA
|
||||
if ua == "" {
|
||||
ua = WSSInitUA
|
||||
}
|
||||
if err := w.WriteString(2, ua); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(3, "HUYA&ZH&2052"); err != nil {
|
||||
@@ -614,6 +564,73 @@ func (r *wsLaunchReq) WriteTo(w *taf.Writer) error {
|
||||
return w.WriteStructEnd()
|
||||
}
|
||||
|
||||
type wsUpdateUserInfoReq struct {
|
||||
AppSrc string
|
||||
GUID string
|
||||
}
|
||||
|
||||
func (r *wsUpdateUserInfoReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteString(0, r.AppSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, r.GUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(2, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(3, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructBegin(6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(0, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(1, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(2, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructEnd(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(7, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteInt32(8, 0)
|
||||
}
|
||||
|
||||
type wsVerifyCookieReq struct {
|
||||
UID int64
|
||||
UA string
|
||||
Cookie string
|
||||
GUID string
|
||||
AutoRegisterUID int32
|
||||
AppSrc string
|
||||
}
|
||||
|
||||
func (r *wsVerifyCookieReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteInt64(0, r.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, r.UA); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(2, r.Cookie); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(3, r.GUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(4, int64(r.AutoRegisterUID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteString(5, r.AppSrc)
|
||||
}
|
||||
|
||||
type wsLaunchResp struct {
|
||||
GUID string
|
||||
ClientIP string
|
||||
@@ -628,37 +645,16 @@ func (r *wsLaunchResp) ReadFrom(rd *taf.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
type getConfigReq struct{}
|
||||
type getConfigReq struct {
|
||||
UID int64
|
||||
GUID string
|
||||
Cookie string
|
||||
}
|
||||
|
||||
func (r *getConfigReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteStructBegin(0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt64(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(1, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(2, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(3, WSSInitUA); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(4, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(5, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(6, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(7, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteStructEnd(); err != nil {
|
||||
user := NewActivityUserID(r.UID, NormalizeWSSCookie(r.Cookie), WSSInitUA)
|
||||
user.GUID = r.GUID
|
||||
if err := w.WriteStruct(0, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteMapStringString(1, nil); err != nil {
|
||||
|
||||
+113
-86
@@ -1,113 +1,140 @@
|
||||
package huya
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"huya-go/internal/taf"
|
||||
)
|
||||
|
||||
const (
|
||||
WSSCommandRPCRequest byte = 0x03
|
||||
WSSCommandRPCResponse byte = 0x04
|
||||
WSSCommandAuth byte = 0x0a
|
||||
WSSCommandHeartbeatSend byte = 0x10
|
||||
WSSCommandHeartbeatRecv byte = 0x11
|
||||
WSSCommandConfirmSend byte = 0x17
|
||||
WSSCommandRegister byte = 0x21
|
||||
|
||||
wssSeqHeartbeat uint32 = 0x1d00000d
|
||||
wssSeqWSLaunch uint32 = 0x1d000078
|
||||
wssSeqGetConfig uint32 = 0x1d000076
|
||||
wssSeqRegister uint32 = 0x1d00003c
|
||||
wssSeqConfirm uint32 = 0x1d000002
|
||||
wssSeqBusiness uint32 = 0x1d000106
|
||||
WSSCmdNull int32 = 0
|
||||
WSSCmdRegisterReq int32 = 1
|
||||
WSSCmdRegisterRsp int32 = 2
|
||||
WSSCmdWupReq int32 = 3
|
||||
WSSCmdWupRsp int32 = 4
|
||||
WSSCmdHeartBeat int32 = 5
|
||||
WSSCmdHeartBeatAck int32 = 6
|
||||
WSSCmdMsgPushReq int32 = 7
|
||||
WSSCmdDeregisterReq int32 = 8
|
||||
WSSCmdDeregisterRsp int32 = 9
|
||||
WSSCmdVerifyCookieReq int32 = 10
|
||||
WSSCmdVerifyCookieRsp int32 = 11
|
||||
WSSCmdHeartBeatReq int32 = 20
|
||||
WSSCmdHeartBeatRsp int32 = 21
|
||||
WSSCmdMsgPushReqV2 int32 = 22
|
||||
WSSCmdUpdateUserExpsReq int32 = 23
|
||||
WSSCmdUpdateUserExpsRsp int32 = 24
|
||||
WSSCmdUpdateUserInfoReq int32 = 33
|
||||
WSSCmdUpdateUserInfoRsp int32 = 34
|
||||
WSSCmdMsgAckReq int32 = 35
|
||||
WSSCmdMsgAckRsp int32 = 36
|
||||
WSSCmdS2CRpcReq int32 = 40
|
||||
WSSCmdC2SRpcRsp int32 = 41
|
||||
WSSCmdS2CRpcRspRsp int32 = 42
|
||||
WSSCmdSignalUpgradeReq int32 = 105
|
||||
WSSCmdSignalUpgradeRsp int32 = 106
|
||||
defaultWSSTraceEnableValue = 3
|
||||
)
|
||||
|
||||
var (
|
||||
wssTailBytes = []byte{0x2c, 0x36, 0x00, 0x4c, 0x5c, 0x66, 0x00}
|
||||
wssBigTailSuffix = []byte{0x4c, 0x5c, 0x66, 0x00}
|
||||
)
|
||||
|
||||
// WSSMessage 是虎牙 WebSocketCommand 的 Go 表达。
|
||||
// WSSCommand 是浏览器 JS 里的 HUYA.WebSocketCommand。
|
||||
//
|
||||
// 外层格式来自浏览器/参考项目:
|
||||
//
|
||||
// 1B version(0) + 1B command + 4B sequence + body
|
||||
type WSSMessage struct {
|
||||
Command byte
|
||||
Sequence uint32
|
||||
Body []byte
|
||||
// 注意:这不是旧版的 6 字节自定义头。浏览器实际把这个 TAF/JCE
|
||||
// 结构直接作为 WebSocket binary payload 发送。
|
||||
type WSSCommand struct {
|
||||
CmdType int32
|
||||
Data []byte
|
||||
RequestID int64
|
||||
TraceID string
|
||||
EncryptType int32
|
||||
Time int64
|
||||
MD5 string
|
||||
}
|
||||
|
||||
func (m WSSMessage) Encode() []byte {
|
||||
out := make([]byte, 6+len(m.Body))
|
||||
out[0] = 0
|
||||
out[1] = m.Command
|
||||
binary.BigEndian.PutUint32(out[2:6], m.Sequence)
|
||||
copy(out[6:], m.Body)
|
||||
return out
|
||||
func (c *WSSCommand) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteInt32(0, int64(c.CmdType)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteBytes(1, c.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt64(2, c.RequestID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteString(3, c.TraceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(4, int64(c.EncryptType)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt64(5, c.Time); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteString(6, c.MD5)
|
||||
}
|
||||
|
||||
func DecodeWSSMessage(data []byte) (WSSMessage, error) {
|
||||
if len(data) < 6 {
|
||||
return WSSMessage{}, fmt.Errorf("wss: 消息太短: %d", len(data))
|
||||
func (c *WSSCommand) ReadFrom(r *taf.Reader) error {
|
||||
var err error
|
||||
if c.CmdType, err = r.ReadInt32(0, false, c.CmdType); err != nil {
|
||||
return err
|
||||
}
|
||||
return WSSMessage{
|
||||
Command: data[1],
|
||||
Sequence: binary.BigEndian.Uint32(data[2:6]),
|
||||
Body: append([]byte(nil), data[6:]...),
|
||||
}, nil
|
||||
if c.Data, err = r.ReadBytes(1, false, c.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.RequestID, err = r.ReadInt64(2, false, c.RequestID); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.TraceID, err = r.ReadString(3, false, c.TraceID); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.EncryptType, err = r.ReadInt32(4, false, c.EncryptType); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Time, err = r.ReadInt64(5, false, c.Time); err != nil {
|
||||
return err
|
||||
}
|
||||
c.MD5, err = r.ReadString(6, false, c.MD5)
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeWSSRPCBody(wupData []byte) []byte {
|
||||
if len(wupData) < 4 {
|
||||
out := append([]byte{}, wupData...)
|
||||
return append(out, wssTailBytes...)
|
||||
func (c WSSCommand) Encode() ([]byte, error) {
|
||||
w := taf.NewWriter()
|
||||
if err := c.WriteTo(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
declaredLen := int(binary.BigEndian.Uint32(wupData[:4]))
|
||||
wupBody := wupData
|
||||
if declaredLen == len(wupData) {
|
||||
wupBody = wupData[4:]
|
||||
}
|
||||
if len(wupBody) <= 0xff {
|
||||
out := append([]byte{}, wupData...)
|
||||
return append(out, wssTailBytes...)
|
||||
}
|
||||
|
||||
trace := NewWSSBigTailTrace()
|
||||
packetLen := len(wupData)
|
||||
out := make([]byte, 0, 1+4+len(wupBody)+len(trace))
|
||||
out = append(out, byte(packetLen&0xff))
|
||||
var lenBuf [4]byte
|
||||
binary.BigEndian.PutUint32(lenBuf[:], uint32(packetLen))
|
||||
out = append(out, lenBuf[:]...)
|
||||
out = append(out, wupBody...)
|
||||
out = append(out, trace...)
|
||||
return out
|
||||
return append([]byte(nil), w.Bytes()...), nil
|
||||
}
|
||||
|
||||
func extractWUPFromWSSBody(body []byte) []byte {
|
||||
if len(body) < 5 {
|
||||
return body
|
||||
func DecodeWSSCommand(data []byte) (WSSCommand, error) {
|
||||
var cmd WSSCommand
|
||||
if len(data) == 0 {
|
||||
return cmd, fmt.Errorf("wss: 空消息")
|
||||
}
|
||||
// 大包格式: [1B prefix][4B wup_len][裸 WUP body][tail]
|
||||
wupLen := int(binary.BigEndian.Uint32(body[1:5]))
|
||||
if 5+wupLen <= len(body) && len(body) >= 7 && body[5] == 0x10 && body[6] == 0x03 {
|
||||
return body[5 : 5+wupLen]
|
||||
if err := cmd.ReadFrom(taf.NewReader(data)); err != nil {
|
||||
return cmd, err
|
||||
}
|
||||
// 小包格式: [4B total_len][WUP body][tail]
|
||||
totalLen := int(binary.BigEndian.Uint32(body[:4]))
|
||||
if 8 <= totalLen && totalLen <= len(body) && body[4] == 0x10 && body[5] == 0x03 {
|
||||
return body[:totalLen]
|
||||
}
|
||||
if 5+wupLen <= len(body) {
|
||||
return body[5 : 5+wupLen]
|
||||
}
|
||||
return body
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func NewWSSBigTailTrace() []byte {
|
||||
func EncodeWSSCommand(cmdType int32, data []byte, traceID string) ([]byte, error) {
|
||||
return WSSCommand{
|
||||
CmdType: cmdType,
|
||||
Data: append([]byte(nil), data...),
|
||||
TraceID: traceID,
|
||||
}.Encode()
|
||||
}
|
||||
|
||||
func encodeWSSStructCommand(cmdType int32, value taf.Marshaler) ([]byte, error) {
|
||||
w := taf.NewWriter()
|
||||
if value != nil {
|
||||
if err := value.WriteTo(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return EncodeWSSCommand(cmdType, w.Bytes(), "")
|
||||
}
|
||||
|
||||
func NewWSSCommandTrace() string {
|
||||
trace := lowerHex16(rand.Uint64())
|
||||
out := []byte(",6%" + trace + ":" + trace + ":0:3")
|
||||
return append(out, wssBigTailSuffix...)
|
||||
return fmt.Sprintf("%s:%s:0:%d", trace, trace, defaultWSSTraceEnableValue)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user