diff --git a/config.go b/config.go index db39b20..4a42018 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,7 @@ import ( type Config struct { Enabled bool `json:"enabled"` WAFURL string `json:"waf_url"` + BackendURL string `json:"backend_url"` // where to proxy allowed traffic ReturnPort int `json:"return_port"` HealthInterval int `json:"health_interval"` TimeoutMs int `json:"timeout_ms"` @@ -34,6 +35,7 @@ func defaultConfig() Config { return Config{ Enabled: false, WAFURL: "http://127.0.0.1:8080", + BackendURL: "http://127.0.0.1:80", ReturnPort: 8080, HealthInterval: 5, TimeoutMs: 500, diff --git a/main.go b/main.go index 87def52..d6a225e 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,9 @@ func handleSaveConfig(w http.ResponseWriter, r *http.Request) { if u := r.FormValue("waf_url"); u != "" { cfg.WAFURL = u } + if u := r.FormValue("back_url"); u != "" { + cfg.BackendURL = u + } if v := r.FormValue("return_port"); v != "" { fmt.Sscanf(v, "%d", &cfg.ReturnPort) } diff --git a/middleware.go b/middleware.go index 3cf4024..1e1ef7a 100644 --- a/middleware.go +++ b/middleware.go @@ -1,11 +1,12 @@ package main import ( + "io" "log" "net/http" + "net/url" "sync/atomic" - - "zoraxy-firewall/zoraxy_plugin" + "time" ) var breaker = newCircuitBreaker() @@ -30,12 +31,9 @@ func metricsSnapshot() map[string]interface{} { func circuitStateStr(s CircuitState) string { switch s { - case StateClosed: - return "closed" - case StateOpen: - return "open" - case StateHalfOpen: - return "half-open" + case StateClosed: return "closed" + case StateOpen: return "open" + case StateHalfOpen: return "half-open" } return "unknown" } @@ -46,23 +44,24 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { if !cfg.Enabled { atomic.AddUint64(&requestsBypassed, 1) - w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) + 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) - w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) + 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() - w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) + proxyToBackend(w, r) return } @@ -71,10 +70,54 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { if status == 403 { atomic.AddUint64(&requestsBlocked, 1) log.Printf("Firewall: BLOCKED %s %s", r.Method, r.URL.Path) - w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED)) + 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) - w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) + 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) } diff --git a/web/app.js b/web/app.js index c30acb1..64ba9b5 100644 --- a/web/app.js +++ b/web/app.js @@ -5,6 +5,7 @@ function loadConfig() { $('#fw-enabled').prop('checked', data.enabled); $('#enabled-label').text(data.enabled ? 'Firewall Enabled' : 'Firewall Disabled'); $('#waf-url').val(data.waf_url); + $('#back-url').val(data.backend_url); $('#return-port').val(data.return_port); $('#health-interval').val(data.health_interval); $('#timeout-ms').val(data.timeout_ms); @@ -32,6 +33,7 @@ function saveConfig() { data: { enabled: $('#fw-enabled').is(':checked'), waf_url: $('#waf-url').val().trim(), + back_url: $('#back-url').val().trim(), return_port: $('#return-port').val(), health_interval: $('#health-interval').val(), timeout_ms: $('#timeout-ms').val() diff --git a/web/index.html b/web/index.html index dcff1bb..9bd18b7 100644 --- a/web/index.html +++ b/web/index.html @@ -31,6 +31,12 @@ The firewall application URL (e.g. Wallarm, ModSecurity, Coraza). +