commit 14c41bce4207853f18aebb68d1d1051b5afe60f5 Author: cclohmar Date: Sat Aug 1 05:54:51 2026 +0000 chore: initial scaffold — Zoraxy WAF plugin (Type 1 Utilities) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83bf4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Binary +dhcp-lease-manager + +# Go +*.exe +*.test +*.out +go.sum + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Temp +tmp/ +.tmp/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..06858e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module zoraxy-waf + +go 1.21 diff --git a/main.go b/main.go new file mode 100644 index 0000000..5670f40 --- /dev/null +++ b/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "embed" + "fmt" + "log" + "net/http" + + "zoraxy-waf/zoraxy_plugin" +) + +//go:embed web/* +var webFS embed.FS + +func main() { + spec := &zoraxy_plugin.IntroSpect{ + ID: "zoraxy-waf", + Name: "Zoraxy WAF", + Author: "Zoraxy Community", + AuthorContact: "", + Description: "Web Application Firewall plugin for Zoraxy.", + URL: "", + Type: zoraxy_plugin.PluginType_Utilities, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + UIPath: "/ui", + } + + config, err := zoraxy_plugin.ServeAndRecvSpec(spec) + if err != nil { + log.Fatalf("failed to receive config: %v", err) + } + + mux := http.NewServeMux() + + uiRouter := zoraxy_plugin.NewPluginEmbedUIRouter( + spec.ID, + &webFS, + "web", + spec.UIPath, + ) + + // Register API endpoints here. + // Example: uiRouter.HandleFunc("/api/status", handleStatus, mux) + + uiRouter.RegisterTerminateHandler(func() { + log.Println("zoraxy-waf shutting down") + }, mux) + + uiRouter.AttachHandlerToMux(mux) + + addr := fmt.Sprintf("127.0.0.1:%d", config.Port) + log.Printf("zoraxy-waf 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) + } +} diff --git a/models.go b/models.go new file mode 100644 index 0000000..28677a9 --- /dev/null +++ b/models.go @@ -0,0 +1,3 @@ +package main + +// Data models go here. diff --git a/server.go b/server.go new file mode 100644 index 0000000..bbee632 --- /dev/null +++ b/server.go @@ -0,0 +1,3 @@ +package main + +// HTTP handlers and core logic go here. diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..39d88b5 --- /dev/null +++ b/web/app.js @@ -0,0 +1,4 @@ +// Zoraxy WAF — client-side logic +$(document).ready(function () { + console.log('Zoraxy WAF plugin loaded'); +}); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f1e81d7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,22 @@ + + + + + + + Zoraxy WAF + + + +
+

Zoraxy WAF

+

Web Application Firewall — coming soon.

+
+
+

Configure firewall rules, rate limits, and IP blocking from this panel.

