继续补全 wss
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
虎牙活动积分兑换/抢购的 Go 复刻版。当前支持两种传输层:
|
||||
|
||||
- `http`:走 `https://cdnws.api.huya.com/`,兼容原有命令。
|
||||
- `wss`:实验实现。浏览器手动抓包确认兑换链路会走 WSS,但当前 Go WSS 外层协议仍需按 TAF `WebSocketCommand` 重做。
|
||||
- `wss`:实验实现。已按浏览器 HAR 改为 TAF `WebSocketCommand` 外层和 WSS 初始化流程,仍建议先用真实 Cookie 小流量验证。
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -79,7 +79,7 @@ CLI 示例:
|
||||
- `-pid`:奖品 PID。`exchange` 必填,`monitor` 可用 `-pid` 或 `-name`。
|
||||
- `-cookie`:单账号 Cookie。
|
||||
- `-cookie-file`:多账号 Cookie 文件。
|
||||
- `-transport`:`http` 或 `wss`,默认 `http`;当前实际使用建议保持 `http`。
|
||||
- `-transport`:`http` 或 `wss`,默认 `http`;WSS 仍为实验通道。
|
||||
- `-wss-host`:WebSocket host。默认值来自 2026-07-06 手动抓包,失效时应从浏览器抓包或 `launch.queryHttpDns` 结果更新。
|
||||
- `-timeout`:单次调用超时秒数。
|
||||
- `-loops`、`-delay`、`-concurrency`:轮询次数、轮询间隔、并发账号数。
|
||||
|
||||
+16
-11
@@ -10,13 +10,14 @@
|
||||
- 浏览器同时建立了 3 条 `wss://77bc035c-ws.va.huya.com/` 连接。
|
||||
- `scoreExchangePrize` 出现在 WSS 连接数据和静态 JS 中,没有出现在 `cdnws` POST 请求中。
|
||||
- 当前 HTTP 版本可用,是因为 `webActUI` 的 WUP over HTTP 编码已经对齐。
|
||||
- 当前 WSS 版本会超时,是因为 Go 里的 WSS 外层协议模型不符合浏览器 JS。
|
||||
- Go 版 WSS 已改为浏览器同款 `WebSocketCommand` 外层,仍需真实 Cookie 联调确认服务端返回。
|
||||
|
||||
## 连接
|
||||
|
||||
- WebSocket URL:`wss://<host>/?baseinfo=<base64 taf>`
|
||||
- 手动 HAR host:`77bc035c-ws.va.huya.com`
|
||||
- host 来自 `launch.queryHttpDns` 或浏览器抓包,不应长期写死。
|
||||
- 手动 HAR 中 `baseinfo.cookie` 与 `traceId` 为空,cookie 通过后续 `VerifyCookie` 命令发送。
|
||||
- 握手启用 `permessage-deflate`。
|
||||
- 浏览器请求扩展:`permessage-deflate; client_max_window_bits`
|
||||
- 服务端返回扩展:`permessage-deflate;server_no_context_takeover;client_no_context_takeover`
|
||||
@@ -65,6 +66,8 @@ ws.send(WebSocketCommand.writeTo(JceOutputStream))
|
||||
- `6`:`EWSCmdS2C_HeartBeatAck`
|
||||
- `10`:`EWSCmdC2S_VerifyCookieReq`
|
||||
- `11`:`EWSCmdS2C_VerifyCookieRsp`
|
||||
- `16`:`EWSCmdC2S_RegisterGroupReq`
|
||||
- `17`:`EWSCmdS2C_RegisterGroupRsp`
|
||||
- `20`:`EWSCmdC2S_HeartBeatReq`
|
||||
- `21`:`EWSCmdS2C_HeartBeatRsp`
|
||||
- `23`:`EWSCmdC2S_UpdateUserExpsReq`
|
||||
@@ -86,11 +89,15 @@ ws.send(WebSocketCommand.writeTo(JceOutputStream))
|
||||
浏览器 WSS session 的关键流程:
|
||||
|
||||
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`。
|
||||
2. 发送 `EWSCmdC2S_RegisterGroupReq(16)`,默认 group 为 `live:0`。
|
||||
3. 通过 WSS 调用 `launch.wsLaunch`,手动 HAR 中 requestId 为 `-1`。
|
||||
4. 发送 `EWSCmdC2S_VerifyCookieReq(10)`,cookie 在这里进入 WSS 通道。
|
||||
5. 通过 WSS 调用 `mobileui.getConfig`。
|
||||
6. 发送 `EWSCmdC2S_UpdateUserExpsReq(23)`,请求体是空 map。
|
||||
7. 再发送一次 `EWSCmdC2S_RegisterGroupReq(16)`。
|
||||
8. 发送 `EWSCmdC2S_HeartBeatReq(20)`,后续维持心跳。
|
||||
9. `launch.queryHttpDns` 仍通过 HTTP `cdnws` 调用。
|
||||
10. 后续 WUP 业务在连接可用时走 `signalSdk.sendWup`,连接不可用时可 fallback 到 `sendWupHttp`。
|
||||
|
||||
响应应按 WUP `iRequestId` 匹配回调,不能只用 FIFO。
|
||||
|
||||
@@ -103,7 +110,7 @@ ws.send(WebSocketCommand.writeTo(JceOutputStream))
|
||||
- `webActUI.getUserPrizeRecords`
|
||||
- `webActUI.scoreExchangePrize`
|
||||
|
||||
WSS 版本仍是实验状态,需要按 `WebSocketCommand` 结构重写。
|
||||
WSS 版本仍是实验状态,但外层 `WebSocketCommand`、初始化帧顺序和 requestId 匹配已按手动 HAR 修正。
|
||||
|
||||
抢购兑换请求结构:
|
||||
|
||||
@@ -130,8 +137,6 @@ tReq.isRole = 1
|
||||
|
||||
## 待完善
|
||||
|
||||
- 实现 TAF `WebSocketCommand` 编解码。
|
||||
- WSS WUP response 按 `iRequestId` 匹配请求。
|
||||
- 实现 `WSUpdateUserInfoReq`、心跳、verify cookie 等浏览器初始化步骤。
|
||||
- 使用真实 Cookie 联调 `list/score/exchange`,确认服务端不再超时。
|
||||
- 自动调用 `launch.queryHttpDns` 获取最新 WSS host。
|
||||
- 增加基于真实抓包样本的 WSS 编解码单测。
|
||||
- 补更多基于真实抓包摘要的 WSS 编解码单测。
|
||||
|
||||
@@ -99,3 +99,42 @@ func GenerateHTTPBaseinfo(uid int64, guid, cookie string) (string, error) {
|
||||
}
|
||||
return url.QueryEscape(base64.StdEncoding.EncodeToString(w.Bytes())), nil
|
||||
}
|
||||
|
||||
// GenerateWSSBaseinfo 生成浏览器 WSS 握手里的 baseinfo。
|
||||
//
|
||||
// 手动抓包里 WSS 握手只带 uid/guid/ua/appSrc 等连接信息,cookie
|
||||
// 通过后续 VerifyCookie 命令发送。
|
||||
func GenerateWSSBaseinfo(uid int64, guid string) (string, error) {
|
||||
w := taf.NewWriter()
|
||||
if err := w.WriteInt64(0, uid); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(1, guid); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(2, HTTPHuyaUA); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(3, "HUYA&ZH&2052"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(4, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(5, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteInt32(6, 0); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(7, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(8, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := w.WriteString(9, ""); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return url.QueryEscape(base64.StdEncoding.EncodeToString(w.Bytes())), nil
|
||||
}
|
||||
|
||||
+64
-13
@@ -22,8 +22,9 @@ const (
|
||||
DefaultWSSOrigin = "https://zt.huya.com"
|
||||
|
||||
WSSCookieUA = "webh5&0.0.1&websocket&&diypc_52775"
|
||||
WSSBizUA = "web&1.0.0&huya"
|
||||
WSSInitUA = "webh5&0.0.1&websocket&&h5_/index.html"
|
||||
WSSBizUA = WSSCookieUA
|
||||
WSSInitUA = WSSCookieUA
|
||||
WSSGroupID = "live:0"
|
||||
)
|
||||
|
||||
type WSSClient struct {
|
||||
@@ -136,7 +137,7 @@ func (c *WSSClient) dialSession(ctx context.Context, uid int64, cookie string) (
|
||||
}
|
||||
|
||||
guid := CookieValue(cookie, "guid")
|
||||
baseinfo, err := GenerateHTTPBaseinfo(uid, guid, NormalizeWSSCookie(cookie))
|
||||
baseinfo, err := GenerateWSSBaseinfo(uid, guid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -231,7 +232,7 @@ func (s *WSSSession) readLoop() {
|
||||
s.resolveRequest(resp.RequestID, rpcResult{resp: &resp})
|
||||
case WSSCmdHeartBeatAck, WSSCmdHeartBeatRsp:
|
||||
// 心跳响应无需处理。
|
||||
case WSSCmdVerifyCookieRsp, WSSCmdUpdateUserInfoRsp, WSSCmdUpdateUserExpsRsp:
|
||||
case WSSCmdRegisterGroupRsp, WSSCmdVerifyCookieRsp, WSSCmdUpdateUserInfoRsp, WSSCmdUpdateUserExpsRsp:
|
||||
s.client.logf("[WSS] 收到控制响应 cmd=%d data=%dB", cmd.CmdType, len(cmd.Data))
|
||||
default:
|
||||
s.client.logf("[WSS] 收到 cmd=%d data=%dB requestID=%d", cmd.CmdType, len(cmd.Data), cmd.RequestID)
|
||||
@@ -303,23 +304,27 @@ func (s *WSSSession) writePayload(ctx context.Context, payload []byte) error {
|
||||
}
|
||||
|
||||
func (s *WSSSession) initialize(ctx context.Context) error {
|
||||
if err := s.sendHeartbeat(ctx); err != nil {
|
||||
if err := s.sendRegisterGroup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.callWSLaunch(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.launchGUID != "" {
|
||||
if err := s.sendUpdateUserInfo(ctx, s.launchGUID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := s.sendVerifyCookie(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.callGetConfig(ctx); err != nil {
|
||||
s.client.logf("[WSS] getConfig 失败但继续: %v", err)
|
||||
}
|
||||
if err := s.sendUpdateUserExps(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendRegisterGroup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendHeartbeat(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -338,9 +343,18 @@ func (s *WSSSession) sendHeartbeat(ctx context.Context) error {
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendRegisterGroup(ctx context.Context) error {
|
||||
req := &wsRegisterGroupReq{GroupIDs: []string{WSSGroupID}}
|
||||
payload, err := encodeWSSStructCommand(WSSCmdRegisterGroupReq, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) callWSLaunch(ctx context.Context) error {
|
||||
req := &wsLaunchReq{UID: s.uid, GUID: s.guid, UA: WSSInitUA}
|
||||
resp, err := s.callWUP(ctx, "launch", "wsLaunch", req)
|
||||
resp, err := s.callWUPWithRequestID(ctx, "launch", "wsLaunch", req, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -373,6 +387,14 @@ func (s *WSSSession) sendUpdateUserInfo(ctx context.Context, launchGUID string)
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendUpdateUserExps(ctx context.Context) error {
|
||||
payload, err := encodeWSSStructCommand(WSSCmdUpdateUserExpsReq, &wsUpdateUserExpsReq{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writePayload(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *WSSSession) sendVerifyCookie(ctx context.Context) error {
|
||||
guid := s.launchGUID
|
||||
if guid == "" {
|
||||
@@ -402,7 +424,10 @@ func (s *WSSSession) nextRequestID() int32 {
|
||||
}
|
||||
|
||||
func (s *WSSSession) callWUP(ctx context.Context, service, method string, reqStruct taf.Marshaler) (*wup.Response, error) {
|
||||
requestID := s.nextRequestID()
|
||||
return s.callWUPWithRequestID(ctx, service, method, reqStruct, s.nextRequestID())
|
||||
}
|
||||
|
||||
func (s *WSSSession) callWUPWithRequestID(ctx context.Context, service, method string, reqStruct taf.Marshaler, requestID int32) (*wup.Response, error) {
|
||||
wupReq := wup.NewRequest(service, method, requestID)
|
||||
wupReq.Timeout = 0
|
||||
if err := wupReq.WriteStruct("tReq", reqStruct); err != nil {
|
||||
@@ -460,7 +485,7 @@ func (s *WSSSession) callRPC(ctx context.Context, service, method string, reqStr
|
||||
}
|
||||
|
||||
func (c *WSSClient) buildUser(uid int64, cookie string) ActivityUserID {
|
||||
user := NewActivityUserID(uid, NormalizeWSSCookie(cookie), WSSBizUA)
|
||||
user := NewActivityUserID(uid, NormalizeCookie(cookie), WSSBizUA)
|
||||
user.GUID = ""
|
||||
user.Token = ""
|
||||
user.DeviceInfo = ""
|
||||
@@ -530,6 +555,26 @@ func NormalizeWSSCookie(cookie string) string {
|
||||
return strings.Join(out, "; ")
|
||||
}
|
||||
|
||||
type wsRegisterGroupReq struct {
|
||||
GroupIDs []string
|
||||
Token string
|
||||
}
|
||||
|
||||
func (r *wsRegisterGroupReq) WriteTo(w *taf.Writer) error {
|
||||
if err := w.WriteHead(0, taf.TypeList); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteInt32(0, int64(len(r.GroupIDs))); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, groupID := range r.GroupIDs {
|
||||
if err := w.WriteString(0, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.WriteString(1, r.Token)
|
||||
}
|
||||
|
||||
type wsLaunchReq struct {
|
||||
UID int64
|
||||
GUID string
|
||||
@@ -603,6 +648,12 @@ func (r *wsUpdateUserInfoReq) WriteTo(w *taf.Writer) error {
|
||||
return w.WriteInt32(8, 0)
|
||||
}
|
||||
|
||||
type wsUpdateUserExpsReq struct{}
|
||||
|
||||
func (r *wsUpdateUserExpsReq) WriteTo(w *taf.Writer) error {
|
||||
return w.WriteMapStringString(0, nil)
|
||||
}
|
||||
|
||||
type wsVerifyCookieReq struct {
|
||||
UID int64
|
||||
UA string
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package huya
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"huya-go/internal/taf"
|
||||
)
|
||||
|
||||
func TestWSRegisterGroupReqEncoding(t *testing.T) {
|
||||
w := taf.NewWriter()
|
||||
req := &wsRegisterGroupReq{GroupIDs: []string{WSSGroupID}}
|
||||
if err := req.WriteTo(w); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := hex.EncodeToString(w.Bytes())
|
||||
want := "09000106066c6976653a301600"
|
||||
if got != want {
|
||||
t.Fatalf("register group bytes = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSUpdateUserExpsReqEncoding(t *testing.T) {
|
||||
w := taf.NewWriter()
|
||||
if err := (&wsUpdateUserExpsReq{}).WriteTo(w); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := hex.EncodeToString(w.Bytes())
|
||||
want := "080c"
|
||||
if got != want {
|
||||
t.Fatalf("update user exps bytes = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWSSBaseinfoMatchesBrowserShape(t *testing.T) {
|
||||
encoded, err := GenerateWSSBaseinfo(123, "guid-value")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unescaped, err := url.QueryUnescape(encoded)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(unescaped)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := taf.NewReader(raw)
|
||||
uid, err := r.ReadInt64(0, true, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
guid, err := r.ReadString(1, true, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ua, err := r.ReadString(2, true, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appSrc, err := r.ReadString(3, true, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cookie, err := r.ReadString(8, false, "not-empty")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
traceID, err := r.ReadString(9, false, "not-empty")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if uid != 123 || guid != "guid-value" || ua != WSSCookieUA || appSrc != "HUYA&ZH&2052" {
|
||||
t.Fatalf("unexpected baseinfo: uid=%d guid=%q ua=%q appSrc=%q", uid, guid, ua, appSrc)
|
||||
}
|
||||
if cookie != "" || traceID != "" {
|
||||
t.Fatalf("wss baseinfo should not carry cookie/trace: cookie=%q trace=%q", cookie, traceID)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,14 @@ const (
|
||||
WSSCmdDeregisterRsp int32 = 9
|
||||
WSSCmdVerifyCookieReq int32 = 10
|
||||
WSSCmdVerifyCookieRsp int32 = 11
|
||||
WSSCmdVerifyHuyaTokenReq int32 = 12
|
||||
WSSCmdVerifyHuyaTokenRsp int32 = 13
|
||||
WSSCmdUnverifyReq int32 = 14
|
||||
WSSCmdUnverifyRsp int32 = 15
|
||||
WSSCmdRegisterGroupReq int32 = 16
|
||||
WSSCmdRegisterGroupRsp int32 = 17
|
||||
WSSCmdUnregisterGroupReq int32 = 18
|
||||
WSSCmdUnregisterGroupRsp int32 = 19
|
||||
WSSCmdHeartBeatReq int32 = 20
|
||||
WSSCmdHeartBeatRsp int32 = 21
|
||||
WSSCmdMsgPushReqV2 int32 = 22
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user