mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 22:27:47 +02:00
Added plugin interface definations
- Added wip plugin interface - Merged in PR for lego update - Minor code optimization
This commit is contained in:
@@ -314,7 +314,7 @@ func (router *Router) Restart() error {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
|
118
src/mod/plugins/includes.go
Normal file
118
src/mod/plugins/includes.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package plugins
|
||||
|
||||
/*
|
||||
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
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
54
src/mod/plugins/introspect.go
Normal file
54
src/mod/plugins/introspect.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoadPlugin loads a plugin from the plugin directory
|
||||
func (m *Manager) IsValidPluginFolder(path string) bool {
|
||||
_, err := m.GetPluginEntryPoint(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
/*
|
||||
LoadPluginSpec loads a plugin specification from the plugin directory
|
||||
Zoraxy will start the plugin binary or the entry point script
|
||||
with -introspect flag to get the plugin specification
|
||||
*/
|
||||
func (m *Manager) LoadPluginSpec(pluginPath string) (*Plugin, error) {
|
||||
pluginEntryPoint, err := m.GetPluginEntryPoint(pluginPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginSpec, err := m.GetPluginSpec(pluginEntryPoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Plugin{
|
||||
Spec: pluginSpec,
|
||||
Enabled: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPluginEntryPoint returns the plugin entry point
|
||||
func (m *Manager) GetPluginSpec(entryPoint string) (*IntroSpect, error) {
|
||||
pluginSpec := &IntroSpect{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, entryPoint, "-introspect")
|
||||
err := cmd.Run()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("plugin introspect timed out")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pluginSpec, nil
|
||||
}
|
67
src/mod/plugins/lifecycle.go
Normal file
67
src/mod/plugins/lifecycle.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (m *Manager) StartPlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
}
|
||||
|
||||
//Get the plugin Entry point
|
||||
pluginEntryPoint, err := m.GetPluginEntryPoint(pluginID)
|
||||
if err != nil {
|
||||
//Plugin removed after introspect?
|
||||
return err
|
||||
}
|
||||
|
||||
//Get the absolute path of the plugin entry point to prevent messing up with the cwd
|
||||
absolutePath, err := filepath.Abs(pluginEntryPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Prepare plugin start configuration
|
||||
pluginConfiguration := ConfigureSpec{
|
||||
Port: getRandomPortNumber(),
|
||||
RuntimeConst: *m.Options.SystemConst,
|
||||
}
|
||||
js, _ := json.Marshal(pluginConfiguration)
|
||||
|
||||
cmd := exec.Command(absolutePath, "-configure="+string(js))
|
||||
cmd.Dir = filepath.Dir(absolutePath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the cmd object so it can be accessed later for stopping the plugin
|
||||
plugin.(*Plugin).Process = cmd
|
||||
plugin.(*Plugin).Enabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the plugin is still running
|
||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return plugin.(*Plugin).Process.ProcessState == nil
|
||||
}
|
||||
|
||||
// BlockUntilAllProcessExited blocks until all the plugins processes have exited
|
||||
func (m *Manager) BlockUntilAllProcessExited() {
|
||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
if m.PluginStillRunning(value.(*Plugin).Spec.ID) {
|
||||
//Wait for the plugin to exit
|
||||
plugin.Process.Wait()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
117
src/mod/plugins/plugins.go
Normal file
117
src/mod/plugins/plugins.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
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
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
LoadedPlugins sync.Map //Storing *Plugin
|
||||
Options *ManagerOptions
|
||||
}
|
||||
|
||||
func NewPluginManager(options *ManagerOptions) *Manager {
|
||||
return &Manager{
|
||||
LoadedPlugins: sync.Map{},
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugins loads all plugins from the plugin directory
|
||||
func (m *Manager) LoadPlugins() error {
|
||||
// Load all plugins from the plugin directory
|
||||
foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, folder := range foldersInPluginDir {
|
||||
if folder.IsDir() {
|
||||
pluginPath := filepath.Join(m.Options.PluginDir, folder.Name())
|
||||
thisPlugin, err := m.LoadPluginSpec(pluginPath)
|
||||
if err != nil {
|
||||
m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err)
|
||||
continue
|
||||
}
|
||||
m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPluginByID returns a plugin by its ID
|
||||
func (m *Manager) GetPluginByID(pluginID string) (*Plugin, error) {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return nil, errors.New("plugin not found")
|
||||
}
|
||||
return plugin.(*Plugin), nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
plugin.(*Plugin).Enabled = true
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Terminate all plugins and exit
|
||||
func (m *Manager) Close() {
|
||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
if plugin.Enabled {
|
||||
m.DisablePlugin(plugin.Spec.ID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
//Wait until all loaded plugin process are terminated
|
||||
m.BlockUntilAllProcessExited()
|
||||
}
|
62
src/mod/plugins/utils.go
Normal file
62
src/mod/plugins/utils.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
Check if the folder contains a valid plugin in either one of the forms
|
||||
|
||||
1. Contain a file that have the same name as its parent directory, either executable or .exe on Windows
|
||||
2. Contain a start.sh or start.bat file
|
||||
|
||||
Return the path of the plugin entry point if found
|
||||
*/
|
||||
func (m *Manager) GetPluginEntryPoint(folderpath string) (string, error) {
|
||||
info, err := os.Stat(folderpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", errors.New("path is not a directory")
|
||||
}
|
||||
expectedBinaryPath := filepath.Join(folderpath, filepath.Base(folderpath))
|
||||
if runtime.GOOS == "windows" {
|
||||
expectedBinaryPath += ".exe"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(expectedBinaryPath); err == nil {
|
||||
return expectedBinaryPath, nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(folderpath, "start.sh")); err == nil {
|
||||
return filepath.Join(folderpath, "start.sh"), nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(folderpath, "start.bat")); err == nil {
|
||||
return filepath.Join(folderpath, "start.bat"), nil
|
||||
}
|
||||
|
||||
return "", errors.New("No valid entry point found")
|
||||
}
|
||||
|
||||
// Log logs a message with an optional error
|
||||
func (m *Manager) Log(message string, err error) {
|
||||
m.Options.Logger.PrintAndLog("plugin-manager", message, err)
|
||||
}
|
||||
|
||||
// getRandomPortNumber generates a random port number between 49152 and 65535
|
||||
func getRandomPortNumber() int {
|
||||
portNo := rand.Intn(65535-49152) + 49152
|
||||
//Check if the port is already in use
|
||||
for netutils.CheckIfPortOccupied(portNo) {
|
||||
portNo = rand.Intn(65535-49152) + 49152
|
||||
}
|
||||
return portNo
|
||||
}
|
Reference in New Issue
Block a user