+
+ + + + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..0228866 --- /dev/null +++ b/web/style.css @@ -0,0 +1,18 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + color: #1a1a2e; + background: #f5f6fa; + padding: 20px; + max-width: 960px; + margin: 0 auto; +} + +h1 { font-size: 20px; margin-bottom: 8px; } +p { color: #636e72; } diff --git a/zoraxy-waf b/zoraxy-waf new file mode 100755 index 0000000..0381614 Binary files /dev/null and b/zoraxy-waf differ diff --git a/zoraxy_plugin/embed_webserver.go b/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..bb2b08a --- /dev/null +++ b/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,135 @@ +package zoraxy_plugin + +import ( + "embed" + "io/fs" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// PluginUiRouter serves an embedded web UI and provides CSRF token injection. +type PluginUiRouter struct { + PluginID string + TargetFs *embed.FS + TargetFsPrefix string + HandlerPrefix string + EnableDebug bool + terminateHandler func() +} + +// NewPluginEmbedUIRouter creates a router backed by an embed.FS. +// targetFsPrefix is the root folder within the embed.FS (e.g. "/web"). +// handlerPrefix is the HTTP path prefix (e.g. "/ui"). +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +// csrfMiddleware intercepts HTML responses and injects the CSRF token. +func (p *PluginUiRouter) csrfMiddleware(r *http.Request, fsHandler http.Handler) http.Handler { + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, ".html") { + targetPath := p.TargetFsPrefix + "/" + strings.TrimPrefix(r.URL.Path, "/") + targetPath = strings.TrimPrefix(targetPath, "/") + content, err := fs.ReadFile(*p.TargetFs, targetPath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := strings.ReplaceAll(string(content), "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } + if strings.HasSuffix(r.URL.Path, "/") { + indexPath := p.TargetFsPrefix + "/" + strings.TrimPrefix(r.URL.Path, "/") + "index.html" + indexPath = strings.TrimPrefix(indexPath, "/") + content, err := fs.ReadFile(*p.TargetFs, indexPath) + if err == nil { + body := strings.ReplaceAll(string(content), "{{.csrfToken}}", csrfToken) + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + w.Write([]byte(body)) + return + } + } + fsHandler.ServeHTTP(w, r) + }) +} + +// Handler returns an http.Handler for the embedded UI. +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rewrittenURL := strings.TrimPrefix(r.RequestURI, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + p.csrfMiddleware(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} + +// RegisterTerminateHandler registers a graceful shutdown endpoint at {prefix}/term. +func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) { + p.terminateHandler = termFunc + if mux == nil { + mux = http.DefaultServeMux + } + mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) { + p.terminateHandler() + w.WriteHeader(http.StatusOK) + go func() { + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) +} + +// HandleFunc registers a handler under the UI path prefix. +func (p *PluginUiRouter) HandleFunc(pattern string, handler http.HandlerFunc, mux *http.ServeMux) { + if mux == nil { + mux = http.DefaultServeMux + } + if !strings.HasPrefix(pattern, p.HandlerPrefix) { + pattern = p.HandlerPrefix + pattern + } + mux.HandleFunc(pattern, handler) +} + +// AttachHandlerToMux attaches the UI file handler to the mux. +func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) { + if mux == nil { + mux = http.DefaultServeMux + } + p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/") + mux.Handle(p.HandlerPrefix+"/", p.Handler()) +} diff --git a/zoraxy_plugin/zoraxy_plugin.go b/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..a60dbab --- /dev/null +++ b/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,119 @@ +// Package zoraxy_plugin provides the Zoraxy plugin interface types and helpers. +// This is a vendored copy from github.com/tobychui/zoraxy/src/mod/plugins/zoraxy_plugin/ +// Licensed under LGPL. +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "strings" +) + +type PluginType int + +const ( + PluginType_Router PluginType = 0 + PluginType_Utilities PluginType = 1 +) + +type StaticCaptureRule struct { + CapturePath string `json:"capture_path"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 + ControlStatusCode_UNHANDLED ControlStatusCode = 284 + ControlStatusCode_ERROR ControlStatusCode = 580 +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` + DevelopmentBuild bool `json:"development_build"` +} + +type PermittedAPIEndpoint struct { + Method string `json:"method"` + Endpoint string `json:"endpoint"` + Reason string `json:"reason"` +} + +type IntroSpect struct { + ID string `json:"id"` + Name string `json:"name"` + Author string `json:"author"` + AuthorContact string `json:"author_contact"` + Description string `json:"description"` + URL string `json:"url"` + Type PluginType `json:"type"` + VersionMajor int `json:"version_major"` + VersionMinor int `json:"version_minor"` + VersionPatch int `json:"version_patch"` + + StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` + StaticCaptureIngress string `json:"static_capture_ingress"` + + DynamicCaptureSniff string `json:"dynamic_capture_sniff"` + DynamicCaptureIngress string `json:"dynamic_capture_ingress"` + + UIPath string `json:"ui_path"` + + SubscriptionPath string `json:"subscription_path"` + SubscriptionsEvents map[string]string `json:"subscriptions_events"` + + PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` +} + +type ConfigureSpec struct { + Port int `json:"port"` + RuntimeConst RuntimeConstantValue `json:"runtime_const"` + APIKey string `json:"api_key,omitempty"` + ZoraxyPort int `json:"zoraxy_port,omitempty"` +} + +// ServeIntroSpect checks for -introspect flag and prints the spec as JSON, then exits. +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +// RecvConfigureSpec reads the -configure flag from command line args. +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var spec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &spec); err != nil { + return nil, err + } + return &spec, nil + } else if arg == "-configure" { + var spec ConfigureSpec + if len(os.Args) > i+1 { + if err := json.Unmarshal([]byte(os.Args[i+1]), &spec); err != nil { + return nil, err + } + return &spec, nil + } + return nil, fmt.Errorf("no argument after -configure flag") + } + } + return nil, fmt.Errorf("-configure flag not found") +} + +// ServeAndRecvSpec serves introspection and returns config in one call. +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +}