diff --git a/example/plugins/debugger/main.go b/example/plugins/debugger/main.go index 84631cc..fc9e5b7 100644 --- a/example/plugins/debugger/main.go +++ b/example/plugins/debugger/main.go @@ -3,7 +3,9 @@ package main import ( "fmt" "net/http" + "sort" "strconv" + "strings" plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin" ) @@ -39,6 +41,9 @@ func main() { }, StaticCaptureIngress: "/s_capture", + DynamicCaptureSniff: "/d_sniff", + DynamicCaptureIngress: "/d_capture", + UIPath: UI_PATH, /* @@ -51,9 +56,13 @@ func main() { panic(err) } - //Create a path handler for the capture paths + // Setup the path router pathRouter := plugin.NewPathRouter() pathRouter.SetDebugPrintMode(true) + + /* + Static Routers + */ pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA)) pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB)) pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -63,7 +72,46 @@ func main() { w.Header().Set("Content-Type", "text/html") w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String())) })) - pathRouter.RegisterHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux) + pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux) + + /* + Dynamic Captures + */ + pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult { + fmt.Println("Dynamic Capture Sniffed Request:") + fmt.Println("Request URI: " + dsfr.RequestURI) + + //In this example, we want to capture all URI + //that start with /test_ and forward it to the dynamic capture handler + if strings.HasPrefix(dsfr.RequestURI, "/test_") { + reqUUID := dsfr.GetRequestUUID() + fmt.Println("Accepting request with UUID: " + reqUUID) + return plugin.SniffResultAccpet + } + + return plugin.SniffResultSkip + }) + pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) { + // This is the dynamic capture handler where it actually captures and handle the request + w.WriteHeader(http.StatusOK) + w.Write([]byte("Welcome to the dynamic capture handler!")) + + // Print all the request info to the response writer + w.Write([]byte("\n\nRequest Info:\n")) + w.Write([]byte("Request URI: " + r.RequestURI + "\n")) + w.Write([]byte("Request Method: " + r.Method + "\n")) + w.Write([]byte("Request Headers:\n")) + headers := make([]string, 0, len(r.Header)) + for key := range r.Header { + headers = append(headers, key) + } + sort.Strings(headers) + for _, key := range headers { + for _, value := range r.Header[key] { + w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value))) + } + } + }) http.HandleFunc(UI_PATH+"/", RenderDebugUI) fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port)) @@ -72,21 +120,21 @@ func main() { // Handle the captured request func HandleCaptureA(w http.ResponseWriter, r *http.Request) { - for key, values := range r.Header { + /*for key, values := range r.Header { for _, value := range values { fmt.Printf("%s: %s\n", key, value) } - } + }*/ w.Header().Set("Content-Type", "text/html") w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String())) } func HandleCaptureB(w http.ResponseWriter, r *http.Request) { - for key, values := range r.Header { + /*for key, values := range r.Header { for _, value := range values { fmt.Printf("%s: %s\n", key, value) } - } + }*/ w.Header().Set("Content-Type", "text/html") w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String())) } diff --git a/example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go new file mode 100644 index 0000000..1dc53ce --- /dev/null +++ b/example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go @@ -0,0 +1,162 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +/* + + Dynamic Path Handler + +*/ + +type SniffResult int + +const ( + SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress + SniffResultSkip // Skip this plugin and let the next plugin handle the request +) + +type SniffHandler func(*DynamicSniffForwardRequest) SniffResult + +/* +RegisterDynamicSniffHandler registers a dynamic sniff handler for a path +You can decide to accept or skip the request based on the request header and paths +*/ +func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) { + if !strings.HasSuffix(sniff_ingress, "/") { + sniff_ingress = sniff_ingress + "/" + } + mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI) + } + + // Decode the request payload + jsonBytes, err := io.ReadAll(r.Body) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error reading request body:", err) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + payload, err := DecodeForwardRequestPayload(jsonBytes) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error decoding request payload:", err) + fmt.Print("Payload: ") + fmt.Println(string(jsonBytes)) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Get the forwarded request UUID + forwardUUID := r.Header.Get("X-Zoraxy-RequestID") + payload.requestUUID = forwardUUID + payload.rawRequest = r + + sniffResult := handler(&payload) + if sniffResult == SniffResultAccpet { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("SKIP")) + } + })) +} + +// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler +func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic capture path: " + r.RequestURI) + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + if !strings.HasPrefix(rewrittenURL, "/") { + rewrittenURL = "/" + rewrittenURL + } + r.RequestURI = rewrittenURL + + handlefunc(w, r) + })) +} + +/* + Sniffing and forwarding + + The following functions are here to help with + sniffing and forwarding requests to the dynamic + router. +*/ +// A custom request object to be used in the dynamic sniffing +type DynamicSniffForwardRequest struct { + Method string `json:"method"` + Hostname string `json:"hostname"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + RemoteAddr string `json:"remote_addr"` + Host string `json:"host"` + RequestURI string `json:"request_uri"` + Proto string `json:"proto"` + ProtoMajor int `json:"proto_major"` + ProtoMinor int `json:"proto_minor"` + + /* Internal use */ + rawRequest *http.Request `json:"-"` + requestUUID string `json:"-"` +} + +// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object +func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest { + return DynamicSniffForwardRequest{ + Method: r.Method, + Hostname: r.Host, + URL: r.URL.String(), + Header: r.Header, + RemoteAddr: r.RemoteAddr, + Host: r.Host, + RequestURI: r.RequestURI, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + rawRequest: r, + } +} + +// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object +func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) { + var payload DynamicSniffForwardRequest + err := json.Unmarshal(jsonBytes, &payload) + if err != nil { + return DynamicSniffForwardRequest{}, err + } + return payload, nil +} + +// GetRequest returns the original http.Request object, for debugging purposes +func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request { + return dsfr.rawRequest +} + +// GetRequestUUID returns the request UUID +// if this UUID is empty string, that might indicate the request +// is not coming from the dynamic router +func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string { + return dsfr.requestUUID +} diff --git a/example/plugins/debugger/mod/zoraxy_plugin/static_router.go b/example/plugins/debugger/mod/zoraxy_plugin/static_router.go index 13b07ff..f4abcb7 100644 --- a/example/plugins/debugger/mod/zoraxy_plugin/static_router.go +++ b/example/plugins/debugger/mod/zoraxy_plugin/static_router.go @@ -43,16 +43,18 @@ func (p *PathRouter) SetDebugPrintMode(enable bool) { p.enableDebugPrint = enable } -func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { +// StartStaticCapture starts the static capture ingress +func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) { if !strings.HasSuffix(capture_ingress, "/") { capture_ingress = capture_ingress + "/" } mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p.ServeHTTP(w, r) + p.staticCaptureServeHTTP(w, r) })) } -func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// staticCaptureServeHTTP serves the static capture path using user defined handler +func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) { capturePath := r.Header.Get("X-Zoraxy-Capture") if capturePath != "" { if p.enableDebugPrint { diff --git a/example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go new file mode 100644 index 0000000..1dc53ce --- /dev/null +++ b/example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go @@ -0,0 +1,162 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +/* + + Dynamic Path Handler + +*/ + +type SniffResult int + +const ( + SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress + SniffResultSkip // Skip this plugin and let the next plugin handle the request +) + +type SniffHandler func(*DynamicSniffForwardRequest) SniffResult + +/* +RegisterDynamicSniffHandler registers a dynamic sniff handler for a path +You can decide to accept or skip the request based on the request header and paths +*/ +func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) { + if !strings.HasSuffix(sniff_ingress, "/") { + sniff_ingress = sniff_ingress + "/" + } + mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI) + } + + // Decode the request payload + jsonBytes, err := io.ReadAll(r.Body) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error reading request body:", err) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + payload, err := DecodeForwardRequestPayload(jsonBytes) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error decoding request payload:", err) + fmt.Print("Payload: ") + fmt.Println(string(jsonBytes)) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Get the forwarded request UUID + forwardUUID := r.Header.Get("X-Zoraxy-RequestID") + payload.requestUUID = forwardUUID + payload.rawRequest = r + + sniffResult := handler(&payload) + if sniffResult == SniffResultAccpet { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("SKIP")) + } + })) +} + +// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler +func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic capture path: " + r.RequestURI) + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + if !strings.HasPrefix(rewrittenURL, "/") { + rewrittenURL = "/" + rewrittenURL + } + r.RequestURI = rewrittenURL + + handlefunc(w, r) + })) +} + +/* + Sniffing and forwarding + + The following functions are here to help with + sniffing and forwarding requests to the dynamic + router. +*/ +// A custom request object to be used in the dynamic sniffing +type DynamicSniffForwardRequest struct { + Method string `json:"method"` + Hostname string `json:"hostname"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + RemoteAddr string `json:"remote_addr"` + Host string `json:"host"` + RequestURI string `json:"request_uri"` + Proto string `json:"proto"` + ProtoMajor int `json:"proto_major"` + ProtoMinor int `json:"proto_minor"` + + /* Internal use */ + rawRequest *http.Request `json:"-"` + requestUUID string `json:"-"` +} + +// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object +func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest { + return DynamicSniffForwardRequest{ + Method: r.Method, + Hostname: r.Host, + URL: r.URL.String(), + Header: r.Header, + RemoteAddr: r.RemoteAddr, + Host: r.Host, + RequestURI: r.RequestURI, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + rawRequest: r, + } +} + +// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object +func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) { + var payload DynamicSniffForwardRequest + err := json.Unmarshal(jsonBytes, &payload) + if err != nil { + return DynamicSniffForwardRequest{}, err + } + return payload, nil +} + +// GetRequest returns the original http.Request object, for debugging purposes +func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request { + return dsfr.rawRequest +} + +// GetRequestUUID returns the request UUID +// if this UUID is empty string, that might indicate the request +// is not coming from the dynamic router +func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string { + return dsfr.requestUUID +} diff --git a/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go b/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go index 13b07ff..f4abcb7 100644 --- a/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go +++ b/example/plugins/helloworld/mod/zoraxy_plugin/static_router.go @@ -43,16 +43,18 @@ func (p *PathRouter) SetDebugPrintMode(enable bool) { p.enableDebugPrint = enable } -func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { +// StartStaticCapture starts the static capture ingress +func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) { if !strings.HasSuffix(capture_ingress, "/") { capture_ingress = capture_ingress + "/" } mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p.ServeHTTP(w, r) + p.staticCaptureServeHTTP(w, r) })) } -func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// staticCaptureServeHTTP serves the static capture path using user defined handler +func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) { capturePath := r.Header.Get("X-Zoraxy-Capture") if capturePath != "" { if p.enableDebugPrint { diff --git a/example/plugins/static_capture/go.mod b/example/plugins/static_capture/go.mod deleted file mode 100644 index 25a7842..0000000 --- a/example/plugins/static_capture/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module aroz.org/zoraxy/example/static_capture - -go 1.23.6 diff --git a/example/plugins/static_capture/main.go b/example/plugins/static_capture/main.go deleted file mode 100644 index 09ac268..0000000 --- a/example/plugins/static_capture/main.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "strconv" - - plugin "aroz.org/zoraxy/example/static_capture/mod/zoraxy_plugin" -) - -const ( - PLUGIN_ID = "org.aroz.zoraxy.static_capture" - UI_PATH = "/ui" - STATIC_CAPTURE_INGRESS = "/static_capture" -) - -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: PLUGIN_ID, - Name: "Static Capture Example", - Author: "aroz.org", - AuthorContact: "https://aroz.org", - Description: "An example plugin implementing static capture", - URL: "https://zoraxy.aroz.org", - Type: plugin.PluginType_Router, - VersionMajor: 1, - VersionMinor: 0, - VersionPatch: 0, - - /* - Static Capture Settings - - Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule - This is faster than dynamic capture, but less flexible - - In this example, we will capture two paths - /test_a and /test_b. Once this plugin is enabled on a HTTP proxy rule, let say - https://example.com the plugin will capture all requests to - https://example.com/test_a and https://example.com/test_b - and reverse proxy it to the StaticCaptureIngress path of your plugin like - /static_capture/test_a and /static_capture/test_b - */ - StaticCapturePaths: []plugin.StaticCaptureRule{ - { - CapturePath: "/test_a/", - }, - { - CapturePath: "/test_b/", - }, - }, - StaticCaptureIngress: STATIC_CAPTURE_INGRESS, - - UIPath: UI_PATH, - }) - if err != nil { - //Terminate or enter standalone mode here - panic(err) - } - - /* - Static Capture Router - - The plugin library already provided a path router to handle the static capture - paths. The path router will capture the requests to the specified paths and - restore the original request path before forwarding it to the handler. - - In this example, we will create a path router to handle the two capture paths - /test_a and /test_b. The path router will capture the requests to these paths - and print the request headers to the console. - */ - pathRouter := plugin.NewPathRouter() - pathRouter.SetDebugPrintMode(true) - pathRouter.RegisterPathHandler("/test_a/", http.HandlerFunc(HandleCaptureA)) - pathRouter.RegisterPathHandler("/test_b/", http.HandlerFunc(HandleCaptureB)) - pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - //In theory this should never be called - //but just in case the request is not captured by the path handlers - //like if you have forgotten to register the handler for the capture path - w.Header().Set("Content-Type", "text/html") - w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String())) - })) - //Lastly, register the path routing to the default http mux (or the mux you are using) - pathRouter.RegisterHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux) - - //Create a path handler for the UI - http.HandleFunc(UI_PATH+"/", RenderDebugUI) - 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 HandleCaptureA(w http.ResponseWriter, r *http.Request) { - fmt.Println("Request captured by A handler") - w.Header().Set("Content-Type", "text/html") - w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String())) -} - -func HandleCaptureB(w http.ResponseWriter, r *http.Request) { - fmt.Println("Request captured by B handler") - w.Header().Set("Content-Type", "text/html") - w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String())) -} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/README.txt b/example/plugins/static_capture/mod/zoraxy_plugin/README.txt deleted file mode 100644 index ed8a405..0000000 --- a/example/plugins/static_capture/mod/zoraxy_plugin/README.txt +++ /dev/null @@ -1,19 +0,0 @@ -# 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/static_capture/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go deleted file mode 100644 index c529e99..0000000 --- a/example/plugins/static_capture/mod/zoraxy_plugin/embed_webserver.go +++ /dev/null @@ -1,128 +0,0 @@ -package zoraxy_plugin - -import ( - "embed" - "fmt" - "io/fs" - "net/http" - "net/url" - "os" - "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 - - terminateHandler func() //The handler to be called when the plugin is terminated -} - -// 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) - }) -} - -// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter -// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager -// if mux is nil, the handler will be registered to http.DefaultServeMux -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() { - //Make sure the response is sent before the plugin is terminated - time.Sleep(100 * time.Millisecond) - os.Exit(0) - }() - }) -} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go b/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go deleted file mode 100644 index 13b07ff..0000000 --- a/example/plugins/static_capture/mod/zoraxy_plugin/static_router.go +++ /dev/null @@ -1,103 +0,0 @@ -package zoraxy_plugin - -import ( - "fmt" - "net/http" - "sort" - "strings" -) - -type PathRouter struct { - enableDebugPrint bool - pathHandlers map[string]http.Handler - defaultHandler http.Handler -} - -// NewPathRouter creates a new PathRouter -func NewPathRouter() *PathRouter { - return &PathRouter{ - enableDebugPrint: false, - pathHandlers: make(map[string]http.Handler), - } -} - -// RegisterPathHandler registers a handler for a path -func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) { - path = strings.TrimSuffix(path, "/") - p.pathHandlers[path] = handler -} - -// RemovePathHandler removes a handler for a path -func (p *PathRouter) RemovePathHandler(path string) { - delete(p.pathHandlers, path) -} - -// SetDefaultHandler sets the default handler for the router -// This handler will be called if no path handler is found -func (p *PathRouter) SetDefaultHandler(handler http.Handler) { - p.defaultHandler = handler -} - -// SetDebugPrintMode sets the debug print mode -func (p *PathRouter) SetDebugPrintMode(enable bool) { - p.enableDebugPrint = enable -} - -func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { - if !strings.HasSuffix(capture_ingress, "/") { - capture_ingress = capture_ingress + "/" - } - mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p.ServeHTTP(w, r) - })) -} - -func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - capturePath := r.Header.Get("X-Zoraxy-Capture") - if capturePath != "" { - if p.enableDebugPrint { - fmt.Printf("Using capture path: %s\n", capturePath) - } - originalURI := r.Header.Get("X-Zoraxy-Uri") - r.URL.Path = originalURI - if handler, ok := p.pathHandlers[capturePath]; ok { - handler.ServeHTTP(w, r) - return - } - } - p.defaultHandler.ServeHTTP(w, r) -} - -func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) { - if p.enableDebugPrint { - fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path) - keys := make([]string, 0, len(r.Header)) - for key := range r.Header { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - for _, value := range r.Header[key] { - fmt.Printf("%s: %s\n", key, value) - } - } - - fmt.Printf("\n\n**Request Details**\n\n") - fmt.Printf("Method: %s\n", r.Method) - fmt.Printf("URL: %s\n", r.URL.String()) - fmt.Printf("Proto: %s\n", r.Proto) - fmt.Printf("Host: %s\n", r.Host) - fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) - fmt.Printf("RequestURI: %s\n", r.RequestURI) - fmt.Printf("ContentLength: %d\n", r.ContentLength) - fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding) - fmt.Printf("Close: %v\n", r.Close) - fmt.Printf("Form: %v\n", r.Form) - fmt.Printf("PostForm: %v\n", r.PostForm) - fmt.Printf("MultipartForm: %v\n", r.MultipartForm) - fmt.Printf("Trailer: %v\n", r.Trailer) - fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr) - fmt.Printf("RequestURI: %s\n", r.RequestURI) - - } -} diff --git a/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go deleted file mode 100644 index 2cf494e..0000000 --- a/example/plugins/static_capture/mod/zoraxy_plugin/zoraxy_plugin.go +++ /dev/null @@ -1,175 +0,0 @@ -package zoraxy_plugin - -import ( - "encoding/json" - "fmt" - "os" - "strings" -) - -/* - 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 StaticCaptureRule struct { - CapturePath string `json:"capture_path"` - //To be expanded -} - -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 - - */ - - /* - Static Capture Settings - - Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule - This is faster than dynamic capture, but less flexible - */ - StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details - StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler) - - /* - Dynamic Capture Settings - - Once plugin is enabled, these rules will be captured and forward to plugin sniff - if the plugin sniff returns 280, the traffic will be captured - otherwise, the traffic will be forwarded to the next plugin - This is slower than static capture, but more flexible - */ - DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff) - DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_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() -} diff --git a/example/plugins/static_capture/ui_info.go b/example/plugins/static_capture/ui_info.go deleted file mode 100644 index 18c307b..0000000 --- a/example/plugins/static_capture/ui_info.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - _ "embed" - "fmt" - "net/http" - "sort" -) - -// Render the debug UI -func RenderDebugUI(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "**static_capture 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/ztnc/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go new file mode 100644 index 0000000..1dc53ce --- /dev/null +++ b/example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go @@ -0,0 +1,162 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +/* + + Dynamic Path Handler + +*/ + +type SniffResult int + +const ( + SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress + SniffResultSkip // Skip this plugin and let the next plugin handle the request +) + +type SniffHandler func(*DynamicSniffForwardRequest) SniffResult + +/* +RegisterDynamicSniffHandler registers a dynamic sniff handler for a path +You can decide to accept or skip the request based on the request header and paths +*/ +func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) { + if !strings.HasSuffix(sniff_ingress, "/") { + sniff_ingress = sniff_ingress + "/" + } + mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI) + } + + // Decode the request payload + jsonBytes, err := io.ReadAll(r.Body) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error reading request body:", err) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + payload, err := DecodeForwardRequestPayload(jsonBytes) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error decoding request payload:", err) + fmt.Print("Payload: ") + fmt.Println(string(jsonBytes)) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Get the forwarded request UUID + forwardUUID := r.Header.Get("X-Zoraxy-RequestID") + payload.requestUUID = forwardUUID + payload.rawRequest = r + + sniffResult := handler(&payload) + if sniffResult == SniffResultAccpet { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("SKIP")) + } + })) +} + +// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler +func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic capture path: " + r.RequestURI) + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + if !strings.HasPrefix(rewrittenURL, "/") { + rewrittenURL = "/" + rewrittenURL + } + r.RequestURI = rewrittenURL + + handlefunc(w, r) + })) +} + +/* + Sniffing and forwarding + + The following functions are here to help with + sniffing and forwarding requests to the dynamic + router. +*/ +// A custom request object to be used in the dynamic sniffing +type DynamicSniffForwardRequest struct { + Method string `json:"method"` + Hostname string `json:"hostname"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + RemoteAddr string `json:"remote_addr"` + Host string `json:"host"` + RequestURI string `json:"request_uri"` + Proto string `json:"proto"` + ProtoMajor int `json:"proto_major"` + ProtoMinor int `json:"proto_minor"` + + /* Internal use */ + rawRequest *http.Request `json:"-"` + requestUUID string `json:"-"` +} + +// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object +func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest { + return DynamicSniffForwardRequest{ + Method: r.Method, + Hostname: r.Host, + URL: r.URL.String(), + Header: r.Header, + RemoteAddr: r.RemoteAddr, + Host: r.Host, + RequestURI: r.RequestURI, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + rawRequest: r, + } +} + +// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object +func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) { + var payload DynamicSniffForwardRequest + err := json.Unmarshal(jsonBytes, &payload) + if err != nil { + return DynamicSniffForwardRequest{}, err + } + return payload, nil +} + +// GetRequest returns the original http.Request object, for debugging purposes +func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request { + return dsfr.rawRequest +} + +// GetRequestUUID returns the request UUID +// if this UUID is empty string, that might indicate the request +// is not coming from the dynamic router +func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string { + return dsfr.requestUUID +} diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go b/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go index 13b07ff..f4abcb7 100644 --- a/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go +++ b/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go @@ -43,16 +43,18 @@ func (p *PathRouter) SetDebugPrintMode(enable bool) { p.enableDebugPrint = enable } -func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { +// StartStaticCapture starts the static capture ingress +func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) { if !strings.HasSuffix(capture_ingress, "/") { capture_ingress = capture_ingress + "/" } mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p.ServeHTTP(w, r) + p.staticCaptureServeHTTP(w, r) })) } -func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// staticCaptureServeHTTP serves the static capture path using user defined handler +func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) { capturePath := r.Header.Get("X-Zoraxy-Capture") if capturePath != "" { if p.enableDebugPrint { diff --git a/src/mod/plugins/dynamic_forwarder.go b/src/mod/plugins/dynamic_forwarder.go new file mode 100644 index 0000000..54fea8c --- /dev/null +++ b/src/mod/plugins/dynamic_forwarder.go @@ -0,0 +1,111 @@ +package plugins + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/google/uuid" + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" +) + +// StartDynamicForwardRouter create and start a dynamic forward router for +// this plugin +func (p *Plugin) StartDynamicForwardRouter() error { + // Create a new dpcore object to forward the traffic to the plugin + targetURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(p.AssignedPort) + p.Spec.DynamicCaptureIngress) + if err != nil { + fmt.Println("Failed to parse target URL: "+targetURL.String(), err) + return err + } + thisRouter := dpcore.NewDynamicProxyCore(targetURL, "", &dpcore.DpcoreOptions{}) + p.dynamicRouteProxy = thisRouter + return nil +} + +// StopDynamicForwardRouter stops the dynamic forward router for this plugin +func (p *Plugin) StopDynamicForwardRouter() { + if p.dynamicRouteProxy != nil { + p.dynamicRouteProxy = nil + } +} + +// AcceptDynamicRoute returns whether this plugin accepts dynamic route +func (p *Plugin) AcceptDynamicRoute() bool { + return p.Spec.DynamicCaptureSniff != "" && p.Spec.DynamicCaptureIngress != "" +} + +func (p *Plugin) HandleDynamicRoute(w http.ResponseWriter, r *http.Request) bool { + //Make sure p.Spec.DynamicCaptureSniff and p.Spec.DynamicCaptureIngress are not empty and start with / + if !p.AcceptDynamicRoute() { + return false + } + + //Make sure the paths start with / and do not end with / + if !strings.HasPrefix(p.Spec.DynamicCaptureSniff, "/") { + p.Spec.DynamicCaptureSniff = "/" + p.Spec.DynamicCaptureSniff + } + p.Spec.DynamicCaptureSniff = strings.TrimSuffix(p.Spec.DynamicCaptureSniff, "/") + if !strings.HasPrefix(p.Spec.DynamicCaptureIngress, "/") { + p.Spec.DynamicCaptureIngress = "/" + p.Spec.DynamicCaptureIngress + } + p.Spec.DynamicCaptureIngress = strings.TrimSuffix(p.Spec.DynamicCaptureIngress, "/") + + //Send the request to the sniff endpoint + sniffURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(p.AssignedPort) + p.Spec.DynamicCaptureSniff + "/") + if err != nil { + //Error when parsing the sniff URL, let the next plugin handle the request + return false + } + + // Create an instance of CustomRequest with the original request's data + forwardReq := zoraxy_plugin.EncodeForwardRequestPayload(r) + + // Encode the custom request object into JSON + jsonData, err := json.Marshal(forwardReq) + if err != nil { + // Error when encoding the request, let the next plugin handle the request + return false + } + + //Generate a unique request ID + uniqueRequestID := uuid.New().String() + + req, err := http.NewRequest("POST", sniffURL.String(), bytes.NewBuffer(jsonData)) + if err != nil { + // Error when creating the request, let the next plugin handle the request + return false + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Zoraxy-RequestID", uniqueRequestID) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + // Error when sending the request, let the next plugin handle the request + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // Sniff endpoint did not return OK, let the next plugin handle the request + return false + } + + p.dynamicRouteProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + UseTLS: false, + OriginalHost: r.Host, + ProxyDomain: "127.0.0.1:" + strconv.Itoa(p.AssignedPort), + NoCache: true, + PathPrefix: p.Spec.DynamicCaptureIngress, + UpstreamHeaders: [][]string{ + {"X-Zoraxy-RequestID", uniqueRequestID}, + }, + }) + return true +} diff --git a/src/mod/plugins/lifecycle.go b/src/mod/plugins/lifecycle.go index e2462e8..ce964a5 100644 --- a/src/mod/plugins/lifecycle.go +++ b/src/mod/plugins/lifecycle.go @@ -46,6 +46,7 @@ func (m *Manager) StartPlugin(pluginID string) error { } js, _ := json.Marshal(pluginConfiguration) + //Start the plugin with given configuration m.Log("Starting plugin "+thisPlugin.Spec.Name+" at :"+strconv.Itoa(pluginConfiguration.Port), nil) cmd := exec.Command(absolutePath, "-configure="+string(js)) cmd.Dir = filepath.Dir(absolutePath) @@ -95,6 +96,12 @@ func (m *Manager) StartPlugin(pluginID string) error { //Create a new static forwarder router for each of the static capture paths plugin.(*Plugin).StartAllStaticPathRouters() + + //If the plugin contains dynamic capture, create a dynamic capture handler + if thisPlugin.AcceptDynamicRoute() { + plugin.(*Plugin).StartDynamicForwardRouter() + } + return nil } @@ -202,6 +209,7 @@ func (m *Manager) StopPlugin(pluginID string) error { thisPlugin.uiProxy = nil plugin.(*Plugin).Enabled = false plugin.(*Plugin).StopAllStaticPathRouters() + plugin.(*Plugin).StopDynamicForwardRouter() return nil } diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index 21799b6..eb2c8e7 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -39,7 +39,8 @@ func NewPluginManager(options *ManagerOptions) *Manager { return &Manager{ LoadedPlugins: sync.Map{}, - TagPluginMap: sync.Map{}, + tagPluginMap: sync.Map{}, + tagPluginList: make(map[string][]*Plugin), Options: options, } } @@ -76,7 +77,7 @@ func (m *Manager) LoadPluginsFromDisk() error { } //Generate the static forwarder radix tree - m.UpdateTagsToTree() + m.UpdateTagsToPluginMaps() return nil } @@ -98,7 +99,7 @@ func (m *Manager) EnablePlugin(pluginID string) error { } m.Options.Database.Write("plugins", pluginID, true) //Generate the static forwarder radix tree - m.UpdateTagsToTree() + m.UpdateTagsToPluginMaps() return nil } @@ -110,7 +111,7 @@ func (m *Manager) DisablePlugin(pluginID string) error { return err } //Generate the static forwarder radix tree - m.UpdateTagsToTree() + m.UpdateTagsToPluginMaps() return nil } @@ -184,11 +185,17 @@ func (m *Plugin) StartAllStaticPathRouters() { } } +// StopAllStaticPathRouters stops all static path routers func (m *Plugin) StopAllStaticPathRouters() { - + for path := range m.staticRouteProxy { + m.staticRouteProxy[path] = nil + delete(m.staticRouteProxy, path) + } + m.staticRouteProxy = make(map[string]*dpcore.ReverseProxy) } -func (p *Plugin) HandleRoute(w http.ResponseWriter, r *http.Request, longestPrefix string) { +// HandleStaticRoute handles the request to the plugin via static path captures (static forwarder) +func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longestPrefix string) { longestPrefix = strings.TrimSuffix(longestPrefix, "/") targetRouter := p.staticRouteProxy[longestPrefix] if targetRouter == nil { diff --git a/src/mod/plugins/static_forwarder.go b/src/mod/plugins/static_forwarder.go index 0f33c06..3acd286 100644 --- a/src/mod/plugins/static_forwarder.go +++ b/src/mod/plugins/static_forwarder.go @@ -15,12 +15,25 @@ import ( request path registered when the plugin started */ -func (m *Manager) UpdateTagsToTree() { +func (m *Manager) UpdateTagsToPluginMaps() { //build the tag to plugin pointer sync.Map - m.TagPluginMap = sync.Map{} + m.tagPluginMap = sync.Map{} for tag, pluginIds := range m.Options.PluginGroups { tree := m.GetForwarderRadixTreeFromPlugins(pluginIds) - m.TagPluginMap.Store(tag, tree) + m.tagPluginMap.Store(tag, tree) + } + + //build the plugin list for each tag + m.tagPluginList = make(map[string][]*Plugin) + for tag, pluginIds := range m.Options.PluginGroups { + for _, pluginId := range pluginIds { + plugin, err := m.GetPluginByID(pluginId) + if err != nil { + m.Log("Failed to get plugin by ID: "+pluginId, err) + continue + } + m.tagPluginList[tag] = append(m.tagPluginList[tag], plugin) + } } } @@ -61,8 +74,6 @@ func (m *Manager) GetForwarderRadixTreeFromPlugins(pluginIds []string) *radix.Tr } else { //The path has already been assigned to another plugin pluginList, _ := r.Get(captureRule.CapturePath) - //pluginList = append(pluginList.([]*Plugin), plugin) - //r.Insert(captureRule.CapturePath, pluginList) //Warn the path is already assigned to another plugin if plugin.Spec.ID == pluginList.([]*Plugin)[0].Spec.ID { diff --git a/src/mod/plugins/static_router.go b/src/mod/plugins/static_router.go deleted file mode 100644 index 0417389..0000000 --- a/src/mod/plugins/static_router.go +++ /dev/null @@ -1,47 +0,0 @@ -package plugins - -import ( - "net/http" - "sync" - - "github.com/armon/go-radix" -) - -// HandleRoute handles the request to the plugin -// return true if the request is handled by the plugin -func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []string) bool { - if len(tags) == 0 { - return false - } - - //For each tag, check if the request path matches the static capture path - var wg sync.WaitGroup //Wait group for the goroutines - var handler []*Plugin //The handler for the request, can be multiple plugins - var longestPrefixAcrossAlltags string = "" //The longest prefix across all tags - for _, tag := range tags { - wg.Add(1) - go func(thisTag string) { - defer wg.Done() - //Get the radix tree for the tag - tree, ok := m.TagPluginMap.Load(thisTag) - if !ok { - return - } - //Check if the request path matches the static capture path - longestPrefix, pluginList, ok := tree.(*radix.Tree).LongestPrefix(r.URL.Path) - if ok { - if longestPrefix > longestPrefixAcrossAlltags { - longestPrefixAcrossAlltags = longestPrefix - handler = pluginList.([]*Plugin) - } - } - }(tag) - } - wg.Wait() - if len(handler) > 0 { - //Handle the request - handler[0].HandleRoute(w, r, longestPrefixAcrossAlltags) - return true - } - return false -} diff --git a/src/mod/plugins/traffic_router.go b/src/mod/plugins/traffic_router.go new file mode 100644 index 0000000..b408a61 --- /dev/null +++ b/src/mod/plugins/traffic_router.go @@ -0,0 +1,73 @@ +package plugins + +import ( + "net/http" + "sync" + + "github.com/armon/go-radix" +) + +// HandleRoute handles the request to the plugin +// return true if the request is handled by the plugin +func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []string) bool { + if len(tags) == 0 { + return false + } + + //For each tag, check if the request path matches the static capture path + wg := sync.WaitGroup{} //Wait group for the goroutines + mutex := sync.Mutex{} //Mutex for the dynamic route handler + var staticRoutehandlers []*Plugin //The handler for the request, can be multiple plugins + var longestPrefixAcrossAlltags string = "" //The longest prefix across all tags + var dynamicRouteHandlers []*Plugin //The handler for the dynamic routes + for _, tag := range tags { + wg.Add(1) + go func(thisTag string) { + defer wg.Done() + //Get the radix tree for the tag + tree, ok := m.tagPluginMap.Load(thisTag) + if !ok { + return + } + + //Check if the request path matches the static capture path + longestPrefix, pluginList, ok := tree.(*radix.Tree).LongestPrefix(r.URL.Path) + if ok { + if longestPrefix > longestPrefixAcrossAlltags { + longestPrefixAcrossAlltags = longestPrefix + staticRoutehandlers = pluginList.([]*Plugin) + } + } + + }(tag) + + //Check if the plugin enabled dynamic route + wg.Add(1) + go func(thisTag string) { + defer wg.Done() + for _, plugin := range m.tagPluginList[thisTag] { + if plugin.Enabled && plugin.Spec.DynamicCaptureSniff != "" && plugin.Spec.DynamicCaptureIngress != "" { + mutex.Lock() + dynamicRouteHandlers = append(dynamicRouteHandlers, plugin) + mutex.Unlock() + } + } + }(tag) + } + wg.Wait() + + //Handle the static route if found + if len(staticRoutehandlers) > 0 { + //Handle the request + staticRoutehandlers[0].HandleStaticRoute(w, r, longestPrefixAcrossAlltags) + return true + } + + //No static route handler found, check for dynamic route handler + for _, plugin := range dynamicRouteHandlers { + if plugin.HandleDynamicRoute(w, r) { + return true + } + } + return false +} diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index d99caa4..8c27721 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -21,23 +21,27 @@ type Plugin struct { Enabled bool //Whether the plugin is enabled //Runtime - AssignedPort int //The assigned port for the plugin - uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI - staticRouteProxy map[string]*dpcore.ReverseProxy //Storing longest prefix => dpcore map for static route - process *exec.Cmd //The process of the plugin + AssignedPort int //The assigned port for the plugin + uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI + staticRouteProxy map[string]*dpcore.ReverseProxy //Storing longest prefix => dpcore map for static route + dynamicRouteProxy *dpcore.ReverseProxy //The reverse proxy for the dynamic route + process *exec.Cmd //The process of the plugin } type ManagerOptions struct { PluginDir string //The directory where the plugins are stored PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs + + /* Runtime */ SystemConst *zoraxyPlugin.RuntimeConstantValue - Database *database.Database - Logger *logger.Logger - CSRFTokenGen func(*http.Request) string //The CSRF token generator function + CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function + Database *database.Database `json:"-"` + Logger *logger.Logger `json:"-"` } type Manager struct { - LoadedPlugins sync.Map //Storing *Plugin - TagPluginMap sync.Map //Storing *radix.Tree for each plugin tag + LoadedPlugins sync.Map //Storing *Plugin + tagPluginMap sync.Map //Storing *radix.Tree for each plugin tag + tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed Options *ManagerOptions } diff --git a/src/mod/plugins/uirouter.go b/src/mod/plugins/ui_router.go similarity index 100% rename from src/mod/plugins/uirouter.go rename to src/mod/plugins/ui_router.go diff --git a/src/mod/plugins/zoraxy_plugin/dynamic_router.go b/src/mod/plugins/zoraxy_plugin/dynamic_router.go new file mode 100644 index 0000000..1dc53ce --- /dev/null +++ b/src/mod/plugins/zoraxy_plugin/dynamic_router.go @@ -0,0 +1,162 @@ +package zoraxy_plugin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +/* + + Dynamic Path Handler + +*/ + +type SniffResult int + +const ( + SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress + SniffResultSkip // Skip this plugin and let the next plugin handle the request +) + +type SniffHandler func(*DynamicSniffForwardRequest) SniffResult + +/* +RegisterDynamicSniffHandler registers a dynamic sniff handler for a path +You can decide to accept or skip the request based on the request header and paths +*/ +func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) { + if !strings.HasSuffix(sniff_ingress, "/") { + sniff_ingress = sniff_ingress + "/" + } + mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI) + } + + // Decode the request payload + jsonBytes, err := io.ReadAll(r.Body) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error reading request body:", err) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + payload, err := DecodeForwardRequestPayload(jsonBytes) + if err != nil { + if p.enableDebugPrint { + fmt.Println("Error decoding request payload:", err) + fmt.Print("Payload: ") + fmt.Println(string(jsonBytes)) + } + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Get the forwarded request UUID + forwardUUID := r.Header.Get("X-Zoraxy-RequestID") + payload.requestUUID = forwardUUID + payload.rawRequest = r + + sniffResult := handler(&payload) + if sniffResult == SniffResultAccpet { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + } else { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("SKIP")) + } + })) +} + +// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler +func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) { + if !strings.HasSuffix(capture_ingress, "/") { + capture_ingress = capture_ingress + "/" + } + mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if p.enableDebugPrint { + fmt.Println("Request captured by dynamic capture path: " + r.RequestURI) + } + + rewrittenURL := r.RequestURI + rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress) + rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/") + if rewrittenURL == "" { + rewrittenURL = "/" + } + if !strings.HasPrefix(rewrittenURL, "/") { + rewrittenURL = "/" + rewrittenURL + } + r.RequestURI = rewrittenURL + + handlefunc(w, r) + })) +} + +/* + Sniffing and forwarding + + The following functions are here to help with + sniffing and forwarding requests to the dynamic + router. +*/ +// A custom request object to be used in the dynamic sniffing +type DynamicSniffForwardRequest struct { + Method string `json:"method"` + Hostname string `json:"hostname"` + URL string `json:"url"` + Header map[string][]string `json:"header"` + RemoteAddr string `json:"remote_addr"` + Host string `json:"host"` + RequestURI string `json:"request_uri"` + Proto string `json:"proto"` + ProtoMajor int `json:"proto_major"` + ProtoMinor int `json:"proto_minor"` + + /* Internal use */ + rawRequest *http.Request `json:"-"` + requestUUID string `json:"-"` +} + +// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object +func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest { + return DynamicSniffForwardRequest{ + Method: r.Method, + Hostname: r.Host, + URL: r.URL.String(), + Header: r.Header, + RemoteAddr: r.RemoteAddr, + Host: r.Host, + RequestURI: r.RequestURI, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + rawRequest: r, + } +} + +// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object +func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) { + var payload DynamicSniffForwardRequest + err := json.Unmarshal(jsonBytes, &payload) + if err != nil { + return DynamicSniffForwardRequest{}, err + } + return payload, nil +} + +// GetRequest returns the original http.Request object, for debugging purposes +func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request { + return dsfr.rawRequest +} + +// GetRequestUUID returns the request UUID +// if this UUID is empty string, that might indicate the request +// is not coming from the dynamic router +func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string { + return dsfr.requestUUID +} diff --git a/src/mod/plugins/zoraxy_plugin/static_router.go b/src/mod/plugins/zoraxy_plugin/static_router.go index 13b07ff..f4abcb7 100644 --- a/src/mod/plugins/zoraxy_plugin/static_router.go +++ b/src/mod/plugins/zoraxy_plugin/static_router.go @@ -43,16 +43,18 @@ func (p *PathRouter) SetDebugPrintMode(enable bool) { p.enableDebugPrint = enable } -func (p *PathRouter) RegisterHandle(capture_ingress string, mux *http.ServeMux) { +// StartStaticCapture starts the static capture ingress +func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) { if !strings.HasSuffix(capture_ingress, "/") { capture_ingress = capture_ingress + "/" } mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - p.ServeHTTP(w, r) + p.staticCaptureServeHTTP(w, r) })) } -func (p *PathRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// staticCaptureServeHTTP serves the static capture path using user defined handler +func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) { capturePath := r.Header.Get("X-Zoraxy-Capture") if capturePath != "" { if p.enableDebugPrint { diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 6dabdbc..ee16336 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -931,7 +931,11 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) // Report the current status of the reverse proxy server func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) { - js, _ := json.Marshal(dynamicProxyRouter) + js, err := json.Marshal(dynamicProxyRouter) + if err != nil { + utils.SendErrorResponse(w, "Unable to marshal status data") + return + } utils.SendJSONResponse(w, string(js)) }