Added plugin context view

- Added plugin context view
- Moved plugin type definition to separate file
- Added wip request forwarder
This commit is contained in:
Toby Chui
2025-02-28 22:05:14 +08:00
parent 214b69b0b8
commit 5abc4ac606
9 changed files with 229 additions and 64 deletions

View File

@ -0,0 +1,26 @@
package plugins
import "net/http"
/*
Forwarder.go
This file handles the dynamic proxy routing forwarding
request to plugin capture path that handles the matching
request path registered when the plugin started
*/
func (m *Manager) GetHandlerPlugins(w http.ResponseWriter, r *http.Request) {
}
func (m *Manager) GetHandlerPluginsSubsets(w http.ResponseWriter, r *http.Request) {
}
func (p *Plugin) HandlePluginRoute(w http.ResponseWriter, r *http.Request) {
//Find the plugin that matches the request path
//If no plugin found, return 404
//If found, forward the request to the plugin
}

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"path/filepath" "path/filepath"
"sort"
"time" "time"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
@ -18,6 +19,11 @@ func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) {
return return
} }
//Sort the plugin by its name
sort.Slice(plugins, func(i, j int) bool {
return plugins[i].Spec.Name < plugins[j].Spec.Name
})
js, err := json.Marshal(plugins) js, err := json.Marshal(plugins)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -1,49 +1,23 @@
package plugins package plugins
/*
Zoraxy Plugin Manager
This module is responsible for managing plugins
loading plugins from the disk
enable / disable plugins
and forwarding traffic to plugins
*/
import ( import (
"errors" "errors"
"net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"sync" "sync"
_ "embed"
"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" "imuslab.com/zoraxy/mod/utils"
) )
type Plugin struct {
RootDir string //The root directory of the plugin
Spec *zoraxyPlugin.IntroSpect //The plugin specification
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
process *exec.Cmd //The process of the plugin
}
type ManagerOptions struct {
PluginDir string
SystemConst *zoraxyPlugin.RuntimeConstantValue
Database *database.Database
Logger *logger.Logger
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
}
type Manager struct {
LoadedPlugins sync.Map //Storing *Plugin
Options *ManagerOptions
}
//go:embed no_img.png
var noImg []byte
// NewPluginManager creates a new plugin manager // NewPluginManager creates a new plugin manager
func NewPluginManager(options *ManagerOptions) *Manager { func NewPluginManager(options *ManagerOptions) *Manager {
//Create plugin directory if not exists //Create plugin directory if not exists

40
src/mod/plugins/typdef.go Normal file
View File

@ -0,0 +1,40 @@
package plugins
import (
_ "embed"
"net/http"
"os/exec"
"sync"
"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"
)
//go:embed no_img.png
var noImg []byte
type Plugin struct {
RootDir string //The root directory of the plugin
Spec *zoraxyPlugin.IntroSpect //The plugin specification
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
process *exec.Cmd //The process of the plugin
}
type ManagerOptions struct {
PluginDir string
SystemConst *zoraxyPlugin.RuntimeConstantValue
Database *database.Database
Logger *logger.Logger
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
}
type Manager struct {
LoadedPlugins sync.Map //Storing *Plugin
Options *ManagerOptions
}

View File

@ -79,9 +79,8 @@ type IntroSpect struct {
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on 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 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 GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler) GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
/* /*
@ -90,20 +89,9 @@ type IntroSpect struct {
Once the plugin is enabled on a given HTTP Proxy rule, Once the plugin is enabled on a given HTTP Proxy rule,
these always applies 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) AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_handler) 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 */ /* 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 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

View File

@ -3,6 +3,10 @@
<h2>Global Area Network</h2> <h2>Global Area Network</h2>
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p> <p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
</div> </div>
<div class="ui yellow message">
<b>Deprecation Notice</b>
<p>Global Area Network will be deprecating in v3.2.x and moved to Plugin</p>
</div>
<div class="gansnetworks"> <div class="gansnetworks">
<div class="ganstats ui basic segment"> <div class="ganstats ui basic segment">
<div style="float: right; max-width: 300px; margin-top: 0.4em;"> <div style="float: right; max-width: 300px; margin-top: 0.4em;">

View File

@ -0,0 +1,52 @@
<div class="">
<iframe id="pluginContextLoader" src="" style="width: 100%; border: none;">
</iframe>
</div>
<script>
function initPluginUIView(){
let pluginID = getPluginIDFromWindowHash();
console.log("Launching plugin UI for plugin with ID:", pluginID);
loadPluginContext(pluginID);
}
function loadPluginContext(pluginID){
let pluginContextURL = `/plugin.ui/${pluginID}/`;
$("#pluginContextLoader").attr("src", pluginContextURL);
}
function getPluginIDFromWindowHash(){
let tabID = window.location.hash.substr(1);
let pluginID = "";
if (tabID.startsWith("{")) {
tabID = decodeURIComponent(tabID);
try {
let parsedData = JSON.parse(tabID);
if (typeof(parsedData.pluginID) != "undefined"){
pluginID = parsedData.pluginID;
}
} catch (e) {
console.error("Invalid JSON data:", e);
}
}
return pluginID;
}
function resizeIframe() {
let iframe = document.getElementById('pluginContextLoader');
let mainMenuHeight = document.getElementById('mainmenu').offsetHeight;
iframe.style.height = mainMenuHeight + 'px';
}
$(window).on("resize", function(){
resizeIframe();
});
//Bind event to tab switch
tabSwitchEventBind["pluginContextWindow"] = function(){
//On switch over to this page, load info
resizeIframe();
}
initPluginUIView();
</script>

View File

@ -1,7 +1,11 @@
<div class="standardContainer"> <div class="standardContainer">
<div class="ui basic segment"> <div class="ui basic segment">
<h2>Plugins</h2> <h2>Plugins</h2>
<p>Custom features on Zoraxy</p> <p>Add custom features to your Zoraxy!</p>
</div>
<div class="ui yellow message">
<div class="header">Experimental Feature</div>
<p>This feature is experimental and may not work as expected. Use with caution.</p>
</div> </div>
<table class="ui basic celled table"> <table class="ui basic celled table">
<thead> <thead>
@ -19,6 +23,29 @@
<script> <script>
function initPluginSideMenu(){
$("#pluginMenu").html("");
$.get(`/api/plugins/list`, function(data){
data.forEach(plugin => {
$("#pluginMenu").append(`
<a class="item" tag="pluginContextWindow" pluginid="${plugin.Spec.id}" onclick="loadPluginUIContextIfAvailable();">
<img style="width: 20px;" class="ui mini right spaced image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}"> ${plugin.Spec.name}
</a>
`);
});
});
}
initPluginSideMenu();
function loadPluginUIContextIfAvailable(){
if(typeof(initPluginUIView) != "undefined"){
initPluginUIView();
}
}
function initiatePluginList(){ function initiatePluginList(){
$.get(`/api/plugins/list`, function(data){ $.get(`/api/plugins/list`, function(data){
const tbody = $("#pluginTable"); const tbody = $("#pluginTable");
@ -49,7 +76,6 @@ function initiatePluginList(){
<div class="ui toggle checkbox"> <div class="ui toggle checkbox">
<input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}> <input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}>
</div> </div>
<button class="ui basic small circular icon button" onclick="openPluginUI('${plugin.Spec.id}');"><i class="ui edit icon"></i></button>
</td> </td>
</tr> </tr>
`; `;
@ -59,10 +85,6 @@ function initiatePluginList(){
}); });
} }
function openPluginUI(pluginid){
showSideWrapper(`/plugin.ui/${pluginid}/`, true);
}
initiatePluginList(); initiatePluginList();
</script> </script>

View File

@ -78,9 +78,6 @@
<i class="simplistic user circle icon"></i> SSO / Oauth <i class="simplistic user circle icon"></i> SSO / Oauth
</a> </a>
<div class="ui divider menudivider">Others</div> <div class="ui divider menudivider">Others</div>
<a class="item" tag="plugins">
<i class="simplistic puzzle piece icon"></i> Plugins
</a>
<a class="item" tag="webserv"> <a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server <i class="simplistic globe icon"></i> Static Web Server
</a> </a>
@ -96,6 +93,15 @@
<a class="item" tag="utils"> <a class="item" tag="utils">
<i class="simplistic paperclip icon"></i> Utilities <i class="simplistic paperclip icon"></i> Utilities
</a> </a>
<a class="item" tag="plugins">
<i class="simplistic puzzle piece icon"></i> Plugins Manager
</a>
<div class="ui divider menudivider">Plugins</div>
<cx id="pluginMenu"></container>
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="green circle check icon"></i> No Installed Plugins
</a>
</cx>
<!-- Add more components here --> <!-- Add more components here -->
</div> </div>
</div> </div>
@ -155,6 +161,12 @@
<!-- Utilities --> <!-- Utilities -->
<div id="utils" class="functiontab" target="utils.html"></div> <div id="utils" class="functiontab" target="utils.html"></div>
<!-- Plugin Context Menu -->
<div id="pluginContextWindow" class="functiontab" target="plugincontext.html"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -246,7 +258,26 @@
if (window.location.hash.length > 1){ if (window.location.hash.length > 1){
let tabID = window.location.hash.substr(1); let tabID = window.location.hash.substr(1);
openTabById(tabID); if (tabID.startsWith("{")) {
tabID = decodeURIComponent(tabID);
//Zoraxy v3.2.x plugin context window
try {
let parsedData = JSON.parse(tabID);
tabID = parsedData.tabID;
//Open the plugin context window
if (tabID == "pluginContextWindow"){
let pluginID = parsedData.pluginID;
let button = $("#pluginMenu").find(`[pluginid="${pluginID}"]`);
openTabById(tabID, button);
loadPluginUIContextIfAvailable();
}
} catch (e) {
console.error("Invalid JSON data:", e);
}
}else{
openTabById(tabID);
}
}else{ }else{
openTabById("status"); openTabById("status");
} }
@ -257,7 +288,7 @@
$("#mainmenu").find(".item").each(function(){ $("#mainmenu").find(".item").each(function(){
$(this).on("click", function(event){ $(this).on("click", function(event){
let tabid = $(this).attr("tag"); let tabid = $(this).attr("tag");
openTabById(tabid); openTabById(tabid, $(this));
}); });
}); });
@ -282,13 +313,19 @@
if ($(".sideWrapper").is(":visible")){ if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false); $(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
} }
if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
}
}else{ }else{
setDarkTheme(true); setDarkTheme(true);
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe //Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
if ($(".sideWrapper").is(":visible")){ if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true); $(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
} }
if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
}
} }
} }
@ -307,8 +344,12 @@
//Select and open a tab by its tag id //Select and open a tab by its tag id
let tabSwitchEventBind = {}; //Bind event to tab switch by tabid let tabSwitchEventBind = {}; //Bind event to tab switch by tabid
function openTabById(tabID){ function openTabById(tabID, object=undefined){
let targetBtn = getTabButtonById(tabID); let targetBtn = object;
if (object == undefined){
//Search tab by its tap id
targetBtn = getTabButtonById(tabID);
}
if (targetBtn == undefined){ if (targetBtn == undefined){
alert("Invalid tabid given"); alert("Invalid tabid given");
return; return;
@ -329,7 +370,19 @@
},100) },100)
}); });
$('html,body').animate({scrollTop: 0}, 'fast'); $('html,body').animate({scrollTop: 0}, 'fast');
window.location.hash = tabID;
if (tabID == "pluginContextWindow"){
let statePayload = {
tabID: tabID,
pluginID: $(targetBtn).attr("pluginid")
}
window.location.hash = JSON.stringify(statePayload);
loadPluginUIContextIfAvailable();
}else{
window.location.hash = tabID;
}
} }
$(window).on("resize", function(){ $(window).on("resize", function(){