chore: initial scaffold — Zoraxy WAF plugin (Type 1 Utilities)
This commit is contained in:
commit
14c41bce42
11 changed files with 389 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module zoraxy-waf
|
||||
|
||||
go 1.21
|
||||
60
main.go
Normal file
60
main.go
Normal 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
3
models.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
// Data models go here.
|
||||
3
server.go
Normal file
3
server.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
// HTTP handlers and core logic go here.
|
||||
4
web/app.js
Normal file
4
web/app.js
Normal 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
22
web/index.html
Normal 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
18
web/style.css
Normal 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
BIN
zoraxy-waf
Executable file
Binary file not shown.
135
zoraxy_plugin/embed_webserver.go
Normal file
135
zoraxy_plugin/embed_webserver.go
Normal 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())
|
||||
}
|
||||
119
zoraxy_plugin/zoraxy_plugin.go
Normal file
119
zoraxy_plugin/zoraxy_plugin.go
Normal 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()
|
||||
}
|
||||
Loading…
Reference in a new issue