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 // dnsFailureCooldown 是 queryHttpDns 失败后的冷却时间。 dnsFailureCooldown = 30 * time.Second ) // 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[PingSignalStartKey] end, okEnd := r.Values[PingSignalEndKey] 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, ".") }