修复 wss 部分错误

This commit is contained in:
yml2213
2026-07-06 18:30:34 +08:00
parent 4235cce4f5
commit 1d15fadfd9
7 changed files with 381 additions and 338 deletions
+5 -5
View File
@@ -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
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}