136 lines
4.9 KiB
Go
136 lines
4.9 KiB
Go
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 {
|
||
if f.Seed < 1 || f.Seed > 2147483647 {
|
||
return errors.New("seed must be 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
|
||
}
|