diff --git a/.gitignore b/.gitignore index 8eea333..2c708ee 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ src/log/ /Dockerfile /Entrypoint.sh example/plugins/zerotiernc/authtoken.secret +example/plugins/ztnc/ztnc.db diff --git a/example/plugins/build_all.sh b/example/plugins/build_all.sh new file mode 100644 index 0000000..76d3792 --- /dev/null +++ b/example/plugins/build_all.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Iterate over all directories in the current directory +for dir in */; do + if [ -d "$dir" ]; then + echo "Processing directory: $dir" + cd "$dir" + + # Execute go mod tidy + echo "Running go mod tidy in $dir" + go mod tidy + + # Execute go build + echo "Running go build in $dir" + go build + + # Return to the parent directory + cd .. + fi +done + +echo "Build process completed for all directories." \ No newline at end of file diff --git a/example/plugins/debugger/go.mod b/example/plugins/debugger/go.mod new file mode 100644 index 0000000..ec640ed --- /dev/null +++ b/example/plugins/debugger/go.mod @@ -0,0 +1,3 @@ +module aroz.org/zoraxy/debugger + +go 1.23.6 diff --git a/example/plugins/debugger/main.go b/example/plugins/debugger/main.go new file mode 100644 index 0000000..4e1b15d --- /dev/null +++ b/example/plugins/debugger/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin" +) + +const ( + PLUGIN_ID = "org.aroz.zoraxy.debugger" + UI_PATH = "/debug" +) + +func main() { + // Serve the plugin intro spect + // This will print the plugin intro spect and exit if the -introspect flag is provided + runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{ + ID: "org.aroz.zoraxy.debugger", + Name: "Plugin Debugger", + Author: "aroz.org", + AuthorContact: "https://aroz.org", + Description: "A debugger for Zoraxy <-> plugin communication pipeline", + URL: "https://zoraxy.aroz.org", + Type: plugin.PluginType_Router, + VersionMajor: 1, + VersionMinor: 0, + VersionPatch: 0, + + GlobalCapturePaths: []plugin.CaptureRule{ + { + CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule + IncludeSubPaths: true, + }, + }, + GlobalCaptureIngress: "", + AlwaysCapturePaths: []plugin.CaptureRule{}, + AlwaysCaptureIngress: "", + + UIPath: UI_PATH, + + /* + SubscriptionPath: "/subept", + SubscriptionsEvents: []plugin.SubscriptionEvent{ + */ + }) + if err != nil { + //Terminate or enter standalone mode here + panic(err) + } + + // Register the shutdown handler + plugin.RegisterShutdownHandler(func() { + // Do cleanup here if needed + fmt.Println("Debugger Terminated") + }) + + http.HandleFunc(UI_PATH+"/", RenderDebugUI) + http.HandleFunc("/gcapture", HandleIngressCapture) + fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) + http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil) +} + +// Handle the captured request +func HandleIngressCapture(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Capture request received") + w.Header().Set("Content-Type", "text/html") + w.Write([]byte("This request is captured by the debugger")) +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/README.txt b/example/plugins/debugger/mod/zoraxy_plugin/README.txt new file mode 100644 index 0000000..ed8a405 --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/README.txt @@ -0,0 +1,19 @@ +# Zoraxy Plugin + +## Overview +This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components. + +## Instructions + +1. **Copy the Module:** + - Copy the entire `zoraxy_plugin` module to your plugin mod folder. + +2. **Include the Structure:** + - Ensure that you maintain the directory structure and file organization as provided in this module. + +3. **Modify as Needed:** + - Customize the copied module to implement the desired functionality for your plugin. + +## Directory Structure + zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup + embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages \ No newline at end of file diff --git a/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go new file mode 100644 index 0000000..d9b3fde --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/embed_webserver.go @@ -0,0 +1,106 @@ +package zoraxy_plugin + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + "net/url" + "strings" + "time" +) + +type PluginUiRouter struct { + PluginID string //The ID of the plugin + TargetFs *embed.FS //The embed.FS where the UI files are stored + TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web + HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui +} + +// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS +// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored +// The targetFsPrefix should be relative to the root of the embed.FS +// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS +// The handlerPrefix is the prefix of the handler used to route this router +// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path +// All prefix should not end with a slash +func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter { + //Make sure all prefix are in /prefix format + if !strings.HasPrefix(targetFsPrefix, "/") { + targetFsPrefix = "/" + targetFsPrefix + } + targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/") + + if !strings.HasPrefix(handlerPrefix, "/") { + handlerPrefix = "/" + handlerPrefix + } + handlerPrefix = strings.TrimSuffix(handlerPrefix, "/") + + //Return the PluginUiRouter + return &PluginUiRouter{ + PluginID: pluginID, + TargetFs: targetFs, + TargetFsPrefix: targetFsPrefix, + HandlerPrefix: handlerPrefix, + } +} + +func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler { + //Get the CSRF token from header + csrfToken := r.Header.Get("X-Zoraxy-Csrf") + if csrfToken == "" { + csrfToken = "missing-csrf-token" + } + + //Return the middleware + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request is for an HTML file + if strings.HasSuffix(r.URL.Path, "/") { + // Redirect to the index.html + http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound) + return + } + if strings.HasSuffix(r.URL.Path, ".html") { + //Read the target file from embed.FS + targetFilePath := strings.TrimPrefix(r.URL.Path, "/") + targetFilePath = p.TargetFsPrefix + "/" + targetFilePath + targetFilePath = strings.TrimPrefix(targetFilePath, "/") + targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + body := string(targetFileContent) + body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken) + http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body)) + return + } + + //Call the next handler + fsHandler.ServeHTTP(w, r) + }) + +} + +// GetHttpHandler returns the http.Handler for the PluginUiRouter +func (p *PluginUiRouter) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + //Remove the plugin UI handler path prefix + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + r.URL, _ = url.Parse(rewrittenURL) + r.RequestURI = rewrittenURL + + //Serve the file from the embed.FS + subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/")) + if err != nil { + fmt.Println(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Replace {{csrf_token}} with the actual CSRF token and serve the file + p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r) + }) +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go new file mode 100644 index 0000000..f3865ea --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/zoraxy_plugin.go @@ -0,0 +1,198 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" +) + +/* + Plugins Includes.go + + This file is copied from Zoraxy source code + You can always find the latest version under mod/plugins/includes.go + Usually this file are backward compatible +*/ + +type PluginType int + +const ( + PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic + PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore +) + +type CaptureRule struct { + CapturePath string `json:"capture_path"` + IncludeSubPaths bool `json:"include_sub_paths"` +} + +type ControlStatusCode int + +const ( + ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic + ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic + ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error +) + +type SubscriptionEvent struct { + EventName string `json:"event_name"` + EventSource string `json:"event_source"` + Payload string `json:"payload"` //Payload of the event, can be empty +} + +type RuntimeConstantValue struct { + ZoraxyVersion string `json:"zoraxy_version"` + ZoraxyUUID string `json:"zoraxy_uuid"` +} + +/* +IntroSpect Payload + +When the plugin is initialized with -introspect flag, +the plugin shell return this payload as JSON and exit +*/ +type IntroSpect struct { + /* Plugin metadata */ + ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname + Name string `json:"name"` //Name of your plugin + Author string `json:"author"` //Author name of your plugin + AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email + Description string `json:"description"` //Description of your plugin + URL string `json:"url"` //URL of your plugin + Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1) + VersionMajor int `json:"version_major"` //Major version of your plugin + VersionMinor int `json:"version_minor"` //Minor version of your plugin + VersionPatch int `json:"version_patch"` //Patch version of your plugin + + /* + + Endpoint Settings + + */ + + /* + Global Capture Settings + + Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on + This captures the whole traffic of Zoraxy + + */ + GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin + GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) + + /* + Always Capture Settings + + Once the plugin is enabled on a given HTTP Proxy rule, + these always applies + */ + AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp) + AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) + + /* UI Path for your plugin */ + UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI + + /* Subscriptions Settings */ + SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered + SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details +} + +/* +ServeIntroSpect Function + +This function will check if the plugin is initialized with -introspect flag, +if so, it will print the intro spect and exit + +Place this function at the beginning of your plugin main function +*/ +func ServeIntroSpect(pluginSpect *IntroSpect) { + if len(os.Args) > 1 && os.Args[1] == "-introspect" { + //Print the intro spect and exit + jsonData, _ := json.MarshalIndent(pluginSpect, "", " ") + fmt.Println(string(jsonData)) + os.Exit(0) + } +} + +/* +ConfigureSpec Payload + +Zoraxy will start your plugin with -configure flag, +the plugin shell read this payload as JSON and configure itself +by the supplied values like starting a web server at given port +that listens to 127.0.0.1:port +*/ +type ConfigureSpec struct { + Port int `json:"port"` //Port to listen + RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values + //To be expanded +} + +/* +RecvExecuteConfigureSpec Function + +This function will read the configure spec from Zoraxy +and return the ConfigureSpec object + +Place this function after ServeIntroSpect function in your plugin main function +*/ +func RecvConfigureSpec() (*ConfigureSpec, error) { + for i, arg := range os.Args { + if strings.HasPrefix(arg, "-configure=") { + var configSpec ConfigureSpec + if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil { + return nil, err + } + return &configSpec, nil + } else if arg == "-configure" { + var configSpec ConfigureSpec + var nextArg string + if len(os.Args) > i+1 { + nextArg = os.Args[i+1] + if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("No port specified after -configure flag") + } + return &configSpec, nil + } + } + return nil, fmt.Errorf("No -configure flag found") +} + +/* +ServeAndRecvSpec Function + +This function will serve the intro spect and return the configure spec +See the ServeIntroSpect and RecvConfigureSpec for more details +*/ +func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) { + ServeIntroSpect(pluginSpect) + return RecvConfigureSpec() +} + +/* + +Shutdown handler + +This function will register a shutdown handler for the plugin +The shutdown callback will be called when the plugin is shutting down +You can use this to clean up resources like closing database connections +*/ + +func RegisterShutdownHandler(shutdownCallback func()) { + // Set up a channel to receive OS signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Start a goroutine to listen for signals + go func() { + <-sigChan + shutdownCallback() + os.Exit(0) + }() +} diff --git a/example/plugins/debugger/ui_info.go b/example/plugins/debugger/ui_info.go new file mode 100644 index 0000000..6c53aeb --- /dev/null +++ b/example/plugins/debugger/ui_info.go @@ -0,0 +1,26 @@ +package main + +import ( + _ "embed" + "fmt" + "net/http" + "sort" +) + +// Render the debug UI +func RenderDebugUI(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n") + + headerKeys := make([]string, 0, len(r.Header)) + for name := range r.Header { + headerKeys = append(headerKeys, name) + } + sort.Strings(headerKeys) + for _, name := range headerKeys { + values := r.Header[name] + for _, value := range values { + fmt.Fprintf(w, "%s: %s\n", name, value) + } + } + w.Header().Set("Content-Type", "text/html") +} diff --git a/example/plugins/helloworld/www/index.html b/example/plugins/helloworld/www/index.html index 2dcf1f1..bc48067 100644 --- a/example/plugins/helloworld/www/index.html +++ b/example/plugins/helloworld/www/index.html @@ -19,6 +19,7 @@ height: 100vh; margin: 0; font-family: Arial, sans-serif; + background:none; } diff --git a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go index 35580dd..2a264a2 100644 --- a/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go +++ b/example/plugins/helloworld/zoraxy_plugin/embed_webserver.go @@ -65,7 +65,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl targetFilePath := strings.TrimPrefix(r.URL.Path, "/") targetFilePath = p.TargetFsPrefix + "/" + targetFilePath targetFilePath = strings.TrimPrefix(targetFilePath, "/") - fmt.Println(targetFilePath) targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath) if err != nil { http.Error(w, "File not found", http.StatusNotFound) diff --git a/example/plugins/ztnc/authtoken.secret b/example/plugins/ztnc/authtoken.secret deleted file mode 100644 index fa08db2..0000000 --- a/example/plugins/ztnc/authtoken.secret +++ /dev/null @@ -1 +0,0 @@ -hgaode9ptnpuaoi1ilbdw9i4 \ No newline at end of file diff --git a/example/plugins/ztnc/mod/database/database_openwrt.go b/example/plugins/ztnc/mod/database/database_openwrt.go index e128a3a..fd3d8b2 100644 --- a/example/plugins/ztnc/mod/database/database_openwrt.go +++ b/example/plugins/ztnc/mod/database/database_openwrt.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "aroz.org/zoraxy/zerotiernc/mod/database/dbinc" + "aroz.org/zoraxy/ztnc/mod/database/dbinc" ) /* diff --git a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go index 8423c56..91ce202 100644 --- a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go +++ b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go @@ -9,7 +9,7 @@ import ( "os/user" "strings" - "aroz.org/zoraxy/zerotiernc/mod/utils" + "aroz.org/zoraxy/ztnc/mod/utils" ) func readAuthTokenAsAdmin() (string, error) { diff --git a/example/plugins/ztnc/web/index.html b/example/plugins/ztnc/web/index.html index 97108bd..34d2974 100644 --- a/example/plugins/ztnc/web/index.html +++ b/example/plugins/ztnc/web/index.html @@ -15,6 +15,11 @@ + diff --git a/example/plugins/ztnc/ztnc.db b/example/plugins/ztnc/ztnc.db deleted file mode 100644 index 70a17b5..0000000 Binary files a/example/plugins/ztnc/ztnc.db and /dev/null differ diff --git a/example/plugins/ztnc/ztnc.db.lock b/example/plugins/ztnc/ztnc.db.lock deleted file mode 100644 index e69de29..0000000