Fetcher mimicks Python libraries behavior for more reliabilty. Removed inline comments from env.example since Go hates it.
This commit is contained in:
21
.env.example
21
.env.example
@@ -2,13 +2,18 @@
|
|||||||
DISCORD_BOT_TOKEN=your_discord_bot_token_here
|
DISCORD_BOT_TOKEN=your_discord_bot_token_here
|
||||||
DISCORD_CHANNEL_ID=your_discord_channel_id_here
|
DISCORD_CHANNEL_ID=your_discord_channel_id_here
|
||||||
DISCORD_GUILD_ID=your_discord_guild_id_here
|
DISCORD_GUILD_ID=your_discord_guild_id_here
|
||||||
DISCORD_PING_USER_ID=your_discord_user_id_here # Get this by right-clicking your Discord profile
|
# Get this by right-clicking your Discord profile
|
||||||
|
DISCORD_PING_USER_ID=your_discord_user_id_here
|
||||||
DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
|
DISCORD_WEBHOOK_URL=your_discord_webhook_url_here
|
||||||
RECONNECT_INTERVAL=5 # Interval between reconnect attempts if connection is lost
|
# Interval between reconnect attempts if connection is lost
|
||||||
SNEEDCHAT_ROOM_ID=1 # Which room will be bridged, append integer at the end of room name. Current options: general.1, gunt.8, keno-kasino.15, fishtank.16, beauty-parlor.18, sports.19,
|
RECONNECT_INTERVAL=5
|
||||||
ENABLE_FILE_LOGGING=false # Enable logging to bridge.log file (true/false, default: false)
|
# Which room will be bridged, append integer at the end of room name. Current options: general.1, gunt.8, keno-kasino.15, fishtank.16, beauty-parlor.18, sports.19,
|
||||||
|
SNEEDCHAT_ROOM_ID=1
|
||||||
|
# Enable logging to bridge.log file for debugging purposes(true/false, default: false)
|
||||||
|
ENABLE_FILE_LOGGING=false
|
||||||
|
|
||||||
# Optional: Prevent echo loops by filtering messages from the bridge bot
|
#Use your Kiwifarm crendeitals for here
|
||||||
# BRIDGE_USER_ID=123456 # Numeric user ID of your bridge bot account on Sneedchat
|
#This USER_ID number is in the url when you go to your profile, its required to prevent Discord from echoing your own messages back to you and to allow pings/push notifications work on Discord
|
||||||
# BRIDGE_USERNAME=YourBridgeBot # Username of your bridge bot account on Sneedchat
|
# BRIDGE_USER_ID=123456
|
||||||
# BRIDGE_PASSWORD=Password # Password of your account
|
# BRIDGE_USERNAME=YourBridgeBot
|
||||||
|
# BRIDGE_PASSWORD=Password
|
||||||
@@ -2,6 +2,7 @@ package cookie
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,14 +34,43 @@ type CookieRefreshService struct {
|
|||||||
|
|
||||||
// NewCookieRefreshService initializes a new cookie service.
|
// NewCookieRefreshService initializes a new cookie service.
|
||||||
func NewCookieRefreshService(username, password, domain string, debug bool) (*CookieRefreshService, error) {
|
func NewCookieRefreshService(username, password, domain string, debug bool) (*CookieRefreshService, error) {
|
||||||
|
// Standard cookie jar (no additional dependencies needed)
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
Timeout: 45 * time.Second,
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CookieRefreshService{
|
return &CookieRefreshService{
|
||||||
@@ -146,11 +176,24 @@ func (crs *CookieRefreshService) FetchFreshCookie() (string, error) {
|
|||||||
// attemptFetchCookie performs one complete login cycle.
|
// attemptFetchCookie performs one complete login cycle.
|
||||||
func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
||||||
baseURL := fmt.Sprintf("https://%s", crs.domain)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: KiwiFlare clearance
|
// Step 1: KiwiFlare clearance
|
||||||
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
||||||
req, _ := http.NewRequest("GET", baseURL+"/", nil)
|
req, _ := http.NewRequest("GET", baseURL+"/", nil)
|
||||||
req.Header.Set("User-Agent", randomUserAgent())
|
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()
|
start := time.Now()
|
||||||
resp, err := crs.client.Do(req)
|
resp, err := crs.client.Do(req)
|
||||||
@@ -188,6 +231,11 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
|||||||
loginURL := fmt.Sprintf("%s/login", baseURL)
|
loginURL := fmt.Sprintf("%s/login", baseURL)
|
||||||
req, _ = http.NewRequest("GET", loginURL, nil)
|
req, _ = http.NewRequest("GET", loginURL, nil)
|
||||||
req.Header.Set("User-Agent", randomUserAgent())
|
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")
|
||||||
|
|
||||||
resp, err = crs.client.Do(req)
|
resp, err = crs.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get login page: %w", err)
|
return "", fmt.Errorf("failed to get login page: %w", err)
|
||||||
@@ -201,30 +249,45 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
|||||||
// Step 3: Extract CSRF token
|
// Step 3: Extract CSRF token
|
||||||
csrfToken := extractCSRF(string(body))
|
csrfToken := extractCSRF(string(body))
|
||||||
if csrfToken == "" {
|
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")
|
return "", fmt.Errorf("missing CSRF token")
|
||||||
}
|
}
|
||||||
log.Printf("✅ Found CSRF token: %s...", trimLong(csrfToken, 10))
|
log.Printf("✅ Found CSRF token: %s...", trimLong(csrfToken, 10))
|
||||||
|
|
||||||
// Step 4: POST login credentials (full browser headers + both redirect fields)
|
// Step 4: POST login credentials (matching Python exactly)
|
||||||
loginPost := fmt.Sprintf("%s/login/login", baseURL)
|
loginPost := fmt.Sprintf("%s/login/login", baseURL)
|
||||||
|
|
||||||
|
// Build form data exactly like Python
|
||||||
data := url.Values{
|
data := url.Values{
|
||||||
"_xfToken": {csrfToken},
|
"_xfToken": {csrfToken},
|
||||||
"login": {crs.username},
|
"login": {crs.username},
|
||||||
"password": {crs.password},
|
"password": {crs.password},
|
||||||
"remember": {"1"},
|
"remember": {"1"},
|
||||||
"redirect": {"/"},
|
}
|
||||||
"_xfRedirect": {baseURL + "/"},
|
|
||||||
|
// 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, "***"))
|
||||||
}
|
}
|
||||||
|
|
||||||
postReq, _ := http.NewRequest("POST", loginPost, strings.NewReader(data.Encode()))
|
postReq, _ := http.NewRequest("POST", loginPost, strings.NewReader(formData))
|
||||||
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
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("User-Agent", randomUserAgent())
|
||||||
postReq.Header.Set("Referer", loginURL)
|
postReq.Header.Set("Referer", loginURL)
|
||||||
postReq.Header.Set("Origin", baseURL)
|
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", "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-Language", "en-US,en;q=0.5")
|
||||||
|
postReq.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
postReq.Header.Set("Connection", "keep-alive")
|
postReq.Header.Set("Connection", "keep-alive")
|
||||||
postReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
postReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||||
|
postReq.Header.Set("Cache-Control", "max-age=0")
|
||||||
|
|
||||||
loginResp, err := crs.client.Do(postReq)
|
loginResp, err := crs.client.Do(postReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,84 +295,180 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
|||||||
}
|
}
|
||||||
defer loginResp.Body.Close()
|
defer loginResp.Body.Close()
|
||||||
|
|
||||||
log.Printf("Login response status: %d", loginResp.StatusCode)
|
log.Printf("Login POST response: %d %s (proto: %s)", loginResp.StatusCode, loginResp.Status, loginResp.Proto)
|
||||||
|
|
||||||
// 🧩 Diagnostic: if status 200, dump first KB of body for debugging
|
if crs.debug {
|
||||||
if loginResp.StatusCode == 200 {
|
log.Println("Login response headers:")
|
||||||
bodyBytes, _ := io.ReadAll(loginResp.Body)
|
for k, v := range loginResp.Header {
|
||||||
snippet := string(bodyBytes)
|
for _, val := range v {
|
||||||
if len(snippet) > 1000 {
|
log.Printf(" ← %s: %s", k, val)
|
||||||
snippet = snippet[:1000]
|
}
|
||||||
}
|
}
|
||||||
log.Printf("🧩 Login 200 body snippet:\n%s", snippet)
|
|
||||||
// Recreate reader for downstream reuse
|
|
||||||
loginResp.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
// 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 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)
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: Check cookies in jar
|
time.Sleep(3 * time.Second) // Increased from 2s - give server more time
|
||||||
|
|
||||||
|
// 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)
|
cookieURL, _ := url.Parse(baseURL)
|
||||||
cookies := crs.client.Jar.Cookies(cookieURL)
|
cookies := crs.client.Jar.Cookies(cookieURL)
|
||||||
hasXfUser := false
|
hasXfUser := false
|
||||||
|
hasXfSession := false
|
||||||
for _, c := range cookies {
|
for _, c := range cookies {
|
||||||
log.Printf("🍪 [After Login POST] %s=%s", c.Name, trimLong(c.Value, 10))
|
log.Printf("🍪 [After Login POST] %s=%s", c.Name, trimLong(c.Value, 10))
|
||||||
if c.Name == "xf_user" {
|
if c.Name == "xf_user" {
|
||||||
hasXfUser = true
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔁 Follow redirect manually if missing xf_user
|
// Step 6: CRITICAL - Manually follow redirects if xf_user missing (Python does this automatically)
|
||||||
if !hasXfUser {
|
maxRedirects := 5
|
||||||
log.Println("🧭 Following post-login redirect manually to capture xf_user...")
|
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)
|
time.Sleep(1 * time.Second)
|
||||||
followReq, _ := http.NewRequest("GET", baseURL+"/", nil)
|
|
||||||
|
followReq, _ := http.NewRequest("GET", currentURL, nil)
|
||||||
followReq.Header.Set("User-Agent", randomUserAgent())
|
followReq.Header.Set("User-Agent", randomUserAgent())
|
||||||
followReq.Header.Set("Referer", baseURL+"/login")
|
followReq.Header.Set("Referer", loginPost)
|
||||||
followReq.Header.Set("Origin", baseURL)
|
|
||||||
followReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
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("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)
|
followResp, ferr := crs.client.Do(followReq)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
log.Printf("⚠️ Redirect follow failed: %v", ferr)
|
log.Printf("⚠️ Redirect follow failed: %v", ferr)
|
||||||
} else {
|
break
|
||||||
followResp.Body.Close()
|
|
||||||
log.Printf("📩 [HTTP GET] %s/ -> %s", baseURL, followResp.Status)
|
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
cookies = crs.client.Jar.Cookies(cookieURL)
|
log.Printf("📩 [HTTP GET] %s -> %s (proto: %s)", currentURL, followResp.Status, followResp.Proto)
|
||||||
for _, c := range cookies {
|
|
||||||
log.Printf("🍪 [After Redirect] %s=%s", c.Name, trimLong(c.Value, 10))
|
// Check for additional redirects
|
||||||
if c.Name == "xf_user" {
|
if followResp.StatusCode >= 300 && followResp.StatusCode < 400 {
|
||||||
hasXfUser = true
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
io.Copy(io.Discard, followResp.Body)
|
||||||
|
followResp.Body.Close()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if hasXfUser {
|
if hasXfUser {
|
||||||
log.Println("✅ xf_user cookie acquired after redirect follow")
|
log.Println("✅ xf_user cookie acquired after redirect follow")
|
||||||
} else {
|
break
|
||||||
log.Println("⚠️ xf_user cookie still missing after redirect follow")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧭 Secondary check — trigger /account/ to issue xf_user if still missing
|
// 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 {
|
if !hasXfUser {
|
||||||
log.Println("🧭 Performing secondary authenticated fetch to /account/ to trigger xf_user...")
|
log.Println("🧭 Performing secondary authenticated fetch to /account/ to trigger xf_user...")
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(2 * time.Second) // Increased wait time for session propagation
|
||||||
|
|
||||||
accountReq, _ := http.NewRequest("GET", baseURL+"/account/", nil)
|
accountReq, _ := http.NewRequest("GET", baseURL+"/account/", nil)
|
||||||
accountReq.Header.Set("User-Agent", randomUserAgent())
|
accountReq.Header.Set("User-Agent", randomUserAgent())
|
||||||
accountReq.Header.Set("Referer", baseURL+"/login")
|
accountReq.Header.Set("Referer", loginPost)
|
||||||
accountReq.Header.Set("Origin", baseURL)
|
|
||||||
accountReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
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("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)
|
accountResp, accErr := crs.client.Do(accountReq)
|
||||||
if accErr != nil {
|
if accErr != nil {
|
||||||
log.Printf("⚠️ Account fetch failed: %v", accErr)
|
log.Printf("⚠️ Account fetch failed: %v", accErr)
|
||||||
} else {
|
} else {
|
||||||
|
io.Copy(io.Discard, accountResp.Body)
|
||||||
accountResp.Body.Close()
|
accountResp.Body.Close()
|
||||||
log.Printf("📩 [HTTP GET] %s/account/ -> %s", baseURL, accountResp.Status)
|
log.Printf("📩 [HTTP GET] %s/account/ -> %s", baseURL, accountResp.Status)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(2 * time.Second) // Additional wait after /account/ fetch
|
||||||
|
|
||||||
cookies = crs.client.Jar.Cookies(cookieURL)
|
cookies = crs.client.Jar.Cookies(cookieURL)
|
||||||
for _, c := range cookies {
|
for _, c := range cookies {
|
||||||
@@ -320,9 +479,53 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
|||||||
}
|
}
|
||||||
if hasXfUser {
|
if hasXfUser {
|
||||||
log.Println("✅ xf_user cookie acquired after /account/ fetch")
|
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 {
|
} else {
|
||||||
log.Println("⚠️ xf_user cookie still missing after /account/ fetch")
|
bodyBytes, _ := io.ReadAll(forumResp.Body)
|
||||||
return "", fmt.Errorf("xf_user still missing after all follow-ups")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,11 +564,18 @@ func trimLong(s string, n int) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func randomUserAgent() string {
|
func randomUserAgent() string {
|
||||||
uas := []string{
|
uas := []string{
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) Safari/537.36",
|
"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) 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) Safari/605.1.15",
|
"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))]
|
return uas[rand.Intn(len(uas))]
|
||||||
}
|
}
|
||||||
@@ -417,7 +627,7 @@ func (crs *CookieRefreshService) solveKiwiFlare(body []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit := fmt.Sprintf("%s/.sssg/api/answer", baseURL(crs.domain))
|
submit := fmt.Sprintf("https://%s/.sssg/api/answer", crs.domain)
|
||||||
form := url.Values{"a": {salt}, "b": {fmt.Sprintf("%d", nonce)}}
|
form := url.Values{"a": {salt}, "b": {fmt.Sprintf("%d", nonce)}}
|
||||||
req, _ := http.NewRequest("POST", submit, strings.NewReader(form.Encode()))
|
req, _ := http.NewRequest("POST", submit, strings.NewReader(form.Encode()))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
@@ -436,8 +646,4 @@ func (crs *CookieRefreshService) solveKiwiFlare(body []byte) (string, error) {
|
|||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("no auth field in KiwiFlare response")
|
return "", fmt.Errorf("no auth field in KiwiFlare response")
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseURL(domain string) string {
|
|
||||||
return fmt.Sprintf("https://%s", domain)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user