Added embed server for plugin library

- Added embeded resources server for plugin library
- Added ztnc plugin for global area network
- Added wide mode for side wrapper
This commit is contained in:
Toby Chui
2025-02-28 15:46:57 +08:00
parent bddff0cf2f
commit 53657e8716
54 changed files with 4870 additions and 42 deletions

View File

@@ -6,6 +6,8 @@ import (
"fmt"
"os/exec"
"time"
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
)
// LoadPlugin loads a plugin from the plugin directory
@@ -42,8 +44,8 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) {
}
// GetPluginEntryPoint returns the plugin entry point
func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) {
pluginSpec := IntroSpect{}
func (m *Manager) GetPluginSpec(entryPoint string) (*zoraxyPlugin.IntroSpect, error) {
pluginSpec := zoraxyPlugin.IntroSpect{}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

View File

@@ -13,6 +13,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
)
func (m *Manager) StartPlugin(pluginID string) error {
@@ -37,7 +38,7 @@ func (m *Manager) StartPlugin(pluginID string) error {
}
//Prepare plugin start configuration
pluginConfiguration := ConfigureSpec{
pluginConfiguration := zoraxyPlugin.ConfigureSpec{
Port: getRandomPortNumber(),
RuntimeConst: *m.Options.SystemConst,
}
@@ -100,18 +101,25 @@ func (m *Manager) StartUIHandlerForPlugin(targetPlugin *Plugin, pluginListeningP
pluginUIRelPath = "/" + pluginUIRelPath
}
// Remove the trailing slash if it exists
pluginUIRelPath = strings.TrimSuffix(pluginUIRelPath, "/")
pluginUIURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(pluginListeningPort) + pluginUIRelPath)
if err != nil {
return err
}
// Generate the plugin subpath to be trimmed
pluginMatchingPath := filepath.ToSlash(filepath.Join("/plugin.ui/"+targetPlugin.Spec.ID+"/")) + "/"
if targetPlugin.Spec.UIPath != "" {
targetPlugin.uiProxy = dpcore.NewDynamicProxyCore(
pluginUIURL,
"",
pluginMatchingPath,
&dpcore.DpcoreOptions{
IgnoreTLSVerification: true,
},
)
targetPlugin.AssignedPort = pluginListeningPort
m.LoadedPlugins.Store(targetPlugin.Spec.ID, targetPlugin)
}
return nil

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

View File

@@ -2,7 +2,7 @@ package plugins
import (
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
@@ -13,23 +13,27 @@ import (
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/info/logger"
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
"imuslab.com/zoraxy/mod/utils"
)
type Plugin struct {
RootDir string //The root directory of the plugin
Spec *IntroSpect //The plugin specification
Enabled bool //Whether the plugin is enabled
RootDir string //The root directory of the plugin
Spec *zoraxyPlugin.IntroSpect //The plugin specification
Enabled bool //Whether the plugin is enabled
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
process *exec.Cmd //The process of the plugin
//Runtime
AssignedPort int //The assigned port for the plugin
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
process *exec.Cmd //The process of the plugin
}
type ManagerOptions struct {
PluginDir string
SystemConst *RuntimeConstantValue
Database *database.Database
Logger *logger.Logger
PluginDir string
SystemConst *zoraxyPlugin.RuntimeConstantValue
Database *database.Database
Logger *logger.Logger
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
}
type Manager struct {
@@ -80,7 +84,6 @@ func (m *Manager) LoadPluginsFromDisk() error {
m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil)
// If the plugin was enabled, start it now
fmt.Println(m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) {
err = m.StartPlugin(thisPlugin.Spec.ID)
if err != nil {

View File

@@ -2,6 +2,9 @@ package plugins
import (
"net/http"
"net/url"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/utils"
@@ -28,14 +31,25 @@ func (m *Manager) HandlePluginUI(pluginID string, w http.ResponseWriter, r *http
return
}
upstreamOrigin := "127.0.0.1:" + strconv.Itoa(plugin.AssignedPort)
matchingPath := "/plugin.ui/" + plugin.Spec.ID
//Rewrite the request path to the plugin UI path
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, matchingPath)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
//Call the plugin UI handler
plugin.uiProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
UseTLS: false,
OriginalHost: r.Host,
ProxyDomain: r.Host,
ProxyDomain: upstreamOrigin,
NoCache: true,
PathPrefix: "/plugin.ui/" + pluginID,
PathPrefix: matchingPath,
Version: m.Options.SystemConst.ZoraxyVersion,
UpstreamHeaders: [][]string{
{"X-Zoraxy-Csrf", m.Options.CSRFTokenGen(r)},
},
})
}

View File

@@ -8,6 +8,7 @@ import (
"runtime"
"imuslab.com/zoraxy/mod/netutils"
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
)
/*
@@ -61,7 +62,7 @@ func getRandomPortNumber() int {
return portNo
}
func validatePluginSpec(pluginSpec *IntroSpect) error {
func validatePluginSpec(pluginSpec *zoraxyPlugin.IntroSpect) error {
if pluginSpec.Name == "" {
return errors.New("plugin name is empty")
}

View File

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

View File

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

View File

@@ -1,18 +1,20 @@
package plugins
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
)
/*
Plugins Includes.go
This file contains the common types and structs that are used by the plugins
If you are building a Zoraxy plugin with Golang, you can use this file to include
the common types and structs that are used by the plugins
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
@@ -184,3 +186,25 @@ 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)
}()
}