CookieRefresh will no longer crash bridge if Xenforo refuses to give a new cookie because its already logged in.
This commit is contained in:
@@ -2,8 +2,6 @@ package cookie
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -18,632 +16,452 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CookieRefreshService manages periodic cookie refreshing.
|
||||
const (
|
||||
CookieRefreshInterval = 4 * time.Hour
|
||||
CookieRetryDelay = 5 * time.Second
|
||||
MaxCookieRetryDelay = 60 * time.Second
|
||||
)
|
||||
|
||||
type CookieRefreshService struct {
|
||||
username string
|
||||
password string
|
||||
domain string
|
||||
client *http.Client
|
||||
debug bool
|
||||
jar http.CookieJar
|
||||
currentCookie string
|
||||
cookieMu sync.RWMutex
|
||||
cookieReady chan struct{}
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
debug bool
|
||||
|
||||
mu sync.RWMutex
|
||||
readyOnce sync.Once
|
||||
readyCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewCookieRefreshService initializes a new cookie service.
|
||||
func NewCookieRefreshService(username, password, domain string, debug bool) (*CookieRefreshService, error) {
|
||||
// Standard cookie jar (no additional dependencies needed)
|
||||
func NewCookieRefreshService(username, password, domain string) (*CookieRefreshService, error) {
|
||||
return NewCookieRefreshServiceWithDebug(username, password, domain, false)
|
||||
}
|
||||
|
||||
func NewCookieRefreshServiceWithDebug(username, password, domain string, debug bool) (*CookieRefreshService, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure TLS to match Python's behavior
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: false,
|
||||
// Support modern cipher suites
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
|
||||
// Configure transport to match Python's behavior
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: false,
|
||||
ForceAttemptHTTP2: true, // Force HTTP/2 like Python's aiohttp
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
tr := &http.Transport{}
|
||||
client := &http.Client{
|
||||
Jar: jar,
|
||||
Timeout: 45 * time.Second,
|
||||
Transport: transport,
|
||||
// CRITICAL: Disable automatic redirects - we handle them manually
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: tr,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return &CookieRefreshService{
|
||||
username: username,
|
||||
password: password,
|
||||
domain: domain,
|
||||
client: client,
|
||||
debug: debug,
|
||||
cookieReady: make(chan struct{}),
|
||||
stopChan: make(chan struct{}),
|
||||
username: username,
|
||||
password: password,
|
||||
domain: domain,
|
||||
client: client,
|
||||
jar: jar,
|
||||
debug: debug,
|
||||
readyCh: make(chan struct{}),
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
//
|
||||
// ---------- Public methods ----------
|
||||
//
|
||||
|
||||
func (crs *CookieRefreshService) Start() {
|
||||
crs.wg.Add(1)
|
||||
go crs.refreshLoop()
|
||||
}
|
||||
|
||||
func (crs *CookieRefreshService) Stop() {
|
||||
close(crs.stopChan)
|
||||
crs.wg.Wait()
|
||||
}
|
||||
|
||||
func (crs *CookieRefreshService) WaitForCookie() {
|
||||
<-crs.cookieReady
|
||||
}
|
||||
|
||||
func (crs *CookieRefreshService) GetCurrentCookie() string {
|
||||
crs.cookieMu.RLock()
|
||||
defer crs.cookieMu.RUnlock()
|
||||
return crs.currentCookie
|
||||
}
|
||||
|
||||
//
|
||||
// ---------- Internal core ----------
|
||||
//
|
||||
|
||||
func (crs *CookieRefreshService) refreshLoop() {
|
||||
defer crs.wg.Done()
|
||||
|
||||
log.Println("🔑 Fetching initial cookie...")
|
||||
fresh, err := crs.FetchFreshCookie()
|
||||
if err != nil {
|
||||
log.Printf("❌ Initial cookie fetch failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
crs.cookieMu.Lock()
|
||||
crs.currentCookie = fresh
|
||||
crs.cookieMu.Unlock()
|
||||
close(crs.cookieReady)
|
||||
log.Println("✅ Initial cookie acquired")
|
||||
|
||||
ticker := time.NewTicker(4 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
log.Println("🔄 Automatic cookie refresh cycle started")
|
||||
cookie, err := crs.FetchFreshCookie()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Cookie refresh failed: %v", err)
|
||||
continue
|
||||
}
|
||||
crs.cookieMu.Lock()
|
||||
crs.currentCookie = cookie
|
||||
crs.cookieMu.Unlock()
|
||||
log.Println("✅ Cookie refresh completed")
|
||||
|
||||
case <-crs.stopChan:
|
||||
func (s *CookieRefreshService) Start() {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
log.Println("⏳ Fetching initial cookie...")
|
||||
c, err := s.FetchFreshCookie()
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to obtain initial cookie: %v", err)
|
||||
s.readyOnce.Do(func() { close(s.readyCh) })
|
||||
return
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.currentCookie = c
|
||||
s.mu.Unlock()
|
||||
s.readyOnce.Do(func() { close(s.readyCh) })
|
||||
}()
|
||||
}
|
||||
|
||||
// FetchFreshCookie attempts full login until success.
|
||||
func (crs *CookieRefreshService) FetchFreshCookie() (string, error) {
|
||||
attempt := 1
|
||||
func (s *CookieRefreshService) WaitForCookie() {
|
||||
<-s.readyCh
|
||||
}
|
||||
|
||||
func (s *CookieRefreshService) Stop() {
|
||||
close(s.stopCh)
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *CookieRefreshService) GetCurrentCookie() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.currentCookie
|
||||
}
|
||||
func (s *CookieRefreshService) FetchFreshCookie() (string, error) {
|
||||
if s.debug {
|
||||
log.Println("💡 Stage: Starting FetchFreshCookie")
|
||||
}
|
||||
|
||||
attempt := 0
|
||||
delay := CookieRetryDelay
|
||||
|
||||
for {
|
||||
log.Printf("🔑 Attempting cookie fetch (attempt %d)", attempt)
|
||||
cookie, err := crs.attemptFetchCookie()
|
||||
if err == nil && strings.Contains(cookie, "xf_user=") {
|
||||
log.Printf("✅ Successfully fetched fresh cookie with xf_user (attempt %d)", attempt)
|
||||
return cookie, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Cookie fetch attempt %d failed: %v", attempt, err)
|
||||
} else {
|
||||
log.Printf("⚠️ Cookie fetch attempt %d failed: retry still missing xf_user cookie", attempt)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
attempt++
|
||||
c, err := s.attemptFetchCookie()
|
||||
if err == nil {
|
||||
if s.debug {
|
||||
log.Printf("✅ Successfully fetched fresh cookie with xf_user (attempt %d)", attempt)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
log.Printf("⚠️ Cookie fetch attempt %d failed: %v", attempt, err)
|
||||
// Exponential backoff, capped
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
if delay > MaxCookieRetryDelay {
|
||||
delay = MaxCookieRetryDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attemptFetchCookie performs one complete login cycle.
|
||||
func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
||||
baseURL := fmt.Sprintf("https://%s", crs.domain)
|
||||
|
||||
// Validate credentials
|
||||
if crs.username == "" || crs.password == "" {
|
||||
return "", fmt.Errorf("username or password is empty")
|
||||
}
|
||||
|
||||
if crs.debug {
|
||||
log.Printf("🔐 Attempting login for user: '%s' (password length: %d)", crs.username, len(crs.password))
|
||||
func (s *CookieRefreshService) attemptFetchCookie() (string, error) {
|
||||
base := fmt.Sprintf("https://%s/", s.domain)
|
||||
loginPage := fmt.Sprintf("https://%s/login", s.domain)
|
||||
loginPost := fmt.Sprintf("https://%s/login/login", s.domain)
|
||||
accountURL := fmt.Sprintf("https://%s/account/", s.domain)
|
||||
rootURL, _ := url.Parse(base)
|
||||
|
||||
// Reset redirect policy for manual control
|
||||
s.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
// don't auto-follow on login POST so we can inspect cookies first
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
// Step 1: KiwiFlare clearance
|
||||
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
||||
req, _ := http.NewRequest("GET", baseURL+"/", nil)
|
||||
req.Header.Set("User-Agent", randomUserAgent())
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
start := time.Now()
|
||||
resp, err := crs.client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("initial GET failed: %w", err)
|
||||
// --- Step 1: KiwiFlare
|
||||
if s.debug {
|
||||
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if crs.debug {
|
||||
log.Printf("📩 [HTTP GET] %s/ -> %d %s", baseURL, resp.StatusCode, resp.Status)
|
||||
for k, v := range resp.Header {
|
||||
for _, val := range v {
|
||||
log.Printf(" ← %s: %s", k, val)
|
||||
}
|
||||
}
|
||||
if err := s.solveKiwiFlareIfPresent(base); err != nil {
|
||||
return "", fmt.Errorf("KiwiFlare solve failed: %w", err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("⏱️ KiwiFlare challenge page loaded in %v", time.Since(start))
|
||||
log.Printf("📄 Body length: %d bytes", len(body))
|
||||
|
||||
if strings.Contains(string(body), "data-sssg-challenge") {
|
||||
auth, err := crs.solveKiwiFlare(body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("KiwiFlare solve error: %w", err)
|
||||
}
|
||||
log.Printf("✅ KiwiFlare clearance cookie confirmed: %s...", trimLong(auth, 10))
|
||||
if s.debug {
|
||||
log.Println("✅ KiwiFlare challenge solved")
|
||||
log.Println("⏳ Waiting 2 seconds for cookie propagation...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Step 2: Login page
|
||||
log.Println("Step 2: Fetching login page...")
|
||||
loginURL := fmt.Sprintf("%s/login", baseURL)
|
||||
req, _ = http.NewRequest("GET", loginURL, nil)
|
||||
req.Header.Set("User-Agent", randomUserAgent())
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
resp, err = crs.client.Do(req)
|
||||
// --- Step 2: GET /login ---
|
||||
if s.debug {
|
||||
log.Println("Step 2: Fetching login page...")
|
||||
}
|
||||
reqLogin, _ := http.NewRequest("GET", loginPage, nil)
|
||||
reqLogin.Header.Set("Cache-Control", "no-cache")
|
||||
reqLogin.Header.Set("Pragma", "no-cache")
|
||||
reqLogin.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
respLogin, err := s.client.Do(reqLogin)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get login page: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer respLogin.Body.Close()
|
||||
|
||||
body, _ = io.ReadAll(resp.Body)
|
||||
log.Printf("→ Using protocol for login page: %s", resp.Proto)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Step 3: Extract CSRF token
|
||||
csrfToken := extractCSRF(string(body))
|
||||
if csrfToken == "" {
|
||||
if crs.debug {
|
||||
log.Printf("🔍 HTML body (first 2000 chars):\n%s", string(body)[:min(2000, len(body))])
|
||||
}
|
||||
return "", fmt.Errorf("missing CSRF token")
|
||||
}
|
||||
log.Printf("✅ Found CSRF token: %s...", trimLong(csrfToken, 10))
|
||||
|
||||
// Step 4: POST login credentials (matching Python exactly)
|
||||
loginPost := fmt.Sprintf("%s/login/login", baseURL)
|
||||
|
||||
// Build form data exactly like Python
|
||||
data := url.Values{
|
||||
"_xfToken": {csrfToken},
|
||||
"login": {crs.username},
|
||||
"password": {crs.password},
|
||||
"remember": {"1"},
|
||||
}
|
||||
|
||||
// Add redirect URL as separate parameter (without the key from Python that was causing issues)
|
||||
data.Set("_xfRedirect", baseURL+"/")
|
||||
|
||||
formData := data.Encode()
|
||||
|
||||
if crs.debug {
|
||||
log.Printf("🔐 POST form data: %s", strings.ReplaceAll(formData, crs.password, "***"))
|
||||
bodyLogin, _ := io.ReadAll(respLogin.Body)
|
||||
if s.debug {
|
||||
log.Printf("📄 Login page HTML (first 1024 bytes):\n%s", firstN(string(bodyLogin), 1024))
|
||||
}
|
||||
|
||||
postReq, _ := http.NewRequest("POST", loginPost, strings.NewReader(formData))
|
||||
// --- Step 3: Extract CSRF---
|
||||
if s.debug {
|
||||
log.Println("Step 3: Extracting CSRF token...")
|
||||
}
|
||||
csrf := extractCSRF(string(bodyLogin))
|
||||
if csrf == "" {
|
||||
return "", fmt.Errorf("CSRF token not found in login page")
|
||||
}
|
||||
if s.debug {
|
||||
log.Printf("✅ Found CSRF token: %s...", abbreviate(csrf, 10))
|
||||
}
|
||||
|
||||
// Record if already have xf_user BEFORE login POST
|
||||
preCookies := s.jar.Cookies(rootURL)
|
||||
hadXfUserBefore := hasCookie(preCookies, "xf_user")
|
||||
|
||||
// --- Step 4: POST /login/login---
|
||||
if s.debug {
|
||||
log.Println("Step 4: Submitting login credentials...")
|
||||
logCookies("Cookies before login POST", preCookies)
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"_xfToken": {csrf},
|
||||
"login": {s.username},
|
||||
"password": {s.password},
|
||||
"_xfRedirect": {base},
|
||||
"remember": {"1"},
|
||||
}
|
||||
postReq, _ := http.NewRequest("POST", loginPost, strings.NewReader(form.Encode()))
|
||||
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
postReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(formData)))
|
||||
postReq.Header.Set("User-Agent", randomUserAgent())
|
||||
postReq.Header.Set("Referer", loginURL)
|
||||
postReq.Header.Set("Origin", baseURL)
|
||||
postReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
postReq.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
postReq.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
postReq.Header.Set("Connection", "keep-alive")
|
||||
postReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
postReq.Header.Set("Cache-Control", "max-age=0")
|
||||
|
||||
loginResp, err := crs.client.Do(postReq)
|
||||
postReq.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
postReq.Header.Set("Referer", loginPage)
|
||||
postReq.Header.Set("Origin", fmt.Sprintf("https://%s", s.domain))
|
||||
postResp, err := s.client.Do(postReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("login POST failed: %w", err)
|
||||
}
|
||||
defer loginResp.Body.Close()
|
||||
defer postResp.Body.Close()
|
||||
|
||||
log.Printf("Login POST response: %d %s (proto: %s)", loginResp.StatusCode, loginResp.Status, loginResp.Proto)
|
||||
|
||||
if crs.debug {
|
||||
log.Println("Login response headers:")
|
||||
for k, v := range loginResp.Header {
|
||||
for _, val := range v {
|
||||
log.Printf(" ← %s: %s", k, val)
|
||||
if s.debug {
|
||||
log.Printf("Login response status: %d", postResp.StatusCode)
|
||||
}
|
||||
|
||||
// XenForo often 303 when successful; 200 might still be fine (AJAX template), so we don't fail on 200 alone.
|
||||
|
||||
// small delay to let cookies propagate
|
||||
if s.debug {
|
||||
log.Println("⏳ Waiting 2 seconds for XenForo to issue cookies...")
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
postCookies := s.jar.Cookies(rootURL)
|
||||
if s.debug {
|
||||
for _, c := range postCookies {
|
||||
log.Printf("Cookie after login: %s=%s...", c.Name, abbreviate(c.Value, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for xf_user after login
|
||||
if hasCookie(postCookies, "xf_user") {
|
||||
return buildCookieString(postCookies), nil
|
||||
}
|
||||
|
||||
// ---- Success path: If we had xf_user before and still no new xf_user now,
|
||||
// try validating the existing session on /account/ and succeed if logged in.
|
||||
if hadXfUserBefore {
|
||||
if s.debug {
|
||||
log.Println("🔍 Missing xf_user after login POST but it existed before; validating current session via /account/ ...")
|
||||
}
|
||||
ok, cookieStr := s.validateSessionUsingAccount(accountURL, rootURL)
|
||||
if ok {
|
||||
if s.debug {
|
||||
log.Println("✅ /account/ shows logged-in; retaining existing session cookie")
|
||||
}
|
||||
return cookieStr, nil
|
||||
}
|
||||
if s.debug {
|
||||
log.Println("⚠️ /account/ did not confirm logged-in; proceeding with failure")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we got a redirect (successful login returns 303)
|
||||
if loginResp.StatusCode >= 300 && loginResp.StatusCode < 400 {
|
||||
location := loginResp.Header.Get("Location")
|
||||
log.Printf("✅ Login successful - got redirect to: %s", location)
|
||||
io.Copy(io.Discard, loginResp.Body)
|
||||
} else if loginResp.StatusCode == 200 {
|
||||
// Status 200 on login POST might mean:
|
||||
// 1. Failed login (shows error)
|
||||
// 2. Already logged in / session reuse
|
||||
// 3. Two-factor auth required
|
||||
bodyBytes, _ := io.ReadAll(loginResp.Body)
|
||||
snippet := string(bodyBytes)
|
||||
|
||||
// Check what kind of 200 response this is
|
||||
isLoggedIn := strings.Contains(snippet, "data-logged-in=\"true\"")
|
||||
hasError := strings.Contains(snippet, "Incorrect password") ||
|
||||
strings.Contains(snippet, "requested user") ||
|
||||
strings.Contains(snippet, "error")
|
||||
|
||||
if isLoggedIn {
|
||||
log.Println("✅ Login successful - already authenticated (200 OK with logged-in=true)")
|
||||
} else if !hasError {
|
||||
// No error message but not logged in = might be session propagation delay
|
||||
log.Println("⚠️ Login POST returned 200 without errors - checking session state...")
|
||||
} else if strings.Contains(snippet, "Incorrect password") {
|
||||
return "", fmt.Errorf("login failed: incorrect password")
|
||||
} else if strings.Contains(snippet, "requested user") {
|
||||
return "", fmt.Errorf("login failed: user not found")
|
||||
} else {
|
||||
log.Println("⚠️ Login POST returned 200 with possible error - will check cookies")
|
||||
// If not successful yet, read body for context & fail
|
||||
bodyBytes, _ := io.ReadAll(postResp.Body)
|
||||
bodyText := string(bodyBytes)
|
||||
if s.debug {
|
||||
log.Printf("📄 Login HTML snippet (first 500 chars):\n%s", firstN(bodyText, 500))
|
||||
}
|
||||
return "", fmt.Errorf("retry still missing xf_user cookie")
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// KiwiFlare handling
|
||||
// -------------------------------------------
|
||||
func (s *CookieRefreshService) solveKiwiFlareIfPresent(base string) error {
|
||||
req, _ := http.NewRequest("GET", base, nil)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
html := string(body)
|
||||
|
||||
// Look for data-sssg-challenge and difficulty
|
||||
re := regexp.MustCompile(`data-sssg-challenge=["']([0-9a-fA-F]+)["'][^>]*data-sssg-difficulty=["'](\d+)["']`)
|
||||
m := re.FindStringSubmatch(html)
|
||||
if len(m) < 3 {
|
||||
if s.debug {
|
||||
log.Println("No KiwiFlare POW detected")
|
||||
}
|
||||
|
||||
if crs.debug && len(snippet) > 1000 {
|
||||
snippet = snippet[:1000]
|
||||
}
|
||||
if crs.debug {
|
||||
log.Printf("🧩 Login 200 body snippet:\n%s", snippet)
|
||||
}
|
||||
} else {
|
||||
// Other status codes
|
||||
io.Copy(io.Discard, loginResp.Body)
|
||||
log.Printf("⚠️ Unexpected login response: %d %s", loginResp.StatusCode, loginResp.Status)
|
||||
return nil
|
||||
}
|
||||
token := m[1]
|
||||
diff, _ := strconv.Atoi(m[2])
|
||||
|
||||
if s.debug {
|
||||
log.Printf("Solving KiwiFlare challenge (difficulty=%d, token=%s...)", diff, abbreviate(token, 10))
|
||||
}
|
||||
nonce, dur, err := s.solvePoW(token, diff)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.debug {
|
||||
log.Printf("✅ KiwiFlare challenge solved in %v (nonce=%s)", dur, nonce)
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second) // Increased from 2s - give server more time
|
||||
// Submit solution
|
||||
answerURL := fmt.Sprintf("https://%s/.sssg/api/answer", s.domain)
|
||||
form := url.Values{"a": {token}, "b": {nonce}}
|
||||
subReq, _ := http.NewRequest("POST", answerURL, strings.NewReader(form.Encode()))
|
||||
subReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
subReq.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
subResp, err := s.client.Do(subReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer subResp.Body.Close()
|
||||
|
||||
// Step 5: Manually extract Set-Cookie headers from POST response
|
||||
// This is critical - Go's cookie jar might not automatically process
|
||||
// Set-Cookie headers when we disable redirects
|
||||
log.Println("Step 5: Extracting cookies from POST response headers...")
|
||||
if setCookies := loginResp.Header["Set-Cookie"]; len(setCookies) > 0 {
|
||||
for _, sc := range setCookies {
|
||||
log.Printf("📩 Set-Cookie header: %s", trimLong(sc, 80))
|
||||
// Parse and add to jar manually if needed
|
||||
// The jar should do this automatically, but let's be explicit
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Check cookies in jar
|
||||
cookieURL, _ := url.Parse(baseURL)
|
||||
cookies := crs.client.Jar.Cookies(cookieURL)
|
||||
hasXfUser := false
|
||||
hasXfSession := false
|
||||
for _, c := range cookies {
|
||||
log.Printf("🍪 [After Login POST] %s=%s", c.Name, trimLong(c.Value, 10))
|
||||
if c.Name == "xf_user" {
|
||||
hasXfUser = true
|
||||
}
|
||||
if c.Name == "xf_session" {
|
||||
hasXfSession = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we have xf_session but not xf_user, login succeeded but we need to trigger xf_user
|
||||
if hasXfSession && !hasXfUser {
|
||||
log.Println("✅ Login successful (have xf_session) - will trigger xf_user cookie")
|
||||
} else if !hasXfSession {
|
||||
log.Println("❌ Login may have failed - no xf_session cookie present")
|
||||
return "", fmt.Errorf("login failed - no session cookie received")
|
||||
if subResp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(subResp.Body)
|
||||
return fmt.Errorf("challenge solve HTTP %d (%s)", subResp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
// Step 6: CRITICAL - Manually follow redirects if xf_user missing (Python does this automatically)
|
||||
maxRedirects := 5
|
||||
redirectCount := 0
|
||||
currentURL := baseURL + "/"
|
||||
|
||||
for !hasXfUser && redirectCount < maxRedirects {
|
||||
redirectCount++
|
||||
log.Printf("🧭 Following redirect manually (attempt %d) to capture xf_user...", redirectCount)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
followReq, _ := http.NewRequest("GET", currentURL, nil)
|
||||
followReq.Header.Set("User-Agent", randomUserAgent())
|
||||
followReq.Header.Set("Referer", loginPost)
|
||||
followReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
followReq.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
followReq.Header.Set("Connection", "keep-alive")
|
||||
followReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
followResp, ferr := crs.client.Do(followReq)
|
||||
if ferr != nil {
|
||||
log.Printf("⚠️ Redirect follow failed: %v", ferr)
|
||||
// Check jar for sssg_clearance
|
||||
rootURL, _ := url.Parse(fmt.Sprintf("https://%s/", s.domain))
|
||||
for _, c := range s.jar.Cookies(rootURL) {
|
||||
if c.Name == "sssg_clearance" {
|
||||
if s.debug {
|
||||
log.Printf("✅ KiwiFlare clearance cookie confirmed: %s...", abbreviate(c.Value, 10))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("📩 [HTTP GET] %s -> %s (proto: %s)", currentURL, followResp.Status, followResp.Proto)
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for additional redirects
|
||||
if followResp.StatusCode >= 300 && followResp.StatusCode < 400 {
|
||||
location := followResp.Header.Get("Location")
|
||||
if location != "" {
|
||||
if !strings.HasPrefix(location, "http") {
|
||||
location = baseURL + location
|
||||
}
|
||||
currentURL = location
|
||||
log.Printf("🔄 Server returned redirect to: %s", currentURL)
|
||||
}
|
||||
}
|
||||
func (s *CookieRefreshService) solvePoW(token string, difficulty int) (string, time.Duration, error) {
|
||||
start := time.Now()
|
||||
nonce := rand.Int63()
|
||||
requiredBytes := difficulty / 8
|
||||
requiredBits := difficulty % 8
|
||||
const maxAttempts = 10_000_000
|
||||
|
||||
io.Copy(io.Discard, followResp.Body)
|
||||
followResp.Body.Close()
|
||||
time.Sleep(1 * time.Second)
|
||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||||
nonce++
|
||||
input := token + fmt.Sprintf("%d", nonce)
|
||||
sum := sha256.Sum256([]byte(input))
|
||||
|
||||
// Check cookies again
|
||||
cookies = crs.client.Jar.Cookies(cookieURL)
|
||||
for _, c := range cookies {
|
||||
log.Printf("🍪 [After Redirect %d] %s=%s", redirectCount, c.Name, trimLong(c.Value, 10))
|
||||
if c.Name == "xf_user" {
|
||||
hasXfUser = true
|
||||
// Check leading zero bits
|
||||
ok := true
|
||||
for i := 0; i < requiredBytes; i++ {
|
||||
if sum[i] != 0 {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasXfUser {
|
||||
log.Println("✅ xf_user cookie acquired after redirect follow")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7: Secondary check — trigger /account/ to issue xf_user if still missing
|
||||
// CRITICAL: For rooms that don't require auth to view, we need to force
|
||||
// session validation by accessing an authenticated endpoint
|
||||
if !hasXfUser {
|
||||
log.Println("🧭 Performing secondary authenticated fetch to /account/ to trigger xf_user...")
|
||||
time.Sleep(2 * time.Second) // Increased wait time for session propagation
|
||||
|
||||
accountReq, _ := http.NewRequest("GET", baseURL+"/account/", nil)
|
||||
accountReq.Header.Set("User-Agent", randomUserAgent())
|
||||
accountReq.Header.Set("Referer", loginPost)
|
||||
accountReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
accountReq.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
accountReq.Header.Set("Connection", "keep-alive")
|
||||
accountReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
accountResp, accErr := crs.client.Do(accountReq)
|
||||
if accErr != nil {
|
||||
log.Printf("⚠️ Account fetch failed: %v", accErr)
|
||||
} else {
|
||||
io.Copy(io.Discard, accountResp.Body)
|
||||
accountResp.Body.Close()
|
||||
log.Printf("📩 [HTTP GET] %s/account/ -> %s", baseURL, accountResp.Status)
|
||||
}
|
||||
time.Sleep(2 * time.Second) // Additional wait after /account/ fetch
|
||||
|
||||
cookies = crs.client.Jar.Cookies(cookieURL)
|
||||
for _, c := range cookies {
|
||||
log.Printf("🍪 [After /account/] %s=%s", c.Name, trimLong(c.Value, 10))
|
||||
if c.Name == "xf_user" {
|
||||
hasXfUser = true
|
||||
if ok && requiredBits > 0 && requiredBytes < len(sum) {
|
||||
mask := byte(0xFF << (8 - requiredBits))
|
||||
if sum[requiredBytes]&mask != 0 {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if hasXfUser {
|
||||
log.Println("✅ xf_user cookie acquired after /account/ fetch")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: FINAL ATTEMPT - Try accessing the actual forum to force session validation
|
||||
// This is critical for non-auth-required rooms like room 16
|
||||
if !hasXfUser {
|
||||
log.Println("🧭 Final attempt: accessing forum index to force session validation...")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
forumReq, _ := http.NewRequest("GET", baseURL+"/forums/", nil)
|
||||
forumReq.Header.Set("User-Agent", randomUserAgent())
|
||||
forumReq.Header.Set("Referer", baseURL)
|
||||
forumReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
forumReq.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
forumReq.Header.Set("Connection", "keep-alive")
|
||||
forumReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
forumResp, forumErr := crs.client.Do(forumReq)
|
||||
if forumErr != nil {
|
||||
log.Printf("⚠️ Forum fetch failed: %v", forumErr)
|
||||
} else {
|
||||
bodyBytes, _ := io.ReadAll(forumResp.Body)
|
||||
forumResp.Body.Close()
|
||||
log.Printf("📩 [HTTP GET] %s/forums/ -> %s", baseURL, forumResp.Status)
|
||||
|
||||
// Check if we're actually logged in by looking for username in page
|
||||
if strings.Contains(string(bodyBytes), crs.username) {
|
||||
log.Println("✅ Confirmed logged in - username found in forum page")
|
||||
if ok {
|
||||
elapsed := time.Since(start)
|
||||
// Stretch to >= ~1.7s to look human
|
||||
if elapsed < 1700*time.Millisecond {
|
||||
time.Sleep(1700*time.Millisecond - elapsed)
|
||||
elapsed = 1700 * time.Millisecond
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
cookies = crs.client.Jar.Cookies(cookieURL)
|
||||
for _, c := range cookies {
|
||||
log.Printf("🍪 [After /forums/] %s=%s", c.Name, trimLong(c.Value, 10))
|
||||
if c.Name == "xf_user" {
|
||||
hasXfUser = true
|
||||
}
|
||||
}
|
||||
if hasXfUser {
|
||||
log.Println("✅ xf_user cookie acquired after forum page fetch")
|
||||
} else {
|
||||
log.Println("⚠️ xf_user cookie still missing after all follow-ups")
|
||||
|
||||
// Don't return error - build cookie string with what we have
|
||||
// The xf_session and xf_csrf might be enough for websocket auth
|
||||
log.Println("⚠️ Proceeding with available cookies (xf_session + xf_csrf)")
|
||||
return fmt.Sprintf("%d", nonce), elapsed, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Build final cookie string
|
||||
var parts []string
|
||||
for _, c := range cookies {
|
||||
if strings.HasPrefix(c.Name, "xf_") || c.Name == "sssg_clearance" {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", c.Name, c.Value))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "; "), nil
|
||||
return "", 0, fmt.Errorf("failed to solve PoW within %d attempts", maxAttempts)
|
||||
}
|
||||
|
||||
//
|
||||
// ---------- Utilities ----------
|
||||
//
|
||||
|
||||
func extractCSRF(body string) string {
|
||||
reList := []*regexp.Regexp{
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`data-csrf=["']([^"']+)["']`),
|
||||
regexp.MustCompile(`"csrf":"([^"]+)"`),
|
||||
regexp.MustCompile(`XF\.config\.csrf\s*=\s*"([^"]+)"`),
|
||||
}
|
||||
for _, re := range reList {
|
||||
if m := re.FindStringSubmatch(body); len(m) > 1 {
|
||||
for _, re := range patterns {
|
||||
if m := re.FindStringSubmatch(body); len(m) >= 2 {
|
||||
return m[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func trimLong(s string, n int) string {
|
||||
if len(s) > n {
|
||||
return s[:n]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func randomUserAgent() string {
|
||||
uas := []string{
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
}
|
||||
return uas[rand.Intn(len(uas))]
|
||||
}
|
||||
|
||||
//
|
||||
// ---------- KiwiFlare Solver ----------
|
||||
//
|
||||
|
||||
func (crs *CookieRefreshService) solveKiwiFlare(body []byte) (string, error) {
|
||||
html := string(body)
|
||||
challenge := regexp.MustCompile(`data-sssg-challenge=["']([^"']+)["']`).FindStringSubmatch(html)
|
||||
difficultyStr := regexp.MustCompile(`data-sssg-difficulty=["'](\d+)["']`).FindStringSubmatch(html)
|
||||
if len(challenge) < 2 || len(difficultyStr) < 2 {
|
||||
return "", fmt.Errorf("missing challenge or difficulty")
|
||||
}
|
||||
salt := challenge[1]
|
||||
diff, _ := strconv.Atoi(difficultyStr[1])
|
||||
log.Printf("Solving KiwiFlare challenge (difficulty=%d)", diff)
|
||||
|
||||
nonce := rand.Int63()
|
||||
start := time.Now()
|
||||
for {
|
||||
nonce++
|
||||
input := fmt.Sprintf("%s%d", salt, nonce)
|
||||
hash := sha256.Sum256([]byte(input))
|
||||
|
||||
fullBytes := diff / 8
|
||||
remainder := diff % 8
|
||||
valid := true
|
||||
for i := 0; i < fullBytes; i++ {
|
||||
if hash[i] != 0 {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if valid && remainder > 0 && fullBytes < len(hash) {
|
||||
mask := byte(0xFF << (8 - remainder))
|
||||
if hash[fullBytes]&mask != 0 {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
log.Printf("✅ Solved KiwiFlare PoW: salt=%s nonce=%d difficulty=%d", trimLong(salt, 16), nonce, diff)
|
||||
elapsed := time.Since(start)
|
||||
if elapsed < 2*time.Second {
|
||||
time.Sleep(2*time.Second - elapsed)
|
||||
}
|
||||
break
|
||||
func hasCookie(cookies []*http.Cookie, name string) bool {
|
||||
for _, c := range cookies {
|
||||
if c.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
submit := fmt.Sprintf("https://%s/.sssg/api/answer", crs.domain)
|
||||
form := url.Values{"a": {salt}, "b": {fmt.Sprintf("%d", nonce)}}
|
||||
req, _ := http.NewRequest("POST", submit, strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", randomUserAgent())
|
||||
func buildCookieString(cookies []*http.Cookie) string {
|
||||
want := map[string]bool{
|
||||
"sssg_clearance": true,
|
||||
"xf_csrf": true,
|
||||
"xf_session": true,
|
||||
"xf_user": true,
|
||||
"xf_toggle": true,
|
||||
}
|
||||
var parts []string
|
||||
for _, c := range cookies {
|
||||
if want[c.Name] {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", c.Name, c.Value))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
resp, err := crs.client.Do(req)
|
||||
func (s *CookieRefreshService) validateSessionUsingAccount(accountURL string, rootURL *url.URL) (bool, string) {
|
||||
if s.debug {
|
||||
log.Println("🔍 Validating session via /account/ ...")
|
||||
}
|
||||
req, _ := http.NewRequest("GET", accountURL, nil)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if s.debug {
|
||||
log.Printf("⚠️ /account/ request error: %v", err)
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyResp, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var parsed map[string]any
|
||||
_ = json.Unmarshal(bodyResp, &parsed)
|
||||
if auth, ok := parsed["auth"].(string); ok {
|
||||
return auth, nil
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
snippet := firstN(string(body), 500)
|
||||
|
||||
if s.debug {
|
||||
log.Printf("🔍 Accessed /account/ (%d)", resp.StatusCode)
|
||||
log.Printf("📄 /account/ HTML snippet:\n%s", snippet)
|
||||
for _, c := range s.jar.Cookies(rootURL) {
|
||||
log.Printf("🍪 Cookie after /account/: %s=%s...", c.Name, abbreviate(c.Value, 10))
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no auth field in KiwiFlare response")
|
||||
}
|
||||
|
||||
// Consider logged-in if data-logged-in="true" or the template isn't "login"
|
||||
if strings.Contains(snippet, `data-logged-in="true"`) ||
|
||||
(!strings.Contains(snippet, `data-template="login"`) && resp.StatusCode == 200) {
|
||||
return true, buildCookieString(s.jar.Cookies(rootURL))
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func logCookies(prefix string, cookies []*http.Cookie) {
|
||||
log.Printf("%s (%d):", prefix, len(cookies))
|
||||
for _, c := range cookies {
|
||||
log.Printf(" - %s = %s...", c.Name, abbreviate(c.Value, 10))
|
||||
}
|
||||
}
|
||||
|
||||
func firstN(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
func abbreviate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
52
main.go
52
main.go
@@ -1,11 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -23,62 +21,20 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.Load(envFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Parse --debug flag from CLI (overrides .env)
|
||||
debug := false
|
||||
for _, a := range os.Args {
|
||||
if a == "--debug" {
|
||||
debug = true
|
||||
}
|
||||
}
|
||||
cfg.Debug = debug
|
||||
|
||||
// ---- File logging (env-driven) ------------------------------------------
|
||||
// Use LOG_FILE or BRIDGE_LOG_FILE (first non-empty wins)
|
||||
logPath := os.Getenv("LOG_FILE")
|
||||
if logPath == "" {
|
||||
logPath = os.Getenv("BRIDGE_LOG_FILE")
|
||||
}
|
||||
if logPath != "" {
|
||||
// Ensure parent dir exists
|
||||
if dir := filepath.Dir(logPath); dir != "" && dir != "." {
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
f, ferr := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if ferr != nil {
|
||||
log.Printf("⚠️ Failed to open log file '%s' (%v). Continuing with stdout only.", logPath, ferr)
|
||||
} else {
|
||||
// Tee logs to both stdout and file
|
||||
log.SetOutput(io.MultiWriter(os.Stdout, f))
|
||||
// microseconds for tighter timing on auth/debug traces
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
log.Printf("📝 File logging enabled: %s", logPath)
|
||||
}
|
||||
} else {
|
||||
// keep default stdout with microseconds for consistency
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
log.Printf("Using .env file: %s", envFile)
|
||||
log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID)
|
||||
log.Printf("Bridge username: %s", cfg.BridgeUsername)
|
||||
if cfg.Debug {
|
||||
log.Println("🪲 Debug mode enabled — full HTTP and cookie trace logging active")
|
||||
}
|
||||
|
||||
// Cookie service (HTTP/1.1/2, KiwiFlare PoW, CSRF, deep debug)
|
||||
cookieSvc, err := cookie.NewCookieRefreshService(cfg.BridgeUsername, cfg.BridgePassword, "kiwifarms.st", cfg.Debug)
|
||||
// Cookie service
|
||||
cookieSvc, err := cookie.NewCookieRefreshService(cfg.BridgeUsername, cfg.BridgePassword, "kiwifarms.st")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create cookie service: %v", err)
|
||||
}
|
||||
cookieSvc.Start()
|
||||
log.Println("⏳ Waiting for initial cookie...")
|
||||
cookieSvc.WaitForCookie()
|
||||
if cookieSvc.GetCurrentCookie() == "" {
|
||||
log.Fatal("❌ Failed to obtain initial cookie, cannot start bridge")
|
||||
@@ -87,7 +43,7 @@ func main() {
|
||||
// Sneedchat client
|
||||
sneedClient := sneed.NewClient(cfg.SneedchatRoomID, cookieSvc)
|
||||
|
||||
// Discord bridge (full parity features)
|
||||
// Discord bridge
|
||||
bridge, err := discord.NewBridge(cfg, sneedClient)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Discord bridge: %v", err)
|
||||
@@ -97,7 +53,7 @@ func main() {
|
||||
}
|
||||
log.Println("🌉 Discord-Sneedchat Bridge started successfully")
|
||||
|
||||
// Auto cookie refresh every 4h (in addition to background loop inside service)
|
||||
// Background 4h refresh loop
|
||||
go func() {
|
||||
t := time.NewTicker(4 * time.Hour)
|
||||
defer t.Stop()
|
||||
|
||||
Reference in New Issue
Block a user