New constructs a Client. The HTTP client is preconfigured for domain fronting per cfg.Fronting.
(cfg Config)
| 338 | // New constructs a Client. The HTTP client is preconfigured for domain |
| 339 | // fronting per cfg.Fronting. |
| 340 | func New(cfg Config) (*Client, error) { |
| 341 | aead, err := frame.NewCryptoFromHexKey(cfg.AESKeyHex) |
| 342 | if err != nil { |
| 343 | return nil, err |
| 344 | } |
| 345 | |
| 346 | endpoints := make([]relayEndpoint, 0, len(cfg.ScriptURLs)) |
| 347 | seen := make(map[string]struct{}, len(cfg.ScriptURLs)) |
| 348 | for i, raw := range cfg.ScriptURLs { |
| 349 | url := strings.TrimSpace(raw) |
| 350 | if url == "" { |
| 351 | continue |
| 352 | } |
| 353 | if _, ok := seen[url]; ok { |
| 354 | continue |
| 355 | } |
| 356 | seen[url] = struct{}{} |
| 357 | account := "" |
| 358 | if i < len(cfg.ScriptAccounts) { |
| 359 | account = strings.TrimSpace(cfg.ScriptAccounts[i]) |
| 360 | } |
| 361 | ep := relayEndpoint{url: url, account: account} |
| 362 | if account != "" { |
| 363 | ep.bucket = "acct:" + account |
| 364 | } else { |
| 365 | ep.bucket = "url:" + url |
| 366 | } |
| 367 | endpoints = append(endpoints, ep) |
| 368 | } |
| 369 | if len(endpoints) == 0 { |
| 370 | return nil, fmt.Errorf("at least one script URL is required") |
| 371 | } |
| 372 | |
| 373 | // Each Google account is one in-flight bucket. Endpoints without an |
| 374 | // account label each get their own bucket (Apps Script throttles per |
| 375 | // account; we can't tell unlabeled deployments apart, so we conservatively |
| 376 | // assume they're all distinct — which matches v1.5 behavior where each |
| 377 | // endpoint was independently rate-managed). The in-flight semaphore on |
| 378 | // each bucket caps concurrent polls hitting that account, preserving the |
| 379 | // per-account anti-abuse protection that motivated v1.6's bucketing |
| 380 | // (issue #56) without partitioning the worker pool itself. |
| 381 | bucketSeen := make(map[string]struct{}, len(endpoints)) |
| 382 | labeled := 0 |
| 383 | for _, ep := range endpoints { |
| 384 | bucketSeen[ep.bucket] = struct{}{} |
| 385 | if ep.account != "" { |
| 386 | labeled++ |
| 387 | } |
| 388 | } |
| 389 | bucketCount := len(bucketSeen) |
| 390 | |
| 391 | var clientID [frame.ClientIDLen]byte |
| 392 | if _, err := rand.Read(clientID[:]); err != nil { |
| 393 | // crypto/rand failure is unrecoverable; fail fast rather than emitting |
| 394 | // an all-zero ID that would collide with every other unupgraded client. |
| 395 | return nil, fmt.Errorf("crypto/rand: %w", err) |
| 396 | } |
| 397 |