- health.go: treat any HTTP response as reachable (only connection errors fail) - main.go: CheckRedirect in handleReturn to preserve 302 + Set-Cookie - middleware.go: Content-Length for POST/PUT/PATCH bodies, CheckRedirect in forwardToWAF and forwardToReturn - web/: replace stats panel with 3-state WAF indicator (DISABLED/ENABLED/FAILED) - README: update defaults, architecture, health check docs - .agents.md: update verified status for Wallarm integration
163 lines
4.3 KiB
Go
163 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"zoraxy-firewall/zoraxy_plugin"
|
|
)
|
|
|
|
//go:embed web/*
|
|
var webFS embed.FS
|
|
|
|
func main() {
|
|
loadConfig()
|
|
loadHostMap()
|
|
|
|
spec := &zoraxy_plugin.IntroSpect{
|
|
ID: "zoraxy-firewall",
|
|
Name: "Firewall",
|
|
Author: "Zoraxy Community",
|
|
AuthorContact: "",
|
|
Description: "Transparent WAF proxy — forwards traffic to firewall for inline inspection.",
|
|
URL: "",
|
|
Type: zoraxy_plugin.PluginType_Router,
|
|
VersionMajor: 1,
|
|
VersionMinor: 0,
|
|
VersionPatch: 0,
|
|
UIPath: "/ui",
|
|
|
|
StaticCapturePaths: []zoraxy_plugin.StaticCaptureRule{{CapturePath: "/"}},
|
|
StaticCaptureIngress: "/inspect",
|
|
}
|
|
|
|
config, err := zoraxy_plugin.ServeAndRecvSpec(spec)
|
|
if err != nil {
|
|
log.Fatalf("failed to receive config: %v", err)
|
|
}
|
|
|
|
startHealthCheck()
|
|
|
|
// Return port listener — receives traffic from WAF, proxies to Zoraxy.
|
|
cfg := getConfig()
|
|
if cfg.ReturnPort > 0 {
|
|
go func() {
|
|
rmux := http.NewServeMux()
|
|
rmux.HandleFunc("/", handleReturn)
|
|
addr := fmt.Sprintf(":%d", cfg.ReturnPort)
|
|
log.Printf("Firewall: return listener on %s", addr)
|
|
if err := http.ListenAndServe(addr, rmux); err != nil {
|
|
log.Printf("Firewall: return listener error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/inspect/", http.HandlerFunc(handleInspect))
|
|
mux.HandleFunc("/inspect", handleInspect)
|
|
|
|
uiRouter := zoraxy_plugin.NewPluginEmbedUIRouter(spec.ID, &webFS, "web", spec.UIPath)
|
|
uiRouter.HandleFunc("/api/config", handleGetConfig, mux)
|
|
uiRouter.HandleFunc("/api/config/save", handleSaveConfig, mux)
|
|
uiRouter.HandleFunc("/api/stats", handleGetStats, mux)
|
|
|
|
uiRouter.RegisterTerminateHandler(func() {
|
|
log.Println("firewall shutting down")
|
|
}, mux)
|
|
uiRouter.AttachHandlerToMux(mux)
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", config.Port)
|
|
log.Printf("firewall v%d.%d.%d listening on %s", spec.VersionMajor, spec.VersionMinor, spec.VersionPatch, addr)
|
|
|
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
log.Fatalf("server error: %v", err)
|
|
}
|
|
}
|
|
|
|
func handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, getConfig())
|
|
}
|
|
|
|
func handleSaveConfig(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid form"})
|
|
return
|
|
}
|
|
|
|
cfgMu.Lock()
|
|
cfg.Enabled = r.FormValue("enabled") == "true"
|
|
if u := r.FormValue("waf_url"); u != "" {
|
|
cfg.WAFURL = u
|
|
}
|
|
if v := r.FormValue("return_port"); v != "" {
|
|
fmt.Sscanf(v, "%d", &cfg.ReturnPort)
|
|
}
|
|
if v := r.FormValue("health_interval"); v != "" {
|
|
fmt.Sscanf(v, "%d", &cfg.HealthInterval)
|
|
}
|
|
if v := r.FormValue("timeout_ms"); v != "" {
|
|
fmt.Sscanf(v, "%d", &cfg.TimeoutMs)
|
|
}
|
|
cfgMu.Unlock()
|
|
|
|
if err := saveConfig(); err != nil {
|
|
log.Printf("ERROR saving config: %v", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "save failed"})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "saved"})
|
|
}
|
|
|
|
func handleGetStats(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, metricsSnapshot())
|
|
}
|
|
|
|
func handleReturn(w http.ResponseWriter, r *http.Request) {
|
|
backend := resolveBackend(r.Host)
|
|
if !strings.HasPrefix(backend, "http") {
|
|
backend = "http://" + backend
|
|
}
|
|
log.Printf("DEBUG return: %s %s -> %s%s (Host: %s)", r.Method, r.URL.Path, backend, r.URL.Path, r.Host)
|
|
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, backend+r.URL.Path, r.Body)
|
|
if err != nil {
|
|
http.Error(w, "proxy error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
proxyReq.Host = r.Host
|
|
for k, vv := range r.Header {
|
|
for _, v := range vv {
|
|
proxyReq.Header.Add(k, v)
|
|
}
|
|
}
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
resp, err := client.Do(proxyReq)
|
|
if err != nil {
|
|
http.Error(w, "backend unreachable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
for k, vv := range resp.Header {
|
|
for _, v := range vv {
|
|
w.Header().Add(k, v)
|
|
}
|
|
}
|
|
w.WriteHeader(resp.StatusCode)
|
|
io.Copy(w, resp.Body)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|