Skip to content

Commit 651013a

Browse files
PoneyClairDeLuneExclude0122
authored andcommitted
Global HTTP headers' masquerading: Add "curl"; Improve version generators (XTLS#5916)
XTLS#5802 XTLS#5689 XTLS#5658
1 parent 61336ab commit 651013a

1 file changed

Lines changed: 82 additions & 20 deletions

File tree

common/utils/browser.go

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package utils
22

33
import (
4+
"hash/fnv"
5+
"math"
46
"math/rand"
57
"strconv"
68
"time"
@@ -10,20 +12,57 @@ import (
1012
"github.com/klauspost/cpuid/v2"
1113
)
1214

15+
func GetRandomizer() *rand.Rand {
16+
// Seed the PRNG with the hash of CPU info, increasing the overall probable space.
17+
fnvHash := fnv.New64()
18+
fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore)))
19+
return rand.New(rand.NewSource(int64(fnvHash.Sum64())))
20+
}
21+
var globalRng *rand.Rand = GetRandomizer()
22+
23+
// The Chrome version generator will suffer from deviation of a normal distribution.
1324
func ChromeVersion() int {
14-
// Use only CPU info as seed for PRNG
15-
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
16-
rng := rand.New(rand.NewSource(seed))
17-
// Start from Chrome 144 released on 2026.1.13
18-
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
19-
version := 144
20-
now := time.Now()
21-
// Each version has random 25-45 day interval
22-
for releaseDate.Before(now) {
23-
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
24-
version++
25+
// Start from Chrome 144, released on 2026.1.13.
26+
var startVersion int = 144
27+
var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400
28+
var timeCurrent int64 = time.Now().Unix() / 86400
29+
var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 105))
30+
return startVersion + (timeDiff / 35) // It's 31.15 currently.
31+
}
32+
33+
var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1,
34+
1, 2, 2, 2, 2, 3, 3, 3, 4, 4,
35+
4, 5, 5, 5, 5, 5, 6, 6, 6, 6}
36+
37+
// The following version generators use deterministic generators, but with the distribution scaled by a curve.
38+
func CurlVersion() string {
39+
// curl 8.0.0 was released on 20/03/2023.
40+
var timeCurrent int64 = time.Now().Unix() / 86400
41+
var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400
42+
var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 165))
43+
var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days.
44+
return "8." + strconv.Itoa(minorValue) + ".0"
45+
}
46+
func FirefoxVersion() int {
47+
// Firefox 128 ESR was released on 09/07/2023.
48+
var timeCurrent int64 = time.Now().Unix() / 86400
49+
var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400
50+
var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2) * 50))
51+
return int(timeDiff / 30) + 128
52+
}
53+
func SafariVersion() string {
54+
var anchoredTime time.Time = time.Now()
55+
var releaseYear int = anchoredTime.Year()
56+
var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
57+
var delayedDays = int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75))
58+
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
59+
if (anchoredTime.Compare(splitPoint) < 0) {
60+
releaseYear --
61+
splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
62+
splitPoint = splitPoint.AddDate(0, 0, delayedDays)
2563
}
26-
return version - 1
64+
var minorVersion = safariMinorMap[(anchoredTime.Unix() - splitPoint.Unix()) / 1296000]
65+
return strconv.Itoa(releaseYear - 1999) + "." + strconv.Itoa(minorVersion)
2766
}
2867

2968
// The full Chromium brand GREASE implementation
@@ -49,7 +88,7 @@ func getGreasedChOrder(brandLength int, seed int) []int {
4988
default:
5089
return clientHintShuffle4[seed % len(clientHintShuffle4)][:]
5190
}
52-
return []int{}
91+
//return []int{}
5392
}
5493
func getUngreasedChUa(majorVersion int, forkName string) []string {
5594
// Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice
@@ -74,11 +113,12 @@ func getGreasedChUa(majorVersion int, forkName string) string {
74113
return strings.Join(shuffledCh, ", ")
75114
}
76115

77-
// It's better to pin on Firefox ESR releases, and there could be a Firefox ESR version generator later.
78-
// However, if the Firefox fingerprint in uTLS doesn't have its update cadence match that of Firefox ESR, then it's better to update the Firefox version manually instead every time a new major ESR release is available.
79-
var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0"
80-
81116
// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG.
117+
var CurlUA = "curl/" + CurlVersion()
118+
var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion())
119+
var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0"
120+
var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15"
121+
// Chromium browsers.
82122
var AnchoredChromeVersion = ChromeVersion()
83123
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36"
84124
var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome")
@@ -106,10 +146,16 @@ func applyMasqueradedHeaders(header http.Header, browser string, variant string)
106146
header.Set("User-Agent", FirefoxUA)
107147
header["DNT"] = []string{"1"}
108148
header.Set("Accept-Language", "en-US,en;q=0.5")
149+
case "safari":
150+
header.Set("User-Agent", SafariUA)
151+
header.Set("Accept-Language", "en-US,en;q=0.9")
109152
case "golang":
110153
// Expose the default net/http header.
111154
header.Del("User-Agent")
112155
return
156+
case "curl":
157+
header.Set("User-Agent", CurlUA)
158+
return
113159
}
114160
// Context-specific.
115161
switch variant {
@@ -125,18 +171,28 @@ func applyMasqueradedHeaders(header http.Header, browser string, variant string)
125171
switch browser {
126172
case "chrome", "edge":
127173
header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
128-
case "firefox":
174+
case "firefox", "safari":
129175
header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
130176
}
131177
}
132178
header.Set("Sec-Fetch-Site", "none")
133179
header.Set("Sec-Fetch-Mode", "navigate")
134-
header.Set("Sec-Fetch-User", "?1")
180+
switch browser {
181+
case "safari":
182+
default:
183+
header.Set("Sec-Fetch-User", "?1")
184+
}
135185
header.Set("Sec-Fetch-Dest", "document")
136186
header.Set("Priority", "u=0, i")
137187
case "ws":
138188
header.Set("Sec-Fetch-Mode", "websocket")
139-
header.Set("Sec-Fetch-Dest", "empty")
189+
switch browser {
190+
case "safari":
191+
// Safari is NOT web-compliant here!
192+
header.Set("Sec-Fetch-Dest", "websocket")
193+
default:
194+
header.Set("Sec-Fetch-Dest", "empty")
195+
}
140196
header.Set("Sec-Fetch-Site", "same-origin")
141197
if header.Get("Cache-Control") == "" {
142198
header.Set("Cache-Control", "no-cache")
@@ -157,6 +213,8 @@ func applyMasqueradedHeaders(header http.Header, browser string, variant string)
157213
header.Set("Priority", "u=1, i")
158214
case "firefox":
159215
header.Set("Priority", "u=4")
216+
case "safari":
217+
header.Set("Priority", "u=3, i")
160218
}
161219
}
162220
if header.Get("Cache-Control") == "" {
@@ -182,8 +240,12 @@ func TryDefaultHeadersWith(header http.Header, variant string) {
182240
applyMasqueradedHeaders(header, "chrome", variant)
183241
case "firefox":
184242
applyMasqueradedHeaders(header, "firefox", variant)
243+
case "safari":
244+
applyMasqueradedHeaders(header, "safari", variant)
185245
case "edge":
186246
applyMasqueradedHeaders(header, "edge", variant)
247+
case "curl":
248+
applyMasqueradedHeaders(header, "curl", variant)
187249
case "golang":
188250
applyMasqueradedHeaders(header, "golang", variant)
189251
}

0 commit comments

Comments
 (0)