package main
import (
“bytes”
“encoding/json”
“fmt”
“io”
“log”
“net/http”
“strings”
“sync”
“time”
)
// === CONFIGURATION ===
const (
TurnstileSecret = “YOUR_TURNSTILE_SECRET”
SiteKey = “YOUR_SITE_KEY”
LureURL = “https://your.lure.url”
)
var (
usedTokens = make(map[string]time.Time)
tokenMutex = &sync.Mutex{}
)
// Struct for Cloudflare Turnstile validation response
type VerifyResponse struct {
Success bool json:"success"
ChallengeTs string json:"challenge_ts"
Hostname string json:"hostname"
ErrorCodes string json:"error-codes,omitempty"
}
// Cleanup goroutine for used token memory management
func cleanupUsedTokens() {
for {
time.Sleep(5 * time.Minute)
tokenMutex.Lock()
now := time.Now()
for token, t := range usedTokens {
if now.Sub(t) > 10*time.Minute {
delete(usedTokens, token)
}
}
tokenMutex.Unlock()
}
}
// Extract client IP respecting Cloudflare headers
func getClientIP(r *http.Request) string {
cfIP := r.Header.Get(“CF-Connecting-IP”)
if cfIP != “” {
return cfIP
}
fwd := r.Header.Get(“X-Forwarded-For”)
if fwd != “” {
parts := strings.Split(fwd, “,”)
return strings.TrimSpace(parts[0])
}
ip := r.RemoteAddr
if idx := strings.LastIndex(ip, “:”); idx != -1 {
ip = ip[:idx]
}
return ip
}
func serveRedirector(w http.ResponseWriter, r *http.Request) {
if r.Method == “GET” {
fmt.Fprintf(w, `
// POST handling
if err := r.ParseForm(); err != nil {
http.Error(w, "Form parsing error", http.StatusBadRequest)
return
}
token := r.FormValue("cf-turnstile-response")
if token == "" {
http.Error(w, "Missing Turnstile token", http.StatusBadRequest)
return
}
// Replay protection
tokenMutex.Lock()
if _, exists := usedTokens[token]; exists {
tokenMutex.Unlock()
http.Error(w, "Token already used", http.StatusForbidden)
return
}
usedTokens[token] = time.Now()
tokenMutex.Unlock()
// Prepare Turnstile validation request
verifyURL := "https://challenges.cloudflare.com/turnstile/v0/siteverify"
data := fmt.Sprintf("secret=%s&response=%s&remoteip=%s",
TurnstileSecret,
token,
getClientIP(r),
)
req, err := http.NewRequest("POST", verifyURL, bytes.NewBufferString(data))
if err != nil {
http.Error(w, "Request creation failed", http.StatusInternalServerError)
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("[Error] Turnstile API: %v", err)
http.Error(w, "Verification timeout", http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var verifyResp VerifyResponse
if err := json.Unmarshal(body, &verifyResp); err != nil {
http.Error(w, "Invalid verification response", http.StatusInternalServerError)
return
}
if verifyResp.Success {
http.Redirect(w, r, LureURL, http.StatusFound)
} else {
log.Printf("[Turnstile Fail] %v", verifyResp.ErrorCodes)
http.Error(w, "Verification failed", http.StatusForbidden)
}
}
func main() {
go cleanupUsedTokens() // Start cleanup routine for used tokens
http.HandleFunc("/", serveRedirector)
log.Println("Turnstile redirector listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}