Files
creator-hub/internal/environment/fingerprint.go
T
rogee 65d0801290 feat(schema): 044 数字主键——全表 bigint IDENTITY、文本标识降级 UNIQUE、FK 重指
- 迁移 044:9 张表换 bigint IDENTITY 主键,account_id/exit_id/competitor_id/work_id/comment_id/job_id/rule_id 降级 UNIQUE;
  引用表 FK 重指 bigint;checkpoint 删派生文本 id;creator_settings 布尔 PK 换 bigint identity + 单行表达式唯一索引;
  social_account.profile_id 死列删除
- seed 派生改 SQL:CreateBoundEnv 用 jsonb_set(account.id+1000),deriveSeed 删除;Fingerprint.Validate 允许 Seed=0(派生态)
- 三域 store SQL 列名适配;appendAudit/审计过滤经 JOIN 读回文本标识;UPDATE..RETURNING..FROM 拆两段式
- 对外 API 契约不变:模型/JSON/URL 全部保持文本 ID
- account 域补 PG 生命周期集成测试(列表/凭据解析/暂停吊销恢复/审计过滤/删除链路),覆盖率 22.4%→73.9%
2026-09-28 22:30:14 +08:00

137 lines
5.0 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 environment
import (
"errors"
"net/url"
"regexp"
"strconv"
"strings"
)
// Fingerprint 是 fingerprint-chromium 的结构化启动参数集合。
// 参数契约来源:https://github.com/adryfish/fingerprint-chromium(BSD-3-clause)。
// 零值字段表示不传递该命令行开关。
type Fingerprint struct {
Seed int64 `json:"seed"`
Platform string `json:"platform,omitempty"`
PlatformVersion string `json:"platform_version,omitempty"`
Brand string `json:"brand,omitempty"`
BrandVersion string `json:"brand_version,omitempty"`
HardwareConcurrency int64 `json:"hardware_concurrency,omitempty"`
Lang string `json:"lang,omitempty"`
AcceptLang string `json:"accept_lang,omitempty"`
Timezone string `json:"timezone,omitempty"`
ProxyServer string `json:"proxy_server,omitempty"`
DisableNonProxiedUDP bool `json:"disable_non_proxied_udp,omitempty"`
DisableSpoofing string `json:"disable_spoofing,omitempty"`
}
var (
platforms = map[string]bool{"windows": true, "linux": true, "macos": true}
brands = map[string]bool{"Chrome": true, "Edge": true, "Opera": true, "Vivaldi": true}
spoofings = map[string]bool{"font": true, "audio": true, "canvas": true, "clientrects": true, "gpu": true}
langPattern = regexp.MustCompile(`^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})?$`)
acceptLangPattern = regexp.MustCompile(
`^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})?(, ?[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*){0,7}$`)
timezonePattern = regexp.MustCompile(`^[A-Za-z0-9_+\-/]{1,64}$`)
shortVersionPatten = regexp.MustCompile(`^[A-Za-z0-9._+~-]{1,32}$`)
)
// Validate 校验全量指纹参数;只做值域校验,参数作为独立 argv 传入容器,无 shell 注入面。
func (f Fingerprint) Validate() error {
// Seed=0 表示由 CreateBoundEnv 从账号 bigint id 派生(id+1000);显式取值仍须在合法区间。
if f.Seed < 0 || f.Seed > 2147483647 {
return errors.New("seed must be 0 (derive) or 1..2147483647")
}
if !optionalIn(f.Platform, platforms) {
return errors.New("platform must be one of windows, linux, macos")
}
if !optionalIn(f.Brand, brands) {
return errors.New("brand must be one of Chrome, Edge, Opera, Vivaldi")
}
if !optionalMatch(f.PlatformVersion, shortVersionPatten) {
return errors.New("platform_version must be a short version like 11.0.0")
}
if !optionalMatch(f.BrandVersion, shortVersionPatten) {
return errors.New("brand_version must be a short version like 132.0.6834.159")
}
if f.HardwareConcurrency < 0 || f.HardwareConcurrency > 128 {
return errors.New("hardware_concurrency must be 0..128, 0 omits the flag")
}
if !optionalMatch(f.Lang, langPattern) {
return errors.New("lang must be a language code like zh-CN")
}
if !optionalMatch(f.AcceptLang, acceptLangPattern) {
return errors.New("accept_lang must be a comma separated language list like zh-CN,en-US")
}
if !optionalMatch(f.Timezone, timezonePattern) {
return errors.New("timezone must be an IANA timezone like Asia/Shanghai")
}
if f.ProxyServer != "" && !validProxyURL(f.ProxyServer) {
return errors.New("proxy_server must be an http/https/socks4/socks5 URL with a host")
}
if f.DisableSpoofing != "" && !validDisableSpoofing(f.DisableSpoofing) {
return errors.New("disable_spoofing must be a comma separated subset of font,audio,canvas,clientrects,gpu")
}
return nil
}
// Args 生成 fingerprint-chromium 命令行参数(不含 URL 尾参)。
func (f Fingerprint) Args() []string {
args := []string{"--fingerprint=" + strconv.FormatInt(f.Seed, 10)}
value := func(flag, setting string) {
if setting != "" {
args = append(args, "--"+flag+"="+setting)
}
}
value("fingerprint-platform", f.Platform)
value("fingerprint-platform-version", f.PlatformVersion)
value("fingerprint-brand", f.Brand)
value("fingerprint-brand-version", f.BrandVersion)
if f.HardwareConcurrency > 0 {
value("fingerprint-hardware-concurrency", strconv.FormatInt(f.HardwareConcurrency, 10))
}
value("lang", f.Lang)
value("accept-lang", f.AcceptLang)
value("timezone", f.Timezone)
value("proxy-server", f.ProxyServer)
if f.DisableNonProxiedUDP {
args = append(args, "--disable-non-proxied-udp")
}
value("disable-spoofing", f.DisableSpoofing)
return args
}
func optionalIn(value string, allowed map[string]bool) bool {
return value == "" || allowed[value]
}
func optionalMatch(value string, pattern *regexp.Regexp) bool {
return value == "" || pattern.MatchString(value)
}
func validProxyURL(raw string) bool {
parsed, err := url.Parse(raw)
if err != nil || parsed.Host == "" || parsed.User != nil {
return false
}
switch parsed.Scheme {
case "http", "https", "socks4", "socks5":
return true
default:
return false
}
}
func validDisableSpoofing(value string) bool {
seen := make(map[string]bool, len(spoofings))
for _, part := range strings.Split(value, ",") {
part = strings.TrimSpace(part)
if !spoofings[part] || seen[part] {
return false
}
seen[part] = true
}
return true
}