package main import ( "io" "log" "net/http" "net/url" "sync/atomic" "time" ) var breaker = newCircuitBreaker() var ( requestsTotal uint64 requestsAllowed uint64 requestsBlocked uint64 requestsBypassed uint64 ) func metricsSnapshot() map[string]interface{} { return map[string]interface{}{ "total": atomic.LoadUint64(&requestsTotal), "allowed": atomic.LoadUint64(&requestsAllowed), "blocked": atomic.LoadUint64(&requestsBlocked), "bypassed": atomic.LoadUint64(&requestsBypassed), "circuit": circuitStateStr(breaker.State()), "enabled": getConfig().Enabled, } } func circuitStateStr(s CircuitState) string { switch s { case StateClosed: return "closed" case StateOpen: return "open" case StateHalfOpen: return "half-open" } return "unknown" } func handleInspect(w http.ResponseWriter, r *http.Request) { atomic.AddUint64(&requestsTotal, 1) cfg := getConfig() if !cfg.Enabled { atomic.AddUint64(&requestsBypassed, 1) proxyToBackend(w, r) return } if breaker.State() == StateOpen { atomic.AddUint64(&requestsBypassed, 1) log.Printf("Firewall: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path) proxyToBackend(w, r) return } // Inspect via WAF. status, err := inspect(r) if err != nil { atomic.AddUint64(&requestsBypassed, 1) log.Printf("Firewall: inspection error — fail-open: %v", err) breaker.RecordFailure() proxyToBackend(w, r) return } breaker.RecordSuccess() if status == 403 { atomic.AddUint64(&requestsBlocked, 1) log.Printf("Firewall: BLOCKED %s %s", r.Method, r.URL.Path) w.Header().Set("Content-Type", "text/html") w.WriteHeader(403) w.Write([]byte("
Blocked by Firewall
")) return } // WAF allows — proxy to backend. atomic.AddUint64(&requestsAllowed, 1) proxyToBackend(w, r) } // proxyToBackend forwards the request to Zoraxy's original upstream. // The original target is passed by Zoraxy via the X-Zoraxy-Uri header. func proxyToBackend(w http.ResponseWriter, r *http.Request) { cfg := getConfig() target, err := url.Parse(cfg.BackendURL) if err != nil { http.Error(w, "invalid backend URL", http.StatusInternalServerError) return } proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target.String()+r.URL.Path, r.Body) if err != nil { http.Error(w, "proxy error", http.StatusInternalServerError) return } for key, vals := range r.Header { for _, v := range vals { proxyReq.Header.Add(key, v) } } proxyReq.Header.Set("X-Forwarded-For", r.RemoteAddr) client := &http.Client{Timeout: time.Duration(cfg.TimeoutMs) * time.Millisecond} resp, err := client.Do(proxyReq) if err != nil { http.Error(w, "backend unreachable", http.StatusBadGateway) return } defer resp.Body.Close() for key, vals := range resp.Header { for _, v := range vals { w.Header().Add(key, v) } } w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) }