-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseragent.go
More file actions
349 lines (292 loc) · 9.57 KB
/
useragent.go
File metadata and controls
349 lines (292 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package commonuseragent
import (
"crypto/rand"
"embed"
"encoding/json"
"errors"
"fmt"
"math/big"
"sync"
)
// Go directive to embed the files in the binary.
//
//go:embed desktop_useragents.json
//go:embed mobile_useragents.json
var content embed.FS
// UserAgent represents a user agent string with its usage percentage
type UserAgent struct {
UA string `json:"ua"`
Pct float64 `json:"pct"`
}
// Config holds runtime configuration for the user agent library
type Config struct {
DesktopFile string
MobileFile string
}
// DefaultConfig returns the default configuration
func DefaultConfig() Config {
return Config{
DesktopFile: "desktop_useragents.json",
MobileFile: "mobile_useragents.json",
}
}
// Manager handles user agent data with thread-safe operations
type Manager struct {
mu sync.RWMutex
desktopAgents []UserAgent
mobileAgents []UserAgent
config Config
}
var (
// ErrEmptyAgentList is returned when trying to get a random agent from an empty list
ErrEmptyAgentList = errors.New("agent list is empty")
// ErrInvalidData is returned when user agent data is invalid
ErrInvalidData = errors.New("invalid user agent data")
// ErrFileNotFound is returned when the embedded file cannot be found
ErrFileNotFound = errors.New("embedded file not found")
)
var (
defaultManager *Manager
defaultManagerOnce sync.Once
initError error
)
// init initializes the default manager with error handling instead of panic
func init() {
defaultManagerOnce.Do(func() {
mgr, err := NewManager(DefaultConfig())
if err != nil {
// Store error for later retrieval instead of panic
initError = err
return
}
defaultManager = mgr
})
}
// GetInitError returns any error that occurred during initialization
func GetInitError() error {
return initError
}
// NewManager creates a new Manager with the given configuration
func NewManager(cfg Config) (*Manager, error) {
if cfg.DesktopFile == "" || cfg.MobileFile == "" {
return nil, fmt.Errorf("%w: desktop and mobile files must be specified", ErrInvalidData)
}
m := &Manager{
config: cfg,
}
if err := m.loadUserAgents(cfg.DesktopFile, &m.desktopAgents); err != nil {
return nil, fmt.Errorf("failed to load desktop agents: %w", err)
}
if err := m.loadUserAgents(cfg.MobileFile, &m.mobileAgents); err != nil {
return nil, fmt.Errorf("failed to load mobile agents: %w", err)
}
// Validate loaded data
if err := m.validate(); err != nil {
return nil, err
}
return m, nil
}
// loadUserAgents reads and unmarshals user agent data from embedded files
func (m *Manager) loadUserAgents(filename string, agents *[]UserAgent) error {
bytes, err := content.ReadFile(filename)
if err != nil {
return fmt.Errorf("%w: %s", ErrFileNotFound, filename)
}
if err := json.Unmarshal(bytes, agents); err != nil {
return fmt.Errorf("failed to parse %s: %w", filename, err)
}
return nil
}
// validate ensures the loaded user agent data is valid
func (m *Manager) validate() error {
m.mu.RLock()
defer m.mu.RUnlock()
if len(m.desktopAgents) == 0 && len(m.mobileAgents) == 0 {
return fmt.Errorf("%w: both desktop and mobile agent lists are empty", ErrInvalidData)
}
// Validate individual agents
for i, agent := range m.desktopAgents {
if err := validateAgent(agent); err != nil {
return fmt.Errorf("invalid desktop agent at index %d: %w", i, err)
}
}
for i, agent := range m.mobileAgents {
if err := validateAgent(agent); err != nil {
return fmt.Errorf("invalid mobile agent at index %d: %w", i, err)
}
}
return nil
}
// validateAgent checks if a single UserAgent is valid
func validateAgent(ua UserAgent) error {
if ua.UA == "" {
return fmt.Errorf("%w: user agent string is empty", ErrInvalidData)
}
if ua.Pct < 0 || ua.Pct > 100 {
return fmt.Errorf("%w: percentage must be between 0 and 100, got %.2f", ErrInvalidData, ua.Pct)
}
// Basic sanity check for UA string length
if len(ua.UA) < 10 || len(ua.UA) > 1000 {
return fmt.Errorf("%w: user agent string length must be between 10 and 1000 characters", ErrInvalidData)
}
return nil
}
// GetAllDesktop returns a copy of all desktop user agents
func (m *Manager) GetAllDesktop() []UserAgent {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to prevent external modification
agents := make([]UserAgent, len(m.desktopAgents))
copy(agents, m.desktopAgents)
return agents
}
// GetAllMobile returns a copy of all mobile user agents
func (m *Manager) GetAllMobile() []UserAgent {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to prevent external modification
agents := make([]UserAgent, len(m.mobileAgents))
copy(agents, m.mobileAgents)
return agents
}
// GetRandomDesktop returns a random desktop UserAgent using weighted random selection
func (m *Manager) GetRandomDesktop() (UserAgent, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.getWeightedRandomAgent(m.desktopAgents)
}
// GetRandomMobile returns a random mobile UserAgent using weighted random selection
func (m *Manager) GetRandomMobile() (UserAgent, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.getWeightedRandomAgent(m.mobileAgents)
}
// GetRandomDesktopUA returns just the UA string of a random desktop user agent
func (m *Manager) GetRandomDesktopUA() (string, error) {
ua, err := m.GetRandomDesktop()
if err != nil {
return "", err
}
return ua.UA, nil
}
// GetRandomMobileUA returns just the UA string of a random mobile user agent
func (m *Manager) GetRandomMobileUA() (string, error) {
ua, err := m.GetRandomMobile()
if err != nil {
return "", err
}
return ua.UA, nil
}
// GetRandomUA returns a random user agent string from either desktop or mobile
func (m *Manager) GetRandomUA() (string, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// Combine agents for weighted selection across all
// Note: This assumes percentages in both files are relative to their own category (sum to ~100)
// If we want to mix them, we might need to normalize or just treat them as one pool.
// For simplicity and robustness, let's treat them as one big pool where weights are relative.
allAgents := make([]UserAgent, 0, len(m.desktopAgents)+len(m.mobileAgents))
allAgents = append(allAgents, m.desktopAgents...)
allAgents = append(allAgents, m.mobileAgents...)
ua, err := m.getWeightedRandomAgent(allAgents)
if err != nil {
return "", err
}
return ua.UA, nil
}
// getWeightedRandomAgent selects an agent based on its Pct value
func (m *Manager) getWeightedRandomAgent(agents []UserAgent) (UserAgent, error) {
if len(agents) == 0 {
return UserAgent{}, ErrEmptyAgentList
}
var totalWeight float64
for _, ua := range agents {
totalWeight += ua.Pct
}
if totalWeight <= 0 {
// Fallback to uniform selection if weights are invalid
idx, err := secureRandomInt(len(agents))
if err != nil {
return UserAgent{}, err
}
return agents[idx], nil
}
// Generate a random value in [0, totalWeight)
// We use a large integer range for precision
const precision = 1_000_000
randInt, err := secureRandomInt(precision)
if err != nil {
return UserAgent{}, fmt.Errorf("failed to generate random value: %w", err)
}
randomWeight := float64(randInt) / float64(precision) * totalWeight
currentWeight := 0.0
for _, ua := range agents {
currentWeight += ua.Pct
if randomWeight < currentWeight {
return ua, nil
}
}
// Should not happen if logic is correct, but return last one as fallback
return agents[len(agents)-1], nil
}
// secureRandomInt generates a cryptographically secure random integer in [0, max)
func secureRandomInt(max int) (int, error) {
if max <= 0 {
return 0, errors.New("max must be positive")
}
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return 0, err
}
return int(nBig.Int64()), nil
}
// Package-level convenience functions that use the default manager
// GetAllDesktop returns all desktop user agents using the default manager
func GetAllDesktop() ([]UserAgent, error) {
if initError != nil {
return nil, fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetAllDesktop(), nil
}
// GetAllMobile returns all mobile user agents using the default manager
func GetAllMobile() ([]UserAgent, error) {
if initError != nil {
return nil, fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetAllMobile(), nil
}
// GetRandomDesktop returns a random desktop UserAgent using the default manager
func GetRandomDesktop() (UserAgent, error) {
if initError != nil {
return UserAgent{}, fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetRandomDesktop()
}
// GetRandomMobile returns a random mobile UserAgent using the default manager
func GetRandomMobile() (UserAgent, error) {
if initError != nil {
return UserAgent{}, fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetRandomMobile()
}
// GetRandomDesktopUA returns just the UA string of a random desktop user agent using the default manager
func GetRandomDesktopUA() (string, error) {
if initError != nil {
return "", fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetRandomDesktopUA()
}
// GetRandomMobileUA returns just the UA string of a random mobile user agent using the default manager
func GetRandomMobileUA() (string, error) {
if initError != nil {
return "", fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetRandomMobileUA()
}
// GetRandomUA returns a random user agent string from either desktop or mobile using the default manager
func GetRandomUA() (string, error) {
if initError != nil {
return "", fmt.Errorf("library not initialized: %w", initError)
}
return defaultManager.GetRandomUA()
}