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"
"net/http"
"path/filepath"
"sort"
"time"
"imuslab.com/zoraxy/mod/utils"
@ -18,6 +19,11 @@ func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) {
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)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -1,49 +1,23 @@
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 (
"errors"
"net/http"
"os"
"os/exec"
"path/filepath"
"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"
)
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
func NewPluginManager(options *ManagerOptions) *Manager {
//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
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)
/*
@ -90,20 +89,9 @@ type IntroSpect struct {
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)
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)
/*
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

View File

@ -3,6 +3,10 @@
<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>
</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="ganstats ui basic segment">
<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="ui basic segment">
<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>
<table class="ui basic celled table">
<thead>
@ -19,6 +23,29 @@
<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(){
$.get(`/api/plugins/list`, function(data){
const tbody = $("#pluginTable");
@ -49,7 +76,6 @@ function initiatePluginList(){
<div class="ui toggle checkbox">
<input type="checkbox" name="enable" ${plugin.Enabled ? 'checked' : ''}>
</div>
<button class="ui basic small circular icon button" onclick="openPluginUI('${plugin.Spec.id}');"><i class="ui edit icon"></i></button>
</td>
</tr>
`;
@ -59,10 +85,6 @@ function initiatePluginList(){
});
}
function openPluginUI(pluginid){
showSideWrapper(`/plugin.ui/${pluginid}/`, true);
}
initiatePluginList();
</script>

View File

@ -78,9 +78,6 @@
<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
</a>
<a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server
</a>
@ -96,6 +93,15 @@
<a class="item" tag="utils">
<i class="simplistic paperclip icon"></i> Utilities
</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 -->
</div>
</div>
@ -155,6 +161,12 @@
<!-- Utilities -->
<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>
@ -246,7 +258,26 @@
if (window.location.hash.length > 1){
let tabID = window.location.hash.substr(1);
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{
openTabById("status");
}
@ -257,7 +288,7 @@
$("#mainmenu").find(".item").each(function(){
$(this).on("click", function(event){
let tabid = $(this).attr("tag");
openTabById(tabid);
openTabById(tabid, $(this));
});
});
@ -282,13 +313,19 @@
if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
}
if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
}
}else{
setDarkTheme(true);
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
if ($(".sideWrapper").is(":visible")){
$(".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
let tabSwitchEventBind = {}; //Bind event to tab switch by tabid
function openTabById(tabID){
let targetBtn = getTabButtonById(tabID);
function openTabById(tabID, object=undefined){
let targetBtn = object;
if (object == undefined){
//Search tab by its tap id
targetBtn = getTabButtonById(tabID);
}
if (targetBtn == undefined){
alert("Invalid tabid given");
return;
@ -329,9 +370,21 @@
},100)
});
$('html,body').animate({scrollTop: 0}, 'fast');
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(){
if (window.innerWidth >= 750 && $(".toolbar").is(":visible") == false){
$(".toolbar").show();