Files
huya-go/internal/huya/launch.go
T
yml2213 36aadae90a 按 10.13 抓包对齐 WSS:host 自动解析、exp 回传、pingSignal 与兑换记录字段
- 新增 launch.queryHttpDns:类型定义 + HTTPClient.QueryHttpDns + WSSClient.resolveHost(TTL 缓存、多 host 轮询、失败回退默认 host);Host 留空或等于历史默认值即视为自动解析
- WSS 握手 baseinfo 补 tag5=exp 与 tag10 空 customHeaders;UpdateUserExpsReq(cmd23) 改为回传 getConfig 的版本 map(缓存供重连复用)
- 新增 launch.pingSignal:HTTP 预热改走 pingSignal,WSS 预热在建连后探测业务通道(失败仅记日志)
- UserPrizeInfo 补 tag12 orderId/tag14 图标/tag16 prizeId/tag17 shopItemId/tag19 itemId/tag20 ext/tag21 exchangeDate,前端复盘表加订单号列
- taf 补 WriteListString/ReadListString/ReadMapStringInt64/ReadMapStringStruct
- 新增 internal/huya/launch_test.go:用抓包真实报文做字节级夹具(baseinfo 122B、queryHttpDns 请求 93B、记录 308B 等)
- CLI -wss-host 默认改为留空;GUI 设置支持留空并给出占位提示;README/WSS_NOTES 同步协议细节
2026-10-13 20:19:14 +08:00

