Fetcher mimicks Python libraries behavior for more reliabilty. Removed inline comments from env.example since Go hates it.

This commit is contained in:
Salastil
2025-10-21 16:22:57 -04:00
parent e35a067484
commit 1c409694fd
2 changed files with 275 additions and 64 deletions

View File

@@ -2,13 +2,18 @@
DISCORD_BOT_TOKEN=your_discord_bot_token_here
DISCORD_CHANNEL_ID=your_discord_channel_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
RECONNECT_INTERVAL=5 # 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,
ENABLE_FILE_LOGGING=false # Enable logging to bridge.log file (true/false, default: false)
# Interval between reconnect attempts if connection is lost
RECONNECT_INTERVAL=5
# 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
# BRIDGE_USER_ID=123456 # Numeric user ID of your bridge bot account on Sneedchat
# BRIDGE_USERNAME=YourBridgeBot # Username of your bridge bot account on Sneedchat
# BRIDGE_PASSWORD=Password # Password of your account
#Use your Kiwifarm crendeitals for here
#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_USER_ID=123456
# BRIDGE_USERNAME=YourBridgeBot
# BRIDGE_PASSWORD=Password

View File

@@ -2,6 +2,7 @@ package cookie
import (
"crypto/sha256"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -33,14 +34,43 @@ type CookieRefreshService struct {
// NewCookieRefreshService initializes a new cookie service.
func NewCookieRefreshService(username, password, domain string, debug bool) (*CookieRefreshService, error) {
// Standard cookie jar (no additional dependencies needed)
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,
}
client := &http.Client{
Jar: jar,
Timeout: 45 * time.Second,
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
},
}
return &CookieRefreshService{
@@ -146,11 +176,24 @@ func (crs *CookieRefreshService) FetchFreshCookie() (string, error) {
// 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))
}
// 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)
@@ -188,6 +231,11 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
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")
resp, err = crs.client.Do(req)
if err != nil {
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
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 (full browser headers + both redirect fields)
// 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"},
"redirect": {"/"},
"_xfRedirect": {baseURL + "/"},
"_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, "***"))
}
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-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)
if err != nil {
@@ -232,84 +295,180 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
}
defer loginResp.Body.Close()
log.Printf("Login response status: %d", loginResp.StatusCode)
// 🧩 Diagnostic: if status 200, dump first KB of body for debugging
if loginResp.StatusCode == 200 {
bodyBytes, _ := io.ReadAll(loginResp.Body)
snippet := string(bodyBytes)
if len(snippet) > 1000 {
snippet = snippet[:1000]
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)
}
}
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)
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")
}
// 🔁 Follow redirect manually if missing xf_user
if !hasXfUser {
log.Println("🧭 Following post-login redirect manually to capture xf_user...")
// 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", baseURL+"/", nil)
followReq, _ := http.NewRequest("GET", currentURL, nil)
followReq.Header.Set("User-Agent", randomUserAgent())
followReq.Header.Set("Referer", baseURL+"/login")
followReq.Header.Set("Origin", baseURL)
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)
} else {
followResp.Body.Close()
log.Printf("📩 [HTTP GET] %s/ -> %s", baseURL, followResp.Status)
break
}
time.Sleep(1 * time.Second)
cookies = crs.client.Jar.Cookies(cookieURL)
for _, c := range cookies {
log.Printf("🍪 [After Redirect] %s=%s", c.Name, trimLong(c.Value, 10))
if c.Name == "xf_user" {
hasXfUser = true
log.Printf("📩 [HTTP GET] %s -> %s (proto: %s)", currentURL, followResp.Status, followResp.Proto)
// 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)
}
}
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 {
log.Println("✅ xf_user cookie acquired after redirect follow")
} else {
log.Println("⚠️ xf_user cookie still missing after redirect follow")
break
}
}
// 🧭 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 {
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.Header.Set("User-Agent", randomUserAgent())
accountReq.Header.Set("Referer", baseURL+"/login")
accountReq.Header.Set("Origin", baseURL)
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(1 * time.Second)
time.Sleep(2 * time.Second) // Additional wait after /account/ fetch
cookies = crs.client.Jar.Cookies(cookieURL)
for _, c := range cookies {
@@ -320,9 +479,53 @@ func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
}
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 {
log.Println("⚠️ xf_user cookie still missing after /account/ fetch")
return "", fmt.Errorf("xf_user still missing after all follow-ups")
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")
}
}
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
}
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) Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) 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 (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))]
}
@@ -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)}}
req, _ := http.NewRequest("POST", submit, strings.NewReader(form.Encode()))
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 "", fmt.Errorf("no auth field in KiwiFlare response")
}
func baseURL(domain string) string {
return fmt.Sprintf("https://%s", domain)
}
}