Expanded debugging + Changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
go.sum
|
go.sum
|
||||||
Sneedchat-Discord-Bridge
|
Sneedchat-Discord-Bridge
|
||||||
|
file.log
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package cookie
|
package cookie
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -11,297 +11,433 @@ import (
|
|||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// CookieRefreshService manages periodic cookie refreshing.
|
||||||
CookieRetryDelay = 5 * time.Second
|
type CookieRefreshService struct {
|
||||||
MaxCookieRetryDelay = 60 * time.Second
|
username string
|
||||||
CookieRefreshEvery = 4 * time.Hour
|
password string
|
||||||
)
|
domain string
|
||||||
|
client *http.Client
|
||||||
type RefreshService struct {
|
debug bool
|
||||||
username, password, domain string
|
|
||||||
client *http.Client
|
|
||||||
|
|
||||||
cookieMu sync.RWMutex
|
|
||||||
currentCookie string
|
currentCookie string
|
||||||
|
cookieMu sync.RWMutex
|
||||||
readyOnce sync.Once
|
cookieReady chan struct{}
|
||||||
readyCh chan struct{}
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
stopCh chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRefreshService(username, password, domain string) *RefreshService {
|
// NewCookieRefreshService initializes a new cookie service.
|
||||||
jar, _ := cookiejar.New(nil)
|
func NewCookieRefreshService(username, password, domain string, debug bool) (*CookieRefreshService, error) {
|
||||||
tr := &http.Transport{
|
jar, err := cookiejar.New(nil)
|
||||||
// Force HTTP/1.1 to avoid Cloudflare/ALPN issues
|
|
||||||
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
|
||||||
}
|
|
||||||
client := &http.Client{
|
|
||||||
Jar: jar,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
Transport: tr,
|
|
||||||
}
|
|
||||||
return &RefreshService{
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
domain: domain,
|
|
||||||
client: client,
|
|
||||||
readyCh: make(chan struct{}),
|
|
||||||
stopCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) Start() {
|
|
||||||
r.wg.Add(1)
|
|
||||||
go r.loop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) Stop() {
|
|
||||||
close(r.stopCh)
|
|
||||||
r.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) WaitForCookie() { <-r.readyCh }
|
|
||||||
|
|
||||||
func (r *RefreshService) GetCurrentCookie() string {
|
|
||||||
r.cookieMu.RLock()
|
|
||||||
defer r.cookieMu.RUnlock()
|
|
||||||
return r.currentCookie
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) loop() {
|
|
||||||
defer r.wg.Done()
|
|
||||||
log.Println("🔑 Fetching initial cookie...")
|
|
||||||
c, err := r.FetchFreshCookie()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Failed to acquire initial cookie: %v", err)
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Jar: jar,
|
||||||
|
Timeout: 45 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CookieRefreshService{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
domain: domain,
|
||||||
|
client: client,
|
||||||
|
debug: debug,
|
||||||
|
cookieReady: make(chan struct{}),
|
||||||
|
stopChan: 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
|
return
|
||||||
}
|
}
|
||||||
r.cookieMu.Lock()
|
|
||||||
r.currentCookie = c
|
|
||||||
r.cookieMu.Unlock()
|
|
||||||
r.readyOnce.Do(func() { close(r.readyCh) })
|
|
||||||
log.Println("✅ Initial cookie acquired")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) FetchFreshCookie() (string, error) {
|
crs.cookieMu.Lock()
|
||||||
attempt := 0
|
crs.currentCookie = fresh
|
||||||
delay := CookieRetryDelay
|
crs.cookieMu.Unlock()
|
||||||
|
close(crs.cookieReady)
|
||||||
|
log.Println("✅ Initial cookie acquired")
|
||||||
|
|
||||||
|
ticker := time.NewTicker(4 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-r.stopCh:
|
case <-ticker.C:
|
||||||
return "", fmt.Errorf("stopped")
|
log.Println("🔄 Automatic cookie refresh cycle started")
|
||||||
default:
|
cookie, err := crs.FetchFreshCookie()
|
||||||
}
|
if err != nil {
|
||||||
|
log.Printf("⚠️ Cookie refresh failed: %v", err)
|
||||||
attempt++
|
continue
|
||||||
if attempt > 1 {
|
|
||||||
log.Printf("🔄 Cookie fetch retry attempt %d (waiting %v)...", attempt, delay)
|
|
||||||
time.Sleep(delay)
|
|
||||||
delay *= 2
|
|
||||||
if delay > MaxCookieRetryDelay {
|
|
||||||
delay = MaxCookieRetryDelay
|
|
||||||
}
|
}
|
||||||
}
|
crs.cookieMu.Lock()
|
||||||
|
crs.currentCookie = cookie
|
||||||
|
crs.cookieMu.Unlock()
|
||||||
|
log.Println("✅ Cookie refresh completed")
|
||||||
|
|
||||||
c, err := r.attemptFetchCookie()
|
case <-crs.stopChan:
|
||||||
if err != nil {
|
return
|
||||||
log.Printf("⚠️ Cookie fetch attempt %d failed: %v", attempt, err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if strings.Contains(c, "xf_user=") {
|
|
||||||
log.Printf("✅ Successfully fetched fresh cookie with xf_user (attempt %d)", attempt)
|
|
||||||
r.cookieMu.Lock()
|
|
||||||
r.currentCookie = c
|
|
||||||
r.cookieMu.Unlock()
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
log.Printf("❌ Cookie fetch attempt %d missing xf_user — retrying...", attempt)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RefreshService) attemptFetchCookie() (string, error) {
|
// FetchFreshCookie attempts full login until success.
|
||||||
// Step 1: KiwiFlare
|
func (crs *CookieRefreshService) FetchFreshCookie() (string, error) {
|
||||||
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
attempt := 1
|
||||||
clearance, err := r.getClearanceToken()
|
for {
|
||||||
if err != nil {
|
log.Printf("🔑 Attempting cookie fetch (attempt %d)", attempt)
|
||||||
return "", fmt.Errorf("clearance token error: %w", err)
|
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++
|
||||||
}
|
}
|
||||||
if clearance != "" {
|
}
|
||||||
|
|
||||||
|
// attemptFetchCookie performs one complete login cycle.
|
||||||
|
func (crs *CookieRefreshService) attemptFetchCookie() (string, error) {
|
||||||
|
baseURL := fmt.Sprintf("https://%s", crs.domain)
|
||||||
|
|
||||||
|
// Step 1: KiwiFlare clearance
|
||||||
|
log.Println("Step 1: Checking for KiwiFlare challenge...")
|
||||||
|
req, _ := http.NewRequest("GET", baseURL+"/", nil)
|
||||||
|
req.Header.Set("User-Agent", randomUserAgent())
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := crs.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("initial GET failed: %w", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
log.Println("✅ KiwiFlare challenge solved")
|
log.Println("✅ KiwiFlare challenge solved")
|
||||||
log.Println("⏳ Waiting 2 seconds for cookie propagation...")
|
log.Println("⏳ Waiting 2 seconds for cookie propagation...")
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: GET /login
|
// Step 2: Login page
|
||||||
log.Println("Step 2: Fetching login page...")
|
log.Println("Step 2: Fetching login page...")
|
||||||
loginURL := fmt.Sprintf("https://%s/login/", r.domain)
|
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("Cache-Control", "no-cache")
|
resp, err = crs.client.Do(req)
|
||||||
req.Header.Set("Pragma", "no-cache")
|
|
||||||
req.URL.RawQuery = fmt.Sprintf("r=%d", rand.Intn(1_000_000))
|
|
||||||
|
|
||||||
resp, err := r.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)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ = io.ReadAll(resp.Body)
|
||||||
log.Printf("→ Using protocol for login page: %s", resp.Proto)
|
log.Printf("→ Using protocol for login page: %s", resp.Proto)
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
bodyStr := string(body)
|
|
||||||
|
|
||||||
log.Println("⏳ Waiting 1 second before processing login page...")
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// Step 3: Extract CSRF
|
// Step 3: Extract CSRF token
|
||||||
log.Println("Step 3: Extracting CSRF token...")
|
csrfToken := extractCSRF(string(body))
|
||||||
var csrf string
|
if csrfToken == "" {
|
||||||
for _, pat := range []*regexp.Regexp{
|
return "", fmt.Errorf("missing CSRF token")
|
||||||
regexp.MustCompile(`<html[^>]*data-csrf=["']([^"']+)["']`),
|
|
||||||
regexp.MustCompile(`name="_xfToken" value="([^"]+)"`),
|
|
||||||
regexp.MustCompile(`data-csrf=["']([^"']+)["']`),
|
|
||||||
regexp.MustCompile(`"csrf":"([^"]+)"`),
|
|
||||||
regexp.MustCompile(`XF\.config\.csrf\s*=\s*"([^"]+)"`),
|
|
||||||
} {
|
|
||||||
if m := pat.FindStringSubmatch(bodyStr); len(m) >= 2 {
|
|
||||||
csrf = m[1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if csrf == "" {
|
log.Printf("✅ Found CSRF token: %s...", trimLong(csrfToken, 10))
|
||||||
log.Printf("⚠️ CSRF token not found. Partial HTML:\n%s", bodyStr[:min(800, len(bodyStr))])
|
|
||||||
return "", fmt.Errorf("CSRF token not found in login page")
|
|
||||||
}
|
|
||||||
log.Printf("✅ Found CSRF token: %s...", csrf[:min(10, len(csrf))])
|
|
||||||
|
|
||||||
// Step 4: POST /login/login
|
// Step 4: POST login credentials (full browser headers + both redirect fields)
|
||||||
log.Println("Step 4: Submitting login credentials...")
|
loginPost := fmt.Sprintf("%s/login/login", baseURL)
|
||||||
postURL := fmt.Sprintf("https://%s/login/login", r.domain)
|
data := url.Values{
|
||||||
form := url.Values{
|
"_xfToken": {csrfToken},
|
||||||
"_xfToken": {csrf},
|
"login": {crs.username},
|
||||||
"_xfRequestUri": {"/"},
|
"password": {crs.password},
|
||||||
"_xfWithData": {"1"},
|
"remember": {"1"},
|
||||||
"login": {r.username},
|
"redirect": {"/"},
|
||||||
"password": {r.password},
|
"_xfRedirect": {baseURL + "/"},
|
||||||
"_xfRedirect": {fmt.Sprintf("https://%s/", r.domain)},
|
|
||||||
"remember": {"1"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cookieURL, _ := url.Parse(fmt.Sprintf("https://%s/", r.domain))
|
postReq, _ := http.NewRequest("POST", loginPost, strings.NewReader(data.Encode()))
|
||||||
for _, c := range r.client.Jar.Cookies(cookieURL) {
|
|
||||||
c.Domain = strings.TrimPrefix(c.Domain, ".")
|
|
||||||
}
|
|
||||||
r.client.Jar.SetCookies(cookieURL, r.client.Jar.Cookies(cookieURL))
|
|
||||||
|
|
||||||
postReq, _ := http.NewRequest("POST", postURL, strings.NewReader(form.Encode()))
|
|
||||||
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
postReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
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", fmt.Sprintf("https://%s", r.domain))
|
postReq.Header.Set("Origin", baseURL)
|
||||||
postReq.Header.Set("X-XF-Token", csrf)
|
|
||||||
postReq.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
||||||
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.9")
|
postReq.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||||
postReq.Header.Set("Accept-Encoding", "gzip, deflate")
|
postReq.Header.Set("Connection", "keep-alive")
|
||||||
|
postReq.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||||
|
|
||||||
loginResp, err := r.client.Do(postReq)
|
loginResp, err := crs.client.Do(postReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("login POST failed: %w", err)
|
return "", fmt.Errorf("login POST failed: %w", err)
|
||||||
}
|
}
|
||||||
defer loginResp.Body.Close()
|
defer loginResp.Body.Close()
|
||||||
|
|
||||||
log.Printf("Login response status: %d", loginResp.StatusCode)
|
log.Printf("Login response status: %d", loginResp.StatusCode)
|
||||||
|
|
||||||
// Follow redirect if present
|
// 🧩 Diagnostic: if status 200, dump first KB of body for debugging
|
||||||
if loginResp.StatusCode >= 300 && loginResp.StatusCode < 400 {
|
if loginResp.StatusCode == 200 {
|
||||||
if loc := loginResp.Header.Get("Location"); loc != "" {
|
bodyBytes, _ := io.ReadAll(loginResp.Body)
|
||||||
log.Printf("Following redirect to %s to check for xf_user...", loc)
|
snippet := string(bodyBytes)
|
||||||
url2 := loc
|
if len(snippet) > 1000 {
|
||||||
if !strings.HasPrefix(loc, "http") {
|
snippet = snippet[:1000]
|
||||||
url2 = fmt.Sprintf("https://%s%s", r.domain, loc)
|
|
||||||
}
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
if fr, err := r.client.Get(url2); err == nil {
|
|
||||||
fr.Body.Close()
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
log.Printf("🧩 Login 200 body snippet:\n%s", snippet)
|
||||||
|
// Recreate reader for downstream reuse
|
||||||
|
loginResp.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode response (gzip)
|
|
||||||
var reader io.ReadCloser
|
|
||||||
if loginResp.Header.Get("Content-Encoding") == "gzip" {
|
|
||||||
gz, ge := gzip.NewReader(loginResp.Body)
|
|
||||||
if ge == nil {
|
|
||||||
reader = gz
|
|
||||||
defer gz.Close()
|
|
||||||
} else {
|
|
||||||
reader = io.NopCloser(loginResp.Body)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reader = io.NopCloser(loginResp.Body)
|
|
||||||
}
|
|
||||||
respHTML, _ := io.ReadAll(reader)
|
|
||||||
if strings.Contains(string(respHTML), `data-logged-in="false"`) {
|
|
||||||
log.Println("⚠️ HTML indicates still logged out (data-logged-in=false)")
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
return r.retryWithFreshCSRF()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before extracting cookies
|
|
||||||
log.Println("⏳ Waiting 2 seconds for XenForo to issue cookies...")
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
// Normalize and show cookies
|
// Step 5: Check cookies in jar
|
||||||
cookies := r.client.Jar.Cookies(cookieURL)
|
cookieURL, _ := url.Parse(baseURL)
|
||||||
|
cookies := crs.client.Jar.Cookies(cookieURL)
|
||||||
|
hasXfUser := false
|
||||||
for _, c := range cookies {
|
for _, c := range cookies {
|
||||||
c.Domain = strings.TrimPrefix(c.Domain, ".")
|
log.Printf("🍪 [After Login POST] %s=%s", c.Name, trimLong(c.Value, 10))
|
||||||
log.Printf("Cookie after login: %s=%s", c.Name, c.Value)
|
if c.Name == "xf_user" {
|
||||||
}
|
hasXfUser = true
|
||||||
r.client.Jar.SetCookies(cookieURL, cookies)
|
|
||||||
|
|
||||||
want := map[string]bool{
|
|
||||||
"xf_user": true,
|
|
||||||
"xf_toggle": true,
|
|
||||||
"xf_csrf": true,
|
|
||||||
"xf_session": true,
|
|
||||||
"sssg_clearance": true,
|
|
||||||
}
|
|
||||||
var parts []string
|
|
||||||
hasUser := false
|
|
||||||
for _, c := range cookies {
|
|
||||||
if want[c.Name] {
|
|
||||||
parts = append(parts, fmt.Sprintf("%s=%s", c.Name, c.Value))
|
|
||||||
if c.Name == "xf_user" {
|
|
||||||
hasUser = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasUser {
|
|
||||||
return "", fmt.Errorf("xf_user cookie missing — authentication failed, will retry")
|
// 🔁 Follow redirect manually if missing xf_user
|
||||||
|
if !hasXfUser {
|
||||||
|
log.Println("🧭 Following post-login redirect manually to capture xf_user...")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
followReq, _ := http.NewRequest("GET", baseURL+"/", nil)
|
||||||
|
followReq.Header.Set("User-Agent", randomUserAgent())
|
||||||
|
followReq.Header.Set("Referer", baseURL+"/login")
|
||||||
|
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-Language", "en-US,en;q=0.5")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasXfUser {
|
||||||
|
log.Println("✅ xf_user cookie acquired after redirect follow")
|
||||||
|
} else {
|
||||||
|
log.Println("⚠️ xf_user cookie still missing after redirect follow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 Secondary check — trigger /account/ to issue xf_user if still missing
|
||||||
|
if !hasXfUser {
|
||||||
|
log.Println("🧭 Performing secondary authenticated fetch to /account/ to trigger xf_user...")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
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("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")
|
||||||
|
accountResp, accErr := crs.client.Do(accountReq)
|
||||||
|
if accErr != nil {
|
||||||
|
log.Printf("⚠️ Account fetch failed: %v", accErr)
|
||||||
|
} else {
|
||||||
|
accountResp.Body.Close()
|
||||||
|
log.Printf("📩 [HTTP GET] %s/account/ -> %s", baseURL, accountResp.Status)
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
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 hasXfUser {
|
||||||
|
log.Println("✅ xf_user cookie acquired after /account/ fetch")
|
||||||
|
} else {
|
||||||
|
log.Println("⚠️ xf_user cookie still missing after /account/ fetch")
|
||||||
|
return "", fmt.Errorf("xf_user still missing after all follow-ups")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 strings.Join(parts, "; "), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomUserAgent() string {
|
//
|
||||||
agents := []string{
|
// ---------- Utilities ----------
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
|
//
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
func extractCSRF(body string) string {
|
||||||
|
reList := []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`data-csrf=["']([^"']+)["']`),
|
||||||
|
regexp.MustCompile(`"csrf":"([^"]+)"`),
|
||||||
|
regexp.MustCompile(`XF\.config\.csrf\s*=\s*"([^"]+)"`),
|
||||||
}
|
}
|
||||||
return agents[rand.Intn(len(agents))]
|
for _, re := range reList {
|
||||||
|
if m := re.FindStringSubmatch(body); len(m) > 1 {
|
||||||
|
return m[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
func trimLong(s string, n int) string {
|
||||||
if a < b {
|
if len(s) > n {
|
||||||
return a
|
return s[:n]
|
||||||
}
|
}
|
||||||
return b
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit := fmt.Sprintf("%s/.sssg/api/answer", baseURL(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())
|
||||||
|
|
||||||
|
resp, err := crs.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no auth field in KiwiFlare response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseURL(domain string) string {
|
||||||
|
return fmt.Sprintf("https://%s", domain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
package cookie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *RefreshService) retryWithFreshCSRF() (string, error) {
|
|
||||||
loginURL := fmt.Sprintf("https://%s/login/", r.domain)
|
|
||||||
resp, err := r.client.Get(loginURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to refetch login page: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
re := regexp.MustCompile(`name="_xfToken" value="([^"]+)"`)
|
|
||||||
m := re.FindSubmatch(body)
|
|
||||||
if len(m) < 2 {
|
|
||||||
return "", fmt.Errorf("csrf retry token not found")
|
|
||||||
}
|
|
||||||
csrf := string(m[1])
|
|
||||||
log.Printf("✅ Retry CSRF token: %.10s...", csrf)
|
|
||||||
|
|
||||||
postURL := fmt.Sprintf("https://%s/login/login", r.domain)
|
|
||||||
form := url.Values{
|
|
||||||
"login": {r.username},
|
|
||||||
"password": {r.password},
|
|
||||||
"_xfToken": {csrf},
|
|
||||||
"_xfRedirect": {"/"},
|
|
||||||
}
|
|
||||||
req, _ := http.NewRequest("POST", postURL, strings.NewReader(form.Encode()))
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
req.Header.Set("User-Agent", randomUserAgent())
|
|
||||||
req.Header.Set("Referer", loginURL)
|
|
||||||
req.Header.Set("Origin", fmt.Sprintf("https://%s", r.domain))
|
|
||||||
req.Header.Set("X-XF-Token", csrf)
|
|
||||||
req.Header.Set("Accept-Encoding", "gzip, deflate")
|
|
||||||
|
|
||||||
resp2, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("retry POST failed: %v", err)
|
|
||||||
}
|
|
||||||
defer resp2.Body.Close()
|
|
||||||
|
|
||||||
var reader io.ReadCloser
|
|
||||||
if resp2.Header.Get("Content-Encoding") == "gzip" {
|
|
||||||
gz, ge := gzip.NewReader(resp2.Body)
|
|
||||||
if ge == nil {
|
|
||||||
reader = gz
|
|
||||||
defer gz.Close()
|
|
||||||
} else {
|
|
||||||
reader = io.NopCloser(resp2.Body)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reader = io.NopCloser(resp2.Body)
|
|
||||||
}
|
|
||||||
_, _ = io.ReadAll(reader)
|
|
||||||
|
|
||||||
cookieURL, _ := url.Parse(fmt.Sprintf("https://%s/", r.domain))
|
|
||||||
for _, c := range r.client.Jar.Cookies(cookieURL) {
|
|
||||||
if c.Name == "xf_user" {
|
|
||||||
log.Printf("✅ Successfully fetched fresh cookie with xf_user: %.12s...", c.Value)
|
|
||||||
return "xf_user=" + c.Value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("retry still missing xf_user cookie")
|
|
||||||
}
|
|
||||||
135
cookie/solver.go
135
cookie/solver.go
@@ -1,135 +0,0 @@
|
|||||||
package cookie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *RefreshService) getClearanceToken() (string, error) {
|
|
||||||
baseURL := fmt.Sprintf("https://%s/", r.domain)
|
|
||||||
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.9")
|
|
||||||
req.Header.Set("Connection", "keep-alive")
|
|
||||||
|
|
||||||
resp, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
patterns := []*regexp.Regexp{
|
|
||||||
regexp.MustCompile(`<html[^>]*id=["']sssg["'][^>]*data-sssg-challenge=["']([^"']+)["'][^>]*data-sssg-difficulty=["'](\d+)["']`),
|
|
||||||
regexp.MustCompile(`<html[^>]*id=["']sssg["'][^>]*data-sssg-difficulty=["'](\d+)["'][^>]*data-sssg-challenge=["']([^"']+)["']`),
|
|
||||||
regexp.MustCompile(`data-sssg-challenge=["']([^"']+)["'][^>]*data-sssg-difficulty=["'](\d+)["']`),
|
|
||||||
}
|
|
||||||
var salt string
|
|
||||||
var difficulty int
|
|
||||||
found := false
|
|
||||||
for i, p := range patterns {
|
|
||||||
if m := p.FindStringSubmatch(string(body)); len(m) >= 3 {
|
|
||||||
if i == 1 {
|
|
||||||
difficulty, _ = strconv.Atoi(m[1])
|
|
||||||
salt = m[2]
|
|
||||||
} else {
|
|
||||||
salt = m[1]
|
|
||||||
difficulty, _ = strconv.Atoi(m[2])
|
|
||||||
}
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found || difficulty == 0 || salt == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Solving KiwiFlare challenge (difficulty=%d)", difficulty)
|
|
||||||
time.Sleep(time.Duration(500+rand.Intn(750)) * time.Millisecond)
|
|
||||||
|
|
||||||
nonce, err := r.solvePoW(salt, difficulty)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(700+rand.Intn(900)) * time.Millisecond)
|
|
||||||
|
|
||||||
submitURL := fmt.Sprintf("https://%s/.sssg/api/answer", r.domain)
|
|
||||||
form := url.Values{"a": {salt}, "b": {nonce}}
|
|
||||||
post, _ := http.NewRequest("POST", submitURL, strings.NewReader(form.Encode()))
|
|
||||||
post.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
post.Header.Set("User-Agent", randomUserAgent())
|
|
||||||
post.Header.Set("Origin", baseURL)
|
|
||||||
post.Header.Set("Referer", baseURL)
|
|
||||||
|
|
||||||
resp2, err := r.client.Do(post)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp2.Body.Close()
|
|
||||||
|
|
||||||
var result map[string]any
|
|
||||||
_ = json.NewDecoder(resp2.Body).Decode(&result)
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(1200+rand.Intn(800)) * time.Millisecond)
|
|
||||||
|
|
||||||
cookieURL, _ := url.Parse(baseURL)
|
|
||||||
for _, c := range r.client.Jar.Cookies(cookieURL) {
|
|
||||||
if c.Name == "sssg_clearance" {
|
|
||||||
log.Printf("✅ KiwiFlare clearance cookie confirmed: %s...", c.Value[:min(10, len(c.Value))])
|
|
||||||
return c.Value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := result["auth"].(string); ok && v != "" {
|
|
||||||
r.client.Jar.SetCookies(cookieURL, []*http.Cookie{{
|
|
||||||
Name: "sssg_clearance",
|
|
||||||
Value: v,
|
|
||||||
Path: "/",
|
|
||||||
Domain: r.domain,
|
|
||||||
}})
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("clearance cookie missing after solve")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RefreshService) solvePoW(salt string, difficulty int) (string, error) {
|
|
||||||
start := time.Now()
|
|
||||||
bytes := difficulty / 8
|
|
||||||
bits := difficulty % 8
|
|
||||||
|
|
||||||
for nonce := rand.Int63(); ; nonce++ {
|
|
||||||
sum := sha256.Sum256([]byte(fmt.Sprintf("%s%d", salt, nonce)))
|
|
||||||
ok := true
|
|
||||||
for i := 0; i < bytes; i++ {
|
|
||||||
if sum[i] != 0 {
|
|
||||||
ok = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok && bits > 0 && bytes < len(sum) {
|
|
||||||
mask := byte(0xFF << (8 - bits))
|
|
||||||
if sum[bytes]&mask != 0 {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
delay := time.Duration(2+rand.Intn(3))*time.Second - time.Since(start)
|
|
||||||
if delay > 0 {
|
|
||||||
time.Sleep(delay)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d", nonce), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
main.go
52
main.go
@@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,16 +23,60 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
cfg, err := config.Load(envFile)
|
cfg, err := config.Load(envFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
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 .env file: %s", envFile)
|
||||||
log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID)
|
log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID)
|
||||||
log.Printf("Bridge username: %s", cfg.BridgeUsername)
|
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, KF PoW, CSRF retry)
|
// Cookie service (HTTP/1.1/2, KiwiFlare PoW, CSRF, deep debug)
|
||||||
cookieSvc := cookie.NewRefreshService(cfg.BridgeUsername, cfg.BridgePassword, "kiwifarms.st")
|
cookieSvc, err := cookie.NewCookieRefreshService(cfg.BridgeUsername, cfg.BridgePassword, "kiwifarms.st", cfg.Debug)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create cookie service: %v", err)
|
||||||
|
}
|
||||||
cookieSvc.Start()
|
cookieSvc.Start()
|
||||||
log.Println("⏳ Waiting for initial cookie...")
|
log.Println("⏳ Waiting for initial cookie...")
|
||||||
cookieSvc.WaitForCookie()
|
cookieSvc.WaitForCookie()
|
||||||
@@ -51,7 +97,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("🌉 Discord-Sneedchat Bridge started successfully")
|
log.Println("🌉 Discord-Sneedchat Bridge started successfully")
|
||||||
|
|
||||||
// Auto cookie refresh every 4h
|
// Auto cookie refresh every 4h (in addition to background loop inside service)
|
||||||
go func() {
|
go func() {
|
||||||
t := time.NewTicker(4 * time.Hour)
|
t := time.NewTicker(4 * time.Hour)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const (
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
wsURL string
|
wsURL string
|
||||||
roomID int
|
roomID int
|
||||||
cookies *cookie.RefreshService
|
cookies *cookie.CookieRefreshService
|
||||||
|
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
connected bool
|
connected bool
|
||||||
@@ -58,7 +58,7 @@ type Client struct {
|
|||||||
bridgeUsername string
|
bridgeUsername string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(roomID int, cookieSvc *cookie.RefreshService) *Client {
|
func NewClient(roomID int, cookieSvc *cookie.CookieRefreshService) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
wsURL: "wss://kiwifarms.st:9443/chat.ws",
|
wsURL: "wss://kiwifarms.st:9443/chat.ws",
|
||||||
roomID: roomID,
|
roomID: roomID,
|
||||||
|
|||||||
Reference in New Issue
Block a user