Initial sanitized code sync
This commit is contained in:
264
pkg/auth/sentinel.go
Normal file
264
pkg/auth/sentinel.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gpt-plus/pkg/httpclient"
|
||||
)
|
||||
|
||||
const (
|
||||
sentinelReqURL = "https://sentinel.openai.com/backend-api/sentinel/req"
|
||||
maxPowAttempts = 500000
|
||||
errorPrefix = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D"
|
||||
defaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
// SentinelGenerator generates openai-sentinel-token values.
|
||||
type SentinelGenerator struct {
|
||||
DeviceID string
|
||||
SID string // session UUID
|
||||
client *httpclient.Client
|
||||
}
|
||||
|
||||
// NewSentinelGenerator creates a new SentinelGenerator with a random DeviceID.
|
||||
func NewSentinelGenerator(client *httpclient.Client) *SentinelGenerator {
|
||||
return &SentinelGenerator{
|
||||
DeviceID: generateUUID(),
|
||||
SID: generateUUID(),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSentinelGeneratorWithDeviceID creates a SentinelGenerator reusing an existing DeviceID.
|
||||
// Use this to keep sentinel.DeviceID consistent with oai-device-id in request headers.
|
||||
func NewSentinelGeneratorWithDeviceID(client *httpclient.Client, deviceID string) *SentinelGenerator {
|
||||
return &SentinelGenerator{
|
||||
DeviceID: deviceID,
|
||||
SID: generateUUID(),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// sentinelRequirements is the response from the sentinel /req endpoint.
|
||||
type sentinelRequirements struct {
|
||||
Token string `json:"token"`
|
||||
ProofOfWork struct {
|
||||
Required bool `json:"required"`
|
||||
Seed string `json:"seed"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
} `json:"proofofwork"`
|
||||
}
|
||||
|
||||
// fnv1a32 computes FNV-1a 32-bit hash with murmurhash3 finalizer mixing,
|
||||
// matching the sentinel SDK's JavaScript implementation.
|
||||
func fnv1a32(text string) string {
|
||||
h := uint32(2166136261) // FNV offset basis
|
||||
for _, c := range []byte(text) {
|
||||
h ^= uint32(c)
|
||||
h *= 16777619 // FNV prime
|
||||
}
|
||||
// xorshift mixing (murmurhash3 finalizer)
|
||||
h ^= h >> 16
|
||||
h *= 2246822507
|
||||
h ^= h >> 13
|
||||
h *= 3266489909
|
||||
h ^= h >> 16
|
||||
return fmt.Sprintf("%08x", h)
|
||||
}
|
||||
|
||||
// buildConfig constructs the 19-element browser environment config array.
|
||||
func (sg *SentinelGenerator) buildConfig() []interface{} {
|
||||
now := time.Now().UTC()
|
||||
dateStr := now.Format("Mon Jan 02 2006 15:04:05 GMT+0000 (Coordinated Universal Time)")
|
||||
|
||||
navRandom := cryptoRandomFloat()
|
||||
perfNow := 100.5 + cryptoRandomFloat()*50
|
||||
timeOrigin := float64(now.UnixMilli()) - perfNow
|
||||
|
||||
config := []interface{}{
|
||||
"1920x1080", // [0] screen
|
||||
dateStr, // [1] date
|
||||
4294705152, // [2] jsHeapSizeLimit
|
||||
0, // [3] nonce placeholder
|
||||
defaultUA, // [4] userAgent
|
||||
"https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", // [5] script src
|
||||
nil, // [6] script version
|
||||
nil, // [7] data build
|
||||
DefaultLanguage, // [8] language
|
||||
0, // [9] elapsed_ms placeholder
|
||||
navRandom, // [10] random float
|
||||
0, // [11] nav val
|
||||
0, // [12] doc key
|
||||
10, // [13] win key
|
||||
perfNow, // [14] performance.now
|
||||
sg.SID, // [15] session UUID
|
||||
"", // [16] URL params
|
||||
16, // [17] hardwareConcurrency
|
||||
timeOrigin, // [18] timeOrigin
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// base64EncodeConfig JSON-encodes the config array with compact separators, then base64 encodes it.
|
||||
func base64EncodeConfig(config []interface{}) string {
|
||||
jsonBytes, _ := json.Marshal(config)
|
||||
return base64.StdEncoding.EncodeToString(jsonBytes)
|
||||
}
|
||||
|
||||
// runCheck performs a single PoW check iteration.
|
||||
func (sg *SentinelGenerator) runCheck(startTime time.Time, seed, difficulty string, config []interface{}, nonce int) string {
|
||||
config[3] = nonce
|
||||
config[9] = int(time.Since(startTime).Milliseconds())
|
||||
|
||||
data := base64EncodeConfig(config)
|
||||
hashHex := fnv1a32(seed + data)
|
||||
|
||||
diffLen := len(difficulty)
|
||||
if diffLen > 0 && hashHex[:diffLen] <= difficulty {
|
||||
return data + "~S"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// solvePoW runs the PoW loop and returns the solution token.
|
||||
func (sg *SentinelGenerator) solvePoW(seed, difficulty string) string {
|
||||
startTime := time.Now()
|
||||
config := sg.buildConfig()
|
||||
|
||||
for i := 0; i < maxPowAttempts; i++ {
|
||||
result := sg.runCheck(startTime, seed, difficulty, config, i)
|
||||
if result != "" {
|
||||
return "gAAAAAB" + result
|
||||
}
|
||||
}
|
||||
|
||||
// PoW failed after max attempts, return error token
|
||||
errData := base64EncodeConfig([]interface{}{nil})
|
||||
return "gAAAAAB" + errorPrefix + errData
|
||||
}
|
||||
|
||||
// generateRequirementsToken creates a requirements token (no server seed needed).
|
||||
// This is the SDK's getRequirementsToken() equivalent.
|
||||
func (sg *SentinelGenerator) generateRequirementsToken() string {
|
||||
config := sg.buildConfig()
|
||||
config[3] = 1
|
||||
config[9] = 5 + int(cryptoRandomFloat()*45) // simulate small delay 5-50ms
|
||||
data := base64EncodeConfig(config)
|
||||
return "gAAAAAC" + data // note: prefix is C, not B
|
||||
}
|
||||
|
||||
// fetchChallenge calls the sentinel backend to get challenge data.
|
||||
// Retries on 403/network errors since the proxy rotates IP every ~30s.
|
||||
func (sg *SentinelGenerator) fetchChallenge(ctx context.Context, client *httpclient.Client, flow string) (*sentinelRequirements, error) {
|
||||
const maxRetries = 5
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
result, err := sg.doFetchChallenge(ctx, client, flow)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
lastErr = err
|
||||
log.Printf("[sentinel] attempt %d/%d failed: %v", attempt, maxRetries, err)
|
||||
|
||||
if attempt < maxRetries {
|
||||
// Wait for proxy IP rotation, use shorter waits for early attempts
|
||||
wait := time.Duration(3*attempt) * time.Second
|
||||
log.Printf("[sentinel] waiting %v for proxy IP rotation before retry...", wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(wait):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("sentinel failed after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
func (sg *SentinelGenerator) doFetchChallenge(ctx context.Context, client *httpclient.Client, flow string) (*sentinelRequirements, error) {
|
||||
pToken := sg.generateRequirementsToken()
|
||||
|
||||
reqBody := map[string]string{
|
||||
"p": pToken,
|
||||
"id": sg.DeviceID,
|
||||
"flow": flow,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, sentinelReqURL, strings.NewReader(string(bodyBytes)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create sentinel request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
|
||||
req.Header.Set("Origin", "https://sentinel.openai.com")
|
||||
req.Header.Set("Referer", "https://sentinel.openai.com/backend-api/sentinel/frame.html?sv=20260219f9f6")
|
||||
req.Header.Set("sec-ch-ua", `"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"`)
|
||||
req.Header.Set("sec-ch-ua-mobile", "?0")
|
||||
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sentinel request: %w", err)
|
||||
}
|
||||
body, err := httpclient.ReadBody(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read sentinel response: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("sentinel returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result sentinelRequirements
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse sentinel response: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GenerateToken builds the full openai-sentinel-token JSON string for a given flow.
|
||||
func (sg *SentinelGenerator) GenerateToken(ctx context.Context, client *httpclient.Client, flow string) (string, error) {
|
||||
if client == nil {
|
||||
client = sg.client
|
||||
}
|
||||
challenge, err := sg.fetchChallenge(ctx, client, flow)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch sentinel challenge: %w", err)
|
||||
}
|
||||
|
||||
cValue := challenge.Token
|
||||
|
||||
var pValue string
|
||||
if challenge.ProofOfWork.Required && challenge.ProofOfWork.Seed != "" {
|
||||
pValue = sg.solvePoW(challenge.ProofOfWork.Seed, challenge.ProofOfWork.Difficulty)
|
||||
} else {
|
||||
pValue = sg.generateRequirementsToken()
|
||||
}
|
||||
|
||||
token := map[string]string{
|
||||
"p": pValue,
|
||||
"t": "",
|
||||
"c": cValue,
|
||||
"id": sg.DeviceID,
|
||||
"flow": flow,
|
||||
}
|
||||
tokenBytes, _ := json.Marshal(token)
|
||||
return string(tokenBytes), nil
|
||||
}
|
||||
|
||||
// cryptoRandomFloat returns a cryptographically random float64 in [0, 1).
|
||||
func cryptoRandomFloat() float64 {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
return float64(n.Int64()) / float64(math.MaxInt64)
|
||||
}
|
||||
Reference in New Issue
Block a user