This commit is contained in:
谢宇宁
2026-09-29 15:30:33 +07:00
parent 0fbdba5ccf
commit 9abe0b1b76
181 changed files with 608 additions and 18501 deletions
+13 -216
View File
@@ -1,23 +1,16 @@
package useragent
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"unicode/utf8"
"videoweb/app/appg"
"github.com/gin-gonic/gin"
)
const (
DefaultVer = "1.0.0"
SDKCommonHeader = "X-SDK-Common"
)
const DefaultVer = "1.0.0"
// Info 与前端 X-User-Agent 字段对齐,供 SSR 组装上游请求头。
type Info struct {
@@ -42,23 +35,17 @@ type BuildOptions struct {
}
var (
reIOSVer = regexp.MustCompile(`OS (\d+[_\d]*) like Mac OS X`)
reAndroidVer = regexp.MustCompile(`Android ([\d.]+)`)
reAndroidModel = regexp.MustCompile(`Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))`)
reWinNT = regexp.MustCompile(`Windows NT ([\d.]+)`)
reMacVer = regexp.MustCompile(`Mac OS X ([\d_]+)`)
reEdge = regexp.MustCompile(`(?i)Edg/([\d.]+)`)
reOpera = regexp.MustCompile(`(?i)(?:OPR|Opera)/([\d.]+)`)
reChrome = regexp.MustCompile(`(?i)Chrome/([\d.]+)`)
reSafari = regexp.MustCompile(`(?i)Version/([\d.]+).*Safari`)
reFirefox = regexp.MustCompile(`(?i)Firefox/([\d.]+)`)
reIE = regexp.MustCompile(`(?i)(?:MSIE\s|Trident.*rv:)([\d.]+)`)
reTrackerRequestID = regexp.MustCompile(`^[a-fA-F0-9]{32}$`)
reTrackerCtx = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
reTrackerEvent = regexp.MustCompile(`^drama_(like|comment|collect|purchase)$`)
reTrackerAppID = regexp.MustCompile(`^[A-Z]{1,6}-[0-9]{1,6}$`)
reTrackerUID = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
reTrackerChannel = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
reIOSVer = regexp.MustCompile(`OS (\d+[_\d]*) like Mac OS X`)
reAndroidVer = regexp.MustCompile(`Android ([\d.]+)`)
reAndroidModel = regexp.MustCompile(`Android\s+[\d.]+;\s*([^;)]+?)(?:\s+Build\/|\))`)
reWinNT = regexp.MustCompile(`Windows NT ([\d.]+)`)
reMacVer = regexp.MustCompile(`Mac OS X ([\d_]+)`)
reEdge = regexp.MustCompile(`(?i)Edg/([\d.]+)`)
reOpera = regexp.MustCompile(`(?i)(?:OPR|Opera)/([\d.]+)`)
reChrome = regexp.MustCompile(`(?i)Chrome/([\d.]+)`)
reSafari = regexp.MustCompile(`(?i)Version/([\d.]+).*Safari`)
reFirefox = regexp.MustCompile(`(?i)Firefox/([\d.]+)`)
reIE = regexp.MustCompile(`(?i)(?:MSIE\s|Trident.*rv:)([\d.]+)`)
)
// ParseDeviceFromUA 从浏览器原生 User-Agent 解析设备信息(服务端无 navigator)。
@@ -335,36 +322,7 @@ func BuildUpstreamHeader(c *gin.Context, ver string) map[string]string {
return header
}
var trackerEventRoutes = map[string]string{
"/api/playlet/chapter/like/set": "drama_like",
"/api/playlet/like/set": "drama_like",
"/api/playlet/chapter/collect/set": "drama_collect",
"/api/playlet/collect/set": "drama_collect",
"/api/comment/send": "drama_comment",
"/api/playlet/chapter/unlock": "drama_purchase",
}
var trackerCommonFieldKeys = map[string]struct{}{
"app_id": {}, "uid": {}, "channel": {}, "sid": {}, "client_ts": {},
"device": {}, "device_id": {}, "device_brand": {}, "device_model": {},
"device_fingerprint": {}, "fp_version": {}, "user_agent": {},
"sdk_version": {}, "app_version": {},
}
var trackerDramaFieldKeys = map[string]struct{}{
"media_id": {}, "drama_id": {}, "episode": {}, "video_id": {}, "recommend_trace_id": {},
}
type trackerContextEnvelope struct {
Version int `json:"v"`
RequestID string `json:"request_id"`
EventName string `json:"event_name"`
Common map[string]json.RawMessage `json:"common"`
Drama map[string]json.RawMessage `json:"drama"`
}
// ApplyToHeader 将来源请求的网络信息与 SDK 原始公共字段快照写入上游 header。
// 埋点上下文只通过 canonical envelope 白名单透传,不在 BFF 重新生成或覆盖。
// ApplyToHeader 将来源请求的网络信息写入上游 header。
func ApplyToHeader(header map[string]string, c *gin.Context, ver string) {
if header == nil {
return
@@ -378,165 +336,4 @@ func ApplyToHeader(header map[string]string, c *gin.Context, ver string) {
}
}
header["X-User-Agent"] = BuildXUserAgentFromRequest(c, ver)
if c == nil || c.Request == nil {
return
}
if sdk := strings.TrimSpace(c.GetHeader(SDKCommonHeader)); sdk != "" {
header[SDKCommonHeader] = sdk
}
requestID := strings.TrimSpace(c.GetHeader("X-Tracker-Request-Id"))
eventName := strings.TrimSpace(c.GetHeader("X-Tracker-Event-Name"))
packedContext := strings.TrimSpace(c.GetHeader("X-DC-Request-Context"))
expectedEvent := trackerEventRoutes[c.FullPath()]
if expectedEvent == "" {
expectedEvent = trackerEventRoutes[c.Request.URL.Path]
}
if c.Request.Method != http.MethodPost || expectedEvent == "" || eventName != expectedEvent ||
!reTrackerRequestID.MatchString(requestID) || !reTrackerEvent.MatchString(eventName) {
return
}
canonicalContext, ok := canonicalPackedTrackerContext(packedContext, requestID, eventName)
if !ok {
return
}
header["X-DC-Request-Context"] = canonicalContext
header["X-Tracker-Request-Id"] = requestID
header["X-Tracker-Event-Name"] = eventName
}
func canonicalPackedTrackerContext(value, requestID, eventName string) (string, bool) {
if value == "" || len(value) > 8192 || !reTrackerCtx.MatchString(value) {
return "", false
}
raw, err := base64.RawURLEncoding.DecodeString(value)
if err != nil || len(raw) > 6144 {
return "", false
}
var envelope trackerContextEnvelope
if err := json.Unmarshal(raw, &envelope); err != nil {
return "", false
}
if envelope.Version != 1 || envelope.RequestID != requestID ||
envelope.EventName != eventName || !reTrackerRequestID.MatchString(envelope.RequestID) ||
!reTrackerEvent.MatchString(envelope.EventName) ||
!hasOnlyTrackerFields(envelope.Common, trackerCommonFieldKeys) ||
!hasOnlyTrackerFields(envelope.Drama, trackerDramaFieldKeys) ||
!validTrackerCommonFields(envelope.Common) || !validTrackerDramaFields(envelope.Drama) {
return "", false
}
// 公共字段的值保持 SDK 原值;重新编码仅移除重复 JSON key,防止上下游解析差异。
canonical, err := json.Marshal(envelope)
if err != nil || len(canonical) > 6144 {
return "", false
}
return base64.RawURLEncoding.EncodeToString(canonical), true
}
func hasOnlyTrackerFields(values map[string]json.RawMessage, allowed map[string]struct{}) bool {
if len(values) != len(allowed) {
return false
}
for key := range values {
if _, ok := allowed[key]; !ok {
return false
}
}
return true
}
func validTrackerCommonFields(values map[string]json.RawMessage) bool {
appID, ok := trackerStringField(values, "app_id", 50, false)
if !ok || !reTrackerAppID.MatchString(appID) || appID != appg.TrackerAppID() {
return false
}
uid, ok := trackerStringField(values, "uid", 32, true)
if !ok || (uid != "" && (utf8.RuneCountInString(uid) < 4 || !reTrackerUID.MatchString(uid) ||
uid == "u_1001" || uid == "stress_user" || uid == "u_lab")) {
return false
}
channel, ok := trackerStringField(values, "channel", 50, true)
if !ok || (channel != "" && (!reTrackerChannel.MatchString(channel) ||
channel == "{}" || channel == "0" || channel == "-" || strings.EqualFold(channel, "unknown"))) {
return false
}
if _, ok = trackerStringField(values, "sid", 50, true); !ok {
return false
}
clientTS, ok := trackerIntegerField(values, "client_ts")
if !ok || clientTS < 1767196800 || clientTS > 9999999999 {
return false
}
device, ok := trackerStringField(values, "device", 30, true)
if !ok || (device != "" && device != "Android" && device != "iOS" && device != "PC" && device != "OTHER") {
return false
}
if _, ok = trackerStringField(values, "device_id", 50, false); !ok {
return false
}
if _, ok = trackerStringField(values, "device_brand", 100, true); !ok {
return false
}
if _, ok = trackerStringField(values, "device_model", 100, true); !ok {
return false
}
if _, ok = trackerStringField(values, "device_fingerprint", 128, true); !ok {
return false
}
if _, ok = trackerStringField(values, "fp_version", 128, true); !ok {
return false
}
if _, ok = trackerStringField(values, "user_agent", 4096, true); !ok {
return false
}
if _, ok = trackerStringField(values, "sdk_version", 128, true); !ok {
return false
}
_, ok = trackerStringField(values, "app_version", 128, true)
return ok
}
func validTrackerDramaFields(values map[string]json.RawMessage) bool {
if _, ok := trackerStringField(values, "media_id", 512, true); !ok {
return false
}
if _, ok := trackerStringField(values, "drama_id", 512, false); !ok {
return false
}
episode, ok := trackerIntegerField(values, "episode")
if !ok || episode <= 0 {
return false
}
if _, ok = trackerStringField(values, "video_id", 512, true); !ok {
return false
}
_, ok = trackerStringField(values, "recommend_trace_id", 2048, true)
return ok
}
func trackerStringField(values map[string]json.RawMessage, key string, maxLen int, allowEmpty bool) (string, bool) {
raw, ok := values[key]
if !ok {
return "", false
}
var decoded *string
if err := json.Unmarshal(raw, &decoded); err != nil || decoded == nil {
return "", false
}
value := *decoded
if utf8.RuneCountInString(value) > maxLen || (!allowEmpty && value == "") {
return "", false
}
return value, true
}
func trackerIntegerField(values map[string]json.RawMessage, key string) (int64, bool) {
raw, ok := values[key]
if !ok {
return 0, false
}
var decoded *int64
if err := json.Unmarshal(raw, &decoded); err != nil || decoded == nil {
return 0, false
}
return *decoded, true
}
-80
View File
@@ -1,80 +0,0 @@
package useragent
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
)
// 限制解析开销;正常 SDK 公共参数远小于此大小。
const maxSDKCommonHeaderSize = 16 * 1024
// SDKCommonFields 接收五项 SDK 公共字段及客户端 app_version,完整 fields 中的其他字段自动忽略。
// 业务 DevID、用户 ID、渠道等继续走原有业务参数,不能被 SDK 对象覆盖。
type SDKCommonFields struct {
SID string `json:"sid" bson:"sid"`
DeviceID string `json:"device_id" bson:"device_id"`
Device string `json:"device" bson:"device"`
DeviceFingerprint string `json:"device_fingerprint" bson:"device_fingerprint"`
FPVersion string `json:"fp_version" bson:"fp_version"`
AppVersion string `json:"app_version" bson:"app_version"` // 客户端版本随下单快照保存,支付回调不使用当前版本补值。
}
// ParseSDKCommonHeader 缺少请求头返回 nil;格式错误交给调用方记录,不阻断业务请求。
// 不修剪、截断或推算字段值,SDK 指纹生成失败时也保留其空值。
func ParseSDKCommonHeader(header string) (*SDKCommonFields, error) {
header = strings.TrimSpace(header)
if header == "" {
return nil, nil
}
if len(header) > maxSDKCommonHeaderSize {
return nil, fmt.Errorf("SDK 公共参数请求头超过 %d 字节", maxSDKCommonHeaderSize)
}
data, err := base64.StdEncoding.DecodeString(header)
if err != nil {
return nil, fmt.Errorf("SDK 公共参数 Base64 解码失败: %w", err)
}
if !utf8.Valid(data) {
return nil, fmt.Errorf("SDK 公共参数不是有效的 UTF-8")
}
var fields *SDKCommonFields
if err := json.Unmarshal(data, &fields); err != nil {
return nil, fmt.Errorf("SDK 公共参数 JSON 解析失败: %w", err)
}
if fields == nil {
return nil, fmt.Errorf("SDK 公共参数必须是 JSON 对象,不能为 null")
}
return fields, nil
}
// Clone 下单时保存独立快照,避免请求对象后续变化影响支付事件。
func (fields *SDKCommonFields) Clone() *SDKCommonFields {
if fields == nil {
return nil
}
snapshot := *fields
return &snapshot
}
// MissingFields 仅用于缺失观测;合法空值仍原样上报,不作为业务失败。
func (fields *SDKCommonFields) MissingFields() []string {
if fields == nil {
fields = &SDKCommonFields{}
}
var missing []string
for _, field := range []struct{ name, value string }{
{"sid", fields.SID},
{"device_id", fields.DeviceID},
{"device", fields.Device},
{"device_fingerprint", fields.DeviceFingerprint},
{"fp_version", fields.FPVersion},
{"app_version", fields.AppVersion},
} {
if strings.TrimSpace(field.value) == "" {
missing = append(missing, field.name)
}
}
return missing
}