2024-08-13 16:31:28 +02:00
package main
import (
"encoding/json"
"fmt"
2024-12-31 02:44:14 +01:00
"io"
2024-08-13 16:31:28 +02:00
"math/rand"
"net/http"
"sort"
"sync"
"time"
)
2025-01-28 21:05:43 +01:00
// BrowserVersion represents the version & global usage from the caniuse data
2024-08-13 16:31:28 +02:00
type BrowserVersion struct {
Version string ` json:"version" `
Global float64 ` json:"global" `
}
2025-01-28 21:05:43 +01:00
// BrowserData holds sets of versions for Firefox and Chromium
2024-08-13 16:31:28 +02:00
type BrowserData struct {
Firefox [ ] BrowserVersion ` json:"firefox" `
Chromium [ ] BrowserVersion ` json:"chrome" `
}
var (
cache = struct {
sync . RWMutex
data map [ string ] string
} {
data : make ( map [ string ] string ) ,
}
2025-01-28 21:05:43 +01:00
2024-08-13 16:31:28 +02:00
browserCache = struct {
sync . RWMutex
data BrowserData
expires time . Time
} {
expires : time . Now ( ) ,
}
)
2025-01-28 21:05:43 +01:00
// fetchLatestBrowserVersions retrieves usage data from caniuse.com’ s fulldata JSON.
2024-08-13 16:31:28 +02:00
func fetchLatestBrowserVersions ( ) ( BrowserData , error ) {
2025-01-28 21:05:43 +01:00
const urlCaniuse = "https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json"
2024-12-31 02:44:14 +01:00
client := & http . Client {
Timeout : 30 * time . Second ,
}
2025-01-28 21:05:43 +01:00
req , err := http . NewRequest ( "GET" , urlCaniuse , nil )
2024-12-31 02:44:14 +01:00
if err != nil {
return BrowserData { } , err
}
2025-01-28 21:05:43 +01:00
// Set a simple custom User-Agent and language
2024-12-31 02:44:14 +01:00
req . Header . Set ( "User-Agent" , "MyCustomAgent/1.0 (compatible; +https://example.com)" )
req . Header . Set ( "Accept-Language" , "en-US,en;q=0.9" )
resp , err := client . Do ( req )
2024-08-13 16:31:28 +02:00
if err != nil {
return BrowserData { } , err
}
defer resp . Body . Close ( )
2024-12-31 02:44:14 +01:00
body , err := io . ReadAll ( resp . Body )
2024-08-13 16:31:28 +02:00
if err != nil {
return BrowserData { } , err
}
2025-01-28 21:05:43 +01:00
var rawData map [ string ] any
2024-08-13 16:31:28 +02:00
if err := json . Unmarshal ( body , & rawData ) ; err != nil {
return BrowserData { } , err
}
2025-01-28 21:05:43 +01:00
stats , ok := rawData [ "agents" ] . ( map [ string ] any )
if ! ok {
return BrowserData { } , fmt . Errorf ( "unexpected JSON structure (no 'agents' field)" )
}
2024-08-13 16:31:28 +02:00
var data BrowserData
2025-01-28 21:05:43 +01:00
// Extract Firefox data
if firefoxData , ok := stats [ "firefox" ] . ( map [ string ] any ) ; ok {
if usageMap , ok := firefoxData [ "usage_global" ] . ( map [ string ] any ) ; ok {
for version , usage := range usageMap {
val , _ := usage . ( float64 )
data . Firefox = append ( data . Firefox , BrowserVersion { Version : version , Global : val } )
}
2024-08-13 16:31:28 +02:00
}
}
2025-01-28 21:05:43 +01:00
// Extract Chrome data
if chromeData , ok := stats [ "chrome" ] . ( map [ string ] any ) ; ok {
if usageMap , ok := chromeData [ "usage_global" ] . ( map [ string ] any ) ; ok {
for version , usage := range usageMap {
val , _ := usage . ( float64 )
data . Chromium = append ( data . Chromium , BrowserVersion { Version : version , Global : val } )
}
2024-08-13 16:31:28 +02:00
}
}
return data , nil
}
2025-01-28 21:05:43 +01:00
// getLatestBrowserVersions checks the cache and fetches new data if expired
2024-08-13 16:31:28 +02:00
func getLatestBrowserVersions ( ) ( BrowserData , error ) {
browserCache . RLock ( )
if time . Now ( ) . Before ( browserCache . expires ) {
data := browserCache . data
browserCache . RUnlock ( )
return data , nil
}
browserCache . RUnlock ( )
data , err := fetchLatestBrowserVersions ( )
if err != nil {
return BrowserData { } , err
}
browserCache . Lock ( )
browserCache . data = data
2025-01-28 21:05:43 +01:00
browserCache . expires = time . Now ( ) . Add ( 24 * time . Hour ) // Refresh daily
2024-08-13 16:31:28 +02:00
browserCache . Unlock ( )
return data , nil
}
2025-01-28 21:05:43 +01:00
// randomUserAgent picks a random browser (Firefox/Chromium), selects a version based on usage,
// picks an OS string, and composes a User-Agent header.
2024-08-13 16:31:28 +02:00
func randomUserAgent ( ) ( string , error ) {
browsers , err := getLatestBrowserVersions ( )
if err != nil {
return "" , err
}
2025-01-28 21:05:43 +01:00
r := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
2024-08-13 16:31:28 +02:00
2025-01-28 21:05:43 +01:00
// Overall usage: 80% chance for Chromium, 20% for Firefox
2024-08-13 16:31:28 +02:00
usageStats := map [ string ] float64 {
2025-01-28 21:05:43 +01:00
"Firefox" : 20.0 ,
"Chromium" : 80.0 ,
2024-08-13 16:31:28 +02:00
}
2025-01-28 21:05:43 +01:00
// Weighted random selection of the browser type
2024-08-13 16:31:28 +02:00
browserType := ""
2025-01-28 21:05:43 +01:00
randVal := r . Float64 ( ) * 100
2024-08-13 16:31:28 +02:00
cumulative := 0.0
2025-01-28 21:05:43 +01:00
for bType , usage := range usageStats {
2024-08-13 16:31:28 +02:00
cumulative += usage
if randVal < cumulative {
2025-01-28 21:05:43 +01:00
browserType = bType
2024-08-13 16:31:28 +02:00
break
}
}
var versions [ ] BrowserVersion
switch browserType {
case "Firefox" :
versions = browsers . Firefox
case "Chromium" :
versions = browsers . Chromium
}
if len ( versions ) == 0 {
return "" , fmt . Errorf ( "no versions found for browser: %s" , browserType )
}
2025-01-28 21:05:43 +01:00
// Sort by global usage descending
2024-08-13 16:31:28 +02:00
sort . Slice ( versions , func ( i , j int ) bool {
return versions [ i ] . Global > versions [ j ] . Global
} )
2025-01-28 21:05:43 +01:00
// Probability distribution for top few versions
probabilities := [ ] float64 { 0.5 , 0.25 , 0.125 , 0.0625 , 0.03125 , 0.015625 , 0.0078125 , 0.00390625 }
2024-08-13 16:31:28 +02:00
version := ""
2025-01-28 21:05:43 +01:00
randVal = r . Float64 ( )
2024-08-13 16:31:28 +02:00
cumulative = 0.0
for i , p := range probabilities {
cumulative += p
if randVal < cumulative && i < len ( versions ) {
version = versions [ i ] . Version
break
}
}
2025-01-28 21:05:43 +01:00
// Fallback to the least used version if none matched
2024-08-13 16:31:28 +02:00
if version == "" {
version = versions [ len ( versions ) - 1 ] . Version
}
2025-01-28 21:05:43 +01:00
userAgent := generateUserAgent ( browserType , version , r )
2024-08-13 16:31:28 +02:00
return userAgent , nil
}
2025-01-28 21:05:43 +01:00
// generateUserAgent composes the final UA string given the browser, version, and OS.
func generateUserAgent ( browser , version string , r * rand . Rand ) string {
2024-08-13 16:31:28 +02:00
oses := [ ] struct {
os string
probability float64
} {
{ "Windows NT 10.0; Win64; x64" , 44.0 } ,
2025-01-28 21:05:43 +01:00
{ "X11; Linux x86_64" , 2.0 } ,
{ "X11; Ubuntu; Linux x86_64" , 2.0 } ,
2024-08-13 16:31:28 +02:00
{ "Macintosh; Intel Mac OS X 10_15_7" , 10.0 } ,
}
2025-01-28 21:05:43 +01:00
// Weighted random selection for OS
randVal := r . Float64 ( ) * 100
2024-08-13 16:31:28 +02:00
cumulative := 0.0
2025-01-28 21:05:43 +01:00
selectedOS := oses [ 0 ] . os // Default in case distribution is off
for _ , entry := range oses {
cumulative += entry . probability
2024-08-13 16:31:28 +02:00
if randVal < cumulative {
2025-01-28 21:05:43 +01:00
selectedOS = entry . os
2024-08-13 16:31:28 +02:00
break
}
}
switch browser {
case "Firefox" :
2025-01-28 21:05:43 +01:00
// Example: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0
2024-08-13 16:31:28 +02:00
return fmt . Sprintf ( "Mozilla/5.0 (%s; rv:%s) Gecko/20100101 Firefox/%s" , selectedOS , version , version )
case "Chromium" :
2025-01-28 21:05:43 +01:00
// Example: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
2024-08-13 16:31:28 +02:00
return fmt . Sprintf ( "Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" , selectedOS , version )
2025-01-28 21:05:43 +01:00
default :
return ""
2024-08-13 16:31:28 +02:00
}
}
2025-01-28 21:05:43 +01:00
// updateCachedUserAgents randomly updates half of the cached UAs to new versions
2024-08-13 16:31:28 +02:00
func updateCachedUserAgents ( newVersions BrowserData ) {
cache . Lock ( )
defer cache . Unlock ( )
2025-01-28 21:05:43 +01:00
r := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
2024-08-13 16:31:28 +02:00
for key , userAgent := range cache . data {
2025-01-28 21:05:43 +01:00
if r . Float64 ( ) < 0.5 {
updatedUserAgent := updateUserAgentVersion ( userAgent , newVersions , r )
2024-08-13 16:31:28 +02:00
cache . data [ key ] = updatedUserAgent
}
}
}
2025-01-28 21:05:43 +01:00
// updateUserAgentVersion tries to parse the old UA, detect its browser, and update the version
func updateUserAgentVersion ( userAgent string , newVersions BrowserData , r * rand . Rand ) string {
2024-08-13 16:31:28 +02:00
var browserType , version string
2025-01-28 21:05:43 +01:00
// Attempt to detect old UA patterns (Chromium or Firefox)
2024-08-13 16:31:28 +02:00
if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" , & version ) ; err == nil {
browserType = "Chromium"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" , & version ) ; err == nil {
browserType = "Chromium"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" , & version ) ; err == nil {
browserType = "Chromium"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36" , & version ) ; err == nil {
browserType = "Chromium"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s" , & version , & version ) ; err == nil {
browserType = "Firefox"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (X11; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s" , & version , & version ) ; err == nil {
browserType = "Firefox"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s" , & version , & version ) ; err == nil {
browserType = "Firefox"
} else if _ , err := fmt . Sscanf ( userAgent , "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; rv:%s) Gecko/20100101 Firefox/%s" , & version , & version ) ; err == nil {
browserType = "Firefox"
}
2025-01-28 21:05:43 +01:00
// Grab the newest version from the fetched data
2024-08-13 16:31:28 +02:00
var latestVersion string
2024-12-31 02:44:14 +01:00
if browserType == "Firefox" && len ( newVersions . Firefox ) > 0 {
2025-01-28 21:05:43 +01:00
// Sort by usage descending
sort . Slice ( newVersions . Firefox , func ( i , j int ) bool {
return newVersions . Firefox [ i ] . Global > newVersions . Firefox [ j ] . Global
} )
2024-08-13 16:31:28 +02:00
latestVersion = newVersions . Firefox [ 0 ] . Version
2024-12-31 02:44:14 +01:00
} else if browserType == "Chromium" && len ( newVersions . Chromium ) > 0 {
2025-01-28 21:05:43 +01:00
// Sort by usage descending
sort . Slice ( newVersions . Chromium , func ( i , j int ) bool {
return newVersions . Chromium [ i ] . Global > newVersions . Chromium [ j ] . Global
} )
2024-08-13 16:31:28 +02:00
latestVersion = newVersions . Chromium [ 0 ] . Version
}
2025-01-28 21:05:43 +01:00
// If we failed to detect the browser or have no data, just return the old UA
if browserType == "" || latestVersion == "" {
return userAgent
}
// Create a new random OS-based UA string with the latest version
return generateUserAgent ( browserType , latestVersion , r )
2024-08-13 16:31:28 +02:00
}
2025-01-28 21:05:43 +01:00
// periodicAgentUpdate periodically refreshes browser data and user agents
2024-12-31 02:44:14 +01:00
func periodicAgentUpdate ( ) {
2024-08-13 16:31:28 +02:00
for {
2025-01-28 21:05:43 +01:00
// Sleep a random interval between 1 and 2 days
r := rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) )
time . Sleep ( time . Duration ( 24 + r . Intn ( 24 ) ) * time . Hour )
2024-08-13 16:31:28 +02:00
// Fetch the latest browser versions
newVersions , err := fetchLatestBrowserVersions ( )
if err != nil {
printWarn ( "Error fetching latest browser versions: %v" , err )
continue
}
// Update the browser version cache
browserCache . Lock ( )
browserCache . data = newVersions
browserCache . expires = time . Now ( ) . Add ( 24 * time . Hour )
browserCache . Unlock ( )
// Update the cached user agents
updateCachedUserAgents ( newVersions )
}
}
2025-01-28 21:05:43 +01:00
// GetUserAgent returns a cached UA for the given key or creates one if none exists.
2024-08-13 16:31:28 +02:00
func GetUserAgent ( cacheKey string ) ( string , error ) {
cache . RLock ( )
userAgent , found := cache . data [ cacheKey ]
cache . RUnlock ( )
if found {
return userAgent , nil
}
userAgent , err := randomUserAgent ( )
if err != nil {
return "" , err
}
cache . Lock ( )
cache . data [ cacheKey ] = userAgent
cache . Unlock ( )
2025-01-28 21:05:43 +01:00
printDebug ( "Generated (cached or new) user agent: %s" , userAgent )
2024-08-13 16:31:28 +02:00
return userAgent , nil
}
2025-01-28 21:05:43 +01:00
// GetNewUserAgent always returns a newly generated UA, overwriting the cache.
2024-08-13 16:31:28 +02:00
func GetNewUserAgent ( cacheKey string ) ( string , error ) {
userAgent , err := randomUserAgent ( )
if err != nil {
return "" , err
}
cache . Lock ( )
cache . data [ cacheKey ] = userAgent
cache . Unlock ( )
2025-01-28 21:05:43 +01:00
printDebug ( "Generated new user agent: %s" , userAgent )
2024-08-13 16:31:28 +02:00
return userAgent , nil
}
// func main() {
2024-12-31 02:44:14 +01:00
// go periodicAgentUpdate() // not needed here
2024-08-13 16:31:28 +02:00
// cacheKey := "image-search"
// userAgent, err := GetUserAgent(cacheKey)
// if err != nil {
// fmt.Println("Error:", err)
// return
// }
// fmt.Println("Generated User Agent:", userAgent)
// // Request a new user agent for the same key
// newUserAgent, err := GetNewUserAgent(cacheKey)
// if err != nil {
// fmt.Println("Error:", err)
// return
// }
// fmt.Println("New User Agent:", newUserAgent)
// AcacheKey := "image-search"
// AuserAgent, err := GetUserAgent(AcacheKey)
// if err != nil {
// fmt.Println("Error:", err)
// return
// }
// fmt.Println("Generated User Agent:", AuserAgent)
// DcacheKey := "image-search"
// DuserAgent, err := GetUserAgent(DcacheKey)
// if err != nil {
// fmt.Println("Error:", err)
// return
// }
// fmt.Println("Generated User Agent:", DuserAgent)
// }