mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
Added plugin prototype
- Added proof of concept plugin prototype - Added wip plugin page
This commit is contained in:
parent
20959cd6cc
commit
ad13b33283
3
example/plugins/helloworld/go.mod
Normal file
3
example/plugins/helloworld/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module example.com/zoraxy/helloworld
|
||||
|
||||
go 1.23.6
|
24
example/plugins/helloworld/index.html
Normal file
24
example/plugins/helloworld/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="text-align: center;">
|
||||
<h1>Hello World</h1>
|
||||
<p>Welcome to your first Zoraxy plugin</p>
|
||||
</div>
|
||||
</body>
|
||||
</html></html>
|
49
example/plugins/helloworld/main.go
Normal file
49
example/plugins/helloworld/main.go
Normal file
@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/helloworld/zoraxy_plugin"
|
||||
)
|
||||
|
||||
//go:embed index.html
|
||||
var indexHTML string
|
||||
|
||||
func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, indexHTML)
|
||||
}
|
||||
|
||||
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: "com.example.helloworld",
|
||||
Name: "Hello World Plugin",
|
||||
Author: "foobar",
|
||||
AuthorContact: "admin@example.com",
|
||||
Description: "A simple hello world plugin",
|
||||
URL: "https://example.com",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
// As this is a utility plugin, we don't need to capture any traffic
|
||||
// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
|
||||
UIPath: "/",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Serve the hello world page
|
||||
// This will serve the index.html file embedded in the binary
|
||||
http.HandleFunc("/", helloWorldHandler)
|
||||
fmt.Println("Server started at http://localhost:" + strconv.Itoa(runtimeCfg.Port))
|
||||
http.ListenAndServe(":"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
}
|
186
example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go
Normal file
186
example/plugins/helloworld/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,186 @@
|
||||
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 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
|
||||
|
||||
Notes: Will raise a warning on the UI when the user enables the plugin on a HTTP Proxy rule
|
||||
*/
|
||||
GlobalCapturePath []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
|
||||
*/
|
||||
AlwaysCapturePath []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)
|
||||
|
||||
/*
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
the plugin can capture the request and decided if the request
|
||||
shall be handled by itself or let it pass through
|
||||
|
||||
*/
|
||||
DynmaicCaptureIngress string `json:"capture_path"` //Traffic capture path of your plugin (e.g. /capture)
|
||||
DynamicHandleIngress string `json:"handle_path"` //Traffic handle path of your plugin (e.g. /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()
|
||||
}
|
@ -30,6 +30,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
"imuslab.com/zoraxy/mod/plugins"
|
||||
"imuslab.com/zoraxy/mod/sshprox"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
@ -42,8 +43,8 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.1.8"
|
||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||
SYSTEM_VERSION = "3.1.9"
|
||||
DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */
|
||||
|
||||
/* System Constants */
|
||||
TMP_FOLDER = "./tmp"
|
||||
@ -139,6 +140,7 @@ var (
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//Authentication Provider
|
||||
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||
|
@ -1,5 +1,12 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
@ -103,6 +110,23 @@ type IntroSpect struct {
|
||||
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
|
||||
|
||||
@ -116,3 +140,47 @@ type ConfigureSpec struct {
|
||||
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()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
@ -29,6 +30,11 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = validatePluginSpec(pluginSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Plugin{
|
||||
Spec: pluginSpec,
|
||||
Enabled: false,
|
||||
@ -37,12 +43,12 @@ func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) {
|
||||
|
||||
// GetPluginEntryPoint returns the plugin entry point
|
||||
func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) {
|
||||
pluginSpec := &IntroSpect{}
|
||||
pluginSpec := IntroSpect{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, entryPoint, "-introspect")
|
||||
err := cmd.Run()
|
||||
output, err := cmd.Output()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("plugin introspect timed out")
|
||||
}
|
||||
@ -50,5 +56,11 @@ func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pluginSpec, nil
|
||||
// Assuming the output is JSON and needs to be unmarshaled into pluginSpec
|
||||
err = json.Unmarshal(output, &pluginSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal plugin spec: %v", err)
|
||||
}
|
||||
|
||||
return &pluginSpec, nil
|
||||
}
|
||||
|
@ -3,8 +3,13 @@ package plugins
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (m *Manager) StartPlugin(pluginID string) error {
|
||||
@ -13,8 +18,10 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
return errors.New("plugin not found")
|
||||
}
|
||||
|
||||
thisPlugin := plugin.(*Plugin)
|
||||
|
||||
//Get the plugin Entry point
|
||||
pluginEntryPoint, err := m.GetPluginEntryPoint(pluginID)
|
||||
pluginEntryPoint, err := m.GetPluginEntryPoint(thisPlugin.RootDir)
|
||||
if err != nil {
|
||||
//Plugin removed after introspect?
|
||||
return err
|
||||
@ -33,18 +40,85 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
}
|
||||
js, _ := json.Marshal(pluginConfiguration)
|
||||
|
||||
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)
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 1)
|
||||
lineBuf := ""
|
||||
for {
|
||||
n, err := stdoutPipe.Read(buf)
|
||||
if n > 0 {
|
||||
lineBuf += string(buf[:n])
|
||||
for {
|
||||
if idx := strings.IndexByte(lineBuf, '\n'); idx != -1 {
|
||||
m.handlePluginSTDOUT(pluginID, lineBuf[:idx])
|
||||
lineBuf = lineBuf[idx+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
m.handlePluginSTDOUT(pluginID, lineBuf) // handle any remaining data
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Store the cmd object so it can be accessed later for stopping the plugin
|
||||
plugin.(*Plugin).Process = cmd
|
||||
plugin.(*Plugin).Enabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handlePluginSTDOUT(pluginID string, line string) {
|
||||
thisPlugin, err := m.GetPluginByID(pluginID)
|
||||
processID := -1
|
||||
if thisPlugin.Process != nil && thisPlugin.Process.Process != nil {
|
||||
// Get the process ID of the plugin
|
||||
processID = thisPlugin.Process.Process.Pid
|
||||
}
|
||||
if err != nil {
|
||||
m.Log("[unknown:"+strconv.Itoa(processID)+"] "+line, err)
|
||||
return
|
||||
}
|
||||
m.Log("["+thisPlugin.Spec.Name+":"+strconv.Itoa(processID)+"] "+line, nil)
|
||||
}
|
||||
|
||||
func (m *Manager) StopPlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
}
|
||||
|
||||
thisPlugin := plugin.(*Plugin)
|
||||
thisPlugin.Process.Process.Signal(os.Interrupt)
|
||||
go func() {
|
||||
//Wait for 10 seconds for the plugin to stop gracefully
|
||||
time.Sleep(10 * time.Second)
|
||||
if thisPlugin.Process.ProcessState == nil || !thisPlugin.Process.ProcessState.Exited() {
|
||||
m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil)
|
||||
thisPlugin.Process.Process.Kill()
|
||||
} else {
|
||||
m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil)
|
||||
}
|
||||
}()
|
||||
plugin.(*Plugin).Enabled = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the plugin is still running
|
||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
|
@ -6,24 +6,24 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
RootDir string //The root directory of the plugin
|
||||
Spec *IntroSpect //The plugin specification
|
||||
Process *exec.Cmd //The process of the plugin
|
||||
Enabled bool //Whether the plugin is enabled
|
||||
}
|
||||
|
||||
type ManagerOptions struct {
|
||||
ZoraxyVersion string
|
||||
PluginDir string
|
||||
SystemConst *RuntimeConstantValue
|
||||
Database database.Database
|
||||
Logger *logger.Logger
|
||||
PluginDir string
|
||||
SystemConst *RuntimeConstantValue
|
||||
Database *database.Database
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@ -31,15 +31,24 @@ type Manager struct {
|
||||
Options *ManagerOptions
|
||||
}
|
||||
|
||||
// NewPluginManager creates a new plugin manager
|
||||
func NewPluginManager(options *ManagerOptions) *Manager {
|
||||
if options.PluginDir == "" {
|
||||
options.PluginDir = "./plugins"
|
||||
}
|
||||
|
||||
if !utils.FileExists(options.PluginDir) {
|
||||
os.MkdirAll(options.PluginDir, 0755)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
LoadedPlugins: sync.Map{},
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugins loads all plugins from the plugin directory
|
||||
func (m *Manager) LoadPlugins() error {
|
||||
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
||||
func (m *Manager) LoadPluginsFromDisk() error {
|
||||
// Load all plugins from the plugin directory
|
||||
foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir)
|
||||
if err != nil {
|
||||
@ -54,9 +63,20 @@ func (m *Manager) LoadPlugins() error {
|
||||
m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err)
|
||||
continue
|
||||
}
|
||||
thisPlugin.RootDir = pluginPath
|
||||
m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin)
|
||||
m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil)
|
||||
|
||||
//TODO: Move this to a separate function
|
||||
// Enable the plugin if it is enabled in the database
|
||||
err = m.StartPlugin(thisPlugin.Spec.ID)
|
||||
if err != nil {
|
||||
m.Log("Failed to enable plugin: "+thisPlugin.Spec.Name, err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -71,34 +91,21 @@ func (m *Manager) GetPluginByID(pluginID string) (*Plugin, error) {
|
||||
|
||||
// EnablePlugin enables a plugin
|
||||
func (m *Manager) EnablePlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
err := m.StartPlugin(pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plugin.(*Plugin).Enabled = true
|
||||
//TODO: Add database record
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisablePlugin disables a plugin
|
||||
func (m *Manager) DisablePlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
err := m.StopPlugin(pluginID)
|
||||
//TODO: Add database record
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thisPlugin := plugin.(*Plugin)
|
||||
thisPlugin.Process.Process.Signal(os.Interrupt)
|
||||
go func() {
|
||||
//Wait for 10 seconds for the plugin to stop gracefully
|
||||
time.Sleep(10 * time.Second)
|
||||
if thisPlugin.Process.ProcessState == nil || !thisPlugin.Process.ProcessState.Exited() {
|
||||
m.Log("Plugin "+thisPlugin.Spec.Name+" failed to stop gracefully, killing it", nil)
|
||||
thisPlugin.Process.Process.Kill()
|
||||
} else {
|
||||
m.Log("Plugin "+thisPlugin.Spec.Name+" background process stopped", nil)
|
||||
}
|
||||
}()
|
||||
thisPlugin.Enabled = false
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -60,3 +60,22 @@ func getRandomPortNumber() int {
|
||||
}
|
||||
return portNo
|
||||
}
|
||||
|
||||
func validatePluginSpec(pluginSpec *IntroSpect) error {
|
||||
if pluginSpec.Name == "" {
|
||||
return errors.New("plugin name is empty")
|
||||
}
|
||||
if pluginSpec.Description == "" {
|
||||
return errors.New("plugin description is empty")
|
||||
}
|
||||
if pluginSpec.Author == "" {
|
||||
return errors.New("plugin author is empty")
|
||||
}
|
||||
if pluginSpec.UIPath == "" {
|
||||
return errors.New("plugin UI path is empty")
|
||||
}
|
||||
if pluginSpec.ID == "" {
|
||||
return errors.New("plugin ID is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
20
src/start.go
20
src/start.go
@ -26,6 +26,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
"imuslab.com/zoraxy/mod/plugins"
|
||||
"imuslab.com/zoraxy/mod/sshprox"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
@ -317,6 +318,25 @@ func startupSequence() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
/*
|
||||
Plugin Manager
|
||||
*/
|
||||
|
||||
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
|
||||
PluginDir: "./plugins",
|
||||
SystemConst: &plugins.RuntimeConstantValue{
|
||||
ZoraxyVersion: SYSTEM_VERSION,
|
||||
ZoraxyUUID: nodeUUID,
|
||||
},
|
||||
Database: sysdb,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
|
||||
err = pluginManager.LoadPluginsFromDisk()
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err)
|
||||
}
|
||||
|
||||
/* Docker UX Optimizer */
|
||||
if runtime.GOOS == "windows" && *runningInDocker {
|
||||
SystemWideLogger.PrintAndLog("warning", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil)
|
||||
|
40
src/web/components/plugins.html
Normal file
40
src/web/components/plugins.html
Normal file
@ -0,0 +1,40 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Plugins Manager</h2>
|
||||
<p>Add custom features to Zoraxy</p>
|
||||
</div>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin Name</th>
|
||||
<th>Descriptions</th>
|
||||
<th>Catergory</th>
|
||||
<th>Version</th>
|
||||
<th>Author</th>
|
||||
<th>Action</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="PluginName">{{plugin.name}}</td>
|
||||
<td data-label="Descriptions">{{plugin.description}}</td>
|
||||
<td data-label="Category">{{plugin.category}}</td>
|
||||
<td data-label="Version">{{plugin.version}}</td>
|
||||
<td data-label="Author">{{plugin.author}}</td>
|
||||
<td data-label="Action">
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" name="enable">
|
||||
</div>
|
||||
<button class="ui basic small circular icon button"><i class="ui edit icon"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -78,6 +78,9 @@
|
||||
<i class="simplistic user circle icon"></i> SSO / Oauth
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="plugins">
|
||||
<i class="simplistic puzzle piece icon"></i> Plugins Manager
|
||||
</a>
|
||||
<a class="item" tag="webserv">
|
||||
<i class="simplistic globe icon"></i> Static Web Server
|
||||
</a>
|
||||
@ -138,7 +141,10 @@
|
||||
<!-- Web Server -->
|
||||
<div id="webserv" class="functiontab" target="webserv.html"></div>
|
||||
|
||||
<!-- Up Time Monitor -->
|
||||
<!-- Plugins -->
|
||||
<div id="plugins" class="functiontab" target="plugins.html"></div>
|
||||
|
||||
<!-- Up Time Monitor -->
|
||||
<div id="utm" class="functiontab" target="uptime.html"></div>
|
||||
|
||||
<!-- Network Tools -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user