chore: initial scaffold — Zoraxy WAF plugin (Type 1 Utilities)

This commit is contained in:
Claus Lohmar 2026-08-01 05:54:51 +00:00
commit 14c41bce42
11 changed files with 389 additions and 0 deletions

22
.gitignore vendored Normal file
View file

@ -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/

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module zoraxy-waf
go 1.21

60
main.go Normal file
View file

@ -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)
}
}

3
models.go Normal file
View file

@ -0,0 +1,3 @@
package main
// Data models go here.

3
server.go Normal file
View file

@ -0,0 +1,3 @@
package main
// HTTP handlers and core logic go here.

4
web/app.js Normal file
View file

@ -0,0 +1,4 @@
// Zoraxy WAF — client-side logic
$(document).ready(function () {
console.log('Zoraxy WAF plugin loaded');
});

22
web/index.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<title>Zoraxy WAF</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<header>
<h1>Zoraxy WAF</h1>
<p>Web Application Firewall — coming soon.</p>
</header>
<main>
<p>Configure firewall rules, rate limits, and IP blocking from this panel.</p>
</main>
<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/utils.js"></script>
<script src="./app.js"></script>
</body>
</html>

18
web/style.css Normal file
View file

@ -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; }

BIN
zoraxy-waf Executable file

Binary file not shown.

View file

@ -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())
}

View file

@ -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()
}