268 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package huya
import (
"sort"
"strings"
"time"
"huya-go/internal/taf"
)
const (
ServantLaunch = "launch"
FuncWsLaunch = "wsLaunch"
FuncQueryHttpDns = "queryHttpDns"
FuncPingSignal = "pingSignal"
FuncGetConfig = "getConfig"
// HttpDnsDomain 是 cdnws 入口域名,launch.queryHttpDns 用它换取 WSS host 列表。
HttpDnsDomain = "cdnws.api.huya.com"
// DefaultDNSTTL 是响应没有 ttl 时的兜底缓存时间。
DefaultDNSTTL = 10 * time.Minute
)
// QueryHttpDnsReq 对应浏览器 launch.queryHttpDns 的 tReq。
//
// 实测字段(2026-10-13 HAR):0=uid, 1=ua, 2=域名列表, 3=appSrc,
// 4="", 5=0, 6=1,iRequestId 固定 -1。
type QueryHttpDnsReq struct {
UID int64
UA string
Domains []string
AppSrc string
Extra string
FlagType int32
FlagVer int32
}
func NewQueryHttpDnsReq(uid int64, ua string, domains []string) *QueryHttpDnsReq {
if len(domains) == 0 {
domains = []string{HttpDnsDomain}
}
return &QueryHttpDnsReq{
UID: uid,
UA: ua,
Domains: domains,
AppSrc: "HUYA&ZH&2052",
FlagType: 0,
FlagVer: 1,
}
}
func (q *QueryHttpDnsReq) WriteTo(w *taf.Writer) error {
if err := w.WriteInt64(0, q.UID); err != nil {
return err
}
if err := w.WriteString(1, q.UA); err != nil {
return err
}
if err := w.WriteListString(2, q.Domains); err != nil {
return err
}
if err := w.WriteString(3, q.AppSrc); err != nil {
return err
}
if err := w.WriteString(4, q.Extra); err != nil {
return err
}
if err := w.WriteInt32(5, int64(q.FlagType)); err != nil {
return err
}
return w.WriteInt32(6, int64(q.FlagVer))
}
func (q *QueryHttpDnsReq) ReadFrom(r *taf.Reader) error {
var err error
if q.UID, err = r.ReadInt64(0, false, q.UID); err != nil {
return err
}
if q.UA, err = r.ReadString(1, false, q.UA); err != nil {
return err
}
if q.Domains, err = r.ReadListString(2, false); err != nil {
return err
}
if q.AppSrc, err = r.ReadString(3, false, q.AppSrc); err != nil {
return err
}
if q.Extra, err = r.ReadString(4, false, q.Extra); err != nil {
return err
}
if q.FlagType, err = r.ReadInt32(5, false, q.FlagType); err != nil {
return err
}
q.FlagVer, err = r.ReadInt32(6, false, q.FlagVer)
return err
}
// HttpDnsHosts 是 tRsp tag0 里单个域名的解析结果:host 列表 + ttl 秒。
type HttpDnsHosts struct {
Hosts []string
TTL int32
Extra []string
}
func (h *HttpDnsHosts) WriteTo(w *taf.Writer) error { return nil }
func (h *HttpDnsHosts) ReadFrom(r *taf.Reader) error {
var err error
if h.Hosts, err = r.ReadListString(0, false); err != nil {
return err
}
if h.TTL, err = r.ReadInt32(1, false, h.TTL); err != nil {
return err
}
h.Extra, err = r.ReadListString(2, false)
return err
}
// QueryHttpDnsResp 对应 launch.queryHttpDns 的 tRsp:tag0=域名->host 列表, tag1=状态。
type QueryHttpDnsResp struct {
Hosts map[string]HttpDnsHosts
Status int32
}
func (r *QueryHttpDnsResp) WriteTo(w *taf.Writer) error { return nil }
func (r *QueryHttpDnsResp) ReadFrom(rd *taf.Reader) error {
items, err := rd.ReadMapStringStruct(0, false, func() taf.Unmarshaler { return &HttpDnsHosts{} })
if err != nil {
return err
}
r.Hosts = make(map[string]HttpDnsHosts, len(items))
for key, item := range items {
if hosts, ok := item.(*HttpDnsHosts); ok {
r.Hosts[key] = *hosts
}
}
r.Status, err = rd.ReadInt32(1, false, r.Status)
return err
}
// WSSHosts 返回指定域名解析到的 WSS host 列表,保持服务端下发顺序。
func (r QueryHttpDnsResp) WSSHosts(domain string) []string {
item, ok := r.Hosts[domain]
if !ok {
return nil
}
out := make([]string, 0, len(item.Hosts))
for _, host := range item.Hosts {
if trimmed := strings.TrimSpace(host); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
// TTL 返回指定域名的缓存时间。
func (r QueryHttpDnsResp) TTL(domain string) time.Duration {
item, ok := r.Hosts[domain]
if !ok || item.TTL <= 0 {
return DefaultDNSTTL
}
return time.Duration(item.TTL) * time.Second
}
// PingSignalReq 对应 launch.pingSignal:0="launch", 1="pingSignal", 2=扩展 map。
type PingSignalReq struct {
Extra map[string]string
}
func (p *PingSignalReq) WriteTo(w *taf.Writer) error {
if err := w.WriteString(0, ServantLaunch); err != nil {
return err
}
if err := w.WriteString(1, FuncPingSignal); err != nil {
return err
}
return w.WriteMapStringString(2, p.Extra)
}
func (p *PingSignalReq) ReadFrom(r *taf.Reader) error {
var err error
if _, err = r.ReadString(0, false, ServantLaunch); err != nil {
return err
}
if _, err = r.ReadString(1, false, FuncPingSignal); err != nil {
return err
}
p.Extra, err = r.ReadMapStringString(2, false)
return err
}
// PingSignalResp 对应 launch.pingSignal 的 tRsp:tag2 是 signal_ap_start/end 等服务端时间戳。
type PingSignalResp struct {
Servant string
Func string
Values map[string]int64
}
func (r *PingSignalResp) WriteTo(w *taf.Writer) error { return nil }
func (r *PingSignalResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Servant, err = rd.ReadString(0, false, r.Servant); err != nil {
return err
}
if r.Func, err = rd.ReadString(1, false, r.Func); err != nil {
return err
}
r.Values, err = rd.ReadMapStringInt64(2, false)
return err
}
// ServerElapsed 返回服务端 signal_ap_start/end 的差值(毫秒),不可得时返回 0。
func (r PingSignalResp) ServerElapsed() time.Duration {
start, okStart := r.Values["signal_ap_start"]
end, okEnd := r.Values["signal_ap_end"]
if !okStart || !okEnd || end < start {
return 0
}
return time.Duration(end-start) * time.Millisecond
}
// GetConfigResp 对应 mobileui.getConfig 的 tRsp:
// tag0=平台开关 map,tag1=按平台下发的配置版本 map。
type GetConfigResp struct {
Flags map[string]string
Exps map[string]string
}
func (r *GetConfigResp) WriteTo(w *taf.Writer) error { return nil }
func (r *GetConfigResp) ReadFrom(rd *taf.Reader) error {
var err error
if r.Flags, err = rd.ReadMapStringString(0, false); err != nil {
return err
}
r.Exps, err = rd.ReadMapStringString(1, false)
return err
}
// Exp 返回握手 baseinfo.tag5 与 UpdateUserExps 使用的版本串。
func (r GetConfigResp) Exp() string {
return FormatConfigExps(r.Exps)
}
// FormatConfigExps 把 {"58664":"107263"} 拼成 "58664.107263"。
//
// 2026-10-13 HAR 中 WSS 握手 baseinfo.tag5 与 UpdateUserExpsReq 都用这个形式;
// 多个平台时按 key 升序用 "." 连接(实测样本只有一个平台)。
func FormatConfigExps(exps map[string]string) string {
if len(exps) == 0 {
return ""
}
keys := make([]string, 0, len(exps))
for key := range exps {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, key+"."+exps[key])
}
return strings.Join(parts, ".")
}