mirror of
https://github.com/verssache/chatgpt-creator.git
synced 2026-05-16 21:59:33 +00:00
Initial commit: ChatGPT Account Registration Bot
This commit is contained in:
48
internal/chrome/profiles.go
Normal file
48
internal/chrome/profiles.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package chrome
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/bogdanfinn/tls-client/profiles"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Major int
|
||||
Impersonate string
|
||||
Build int
|
||||
PatchMin int
|
||||
PatchMax int
|
||||
SecChUA string
|
||||
}
|
||||
|
||||
var chromeProfiles = []Profile{
|
||||
{131, "chrome131", 6778, 0, 300, "\"Chromium\";v=\"131\", \"Google Chrome\";v=\"131\", \"Not_A Brand\";v=\"24\""},
|
||||
}
|
||||
|
||||
func RandomChromeVersion() (Profile, string, string) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
profile := chromeProfiles[rand.Intn(len(chromeProfiles))]
|
||||
patch := rand.Intn(profile.PatchMax-profile.PatchMin+1) + profile.PatchMin
|
||||
fullVersion := fmt.Sprintf("%d.0.%d.%d", profile.Major, profile.Build, patch)
|
||||
userAgent := fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", fullVersion)
|
||||
return profile, fullVersion, userAgent
|
||||
}
|
||||
|
||||
func MapToTLSProfile(impersonate string) profiles.ClientProfile {
|
||||
switch impersonate {
|
||||
case "chrome131":
|
||||
return profiles.Chrome_131
|
||||
case "chrome133a":
|
||||
return profiles.Chrome_133
|
||||
case "chrome136":
|
||||
// Fallback to Chrome_133 as Chrome_136 is not available in tls-client v1.14.0
|
||||
return profiles.Chrome_133
|
||||
case "chrome142":
|
||||
// Fallback to Chrome_133 as Chrome_142 is not available in tls-client v1.14.0
|
||||
return profiles.Chrome_133
|
||||
default:
|
||||
return profiles.Chrome_133
|
||||
}
|
||||
}
|
||||
59
internal/config/config.go
Normal file
59
internal/config/config.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Config holds the application configuration.
|
||||
type Config struct {
|
||||
Proxy string `json:"proxy"`
|
||||
OutputFile string `json:"output_file"`
|
||||
DefaultPassword string `json:"default_password"`
|
||||
DefaultDomain string `json:"default_domain"`
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultProxy = ""
|
||||
DefaultOutputFile = "results.txt"
|
||||
DefaultConfigFilename = "config.json"
|
||||
DefaultPassword = "" // Min 12 characters
|
||||
DefaultDomainValue = ""
|
||||
)
|
||||
|
||||
// DefaultConfigPath returns the default path to the config file.
|
||||
func DefaultConfigPath() string {
|
||||
return DefaultConfigFilename
|
||||
}
|
||||
|
||||
// Load reads the config from a JSON file and applies environment variable overrides.
|
||||
func Load(path string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Proxy: DefaultProxy,
|
||||
OutputFile: DefaultOutputFile,
|
||||
DefaultPassword: DefaultPassword,
|
||||
DefaultDomain: DefaultDomainValue,
|
||||
}
|
||||
|
||||
// Try to read the file
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
// Validate password length
|
||||
if cfg.DefaultPassword != "" && len(cfg.DefaultPassword) < 12 {
|
||||
return nil, fmt.Errorf("default_password must be at least 12 characters (got %d)", len(cfg.DefaultPassword))
|
||||
}
|
||||
|
||||
// Environment variable overrides
|
||||
if proxy := os.Getenv("PROXY"); proxy != "" {
|
||||
cfg.Proxy = proxy
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
153
internal/email/generator.go
Normal file
153
internal/email/generator.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
fhttp "github.com/bogdanfinn/fhttp"
|
||||
"github.com/bogdanfinn/tls-client"
|
||||
"github.com/bogdanfinn/tls-client/profiles"
|
||||
|
||||
"github.com/verssache/chatgpt-creator/internal/util"
|
||||
)
|
||||
|
||||
// CreateTempEmail fetches a new temp email using a random profile and gofakeit names.
|
||||
func CreateTempEmail(defaultDomain string) (string, error) {
|
||||
// If defaultDomain is set, skip fetching from generator.email
|
||||
if defaultDomain != "" {
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := strings.ToLower(firstName+lastName+util.RandStr(5)) + "@" + defaultDomain
|
||||
return email, nil
|
||||
}
|
||||
|
||||
options := []tls_client.HttpClientOption{
|
||||
tls_client.WithClientProfile(profiles.Chrome_131),
|
||||
}
|
||||
|
||||
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create tls client: %w", err)
|
||||
}
|
||||
|
||||
req, err := fhttp.NewRequest(http.MethodGet, "https://generator.email/", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch generator.email: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("generator.email returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse HTML: %w", err)
|
||||
}
|
||||
|
||||
domains := []string{"smartmail.de", "enayu.com", "crazymailing.com"}
|
||||
doc.Find(".e7m.tt-suggestions div > p").Each(func(i int, s *goquery.Selection) {
|
||||
domain := strings.TrimSpace(s.Text())
|
||||
if domain != "" {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
})
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
randomDomain := domains[r.Intn(len(domains))]
|
||||
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := strings.ToLower(firstName+lastName+util.RandStr(5)) + "@" + randomDomain
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// GetVerificationCode polls generator.email for the OTP using a custom cookie.
|
||||
func GetVerificationCode(email string, maxRetries int, delay time.Duration) (string, error) {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid email format: %s", email)
|
||||
}
|
||||
username := parts[0]
|
||||
domain := parts[1]
|
||||
|
||||
otpRegex := regexp.MustCompile(`\d{6}`)
|
||||
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
options := []tls_client.HttpClientOption{
|
||||
tls_client.WithClientProfile(profiles.Chrome_131),
|
||||
}
|
||||
|
||||
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create tls client: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://generator.email/%s/%s", domain, username)
|
||||
req, err := fhttp.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Critical: Set request header Cookie: surl={domain}/{username} explicitly.
|
||||
req.Header.Set("Cookie", fmt.Sprintf("surl=%s/%s", domain, username))
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Log error and continue retrying
|
||||
time.Sleep(delay)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
time.Sleep(delay)
|
||||
continue
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
time.Sleep(delay)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find OTP in #email-table > div.e7m.list-group-item.list-group-item-info > div.e7m.subj_div_45g45gg
|
||||
otp := ""
|
||||
doc.Find("#email-table > div.e7m.list-group-item.list-group-item-info > div.e7m.subj_div_45g45gg").EachWithBreak(func(i int, s *goquery.Selection) bool {
|
||||
text := s.Text()
|
||||
matches := otpRegex.FindStringSubmatch(text)
|
||||
if len(matches) > 0 {
|
||||
code := matches[0]
|
||||
// Skip code "177010" explicitly
|
||||
if code == "177010" {
|
||||
return true // continue to next if any, but we only expect one
|
||||
}
|
||||
otp = code
|
||||
return false // break
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if otp != "" {
|
||||
return otp, nil
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to get verification code after %d retries", maxRetries)
|
||||
}
|
||||
134
internal/register/batch.go
Normal file
134
internal/register/batch.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package register
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/verssache/chatgpt-creator/internal/email"
|
||||
"github.com/verssache/chatgpt-creator/internal/util"
|
||||
)
|
||||
|
||||
// registerOne handles a single account registration.
|
||||
func registerOne(workerID int, tag string, proxy, outputFile, defaultPassword, defaultDomain string, printMu, fileMu *sync.Mutex) (bool, string, string) {
|
||||
client, err := NewClient(proxy, tag, workerID, printMu, fileMu)
|
||||
if err != nil {
|
||||
return false, "", fmt.Sprintf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
emailAddr, err := email.CreateTempEmail(defaultDomain)
|
||||
if err != nil {
|
||||
return false, "", fmt.Sprintf("failed to create temp email: %v", err)
|
||||
}
|
||||
|
||||
password := defaultPassword
|
||||
if password == "" {
|
||||
password = util.GeneratePassword(14)
|
||||
}
|
||||
|
||||
firstName, lastName := util.RandomName()
|
||||
birthdate := util.RandomBirthdate()
|
||||
|
||||
client.print(fmt.Sprintf("Starting registration for %s", emailAddr))
|
||||
|
||||
err = client.RunRegister(emailAddr, password, firstName+" "+lastName, birthdate)
|
||||
if err != nil {
|
||||
return false, emailAddr, err.Error()
|
||||
}
|
||||
|
||||
// Append to file
|
||||
fileMu.Lock()
|
||||
defer fileMu.Unlock()
|
||||
|
||||
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return false, emailAddr, fmt.Sprintf("failed to open output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
line := fmt.Sprintf("%s|%s\n", emailAddr, password)
|
||||
if _, err := f.WriteString(line); err != nil {
|
||||
return false, emailAddr, fmt.Sprintf("failed to write to output file: %v", err)
|
||||
}
|
||||
|
||||
return true, emailAddr, ""
|
||||
}
|
||||
|
||||
// RunBatch runs concurrent registration tasks with retry until target success count is reached.
|
||||
func RunBatch(totalAccounts int, outputFile string, maxWorkers int, proxy, defaultPassword, defaultDomain string) {
|
||||
var printMu sync.Mutex
|
||||
var fileMu sync.Mutex
|
||||
|
||||
var remaining int64 = int64(totalAccounts)
|
||||
var successCount int64
|
||||
var failureCount int64
|
||||
var attemptNum int64
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for w := 1; w <= maxWorkers; w++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
// Claim a slot before starting work
|
||||
if atomic.AddInt64(&remaining, -1) < 0 {
|
||||
// No more slots needed, put it back and exit
|
||||
atomic.AddInt64(&remaining, 1)
|
||||
return
|
||||
}
|
||||
|
||||
attempt := atomic.AddInt64(&attemptNum, 1)
|
||||
tag := fmt.Sprintf("%d/%d", attempt, totalAccounts)
|
||||
|
||||
success, emailAddr, errStr := registerOne(workerID, tag, proxy, outputFile, defaultPassword, defaultDomain, &printMu, &fileMu)
|
||||
if success {
|
||||
atomic.AddInt64(&successCount, 1)
|
||||
ts := time.Now().Format("15:04:05")
|
||||
printMu.Lock()
|
||||
fmt.Printf("[%s] [W%d] ✓ SUCCESS: %s\n", ts, workerID, emailAddr)
|
||||
printMu.Unlock()
|
||||
} else {
|
||||
atomic.AddInt64(&failureCount, 1)
|
||||
// Failed — return the slot so it gets retried
|
||||
atomic.AddInt64(&remaining, 1)
|
||||
ts := time.Now().Format("15:04:05")
|
||||
printMu.Lock()
|
||||
fmt.Printf("[%s] [W%d] ✗ FAILURE: %s | %s\n", ts, workerID, emailAddr, errStr)
|
||||
printMu.Unlock()
|
||||
}
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
elapsedStr := formatDuration(elapsed)
|
||||
|
||||
fmt.Printf("\n--- Batch Registration Summary ---\n")
|
||||
fmt.Printf("Target: %d\n", totalAccounts)
|
||||
fmt.Printf("Success: %d\n", successCount)
|
||||
fmt.Printf("Attempts: %d\n", attemptNum)
|
||||
fmt.Printf("Failures: %d\n", failureCount)
|
||||
fmt.Printf("Elapsed: %s\n", elapsedStr)
|
||||
fmt.Printf("----------------------------------\n")
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
h := int(d.Hours())
|
||||
m := int(d.Minutes()) % 60
|
||||
s := int(d.Seconds()) % 60
|
||||
|
||||
if h > 0 {
|
||||
return fmt.Sprintf("%dh %dm %ds", h, m, s)
|
||||
}
|
||||
if m > 0 {
|
||||
return fmt.Sprintf("%dm %ds", m, s)
|
||||
}
|
||||
return fmt.Sprintf("%ds", s)
|
||||
}
|
||||
126
internal/register/client.go
Normal file
126
internal/register/client.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package register
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
http "github.com/bogdanfinn/fhttp"
|
||||
"github.com/bogdanfinn/tls-client"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/verssache/chatgpt-creator/internal/chrome"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://chatgpt.com"
|
||||
authURL = "https://auth.openai.com"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
session tls_client.HttpClient
|
||||
proxy string
|
||||
tag string
|
||||
workerID int
|
||||
deviceID string
|
||||
impersonate string
|
||||
major int
|
||||
fullVersion string
|
||||
ua string
|
||||
secChUA string
|
||||
printMu *sync.Mutex
|
||||
fileMu *sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(proxy, tag string, workerID int, printMu, fileMu *sync.Mutex) (*Client, error) {
|
||||
profile, fullVersion, ua := chrome.RandomChromeVersion()
|
||||
impersonate := profile.Impersonate
|
||||
mappedProfile := chrome.MapToTLSProfile(impersonate)
|
||||
|
||||
options := []tls_client.HttpClientOption{
|
||||
tls_client.WithClientProfile(mappedProfile),
|
||||
tls_client.WithCookieJar(tls_client.NewCookieJar()),
|
||||
}
|
||||
|
||||
if proxy != "" {
|
||||
options = append(options, tls_client.WithProxyUrl(proxy))
|
||||
}
|
||||
|
||||
session, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create http client: %w", err)
|
||||
}
|
||||
|
||||
deviceID := uuid.New().String()
|
||||
|
||||
c := &Client{
|
||||
session: session,
|
||||
proxy: proxy,
|
||||
tag: tag,
|
||||
workerID: workerID,
|
||||
deviceID: deviceID,
|
||||
impersonate: impersonate,
|
||||
fullVersion: fullVersion,
|
||||
ua: ua,
|
||||
printMu: printMu,
|
||||
fileMu: fileMu,
|
||||
}
|
||||
|
||||
// major version for sec-ch-ua
|
||||
c.major = profile.Major
|
||||
c.secChUA = profile.SecChUA
|
||||
|
||||
// Add initial cookie
|
||||
u, _ := url.Parse(baseURL)
|
||||
cookies := []*http.Cookie{
|
||||
{
|
||||
Name: "oai-did",
|
||||
Value: deviceID,
|
||||
Domain: "chatgpt.com",
|
||||
Path: "/",
|
||||
},
|
||||
}
|
||||
session.GetCookieJar().SetCookies(u, cookies)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request) (*http.Response, error) {
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", c.ua)
|
||||
}
|
||||
if req.Header.Get("Accept") == "" {
|
||||
req.Header.Set("Accept", "*/*")
|
||||
}
|
||||
if req.Header.Get("Accept-Language") == "" {
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
}
|
||||
if req.Header.Get("sec-ch-ua") == "" {
|
||||
req.Header.Set("sec-ch-ua", c.secChUA)
|
||||
}
|
||||
if req.Header.Get("sec-ch-ua-mobile") == "" {
|
||||
req.Header.Set("sec-ch-ua-mobile", "?0")
|
||||
}
|
||||
if req.Header.Get("sec-ch-ua-platform") == "" {
|
||||
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
|
||||
}
|
||||
|
||||
return c.session.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) log(step string, status int) {
|
||||
c.printMu.Lock()
|
||||
defer c.printMu.Unlock()
|
||||
|
||||
ts := time.Now().Format("15:04:05")
|
||||
fmt.Printf("[%s] [W%d] [%s] %s | %d\n", ts, c.workerID, c.tag, step, status)
|
||||
}
|
||||
|
||||
func (c *Client) print(msg string) {
|
||||
c.printMu.Lock()
|
||||
defer c.printMu.Unlock()
|
||||
|
||||
ts := time.Now().Format("15:04:05")
|
||||
fmt.Printf("[%s] [W%d] [%s] %s\n", ts, c.workerID, c.tag, msg)
|
||||
}
|
||||
412
internal/register/flow.go
Normal file
412
internal/register/flow.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package register
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
http "github.com/bogdanfinn/fhttp"
|
||||
"github.com/verssache/chatgpt-creator/internal/email"
|
||||
"github.com/verssache/chatgpt-creator/internal/util"
|
||||
)
|
||||
|
||||
// visitHomepage visits chatgpt.com to initialize session
|
||||
func (c *Client) visitHomepage() error {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for retry := 0; retry < 3; retry++ {
|
||||
req, _ := http.NewRequest("GET", baseURL+"/", nil)
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
resp, err = c.do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.log(fmt.Sprintf("Visit Homepage (Try %d)", retry+1), resp.StatusCode)
|
||||
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 302 || resp.StatusCode == 307 {
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("failed to visit homepage after 3 retries (status: %d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
// getCSRF retrieves the CSRF token from chatgpt.com
|
||||
func (c *Client) getCSRF() (string, error) {
|
||||
req, _ := http.NewRequest("GET", baseURL+"/api/auth/csrf", nil)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Referer", baseURL+"/")
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data struct {
|
||||
CSRFToken string `json:"csrfToken"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c.log("Get CSRF", resp.StatusCode)
|
||||
if data.CSRFToken == "" {
|
||||
return "", fmt.Errorf("csrf token not found")
|
||||
}
|
||||
return data.CSRFToken, nil
|
||||
}
|
||||
|
||||
// signin initiates the signin process and returns the authorize URL
|
||||
func (c *Client) signin(email, csrf string) (string, error) {
|
||||
signinURL := baseURL + "/api/auth/signin/openai"
|
||||
params := url.Values{}
|
||||
params.Set("prompt", "login")
|
||||
params.Set("ext-oai-did", c.deviceID)
|
||||
params.Set("auth_session_logging_id", util.GenerateUUID()) // Assuming util has this or use google/uuid
|
||||
params.Set("screen_hint", "login_or_signup")
|
||||
params.Set("login_hint", email)
|
||||
|
||||
fullURL := signinURL + "?" + params.Encode()
|
||||
|
||||
formData := url.Values{}
|
||||
formData.Set("callbackUrl", baseURL+"/")
|
||||
formData.Set("csrfToken", csrf)
|
||||
formData.Set("json", "true")
|
||||
|
||||
req, _ := http.NewRequest("POST", fullURL, strings.NewReader(formData.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Referer", baseURL+"/")
|
||||
req.Header.Set("Origin", baseURL)
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
c.log("Signin", resp.StatusCode)
|
||||
if data.URL == "" {
|
||||
return "", fmt.Errorf("authorize url not found")
|
||||
}
|
||||
return data.URL, nil
|
||||
}
|
||||
|
||||
// authorize visits the authorize URL and returns the final redirect URL
|
||||
func (c *Client) authorize(authURL string) (string, error) {
|
||||
req, _ := http.NewRequest("GET", authURL, nil)
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
req.Header.Set("Referer", baseURL+"/")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
finalURL := resp.Request.URL.String()
|
||||
c.log("Authorize", resp.StatusCode)
|
||||
return finalURL, nil
|
||||
}
|
||||
|
||||
// register registers the user with email and password
|
||||
func (c *Client) register(email, password string) (int, map[string]interface{}, error) {
|
||||
regURL := authURL + "/api/accounts/user/register"
|
||||
payload := map[string]string{
|
||||
"username": email,
|
||||
"password": password,
|
||||
}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", regURL, strings.NewReader(string(jsonPayload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Referer", authURL+"/create-account/password")
|
||||
req.Header.Set("Origin", authURL)
|
||||
|
||||
// Add trace headers if available in util
|
||||
traceHeaders := util.MakeTraceHeaders()
|
||||
for k, v := range traceHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
c.log("Register", resp.StatusCode)
|
||||
return resp.StatusCode, data, nil
|
||||
}
|
||||
|
||||
// sendOTP sends the OTP to the user's email
|
||||
func (c *Client) sendOTP() (int, map[string]interface{}, error) {
|
||||
otpURL := authURL + "/api/accounts/email-otp/send"
|
||||
req, _ := http.NewRequest("GET", otpURL, nil)
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
req.Header.Set("Referer", authURL+"/create-account/password")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
data = map[string]interface{}{"text": string(body)}
|
||||
}
|
||||
|
||||
c.log("Send OTP", resp.StatusCode)
|
||||
return resp.StatusCode, data, nil
|
||||
}
|
||||
|
||||
// validateOTP validates the OTP code
|
||||
func (c *Client) validateOTP(code string) (int, map[string]interface{}, error) {
|
||||
valURL := authURL + "/api/accounts/email-otp/validate"
|
||||
payload := map[string]string{"code": code}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", valURL, strings.NewReader(string(jsonPayload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Referer", authURL+"/email-verification")
|
||||
req.Header.Set("Origin", authURL)
|
||||
|
||||
traceHeaders := util.MakeTraceHeaders()
|
||||
for k, v := range traceHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
c.log(fmt.Sprintf("Validate OTP [%s]", code), resp.StatusCode)
|
||||
return resp.StatusCode, data, nil
|
||||
}
|
||||
|
||||
// createAccount creates the user account with name and birthdate
|
||||
func (c *Client) createAccount(name, birthdate string) (int, map[string]interface{}, error) {
|
||||
createURL := authURL + "/api/accounts/create_account"
|
||||
payload := map[string]string{
|
||||
"name": name,
|
||||
"birthdate": birthdate,
|
||||
}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
|
||||
req, _ := http.NewRequest("POST", createURL, strings.NewReader(string(jsonPayload)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Referer", authURL+"/about-you")
|
||||
req.Header.Set("Origin", authURL)
|
||||
|
||||
traceHeaders := util.MakeTraceHeaders()
|
||||
for k, v := range traceHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
c.log("Create Account", resp.StatusCode)
|
||||
return resp.StatusCode, data, nil
|
||||
}
|
||||
|
||||
// callback handles the callback URL
|
||||
func (c *Client) callback(cbURL string) (int, map[string]interface{}, error) {
|
||||
if cbURL == "" {
|
||||
return 0, nil, fmt.Errorf("empty callback url")
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", cbURL, nil)
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
resp, err := c.do(req)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
c.log("Callback", resp.StatusCode)
|
||||
return resp.StatusCode, map[string]interface{}{"final_url": resp.Request.URL.String()}, nil
|
||||
}
|
||||
|
||||
func (c *Client) RunRegister(emailAddr, password, name, birthdate string) error {
|
||||
c.print("Starting registration flow...")
|
||||
|
||||
if err := c.visitHomepage(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.randomDelay(0.3, 0.8)
|
||||
|
||||
csrf, err := c.getCSRF()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.randomDelay(0.2, 0.5)
|
||||
|
||||
authURL, err := c.signin(emailAddr, csrf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.randomDelay(0.3, 0.8)
|
||||
|
||||
finalURL, err := c.authorize(authURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.randomDelay(0.3, 0.8)
|
||||
|
||||
u, _ := url.Parse(finalURL)
|
||||
finalPath := u.Path
|
||||
|
||||
|
||||
needOTP := false
|
||||
|
||||
if strings.Contains(finalPath, "create-account/password") {
|
||||
c.randomDelay(0.5, 1.0)
|
||||
status, data, err := c.register(emailAddr, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 {
|
||||
return fmt.Errorf("register failed (%d): %v", status, data)
|
||||
}
|
||||
c.randomDelay(0.3, 0.8)
|
||||
c.sendOTP()
|
||||
needOTP = true
|
||||
} else if strings.Contains(finalPath, "email-verification") || strings.Contains(finalPath, "email-otp") {
|
||||
c.print("Jump to OTP verification stage")
|
||||
needOTP = true
|
||||
} else if strings.Contains(finalPath, "about-you") {
|
||||
c.print("Jump to fill information stage")
|
||||
c.randomDelay(0.5, 1.0)
|
||||
status, data, err := c.createAccount(name, birthdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 {
|
||||
return fmt.Errorf("create account failed (%d): %v", status, data)
|
||||
}
|
||||
c.randomDelay(0.3, 0.5)
|
||||
|
||||
var cbURL string
|
||||
if u, ok := data["continue_url"].(string); ok {
|
||||
cbURL = u
|
||||
} else if u, ok := data["url"].(string); ok {
|
||||
cbURL = u
|
||||
} else if u, ok := data["redirect_url"].(string); ok {
|
||||
cbURL = u
|
||||
}
|
||||
c.callback(cbURL)
|
||||
return nil
|
||||
} else if strings.Contains(finalPath, "callback") || strings.Contains(finalURL, "chatgpt.com") {
|
||||
c.print("Account registration completed")
|
||||
return nil
|
||||
} else {
|
||||
c.print(fmt.Sprintf("Unknown jump: %s", finalURL))
|
||||
c.register(emailAddr, password)
|
||||
c.sendOTP()
|
||||
needOTP = true
|
||||
}
|
||||
|
||||
if needOTP {
|
||||
otpCode, err := email.GetVerificationCode(emailAddr, 20, 3*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.randomDelay(0.3, 0.8)
|
||||
status, data, err := c.validateOTP(otpCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
c.print("Verification code failed, retrying...")
|
||||
c.sendOTP()
|
||||
c.randomDelay(1.0, 2.0)
|
||||
otpCode, err = email.GetVerificationCode(emailAddr, 10, 3*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.randomDelay(0.3, 0.8)
|
||||
status, data, err = c.validateOTP(otpCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 {
|
||||
return fmt.Errorf("verification code failed after retry (%d): %v", status, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.randomDelay(0.5, 1.5)
|
||||
status, data, err := c.createAccount(name, birthdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 {
|
||||
return fmt.Errorf("create account failed (%d): %v", status, data)
|
||||
}
|
||||
|
||||
c.randomDelay(0.2, 0.5)
|
||||
var cbURL string
|
||||
if u, ok := data["continue_url"].(string); ok {
|
||||
cbURL = u
|
||||
} else if u, ok := data["url"].(string); ok {
|
||||
cbURL = u
|
||||
} else if u, ok := data["redirect_url"].(string); ok {
|
||||
cbURL = u
|
||||
}
|
||||
c.callback(cbURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) randomDelay(low, high float64) {
|
||||
delay := low + rand.Float64()*(high-low)
|
||||
time.Sleep(time.Duration(delay * float64(time.Second)))
|
||||
}
|
||||
24
internal/util/helpers.go
Normal file
24
internal/util/helpers.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// RandStr generates a random alphanumeric string of given length.
|
||||
func RandStr(length int) string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = alphanumeric[r.Intn(len(alphanumeric))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
// GenerateUUID generates a random UUID.
|
||||
func GenerateUUID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
23
internal/util/names.go
Normal file
23
internal/util/names.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
)
|
||||
|
||||
// RandomName returns a random first and last name using gofakeit.
|
||||
func RandomName() (string, string) {
|
||||
return gofakeit.FirstName(), gofakeit.LastName()
|
||||
}
|
||||
|
||||
// RandomBirthdate returns a random birthdate string in YYYY-MM-DD format from 1985-2002.
|
||||
func RandomBirthdate() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
year := r.Intn(2002-1985+1) + 1985
|
||||
month := r.Intn(12) + 1
|
||||
day := r.Intn(28) + 1
|
||||
return fmt.Sprintf("%04d-%02d-%02d", year, month, day)
|
||||
}
|
||||
40
internal/util/password.go
Normal file
40
internal/util/password.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lowerChars = "abcdefghijklmnopqrstuvwxyz"
|
||||
upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
digitChars = "0123456789"
|
||||
specialChars = "!@#$%&*"
|
||||
allChars = lowerChars + upperChars + digitChars + specialChars
|
||||
)
|
||||
|
||||
// GeneratePassword generates a random password of given length (default 14).
|
||||
// It guarantees at least 1 lowercase, 1 uppercase, 1 digit, and 1 special character.
|
||||
func GeneratePassword(length int) string {
|
||||
if length <= 0 {
|
||||
length = 14
|
||||
}
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
password := make([]byte, length)
|
||||
password[0] = lowerChars[r.Intn(len(lowerChars))]
|
||||
password[1] = upperChars[r.Intn(len(upperChars))]
|
||||
password[2] = digitChars[r.Intn(len(digitChars))]
|
||||
password[3] = specialChars[r.Intn(len(specialChars))]
|
||||
|
||||
for i := 4; i < length; i++ {
|
||||
password[i] = allChars[r.Intn(len(allChars))]
|
||||
}
|
||||
|
||||
r.Shuffle(len(password), func(i, j int) {
|
||||
password[i], password[j] = password[j], password[i]
|
||||
})
|
||||
|
||||
return string(password)
|
||||
}
|
||||
38
internal/util/trace.go
Normal file
38
internal/util/trace.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// MakeTraceHeaders generates Datadog-compatible trace headers.
|
||||
func MakeTraceHeaders() map[string]string {
|
||||
traceID := make([]byte, 16)
|
||||
rand.Read(traceID)
|
||||
spanID := make([]byte, 8)
|
||||
rand.Read(spanID)
|
||||
|
||||
traceIDHex := hex.EncodeToString(traceID)
|
||||
spanIDHex := hex.EncodeToString(spanID)
|
||||
|
||||
// traceparent: 00-{traceID}-{spanID}-01
|
||||
traceparent := fmt.Sprintf("00-%s-%s-01", traceIDHex, spanIDHex)
|
||||
|
||||
// tracestate: dd=t.dm:-1;t.tid:{first16ofTraceID};s:-1
|
||||
tracestate := fmt.Sprintf("dd=t.dm:-1;t.tid:%s;s:-1", traceIDHex[:16])
|
||||
|
||||
// x-datadog-trace-id: decimal conversion of last 16 hex chars (last 8 bytes)
|
||||
traceIDInt := new(big.Int).SetBytes(traceID[8:])
|
||||
// x-datadog-parent-id: decimal conversion of spanID
|
||||
spanIDInt := new(big.Int).SetBytes(spanID)
|
||||
|
||||
return map[string]string{
|
||||
"traceparent": traceparent,
|
||||
"tracestate": tracestate,
|
||||
"x-datadog-trace-id": traceIDInt.String(),
|
||||
"x-datadog-parent-id": spanIDInt.String(),
|
||||
"x-datadog-sampling-priority": "-1",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user