41 Commits

Author SHA1 Message Date
Toby Chui
c3afdefe45 Added wip log rotate feature
- Added log rotate function interface
- Added darwin amd64 support in make file (Intel Macs)
- Added log summary and error API
2025-08-31 22:22:45 +08:00
Toby Chui
d9fd38260f Changed LogView tool type
- Changed logview representation form from snippet to new tab
2025-08-31 14:29:20 +08:00
Toby Chui
bf5ffa100c Update logview.html
Added more logview logic
2025-08-31 14:02:58 +08:00
Toby Chui
a175c258c9 Added support for MacOS WebSSH
- Added MacOS webssh feature
- Fixed bug on no proxy rule will cause tls option null exception
2025-08-31 12:35:11 +08:00
Toby Chui
7c3a1a9cfc Added wip new log viewer
- Added DNS challenge maintainer tag
- Added wip log viewer
2025-08-31 11:17:07 +08:00
Toby Chui
471e94c893 Merge pull request #795 from zen8841/v3.2.6
Update lego to v4.25.2
2025-08-30 14:54:10 +08:00
Zen Wen
19fd6057e0 Update lego to v4.25.2 2025-08-30 14:39:32 +08:00
Zen Wen
3ad8e5acb3 Remove acmedns file inside .gitignore 2025-08-30 14:38:31 +08:00
Zen Wen
dda922cb64 Update code-gen tools
Add dnshomede and myaddr to excluded dns provider list
2025-08-30 14:29:26 +08:00
Zen Wen
d4d0adb297 Update acme code-gen Script: update.sh
Change the pull branch to latest version tags
Avoid generating config that not include in the latest lego release
2025-08-30 14:28:10 +08:00
Zen Wen
e4950bbbe6 Add Shebang for tools Script 2025-08-30 14:26:07 +08:00
Zen Wen
e1fd28f595 Add execute permission for tools script 2025-08-30 14:24:09 +08:00
Toby Chui
c2866f27f8 Added #263
- Added IP / CIDR as Basic Auth exclusion rule
- Fixed side frame not closing when open proxy rule editor bug
2025-08-17 14:25:38 +08:00
Toby Chui
2daf3cd2cb Optimized plugin examples
- Fixed build script bug in plugin module copy logic
- Fixed plugin example typos
- Fixed the missing embeded web server handleFunc interface
2025-08-17 10:34:10 +08:00
Toby Chui
51145edae7 Merge pull request #772 from AnthonyMichaelTDM/issue-771
fix(issue 771): panics when rewriting headers for websockets, and strange issue with logging across a month boundary
2025-08-16 21:34:09 +08:00
Anthony Rubick
bd5d225a94 fix: out of bounds index when rewriting websocket headers 2025-08-01 02:12:50 -07:00
Toby Chui
4e32f31f0a Updated version number 2025-07-20 15:55:12 +08:00
Toby Chui
381184cd92 Merge pull request #746 from AnthonyMichaelTDM/plugin-improvements-api-keys
feat(plugins): Implement plugin API key management and authentication middleware
2025-07-20 15:45:14 +08:00
Toby Chui
223ae9e112 Merge pull request #751 from tobychui/v3.2.5
* Added new API endpoint /api/proxy/setTlsConfig (for HTTP Proxy Editor TLS tab)
* Refactored TLS certificate management APIs with new handlers
* Removed redundant functions from src/cert.go and delegated to tlsCertManager
* Code optimization in tlscert module
* Introduced a new constant CONF_FOLDER and updated configuration storage paths (phasing out hard coded paths) 
* Updated functions to set default TLS options when missing, default to SNI 

By @jemmy1794 

* Added Proxy Protocol v1 support in stream proxy
* Fixed Proxy UI bug
2025-07-20 14:27:56 +08:00
Toby Chui
aff1975c5a Updated version code and defs
- Updated version code
- Replaced hardcoded path of some config folder string with const value
2025-07-20 14:03:39 +08:00
Anthony Rubick
ad2519d894 fix(example plugin): update PermittedAPIEndpoints 2025-07-19 22:50:42 -07:00
Anthony Rubick
40f915f7fb fix: update example plugin 2025-07-19 22:35:44 -07:00
Anthony Rubick
e3e31d9f22 feat: add the plugin accessible endpoints 2025-07-19 22:29:22 -07:00
Anthony Rubick
be5f631b9f refactor: reuse PluginAuthMiddleware as AuthAgent for plugin accessible endpoints 2025-07-19 22:29:02 -07:00
Anthony Rubick
f9e51bfd27 remove unused functions 2025-07-19 22:23:19 -07:00
Anthony Rubick
39b5da36d9 refactor: partial revert of dd93f9a2c4 2025-07-19 22:23:19 -07:00
Toby Chui
5c6950ca56 Merge pull request #744 from jemmy1794/Stream_Proxy_v3.2.5
Fix Stream Proxy TCP/UDP selection not saved initially #742
2025-07-19 12:57:25 +08:00
Anthony Rubick
d187c32a8a feat(plugins): add an example api call to an accessible but unpermitted endpoint 2025-07-17 23:21:57 -07:00
Anthony Rubick
ed8f9b7337 fix(plugin-auth): check both endpoint and method 2025-07-17 23:18:40 -07:00
Anthony Rubick
46cfc02493 feat(webui/plugininfo): Add section for permitted API endpoints 2025-07-17 22:47:57 -07:00
Anthony Rubick
2d43890fcf feat: Add .gitignore for Docker to exclude example and src directories 2025-07-17 22:20:09 -07:00
Anthony Rubick
5a38c1d407 feat(plugins): Add API Call Example Plugin 2025-07-17 22:20:09 -07:00
Anthony Rubick
dd93f9a2c4 feat(plugins): Implement plugin API key management and authentication middleware
The purpose of this is to allow plugins to access certain internal APIs via

- Added PluginAPIKey and APIKeyManager for managing API keys associated with plugins.
- Introduced PluginAuthMiddleware to handle API key validation for plugin requests.
- Updated RouterDef to support plugin accessible endpoints with authentication.
- Modified various API registration functions to include plugin accessibility checks.
- Enhanced plugin lifecycle management to generate and revoke API keys as needed.
- Updated plugin specifications to include permitted API endpoints for access control.
2025-07-17 22:20:09 -07:00
Toby Chui
70b1ccfa6e Merge pull request #739 from AnthonyMichaelTDM/typo
fix: typo in dynamic_router.go
2025-07-16 14:20:02 +08:00
Anthony Rubick
100c1e9c04 fix: typo in dynamic_router.go
SniffResultAccpet should be SniffResultAccept
2025-07-15 22:05:06 -07:00
Jemmy
a33600d3e2 Fix Stream Proxy TCP/UDP selection not saved initially #742
- Reset the value of the form correctly in `streamprox.html`. Ref #742.
2025-07-16 08:11:25 +08:00
Toby Chui
c4c10d2130 Fixed #713
- Fixed sorting destination not working bug
2025-07-12 19:52:59 +08:00
Toby Chui
4d3d1b25cb Restructure TLS options
- Moved certification related functions into tlscert module
- Added specific host TLS behavior logic
- Added support for disabling SNI and manually overwrite preferred certificate to serve
- Fixed SSO requestHeaders null bug
2025-07-12 19:30:55 +08:00
Toby Chui
a0a394885c Merge pull request #728 from PassiveLemon/hardening
Docker changes
2025-07-08 09:37:27 +08:00
PassiveLemon
51334a3a75 Docker: Switch to a python entrypoint 2025-07-07 13:34:50 -04:00
PassiveLemon
6f5fadc085 Docker: Do not automatically build Zoraxy plugins 2025-07-07 13:33:57 -04:00
85 changed files with 5187 additions and 6016 deletions

11
.gitignore vendored
View File

@@ -29,8 +29,6 @@ src/Zoraxy_*_*
src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/docker-compose.yaml
src/mod/acme/test/stackoverflow.pem
/tools/dns_challenge_update/code-gen/acmedns
/tools/dns_challenge_update/code-gen/lego
@@ -41,11 +39,15 @@ src/sys.uuid
src/zoraxy
src/log/
# dev-tags
/Dockerfile
/Entrypoint.sh
# docker testing stuff
docker/test/
docker/container-builder.sh
docker/docker-compose.yaml
# plugins
example/plugins/ztnc/ztnc.db
example/plugins/ztnc/authtoken.secret
@@ -58,3 +60,6 @@ sys.*
www/html/index.html
*.exe
/src/dist
/src/plugins
.DS_Store

View File

@@ -198,6 +198,12 @@ Some section of Zoraxy are contributed by our amazing community and if you have
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
- ACME
- ACME integration (Looking for maintainer)
- DNS Challenge by [@zen8841](https://github.com/zen8841)
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)

2
docker/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
example/
src/

View File

@@ -34,34 +34,18 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
chmod 755 /usr/local/bin/zerotier-one
## Fetch plugin
FROM docker.io/golang:alpine AS fetch-plugin
RUN mkdir -p /opt/zoraxy/zoraxy_plugin/
RUN apk add --update --no-cache git
WORKDIR /opt/zoraxy/
RUN git clone https://github.com/aroz-online/zoraxy-official-plugins &&\
cp -r ./zoraxy-official-plugins/src/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
## Main
FROM docker.io/golang:alpine
FROM docker.io/alpine:latest
# If you build it yourself, you will need to add the example directory into the docker directory.
RUN apk add --update --no-cache python3 sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
rm -rf /var/cache/apk/* /tmp/*
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
COPY --from=fetch-plugin --chmod=700 /opt/zoraxy/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
COPY --chmod=700 ./entrypoint.py /opt/zoraxy/
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
RUN apk add --update --no-cache bash sudo netcat-openbsd libressl-dev openssh ca-certificates libc6-compat libstdc++ &&\
mkdir -p /opt/zoraxy/plugin/ &&\
RUN mkdir -p /opt/zoraxy/plugin/ &&\
echo "tun" | tee -a /etc/modules
WORKDIR /opt/zoraxy/config/
@@ -89,7 +73,7 @@ VOLUME [ "/opt/zoraxy/config/" ]
LABEL com.imuslab.zoraxy.container-identifier="Zoraxy"
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
ENTRYPOINT [ "python3", "-u", "/opt/zoraxy/entrypoint.py" ]
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1

View File

@@ -119,18 +119,14 @@ Or for Docker Compose:
### Plugins
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
> [!IMPORTANT]
> Plugins are currently experimental.
Zoraxy includes a (experimental) store to download and use official plugins right from inside Zoraxy, no preparation required.
For those looking to use custom plugins, build your plugins and place them inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location).
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
- Run the build command with `docker build -t zoraxy_build .`
- You can now use the image `zoraxy_build`
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
echo "Copying zoraxy_plugin to all mods..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
fi
done
echo "Running go mod tidy and go build for all directories..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cd "$dir" || exit 1
go mod tidy
go build
cd "$1" || exit 1
fi
done

128
docker/entrypoint.py Normal file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import os
import signal
import subprocess
import sys
import time
zoraxy_proc = None
zerotier_proc = None
def getenv(key, default=None):
return os.environ.get(key, default)
def run(command):
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
print(f"Command failed: {command} - {e}")
sys.exit(1)
def popen(command):
proc = subprocess.Popen(command)
time.sleep(1)
if proc.poll() is not None:
print(f"{command} exited early with code {proc.returncode}")
raise RuntimeError(f"Failed to start {command}")
return proc
def cleanup(_signum, _frame):
print("Shutdown signal received. Cleaning up...")
global zoraxy_proc, zerotier_proc
if zoraxy_proc and zoraxy_proc.poll() is None:
print("Terminating Zoraxy...")
zoraxy_proc.terminate()
if zerotier_proc and zerotier_proc.poll() is None:
print("Terminating ZeroTier-One...")
zerotier_proc.terminate()
if zoraxy_proc:
try:
zoraxy_proc.wait(timeout=8)
except subprocess.TimeoutExpired:
zoraxy_proc.kill()
zoraxy_proc.wait()
if zerotier_proc:
try:
zerotier_proc.wait(timeout=8)
except subprocess.TimeoutExpired:
zerotier_proc.kill()
zerotier_proc.wait()
try:
os.unlink("/var/lib/zerotier-one")
except FileNotFoundError:
pass
except Exception as e:
print(f"Failed to unlink ZeroTier socket: {e}")
sys.exit(0)
def start_zerotier():
print("Starting ZeroTier...")
global zerotier_proc
config_dir = "/opt/zoraxy/config/zerotier/"
zt_path = "/var/lib/zerotier-one"
os.makedirs(config_dir, exist_ok=True)
os.symlink(config_dir, zt_path, target_is_directory=True)
zerotier_proc = popen(["zerotier-one"])
def start_zoraxy():
print("Starting Zoraxy...")
global zoraxy_proc
zoraxy_args = [
"zoraxy",
f"-autorenew={getenv('AUTORENEW', '86400')}",
f"-cfgupgrade={getenv('CFGUPGRADE', 'true')}",
f"-db={getenv('DB', 'auto')}",
f"-docker={getenv('DOCKER', 'true')}",
f"-earlyrenew={getenv('EARLYRENEW', '30')}",
f"-fastgeoip={getenv('FASTGEOIP', 'false')}",
f"-mdns={getenv('MDNS', 'true')}",
f"-mdnsname={getenv('MDNSNAME', "''")}",
f"-noauth={getenv('NOAUTH', 'false')}",
f"-plugin={getenv('PLUGIN', '/opt/zoraxy/plugin/')}",
f"-port=:{getenv('PORT', '8000')}",
f"-sshlb={getenv('SSHLB', 'false')}",
f"-update_geoip={getenv('UPDATE_GEOIP', 'false')}",
f"-version={getenv('VERSION', 'false')}",
f"-webfm={getenv('WEBFM', 'true')}",
f"-webroot={getenv('WEBROOT', './www')}",
]
zoraxy_proc = popen(zoraxy_args)
def main():
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)
print("Updating CA certificates...")
run(["update-ca-certificates"])
print("Updating GeoIP data...")
run(["zoraxy", "-update_geoip=true"])
os.chdir("/opt/zoraxy/config/")
if getenv("ZEROTIER", "false") == "true":
start_zerotier()
start_zoraxy()
signal.pause()
if __name__ == "__main__":
main()

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env bash
cleanup() {
echo "Stop signal received. Shutting down..."
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
unlink /var/lib/zerotier-one/zerotier/
exit 0
}
trap cleanup SIGTERM SIGINT TERM INT
update-ca-certificates && echo "CA certificates updated."
zoraxy -update_geoip=true && echo "GeoIP data updated ."
echo "Building plugins..."
cd /opt/zoraxy/plugin/ || exit 1
build_plugins "$PWD"
echo "Plugins built."
cd /opt/zoraxy/config/ || exit 1
if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d &
zerotierpid=$!
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
-mdns="$MDNS" \
-mdnsname="$MDNSNAME" \
-noauth="$NOAUTH" \
-plugin="$PLUGIN" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
&
zoraxypid=$!
wait "$zoraxypid"
wait "$zerotierpid"

View File

@@ -0,0 +1,3 @@
module aroz.org/zoraxy/api-call-example
go 1.24.5

View File

@@ -0,0 +1,54 @@
package main
import (
"fmt"
"net/http"
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.api_call_example"
UI_PATH = "/ui"
)
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: PLUGIN_ID,
Name: "API Call Example Plugin",
Author: "Anthony Rubick",
AuthorContact: "",
Description: "An example plugin for making API calls",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
UIPath: UI_PATH,
/* API Access Control */
PermittedAPIEndpoints: []plugin.PermittedAPIEndpoint{
{
Method: http.MethodGet,
Endpoint: "/plugin/api/access/list",
Reason: "Used to display all configured Access Rules",
},
},
})
if err != nil {
fmt.Printf("Error serving introspect: %v\n", err)
return
}
// Start the HTTP server
http.HandleFunc(UI_PATH+"/", func(w http.ResponseWriter, r *http.Request) {
RenderUI(runtimeCfg, w, r)
})
serverAddr := fmt.Sprintf("127.0.0.1:%d", runtimeCfg.Port)
fmt.Printf("Starting API Call Example Plugin on %s\n", serverAddr)
http.ListenAndServe(serverAddr, nil)
}

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,145 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"os"
"strings"
"time"
)
type PluginUiDebugRouter struct {
PluginID string //The ID of the plugin
TargetDir string //The directory where the UI files are stored
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
// The targetDir is the directory where the UI files are stored (e.g. ./www)
// 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 NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiDebugRouter{
PluginID: pluginID,
TargetDir: targetDir,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiDebugRouter) 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, ".html") {
//Read the target file from file system
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
//Check if the request is for a directory
//Check if the directory has an index.html file
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
if _, err := os.Stat(targetFilePath); err == nil {
//Serve the index.html file
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiDebugRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL.Path = rewrittenURL
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the file system
fsHandler := http.FileServer(http.Dir(p.TargetDir))
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the file system UI handler to the target http.ServeMux
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@@ -0,0 +1,162 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
/*
Dynamic Path Handler
*/
type SniffResult int
const (
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
/*
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
You can decide to accept or skip the request based on the request header and paths
*/
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
if !strings.HasSuffix(sniff_ingress, "/") {
sniff_ingress = sniff_ingress + "/"
}
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
}
// Decode the request payload
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error reading request body:", err)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
payload, err := DecodeForwardRequestPayload(jsonBytes)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error decoding request payload:", err)
fmt.Print("Payload: ")
fmt.Println(string(jsonBytes))
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Get the forwarded request UUID
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
payload.requestUUID = forwardUUID
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("SKIP"))
}
}))
}
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
if rewrittenURL == "" {
rewrittenURL = "/"
}
if !strings.HasPrefix(rewrittenURL, "/") {
rewrittenURL = "/" + rewrittenURL
}
r.RequestURI = rewrittenURL
handlefunc(w, r)
}))
}
/*
Sniffing and forwarding
The following functions are here to help with
sniffing and forwarding requests to the dynamic
router.
*/
// A custom request object to be used in the dynamic sniffing
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
/* Internal use */
rawRequest *http.Request `json:"-"`
requestUUID string `json:"-"`
}
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
return DynamicSniffForwardRequest{
Method: r.Method,
Hostname: r.Host,
URL: r.URL.String(),
Header: r.Header,
RemoteAddr: r.RemoteAddr,
Host: r.Host,
RequestURI: r.RequestURI,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
rawRequest: r,
}
}
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
var payload DynamicSniffForwardRequest
err := json.Unmarshal(jsonBytes, &payload)
if err != nil {
return DynamicSniffForwardRequest{}, err
}
return payload, nil
}
// GetRequest returns the original http.Request object, for debugging purposes
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
return dsfr.rawRequest
}
// GetRequestUUID returns the request UUID
// if this UUID is empty string, that might indicate the request
// is not coming from the dynamic router
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
return dsfr.requestUUID
}

View File

@@ -0,0 +1,174 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"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
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// 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, ".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)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
// Check if the directory has an index.html file
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
if err == nil {
body := string(indexFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(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
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//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)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// HandleFunc registers a handler function for the given pattern
// The pattern should start with the handler prefix, e.g. /ui/hello
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
// If mux is nil, use the default ServeMux
if mux == nil {
mux = http.DefaultServeMux
}
// Make sure the pattern starts with the handler prefix
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
pattern = p.HandlerPrefix + pattern
}
// Register the handler with the http.ServeMux
mux.HandleFunc(pattern, handler)
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@@ -0,0 +1,105 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"sort"
"strings"
)
type PathRouter struct {
enableDebugPrint bool
pathHandlers map[string]http.Handler
defaultHandler http.Handler
}
// NewPathRouter creates a new PathRouter
func NewPathRouter() *PathRouter {
return &PathRouter{
enableDebugPrint: false,
pathHandlers: make(map[string]http.Handler),
}
}
// RegisterPathHandler registers a handler for a path
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
path = strings.TrimSuffix(path, "/")
p.pathHandlers[path] = handler
}
// RemovePathHandler removes a handler for a path
func (p *PathRouter) RemovePathHandler(path string) {
delete(p.pathHandlers, path)
}
// SetDefaultHandler sets the default handler for the router
// This handler will be called if no path handler is found
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
p.defaultHandler = handler
}
// SetDebugPrintMode sets the debug print mode
func (p *PathRouter) SetDebugPrintMode(enable bool) {
p.enableDebugPrint = enable
}
// StartStaticCapture starts the static capture ingress
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.staticCaptureServeHTTP(w, r)
}))
}
// staticCaptureServeHTTP serves the static capture path using user defined handler
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
capturePath := r.Header.Get("X-Zoraxy-Capture")
if capturePath != "" {
if p.enableDebugPrint {
fmt.Printf("Using capture path: %s\n", capturePath)
}
originalURI := r.Header.Get("X-Zoraxy-Uri")
r.URL.Path = originalURI
if handler, ok := p.pathHandlers[capturePath]; ok {
handler.ServeHTTP(w, r)
return
}
}
p.defaultHandler.ServeHTTP(w, r)
}
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
if p.enableDebugPrint {
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
keys := make([]string, 0, len(r.Header))
for key := range r.Header {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
for _, value := range r.Header[key] {
fmt.Printf("%s: %s\n", key, value)
}
}
fmt.Printf("\n\n**Request Details**\n\n")
fmt.Printf("Method: %s\n", r.Method)
fmt.Printf("URL: %s\n", r.URL.String())
fmt.Printf("Proto: %s\n", r.Proto)
fmt.Printf("Host: %s\n", r.Host)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
fmt.Printf("ContentLength: %d\n", r.ContentLength)
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
fmt.Printf("Close: %v\n", r.Close)
fmt.Printf("Form: %v\n", r.Form)
fmt.Printf("PostForm: %v\n", r.PostForm)
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
fmt.Printf("Trailer: %v\n", r.Trailer)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
}
}

View File

@@ -0,0 +1,187 @@
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 StaticCaptureRule struct {
CapturePath string `json:"capture_path"`
//To be expanded
}
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"`
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
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
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_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, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
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
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//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()
}

View File

@@ -0,0 +1,272 @@
package main
import (
"fmt"
"html"
"net/http"
"net/http/httputil"
"strconv"
plugin "aroz.org/zoraxy/api-call-example/mod/zoraxy_plugin"
)
func allowedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
// Make an API call to the permitted endpoint
client := &http.Client{}
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Make sure to set the Authorization header
req.Header.Set("Authorization", "Bearer "+cfg.APIKey) // Use the API key from the runtime config
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error making API call: %v", err)
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return "", fmt.Errorf("error dumping response: %v", err)
}
// Check if the response status is OK
if resp.StatusCode != http.StatusOK {
return string(respDump), fmt.Errorf("received non-OK response status %d", resp.StatusCode)
}
return string(respDump), nil
}
func allowedEndpointInvalidKey(cfg *plugin.ConfigureSpec) (string, error) {
// Make an API call to the permitted endpoint with an invalid key
client := &http.Client{}
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/access/list", cfg.ZoraxyPort)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Use an invalid API key
req.Header.Set("Authorization", "Bearer invalid-key")
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error making API call: %v", err)
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return "", fmt.Errorf("error dumping response: %v", err)
}
return string(respDump), nil
}
func unaccessibleEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
// Make an API call to an endpoint that is not permitted
client := &http.Client{}
apiURL := fmt.Sprintf("http://localhost:%d/api/acme/listExpiredDomains", cfg.ZoraxyPort)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Use the API key from the runtime config
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error making API call: %v", err)
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return "", fmt.Errorf("error dumping response: %v", err)
}
return string(respDump), nil
}
func unpermittedEndpoint(cfg *plugin.ConfigureSpec) (string, error) {
// Make an API call to an endpoint that is plugin-accessible but is not permitted
client := &http.Client{}
apiURL := fmt.Sprintf("http://localhost:%d/plugin/api/proxy/list", cfg.ZoraxyPort)
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return "", fmt.Errorf("error creating request: %v", err)
}
// Use the API key from the runtime config
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error making API call: %v", err)
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return "", fmt.Errorf("error dumping response: %v", err)
}
return string(respDump), nil
}
func RenderUI(config *plugin.ConfigureSpec, w http.ResponseWriter, r *http.Request) {
// make several types of API calls to demonstrate the plugin functionality
accessList, err := allowedEndpoint(config)
var RenderedAccessListHTML string
if err != nil {
if accessList != "" {
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p><pre>%s</pre>", err, html.EscapeString(accessList))
} else {
RenderedAccessListHTML = fmt.Sprintf("<p>Error fetching access list: %v</p>", err)
}
} else {
// Render the access list as HTML
RenderedAccessListHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(accessList))
}
// Make an API call with an invalid key
invalidKeyResponse, err := allowedEndpointInvalidKey(config)
var RenderedInvalidKeyResponseHTML string
if err != nil {
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<p>Error with invalid key: %v</p>", err)
} else {
// Render the invalid key response as HTML
RenderedInvalidKeyResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(invalidKeyResponse))
}
// Make an API call to an endpoint that is not plugin-accessible
unaccessibleResponse, err := unaccessibleEndpoint(config)
var RenderedUnaccessibleResponseHTML string
if err != nil {
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<p>Error with unaccessible endpoint: %v</p>", err)
} else {
// Render the unaccessible response as HTML
RenderedUnaccessibleResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unaccessibleResponse))
}
// Make an API call to an endpoint that is plugin-accessible but is not permitted
unpermittedResponse, err := unpermittedEndpoint(config)
var RenderedUnpermittedResponseHTML string
if err != nil {
RenderedUnpermittedResponseHTML = fmt.Sprintf("<p>Error with unpermitted endpoint: %v</p>", err)
} else {
// Render the unpermitted response as HTML
RenderedUnpermittedResponseHTML = fmt.Sprintf("<pre>%s</pre>", html.EscapeString(unpermittedResponse))
}
// Render the UI for the plugin
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
html := `
<!DOCTYPE html>
<html>
<head>
<title>API Call Example Plugin UI</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/main.css">
<script src="/script/jquery-3.6.0.min.js"></script>
<style>
.response-block {
background-color: var(--theme_bg_primary);
border: 1px solid var(--theme_divider);
border-radius: 8px;
padding: 20px;
margin: 15px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
.response-block:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.response-block h3 {
margin-top: 0;
color: var(--text_color);
border-bottom: 2px solid #007bff;
padding-bottom: 8px;
}
.response-block.success {
border-left: 4px solid #28a745;
}
.response-block.error {
border-left: 4px solid #dc3545;
}
.response-block.warning {
border-left: 4px solid #ffc107;
}
.response-content {
margin-top: 10px;
}
.response-content pre {
background-color: var(--theme_highlight);
border: 1px solid var(--theme_divider);
border-radius: 4px;
padding: 10px;
overflow: auto;
font-size: 12px;
line-height: 1.4;
height: 200px;
max-height: 80vh;
min-height: 100px;
resize: vertical;
box-sizing: border-box;
}
</style>
</head>
<body>
<!-- Dark theme script must be included after body tag-->
<link rel="stylesheet" href="/darktheme.css">
<script src="/script/darktheme.js"></script>
<h1>Welcome to the API Call Example Plugin UI</h1>
<p>Plugin is running on port: ` + strconv.Itoa(config.Port) + `</p>
<h2>API Call Examples</h2>
<div class="response-block success">
<h3>✅ Allowed Endpoint (Valid API Key)</h3>
<p>Making a GET request to <code>/plugin/api/access/list</code> with a valid API key:</p>
<div class="response-content">
` + RenderedAccessListHTML + `
</div>
</div>
<div class="response-block warning">
<h3>⚠️ Invalid API Key</h3>
<p>Making a GET request to <code>/plugin/api/access/list</code> with an invalid API key:</p>
<div class="response-content">
` + RenderedInvalidKeyResponseHTML + `
</div>
</div>
<div class="response-block warning">
<h3>⚠️ Unpermitted Endpoint</h3>
<p>Making a GET request to <code>/plugin/api/proxy/list</code> (not a permitted endpoint):</p>
<div class="response-content">
` + RenderedUnpermittedResponseHTML + `
</div>
</div>
<div class="response-block error">
<h3>❌ Disallowed Endpoint</h3>
<p>Making a GET request to <code>/api/acme/listExpiredDomains</code> (not a plugin-accessible endpoint):</p>
<div class="response-content">
` + RenderedUnaccessibleResponseHTML + `
</div>
</div>
</body>
</html>`
w.Write([]byte(html))
}

View File

@@ -4,7 +4,7 @@
echo "Copying zoraxy_plugin to all mods"
for dir in ./*; do
if [ -d "$dir" ]; then
cp -r ../mod/plugins/zoraxy_plugin "$dir/mod"
cp -r ../../src/mod/plugins/zoraxy_plugin "$dir/mod"
fi
done

View File

@@ -86,7 +86,7 @@ func main() {
if strings.HasPrefix(dsfr.RequestURI, "/test_") {
reqUUID := dsfr.GetRequestUUID()
fmt.Println("Accepting request with UUID: " + reqUUID)
return plugin.SniffResultAccpet
return plugin.SniffResultAccept
}
return plugin.SniffResultSkip

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
})
}
// HandleFunc registers a handler function for the given pattern
// The pattern should start with the handler prefix, e.g. /ui/hello
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
// If mux is nil, use the default ServeMux
if mux == nil {
mux = http.DefaultServeMux
}
// Make sure the pattern starts with the handler prefix
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
pattern = p.HandlerPrefix + pattern
}
// Register the handler with the http.ServeMux
mux.HandleFunc(pattern, handler)
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -77,8 +77,8 @@ func main() {
fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
// We want to handle this request, reply with aSniffResultAccept
return plugin.SniffResultAccpet
// We want to handle this request, reply with a SniffResultAccept
return plugin.SniffResultAccept
}
// If the request URI does not match, we skip this request

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
})
}
// HandleFunc registers a handler function for the given pattern
// The pattern should start with the handler prefix, e.g. /ui/hello
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
// If mux is nil, use the default ServeMux
if mux == nil {
mux = http.DefaultServeMux
}
// Make sure the pattern starts with the handler prefix
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
pattern = p.HandlerPrefix + pattern
}
// Register the handler with the http.ServeMux
mux.HandleFunc(pattern, handler)
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
})
}
// HandleFunc registers a handler function for the given pattern
// The pattern should start with the handler prefix, e.g. /ui/hello
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
// If mux is nil, use the default ServeMux
if mux == nil {
mux = http.DefaultServeMux
}
// Make sure the pattern starts with the handler prefix
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
pattern = p.HandlerPrefix + pattern
}
// Register the handler with the http.ServeMux
mux.HandleFunc(pattern, handler)
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -1,5 +1,5 @@
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64 darwin/amd64
temp = $(subst /, ,$@)
os = $(word 1, $(temp))
arch = $(word 2, $(temp))

View File

@@ -72,15 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
// Register the APIs for TLS / SSL certificate management functions
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
//Global certificate settings
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
//Certificate store functions
authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload)
authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove)
authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate)
}
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
@@ -377,7 +382,8 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
authRouter.HandleFunc("/api/log/errors", LogViewer.HandleLogErrorSummary)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
}

View File

@@ -1,188 +1,14 @@
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/utils"
)
// Check if the default certificates is correctly setup
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
type CheckResult struct {
DefaultPubExists bool
DefaultPriExists bool
}
pub, pri := tlsCertManager.DefaultCertExistsSep()
js, _ := json.Marshal(CheckResult{
pub,
pri,
})
utils.SendJSONResponse(w, string(js))
}
// Return a list of domains where the certificates covers
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
filenames, err := tlsCertManager.ListCertDomains()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
showDate, _ := utils.GetPara(r, "date")
if showDate == "true" {
type CertInfo struct {
Domain string
LastModifiedDate string
ExpireDate string
RemainingDays int
UseDNS bool
}
results := []*CertInfo{}
for _, filename := range filenames {
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath)
if err != nil {
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
return
}
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
expiredIn := 0
if err != nil {
//Unable to load this file
continue
} else {
//Cert loaded. Check its expire time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
duration := cert.NotAfter.Sub(time.Now())
// Convert the duration to days
expiredIn = int(duration.Hours() / 24)
}
}
}
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
useDNSValidation := false //Default to false for HTTP TLS certificates
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
if err == nil {
useDNSValidation = certInfo.UseDNS
}
thisCertInfo := CertInfo{
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
UseDNS: useDNSValidation,
}
results = append(results, &thisCertInfo)
}
// convert ExpireDate to date object and sort asc
sort.Slice(results, func(i, j int) bool {
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
return date1.Before(date2)
})
js, _ := json.Marshal(results)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
} else {
response, err := json.Marshal(filenames)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
}
// List all certificates and map all their domains to the cert filename
func handleListDomains(w http.ResponseWriter, r *http.Request) {
filenames, err := os.ReadDir("./conf/certs/")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
certnameToDomainMap := map[string]string{}
for _, filename := range filenames {
if filename.IsDir() {
continue
}
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBtyes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
for _, dnsName := range cert.DNSNames {
certnameToDomainMap[dnsName] = certname
}
certnameToDomainMap[cert.Subject.CommonName] = certname
}
}
}
}
requireCompact, _ := utils.GetPara(r, "compact")
if requireCompact == "true" {
result := make(map[string][]string)
for key, value := range certnameToDomainMap {
if _, ok := result[value]; !ok {
result[value] = make([]string, 0)
}
result[value] = append(result[value], key)
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
return
}
js, _ := json.Marshal(certnameToDomainMap)
utils.SendJSONResponse(w, string(js))
}
// Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := true //Default to true
@@ -193,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
sysdb.Read("settings", "usetls", &currentTlsSetting)
}
if r.Method == http.MethodGet {
switch r.Method {
case http.MethodGet:
//Get the current status
js, _ := json.Marshal(currentTlsSetting)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
case http.MethodPost:
newState, err := utils.PostBool(r, "set")
if err != nil {
utils.SendErrorResponse(w, "new state not set or invalid")
@@ -213,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
dynamicProxyRouter.UpdateTLSSetting(false)
}
utils.SendOK(w)
} else {
default:
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
@@ -231,135 +58,21 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(reqLatestTLS)
utils.SendJSONResponse(w, string(js))
} else {
if newState == "true" {
switch newState {
case "true":
sysdb.Write("settings", "forceLatestTLS", true)
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true)
} else if newState == "false" {
case "false":
sysdb.Write("settings", "forceLatestTLS", false)
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false)
} else {
default:
utils.SendErrorResponse(w, "invalid state given")
}
}
}
// Handle download of the selected certificate
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
// get the certificate name
certname, err := utils.GetPara(r, "certname")
if err != nil {
utils.SendErrorResponse(w, "invalid certname given")
return
}
certname = filepath.Base(certname) //prevent path escape
// check if the cert exists
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
//Zip them and serve them via http download
seeking, _ := utils.GetBool(r, "seek")
if seeking {
//This request only check if the key exists. Do not provide download
utils.SendOK(w)
return
}
//Serve both file in zip
zipTmpFolder := "./tmp/download"
os.MkdirAll(zipTmpFolder, 0775)
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
err := utils.ZipFiles(zipFileName, pubKey, priKey)
if err != nil {
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
return
}
defer os.Remove(zipFileName) // Clean up the zip file after serving
// Serve the zip file
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
w.Header().Set("Content-Type", "application/zip")
http.ServeFile(w, r, zipFileName)
} else {
//Not both key exists
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
return
}
}
// Handle upload of the certificate
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// get the key type
keytype, err := utils.GetPara(r, "ktype")
overWriteFilename := ""
if err != nil {
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
return
}
// get the domain
domain, err := utils.GetPara(r, "domain")
if err != nil {
//Assume localhost
domain = "default"
}
if keytype == "pub" {
overWriteFilename = domain + ".pem"
} else if keytype == "pri" {
overWriteFilename = domain + ".key"
} else {
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
return
}
// parse multipart form data
err = r.ParseMultipartForm(10 << 20) // 10 MB
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
// get file from form data
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to get file", http.StatusBadRequest)
return
}
defer file.Close()
// create file in upload directory
os.MkdirAll("./conf/certs", 0775)
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return
}
defer f.Close()
// copy file contents to destination file
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
//Update cert list
tlsCertManager.UpdateLoadedCertList()
// send response
fmt.Fprintln(w, "File upload successful!")
}
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
// get the domain
domain, err := utils.GetPara(r, "domain")
@@ -441,15 +154,40 @@ func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js))
}
// Handle cert remove
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
//Get the domain
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "invalid domain given")
return
}
err = tlsCertManager.RemoveCert(domain)
//Get the certificate name
certName, err := utils.PostPara(r, "certname")
if err != nil {
utils.SendErrorResponse(w, err.Error())
utils.SendErrorResponse(w, "invalid certificate name given")
return
}
//Load the target endpoint
ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true)
if err != nil {
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
return
}
//Set the preferred certificate for the domain
err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName)
if err != nil {
utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error())
return
}
err = SaveReverseProxyConfig(ept)
if err != nil {
utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error())
return
}
utils.SendOK(w)
}

View File

@@ -108,9 +108,9 @@ func filterProxyConfigFilename(filename string) string {
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
filename := filepath.Join(CONF_HTTP_PROXY, endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
filename = "./conf/proxy/root.config"
filename = filepath.Join(CONF_HTTP_PROXY, "root.config")
}
filename = filterProxyConfigFilename(filename)
@@ -125,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
}
func RemoveReverseProxyConfig(endpoint string) error {
filename := filepath.Join("./conf/proxy/", endpoint+".config")
filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config")
if endpoint == "/" {
filename = "./conf/proxy/root.config"
filename = filepath.Join(CONF_HTTP_PROXY, "/root.config")
}
filename = filterProxyConfigFilename(filename)
@@ -179,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
}
// Specify the folder path to be zipped
if !utils.FileExists("./conf") {
if !utils.FileExists(CONF_FOLDER) {
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
return
}
folderPath := "./conf"
folderPath := CONF_FOLDER
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
@@ -284,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
return
}
// Create the target directory to unzip the files
targetDir := "./conf"
targetDir := CONF_FOLDER
if utils.FileExists(targetDir) {
//Backup the old config to old
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
//os.Rename(*path_conf, backupPath)
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
os.Rename(CONF_FOLDER, CONF_FOLDER+".old_"+strconv.Itoa(int(time.Now().Unix())))
}
err = os.MkdirAll(targetDir, os.ModePerm)

View File

@@ -44,7 +44,7 @@ import (
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.4"
SYSTEM_VERSION = "3.2.6"
DEVELOPMENT_BUILD = false
/* System Constants */
@@ -63,14 +63,19 @@ const (
LOG_EXTENSION = ".log"
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
/* Configuration Folder Storage Path Constants */
CONF_HTTP_PROXY = "./conf/proxy"
CONF_STREAM_PROXY = "./conf/streamproxy"
CONF_CERT_STORE = "./conf/certs"
CONF_REDIRECTION = "./conf/redirect"
CONF_ACCESS_RULE = "./conf/access"
CONF_PATH_RULE = "./conf/rules/pathrules"
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
/*
Configuration Folder Storage Path Constants
Note: No tailing slash in the path
*/
CONF_FOLDER = "./conf"
CONF_HTTP_PROXY = CONF_FOLDER + "/proxy"
CONF_STREAM_PROXY = CONF_FOLDER + "/streamproxy"
CONF_CERT_STORE = CONF_FOLDER + "/certs"
CONF_REDIRECTION = CONF_FOLDER + "/redirect"
CONF_ACCESS_RULE = CONF_FOLDER + "/access"
CONF_PATH_RULE = CONF_FOLDER + "/rules/pathrules"
CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json"
CONF_GEODB_PATH = CONF_FOLDER + "/geodb"
)
/* System Startup Flags */
@@ -144,6 +149,9 @@ var (
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
pluginManager *plugins.Manager //Plugin manager for managing plugins
//Plugin auth related
pluginApiKeyManager *auth.APIKeyManager //API key manager for plugin authentication
//Authentication Provider
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication

View File

@@ -1,14 +1,14 @@
module imuslab.com/zoraxy
go 1.22.0
go 1.24.0
toolchain go1.22.2
toolchain go1.24.6
require (
github.com/armon/go-radix v1.0.0
github.com/boltdb/bolt v1.3.1
github.com/docker/docker v27.0.0+incompatible
github.com/go-acme/lego/v4 v4.21.0
github.com/go-acme/lego/v4 v4.25.2
github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2
@@ -20,109 +20,121 @@ require (
github.com/shirou/gopsutil/v4 v4.25.1
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
golang.org/x/net v0.33.0
golang.org/x/oauth2 v0.24.0
golang.org/x/text v0.21.0
golang.org/x/net v0.42.0
golang.org/x/oauth2 v0.30.0
golang.org/x/text v0.27.0
)
require (
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.8 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/alibabacloud-go/tea v1.3.9 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.235 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.5.10 // indirect
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/yandex-cloud/go-sdk/services/dns v0.0.3 // indirect
github.com/yandex-cloud/go-sdk/v2 v2.0.8 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
)
require (
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest v0.11.30 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.18 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.71 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.5 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.112.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-resty/resty/v2 v2.16.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gorilla/csrf v1.7.2
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.159 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.44.0 // indirect
github.com/linode/linodego v1.53.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/miekg/dns v1.1.67 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -131,13 +143,12 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.10.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goinwx v0.10.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
@@ -145,57 +156,51 @@ require (
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/ovh/go-ovh v1.6.0 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.14.0 // indirect
github.com/sacloud/packages-go v0.0.10 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/sacloud/api-client-go v0.3.2 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.16.1 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.7 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
github.com/vultr/govultr/v3 v3.21.1 // indirect
github.com/yandex-cloud/go-genproto v0.14.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/ratelimit v0.3.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/api v0.214.0 // indirect
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.2 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.14.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ func main() {
os.Exit(0)
}
if *geoDbUpdate {
geodb.DownloadGeoDBUpdate("./conf/geodb")
geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH)
os.Exit(0)
}
@@ -115,6 +115,7 @@ func main() {
//Initiate management interface APIs
requireAuth = !(*noauth)
initAPIs(webminPanelMux)
initRestAPI(webminPanelMux)
//Start the reverse proxy server in go routine
go func() {

View File

@@ -1,5 +1,4 @@
package acmedns
/*
THIS MODULE IS GENERATED AUTOMATICALLY
DO NOT EDIT THIS FILE
@@ -10,15 +9,20 @@ import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/active24"
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/allinkl"
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
"github.com/go-acme/lego/v4/providers/dns/auroradns"
"github.com/go-acme/lego/v4/providers/dns/autodns"
"github.com/go-acme/lego/v4/providers/dns/axelname"
"github.com/go-acme/lego/v4/providers/dns/azion"
"github.com/go-acme/lego/v4/providers/dns/azure"
"github.com/go-acme/lego/v4/providers/dns/azuredns"
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
"github.com/go-acme/lego/v4/providers/dns/bindman"
"github.com/go-acme/lego/v4/providers/dns/bluecat"
"github.com/go-acme/lego/v4/providers/dns/bookmyname"
"github.com/go-acme/lego/v4/providers/dns/brandit"
"github.com/go-acme/lego/v4/providers/dns/bunny"
"github.com/go-acme/lego/v4/providers/dns/checkdomain"
@@ -29,13 +33,13 @@ import (
"github.com/go-acme/lego/v4/providers/dns/cloudru"
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
"github.com/go-acme/lego/v4/providers/dns/conoha"
"github.com/go-acme/lego/v4/providers/dns/conohav3"
"github.com/go-acme/lego/v4/providers/dns/constellix"
"github.com/go-acme/lego/v4/providers/dns/cpanel"
"github.com/go-acme/lego/v4/providers/dns/derak"
"github.com/go-acme/lego/v4/providers/dns/desec"
"github.com/go-acme/lego/v4/providers/dns/digitalocean"
"github.com/go-acme/lego/v4/providers/dns/directadmin"
"github.com/go-acme/lego/v4/providers/dns/dnshomede"
"github.com/go-acme/lego/v4/providers/dns/dnsimple"
"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
"github.com/go-acme/lego/v4/providers/dns/dnspod"
@@ -44,10 +48,12 @@ import (
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
"github.com/go-acme/lego/v4/providers/dns/duckdns"
"github.com/go-acme/lego/v4/providers/dns/dyn"
"github.com/go-acme/lego/v4/providers/dns/dyndnsfree"
"github.com/go-acme/lego/v4/providers/dns/dynu"
"github.com/go-acme/lego/v4/providers/dns/easydns"
"github.com/go-acme/lego/v4/providers/dns/efficientip"
"github.com/go-acme/lego/v4/providers/dns/epik"
"github.com/go-acme/lego/v4/providers/dns/f5xc"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/gandi"
"github.com/go-acme/lego/v4/providers/dns/gandiv5"
@@ -80,7 +86,9 @@ import (
"github.com/go-acme/lego/v4/providers/dns/loopia"
"github.com/go-acme/lego/v4/providers/dns/luadns"
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
"github.com/go-acme/lego/v4/providers/dns/manageengine"
"github.com/go-acme/lego/v4/providers/dns/metaname"
"github.com/go-acme/lego/v4/providers/dns/metaregistrar"
"github.com/go-acme/lego/v4/providers/dns/mijnhost"
"github.com/go-acme/lego/v4/providers/dns/mittwald"
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
@@ -91,6 +99,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/netcup"
"github.com/go-acme/lego/v4/providers/dns/netlify"
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
"github.com/go-acme/lego/v4/providers/dns/nicru"
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
"github.com/go-acme/lego/v4/providers/dns/njalla"
"github.com/go-acme/lego/v4/providers/dns/nodion"
@@ -101,7 +110,9 @@ import (
"github.com/go-acme/lego/v4/providers/dns/plesk"
"github.com/go-acme/lego/v4/providers/dns/porkbun"
"github.com/go-acme/lego/v4/providers/dns/rackspace"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/go-acme/lego/v4/providers/dns/rcodezero"
"github.com/go-acme/lego/v4/providers/dns/regfish"
"github.com/go-acme/lego/v4/providers/dns/regru"
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
"github.com/go-acme/lego/v4/providers/dns/rimuhosting"
@@ -115,7 +126,9 @@ import (
"github.com/go-acme/lego/v4/providers/dns/shellrent"
"github.com/go-acme/lego/v4/providers/dns/simply"
"github.com/go-acme/lego/v4/providers/dns/sonic"
"github.com/go-acme/lego/v4/providers/dns/spaceship"
"github.com/go-acme/lego/v4/providers/dns/stackpath"
"github.com/go-acme/lego/v4/providers/dns/technitium"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/go-acme/lego/v4/providers/dns/transip"
"github.com/go-acme/lego/v4/providers/dns/ultradns"
@@ -130,20 +143,32 @@ import (
"github.com/go-acme/lego/v4/providers/dns/webnames"
"github.com/go-acme/lego/v4/providers/dns/websupport"
"github.com/go-acme/lego/v4/providers/dns/wedos"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"github.com/go-acme/lego/v4/providers/dns/yandex"
"github.com/go-acme/lego/v4/providers/dns/yandex360"
"github.com/go-acme/lego/v4/providers/dns/yandexcloud"
"github.com/go-acme/lego/v4/providers/dns/zoneedit"
"github.com/go-acme/lego/v4/providers/dns/zoneee"
"github.com/go-acme/lego/v4/providers/dns/zonomi"
)
// name is the DNS provider name, e.g. cloudflare or gandi
// JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64) (challenge.Provider, error) {
//name is the DNS provider name, e.g. cloudflare or gandi
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64, pollingInterval int64)(challenge.Provider, error){
pgDuration := time.Duration(propagationTimeout) * time.Second
plInterval := time.Duration(pollingInterval) * time.Second
switch name {
case "active24":
cfg := active24.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return active24.NewDNSProviderConfig(cfg)
case "alidns":
cfg := alidns.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -189,6 +214,24 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return autodns.NewDNSProviderConfig(cfg)
case "axelname":
cfg := axelname.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return axelname.NewDNSProviderConfig(cfg)
case "azion":
cfg := azion.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return azion.NewDNSProviderConfig(cfg)
case "azure":
cfg := azure.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -207,6 +250,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return azuredns.NewDNSProviderConfig(cfg)
case "baiducloud":
cfg := baiducloud.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return baiducloud.NewDNSProviderConfig(cfg)
case "bindman":
cfg := bindman.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -225,6 +277,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return bluecat.NewDNSProviderConfig(cfg)
case "bookmyname":
cfg := bookmyname.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return bookmyname.NewDNSProviderConfig(cfg)
case "brandit":
cfg := brandit.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -315,6 +376,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return conoha.NewDNSProviderConfig(cfg)
case "conohav3":
cfg := conohav3.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return conohav3.NewDNSProviderConfig(cfg)
case "constellix":
cfg := constellix.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -369,15 +439,6 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return directadmin.NewDNSProviderConfig(cfg)
case "dnshomede":
cfg := dnshomede.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return dnshomede.NewDNSProviderConfig(cfg)
case "dnsimple":
cfg := dnsimple.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -450,6 +511,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return dyn.NewDNSProviderConfig(cfg)
case "dyndnsfree":
cfg := dyndnsfree.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return dyndnsfree.NewDNSProviderConfig(cfg)
case "dynu":
cfg := dynu.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -486,6 +556,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return epik.NewDNSProviderConfig(cfg)
case "f5xc":
cfg := f5xc.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return f5xc.NewDNSProviderConfig(cfg)
case "freemyip":
cfg := freemyip.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -774,6 +853,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return mailinabox.NewDNSProviderConfig(cfg)
case "manageengine":
cfg := manageengine.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return manageengine.NewDNSProviderConfig(cfg)
case "metaname":
cfg := metaname.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -783,6 +871,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return metaname.NewDNSProviderConfig(cfg)
case "metaregistrar":
cfg := metaregistrar.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return metaregistrar.NewDNSProviderConfig(cfg)
case "mijnhost":
cfg := mijnhost.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -873,6 +970,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return nicmanager.NewDNSProviderConfig(cfg)
case "nicru":
cfg := nicru.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return nicru.NewDNSProviderConfig(cfg)
case "nifcloud":
cfg := nifcloud.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -963,6 +1069,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return rackspace.NewDNSProviderConfig(cfg)
case "rainyun":
cfg := rainyun.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return rainyun.NewDNSProviderConfig(cfg)
case "rcodezero":
cfg := rcodezero.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -972,6 +1087,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return rcodezero.NewDNSProviderConfig(cfg)
case "regfish":
cfg := regfish.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return regfish.NewDNSProviderConfig(cfg)
case "regru":
cfg := regru.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -1089,6 +1213,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return sonic.NewDNSProviderConfig(cfg)
case "spaceship":
cfg := spaceship.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return spaceship.NewDNSProviderConfig(cfg)
case "stackpath":
cfg := stackpath.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -1098,6 +1231,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return stackpath.NewDNSProviderConfig(cfg)
case "technitium":
cfg := technitium.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return technitium.NewDNSProviderConfig(cfg)
case "tencentcloud":
cfg := tencentcloud.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -1224,6 +1366,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return wedos.NewDNSProviderConfig(cfg)
case "westcn":
cfg := westcn.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return westcn.NewDNSProviderConfig(cfg)
case "yandex":
cfg := yandex.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
@@ -1251,6 +1402,15 @@ func GetDNSProviderByJsonConfig(name string, js string, propagationTimeout int64
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return yandexcloud.NewDNSProviderConfig(cfg)
case "zoneedit":
cfg := zoneedit.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = pgDuration
cfg.PollingInterval = plInterval
return zoneedit.NewDNSProviderConfig(cfg)
case "zoneee":
cfg := zoneee.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)

View File

@@ -1,4 +1,31 @@
{
"active24": {
"Name": "active24",
"ConfigableFields": [
{
"Title": "APIKey",
"Datatype": "string"
},
{
"Title": "Secret",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"alidns": {
"Name": "alidns",
"ConfigableFields": [
@@ -144,6 +171,60 @@
}
]
},
"axelname": {
"Name": "axelname",
"ConfigableFields": [
{
"Title": "Nickname",
"Datatype": "string"
},
{
"Title": "Token",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"azion": {
"Name": "azion",
"ConfigableFields": [
{
"Title": "PersonalToken",
"Datatype": "string"
},
{
"Title": "PageSize",
"Datatype": "int"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"azure": {
"Name": "azure",
"ConfigableFields": [
@@ -278,6 +359,28 @@
}
]
},
"baiducloud": {
"Name": "baiducloud",
"ConfigableFields": [
{
"Title": "AccessKeyID",
"Datatype": "string"
},
{
"Title": "SecretAccessKey",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": []
},
"bindman": {
"Name": "bindman",
"ConfigableFields": [
@@ -348,6 +451,33 @@
}
]
},
"bookmyname": {
"Name": "bookmyname",
"ConfigableFields": [
{
"Title": "Username",
"Datatype": "string"
},
{
"Title": "Password",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"brandit": {
"Name": "brandit",
"ConfigableFields": [
@@ -423,10 +553,6 @@
"civo": {
"Name": "civo",
"ConfigableFields": [
{
"Title": "ProjectID",
"Datatype": "string"
},
{
"Title": "Token",
"Datatype": "string"
@@ -440,7 +566,12 @@
"Datatype": "time.Duration"
}
],
"HiddenFields": []
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"clouddns": {
"Name": "clouddns",
@@ -492,6 +623,10 @@
"Title": "ZoneToken",
"Datatype": "string"
},
{
"Title": "BaseURL",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
@@ -632,6 +767,41 @@
}
]
},
"conohav3": {
"Name": "conohav3",
"ConfigableFields": [
{
"Title": "Region",
"Datatype": "string"
},
{
"Title": "TenantID",
"Datatype": "string"
},
{
"Title": "UserID",
"Datatype": "string"
},
{
"Title": "Password",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"constellix": {
"Name": "constellix",
"ConfigableFields": [
@@ -806,29 +976,6 @@
}
]
},
"dnshomede": {
"Name": "dnshomede",
"ConfigableFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "Credentials",
"Datatype": "map[string]string"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"dnsimple": {
"Name": "dnsimple",
"ConfigableFields": [
@@ -1044,6 +1191,33 @@
}
]
},
"dyndnsfree": {
"Name": "dyndnsfree",
"ConfigableFields": [
{
"Title": "Username",
"Datatype": "string"
},
{
"Title": "Password",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"dynu": {
"Name": "dynu",
"ConfigableFields": [
@@ -1164,6 +1338,37 @@
}
]
},
"f5xc": {
"Name": "f5xc",
"ConfigableFields": [
{
"Title": "APIToken",
"Datatype": "string"
},
{
"Title": "TenantName",
"Datatype": "string"
},
{
"Title": "GroupName",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"freemyip": {
"Name": "freemyip",
"ConfigableFields": [
@@ -1608,6 +1813,10 @@
"Title": "SSLVerify",
"Datatype": "bool"
},
{
"Title": "CACertificate",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
@@ -2020,6 +2229,28 @@
],
"HiddenFields": []
},
"manageengine": {
"Name": "manageengine",
"ConfigableFields": [
{
"Title": "ClientID",
"Datatype": "string"
},
{
"Title": "ClientSecret",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": []
},
"metaname": {
"Name": "metaname",
"ConfigableFields": [
@@ -2042,6 +2273,29 @@
],
"HiddenFields": []
},
"metaregistrar": {
"Name": "metaregistrar",
"ConfigableFields": [
{
"Title": "APIToken",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"mijnhost": {
"Name": "mijnhost",
"ConfigableFields": [
@@ -2327,6 +2581,36 @@
}
]
},
"nicru": {
"Name": "nicru",
"ConfigableFields": [
{
"Title": "Username",
"Datatype": "string"
},
{
"Title": "Password",
"Datatype": "string"
},
{
"Title": "ServiceID",
"Datatype": "string"
},
{
"Title": "Secret",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": []
},
"nifcloud": {
"Name": "nifcloud",
"ConfigableFields": [
@@ -2633,6 +2917,29 @@
}
]
},
"rainyun": {
"Name": "rainyun",
"ConfigableFields": [
{
"Title": "APIKey",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"rcodezero": {
"Name": "rcodezero",
"ConfigableFields": [
@@ -2656,6 +2963,29 @@
}
]
},
"regfish": {
"Name": "regfish",
"ConfigableFields": [
{
"Title": "APIKey",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"regru": {
"Name": "regru",
"ConfigableFields": [
@@ -2698,6 +3028,10 @@
"Title": "Nameserver",
"Datatype": "string"
},
{
"Title": "TSIGFile",
"Datatype": "string"
},
{
"Title": "TSIGAlgorithm",
"Datatype": "string"
@@ -2789,6 +3123,10 @@
}
],
"HiddenFields": [
{
"Title": "PrivateZone",
"Datatype": "bool"
},
{
"Title": "WaitForRecordSetsChanged",
"Datatype": "bool"
@@ -3045,6 +3383,33 @@
}
]
},
"spaceship": {
"Name": "spaceship",
"ConfigableFields": [
{
"Title": "APIKey",
"Datatype": "string"
},
{
"Title": "APISecret",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"stackpath": {
"Name": "stackpath",
"ConfigableFields": [
@@ -3071,6 +3436,33 @@
],
"HiddenFields": []
},
"technitium": {
"Name": "technitium",
"ConfigableFields": [
{
"Title": "BaseURL",
"Datatype": "string"
},
{
"Title": "APIToken",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"tencentcloud": {
"Name": "tencentcloud",
"ConfigableFields": [
@@ -3285,7 +3677,12 @@
"Datatype": "time.Duration"
}
],
"HiddenFields": []
"HiddenFields": [
{
"Title": "QuoteValue",
"Datatype": "bool"
}
]
},
"vkcloud": {
"Name": "vkcloud",
@@ -3452,6 +3849,33 @@
}
]
},
"westcn": {
"Name": "westcn",
"ConfigableFields": [
{
"Title": "Username",
"Datatype": "string"
},
{
"Title": "Password",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"yandex": {
"Name": "yandex",
"ConfigableFields": [
@@ -3524,6 +3948,33 @@
],
"HiddenFields": []
},
"zoneedit": {
"Name": "zoneedit",
"ConfigableFields": [
{
"Title": "User",
"Datatype": "string"
},
{
"Title": "AuthToken",
"Datatype": "string"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
}
],
"HiddenFields": [
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
},
"zoneee": {
"Name": "zoneee",
"ConfigableFields": [

View File

@@ -0,0 +1,112 @@
package auth
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"sync"
"time"
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
)
// PluginAPIKey represents an API key for a plugin
type PluginAPIKey struct {
PluginID string
APIKey string
PermittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint // List of permitted API endpoints
CreatedAt time.Time
}
// APIKeyManager manages API keys for plugins
type APIKeyManager struct {
keys map[string]*PluginAPIKey // key: API key, value: plugin info
mutex sync.RWMutex
}
// NewAPIKeyManager creates a new API key manager
func NewAPIKeyManager() *APIKeyManager {
return &APIKeyManager{
keys: make(map[string]*PluginAPIKey),
mutex: sync.RWMutex{},
}
}
// GenerateAPIKey generates a new API key for a plugin
func (m *APIKeyManager) GenerateAPIKey(pluginID string, permittedEndpoints []zoraxy_plugin.PermittedAPIEndpoint) (*PluginAPIKey, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
// Generate a cryptographically secure random key
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
}
// Hash the random bytes to create the API key
hash := sha256.Sum256(bytes)
apiKey := hex.EncodeToString(hash[:])
// Create the plugin API key
pluginAPIKey := &PluginAPIKey{
PluginID: pluginID,
APIKey: apiKey,
PermittedEndpoints: permittedEndpoints,
CreatedAt: time.Now(),
}
// Store the API key
m.keys[apiKey] = pluginAPIKey
return pluginAPIKey, nil
}
// ValidateAPIKey validates an API key and returns the associated plugin information
func (m *APIKeyManager) ValidateAPIKey(apiKey string) (*PluginAPIKey, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
pluginAPIKey, exists := m.keys[apiKey]
if !exists {
return nil, fmt.Errorf("invalid API key")
}
return pluginAPIKey, nil
}
// ValidateAPIKeyForEndpoint validates an API key for a specific endpoint
func (m *APIKeyManager) ValidateAPIKeyForEndpoint(endpoint string, method string, apiKey string) (*PluginAPIKey, error) {
pluginAPIKey, err := m.ValidateAPIKey(apiKey)
if err != nil {
return nil, err
}
// Check if the endpoint is permitted
for _, permittedEndpoint := range pluginAPIKey.PermittedEndpoints {
if permittedEndpoint.Endpoint == endpoint && permittedEndpoint.Method == method {
return pluginAPIKey, nil
}
}
return nil, fmt.Errorf("endpoint not permitted for this API key")
}
// RevokeAPIKeysForPlugin revokes all API keys for a specific plugin
func (m *APIKeyManager) RevokeAPIKeysForPlugin(pluginID string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
keysToRemove := []string{}
for apiKey, pluginAPIKey := range m.keys {
if pluginAPIKey.PluginID == pluginID {
keysToRemove = append(keysToRemove, apiKey)
}
}
for _, apiKey := range keysToRemove {
delete(m.keys, apiKey)
}
return nil
}

View File

@@ -0,0 +1,94 @@
// Handles the API-Key based authentication for plugins
package auth
import (
"errors"
"fmt"
"net/http"
"strings"
)
const (
PLUGIN_API_PREFIX = "/plugin"
)
type PluginMiddlewareOptions struct {
DeniedHandler http.HandlerFunc //Thing(s) to do when request is rejected
ApiKeyManager *APIKeyManager
TargetMux *http.ServeMux
}
// PluginAuthMiddleware provides authentication middleware for plugin API requests
type PluginAuthMiddleware struct {
option PluginMiddlewareOptions
endpoints map[string]http.HandlerFunc
}
// NewPluginAuthMiddleware creates a new plugin authentication middleware
func NewPluginAuthMiddleware(option PluginMiddlewareOptions) *PluginAuthMiddleware {
return &PluginAuthMiddleware{
option: option,
endpoints: make(map[string]http.HandlerFunc),
}
}
func (m *PluginAuthMiddleware) HandleAuthCheck(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
// Check for API key in the Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
// No authorization header
m.option.DeniedHandler(w, r)
return
}
// Check if it's a plugin API key (Bearer token)
if !strings.HasPrefix(authHeader, "Bearer ") {
// Not a Bearer token
m.option.DeniedHandler(w, r)
return
}
// Extract the API key
apiKey := strings.TrimPrefix(authHeader, "Bearer ")
// Validate the API key for this endpoint
_, err := m.option.ApiKeyManager.ValidateAPIKeyForEndpoint(r.URL.Path, r.Method, apiKey)
if err != nil {
// Invalid API key or endpoint not permitted
m.option.DeniedHandler(w, r)
return
}
// Call the original handler
handler(w, r)
}
// wraps an HTTP handler with plugin authentication middleware
func (m *PluginAuthMiddleware) HandleFunc(endpoint string, handler http.HandlerFunc) error {
// ensure the endpoint is prefixed with PLUGIN_API_PREFIX
if !strings.HasPrefix(endpoint, PLUGIN_API_PREFIX) {
endpoint = PLUGIN_API_PREFIX + endpoint
}
// Check if the endpoint already registered
if _, exist := m.endpoints[endpoint]; exist {
fmt.Println("WARNING! Duplicated registering of plugin api endpoint: " + endpoint)
return errors.New("endpoint register duplicated")
}
m.endpoints[endpoint] = handler
wrappedHandler := func(w http.ResponseWriter, r *http.Request) {
m.HandleAuthCheck(w, r, handler)
}
// Ok. Register handler
if m.option.TargetMux == nil {
http.HandleFunc(endpoint, wrappedHandler)
} else {
m.option.TargetMux.HandleFunc(endpoint, wrappedHandler)
}
return nil
}

View File

@@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
sep := h.Parent.GetProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
//Matching proxy rule found
//Access Check (blacklist / whitelist)

View File

@@ -6,6 +6,7 @@ import (
"strings"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/netutils"
)
/*
@@ -70,9 +71,36 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
exceptionType := exceptionRule.RuleType
switch exceptionType {
case AuthExceptionType_Paths:
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
case AuthExceptionType_CIDR:
requesterIp := netutils.GetRequesterIP(r)
if requesterIp != "" {
if requesterIp == exceptionRule.CIDR {
// This IP is excluded from basic auth
return nil
}
wildcardMatch := netutils.MatchIpWildcard(requesterIp, exceptionRule.CIDR)
if wildcardMatch {
// This IP is excluded from basic auth
return nil
}
cidrMatch := netutils.MatchIpCIDR(requesterIp, exceptionRule.CIDR)
if cidrMatch {
// This IP is excluded from basic auth
return nil
}
}
default:
//Unknown exception type, skip this rule
continue
}
}
}

View File

@@ -0,0 +1,59 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"fmt"
"imuslab.com/zoraxy/mod/tlscert"
)
func (router *Router) ResolveHostSpecificTlsBehaviorForHostname(hostname string) (*tlscert.HostSpecificTlsBehavior, error) {
if hostname == "" {
return nil, errors.New("hostname cannot be empty")
}
ept := router.GetProxyEndpointFromHostname(hostname)
if ept == nil {
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
}
// Check if the endpoint has a specific TLS behavior
if ept.TlsOptions != nil {
imported := &tlscert.HostSpecificTlsBehavior{}
router.tlsBehaviorMutex.RLock()
// Deep copy the TlsOptions using JSON marshal/unmarshal
data, err := json.Marshal(ept.TlsOptions)
if err != nil {
router.tlsBehaviorMutex.RUnlock()
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
}
router.tlsBehaviorMutex.RUnlock()
if err := json.Unmarshal(data, imported); err != nil {
return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err)
}
return imported, nil
}
return tlscert.GetDefaultHostSpecificTlsBehavior(), nil
}
func (router *Router) SetPreferredCertificateForDomain(ept *ProxyEndpoint, domain string, certName string) error {
if ept == nil || certName == "" {
return errors.New("endpoint and certificate name cannot be empty")
}
// Set the preferred certificate for the endpoint
if ept.TlsOptions == nil {
ept.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
}
router.tlsBehaviorMutex.Lock()
if ept.TlsOptions.PreferredCertificate == nil {
ept.TlsOptions.PreferredCertificate = make(map[string]string)
}
ept.TlsOptions.PreferredCertificate[domain] = certName
router.tlsBehaviorMutex.Unlock()
return nil
}

View File

@@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := router.getProxyEndpointFromHostname(domainOnly)
sep := router.GetProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler
originalHostHeader := r.Host
@@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
hostname = r.Host
}
hostname = strings.Split(hostname, ":")[0]
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
return subdEndpoint != nil
}

View File

@@ -34,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
}
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
hostname = strings.ToLower(hostname)
ep, ok := router.ProxyEndpoints.Load(hostname)
@@ -63,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
}
//Wildcard not match. Check for alias
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
if len(ep.MatchingDomainAlias) > 0 {
for _, aliasDomain := range ep.MatchingDomainAlias {
match, err := filepath.Match(aliasDomain, hostname)
if err != nil {

View File

@@ -75,5 +75,9 @@ func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string,
downstreamHeaderCounter++
}
// Slice the arrays to only include the filled portions to prevent nil slice access
upstreamHeaders = upstreamHeaders[:upstreamHeaderCounter]
downstreamHeaders = downstreamHeaders[:downstreamHeaderCounter]
return upstreamHeaders, downstreamHeaders
}

View File

@@ -75,16 +75,20 @@ type RouterOption struct {
/* Router Object */
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
Running bool //If the router is running
Root *ProxyEndpoint //Root proxy endpoint, default site
mux http.Handler //HTTP handler
server *http.Server //HTTP server
tlsListener net.Listener //TLS listener, handle SNI routing
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
Running bool //If the router is running
Root *ProxyEndpoint //Root proxy endpoint, default site
/* Internals */
mux http.Handler //HTTP handler
server *http.Server //HTTP server
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
tlsListener net.Listener //TLS listener, handle SNI routing
tlsBehaviorMutex sync.RWMutex //Mutex for tlsBehavior map
tlsRedirectStop chan bool //Stop channel for tls redirection server
tlsRedirectStop chan bool //Stop channel for tls redirection server
rateLimterStop chan bool //Stop channel for rate limiter
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
}
@@ -102,9 +106,18 @@ type BasicAuthUnhashedCredentials struct {
Password string
}
type AuthExceptionType int
const (
AuthExceptionType_Paths AuthExceptionType = iota //Path exception, match by path prefix
AuthExceptionType_CIDR //CIDR exception, match by CIDR
)
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
RuleType AuthExceptionType //The type of the exception rule
PathPrefix string //Path prefix to match, e.g. /api/v1/
CIDR string //CIDR to match, e.g. 192.168.1.0/24 or IP address, e.g. 192.168.1.1
}
/* Routing Rule Data Structures */

View File

@@ -18,9 +18,10 @@ import (
*/
type Logger struct {
Prefix string //Prefix for log files
LogFolder string //Folder to store the log file
CurrentLogFile string //Current writing filename
Prefix string //Prefix for log files
LogFolder string //Folder to store the log file
CurrentLogFile string //Current writing filename
RotateOption RotateOption //Options for log rotation, see rotate.go
logger *log.Logger
file *os.File
}

View File

@@ -0,0 +1,127 @@
package logger
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"time"
)
type RotateOption struct {
Enabled bool //Whether log rotation is enabled
MaxSize int64 //Maximum size of the log file in bytes before rotation (e.g. 10 * 1024 * 1024 for 10MB)
MaxBackups int //Maximum number of backup files to keep
Compress bool //Whether to compress the rotated files
BackupDir string //Directory to store backup files, if empty, use the same directory as the log file
}
func (l *Logger) LogNeedRotate(filename string) bool {
if !l.RotateOption.Enabled {
return false
}
info, err := os.Stat(filename)
if err != nil {
return false
}
return info.Size() >= l.RotateOption.MaxSize
}
func (l *Logger) RotateLog() error {
if !l.RotateOption.Enabled {
return nil
}
needRotate := l.LogNeedRotate(l.CurrentLogFile)
if !needRotate {
return nil
}
//Close current file
if l.file != nil {
l.file.Close()
}
// Determine backup directory
backupDir := l.RotateOption.BackupDir
if backupDir == "" {
backupDir = filepath.Dir(l.CurrentLogFile)
}
// Ensure backup directory exists
if err := os.MkdirAll(backupDir, 0755); err != nil {
return err
}
// Generate rotated filename with timestamp
timestamp := time.Now().Format("20060102-150405")
baseName := filepath.Base(l.CurrentLogFile)
rotatedName := fmt.Sprintf("%s.%s", baseName, timestamp)
rotatedPath := filepath.Join(backupDir, rotatedName)
// Rename current log file to rotated file
if err := os.Rename(l.CurrentLogFile, rotatedPath); err != nil {
return err
}
// Optionally compress the rotated file
if l.RotateOption.Compress {
if err := compressFile(rotatedPath); err != nil {
return err
}
// Remove the uncompressed rotated file after compression
os.Remove(rotatedPath)
rotatedPath += ".gz"
}
// Remove old backups if exceeding MaxBackups
if l.RotateOption.MaxBackups > 0 {
files, err := filepath.Glob(filepath.Join(backupDir, baseName+".*"))
if err == nil && len(files) > l.RotateOption.MaxBackups {
sort.Slice(files, func(i, j int) bool {
return files[i] < files[j]
})
for _, old := range files[:len(files)-l.RotateOption.MaxBackups] {
os.Remove(old)
}
}
}
// Reopen a new log file
file, err := os.OpenFile(l.CurrentLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
l.file = file
return nil
}
// compressFile compresses the given file using zip format and creates a .gz file.
func compressFile(filename string) error {
zipFilename := filename + ".gz"
outFile, err := os.Create(zipFilename)
if err != nil {
return err
}
defer outFile.Close()
zipWriter := zip.NewWriter(outFile)
defer zipWriter.Close()
fileToCompress, err := os.Open(filename)
if err != nil {
return err
}
defer fileToCompress.Close()
w, err := zipWriter.Create(filepath.Base(filename))
if err != nil {
return err
}
_, err = io.Copy(w, fileToCompress)
return err
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/utils"
@@ -21,6 +22,21 @@ type Viewer struct {
option *ViewerOption
}
type LogSummary struct {
TotalReqests int64 `json:"total_requests"`
TotalValid int64 `json:"total_valid"`
TotalErrors int64 `json:"total_errors"`
LogSource string `json:"log_source"`
RequestMethods map[string]int64 `json:"request_methods"` //Request methods (key: method, value: hit count)
HitPerDay map[string]int64 `json:"hit_per_day"` //Total hit count per day (key: date, value: hit count)
HiPerSite map[string][]int64 `json:"hit_per_site"` //origin to hit count per day (key: origin, value: []int64{hit count per day})
UniqueIPs map[string]int64 `json:"unique_ips"` //Unique IPs per day (key: date, value: unique IP count)
TopOrigins map[string]int64 `json:"top_origins"` //Top origins (key: origin, value: hit count)
TopUserAgents map[string]int64 `json:"top_user_agents"` //Top user agents (key: user agent, value: hit count)
TopPaths map[string]int64 `json:"top_paths"` //Top paths (key: path, value: hit count)
TotalSize int64 `json:"total_size"` //Total size of the log file
}
type LogFile struct {
Title string
Filename string
@@ -51,15 +67,111 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
return
}
filter, err := utils.GetPara(r, "filter")
if err != nil {
filter = ""
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//If filter is given, only return lines that contains the filter string
if filter != "" {
lines := strings.Split(content, "\n")
filteredLines := []string{}
for _, line := range lines {
switch filter {
case "error":
if strings.Contains(line, ":error]") {
filteredLines = append(filteredLines, line)
}
case "request":
if strings.Contains(line, "[router:") {
filteredLines = append(filteredLines, line)
}
case "system":
if strings.Contains(line, "[system:") {
filteredLines = append(filteredLines, line)
}
case "all":
filteredLines = append(filteredLines, line)
default:
if strings.Contains(line, filter) {
filteredLines = append(filteredLines, line)
}
}
}
content = strings.Join(filteredLines, "\n")
}
utils.SendTextResponse(w, content)
}
func (v *Viewer) HandleReadLogSummary(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filename given")
return
}
summary, err := v.LoadLogSummary(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendJSONResponse(w, summary)
}
func (v *Viewer) HandleLogErrorSummary(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filename given")
return
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Generate the error summary for log that is request and non 100 - 200 range status code
errorLines := [][]string{}
lines := strings.Split(content, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
// Only process router logs with a status code not in 1xx or 2xx
if strings.Contains(line, "[router:") {
//Extract date time from the line
timestamp := ""
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") {
timestamp = line[1:strings.Index(line, "]")]
}
//Trim out the request metadata
line = line[strings.LastIndex(line, "]")+1:]
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) > 0 {
statusStr := fields[2]
if len(statusStr) == 3 && (statusStr[0] != '1' && statusStr[0] != '2' && statusStr[0] != '3') {
fieldsWithTimestamp := append([]string{timestamp}, strings.Fields(strings.TrimSpace(line))...)
errorLines = append(errorLines, fieldsWithTimestamp)
}
}
}
}
js, _ := json.Marshal(errorLines)
utils.SendJSONResponse(w, string(js))
}
/*
Log Access Functions
*/
@@ -116,3 +228,143 @@ func (v *Viewer) LoadLogFile(filename string) (string, error) {
return "", errors.New("log file not found")
}
}
func (v *Viewer) LoadLogSummary(filename string) (string, error) {
filename = filepath.ToSlash(filename)
filename = strings.ReplaceAll(filename, "../", "")
logFilepath := filepath.Join(v.option.RootFolder, filename)
if utils.FileExists(logFilepath) {
//Load it
content, err := os.ReadFile(logFilepath)
if err != nil {
return "", err
}
var summary LogSummary
summary.LogSource = filepath.Base(filename)
summary.TotalSize = int64(len(content))
summary.RequestMethods = map[string]int64{}
summary.HitPerDay = map[string]int64{} // Initialize to avoid nil map error
summary.HiPerSite = map[string][]int64{}
summary.UniqueIPs = map[string]int64{}
summary.TopOrigins = map[string]int64{}
summary.TopUserAgents = map[string]int64{}
summary.TopPaths = map[string]int64{}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue // Skip empty lines
}
if !strings.Contains(line, "[router:") {
continue // Only process router: type logs
}
summary.TotalReqests++
// Extract the date from the log line
parts := strings.Split(line, "]")
if len(parts) < 2 {
continue // Skip malformed lines
}
datePart := strings.TrimSpace(parts[0][1:]) // Remove the leading '['
date := datePart[:10] // Get the date part (YYYY-MM-DD)
// Increment hit count for the day
summary.HitPerDay[date]++
// Extract origin, user agent, and path
origin := ""
userAgent := ""
path := ""
method := ""
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "[origin:") {
origin = strings.TrimPrefix(part, "[origin:")
origin = strings.TrimSuffix(origin, "]")
} else if strings.HasPrefix(part, "[useragent:") {
userAgent = strings.TrimPrefix(part, "[useragent:")
userAgent = strings.TrimSuffix(userAgent, "]")
} else if !strings.HasPrefix(part, "[") && !strings.HasSuffix(part, "]") && method == "" {
// This is likely the HTTP method (GET, POST, etc.)
fields := strings.Fields(part)
if len(fields) > 0 {
method = fields[0]
summary.RequestMethods[method]++
if len(fields) > 1 {
path = fields[1] // The path is the second field
}
}
}
}
if origin != "" {
if _, exists := summary.HiPerSite[origin]; !exists {
summary.HiPerSite[origin] = make([]int64, 32) // Initialize for 31 days
}
//Get the day of month from date
dayIndex := 0
if len(date) >= 10 {
dayStr := date[8:10] // Get the day part (DD)
dayIndex, _ = strconv.Atoi(dayStr) // Convert to integer
}
if dayIndex < 1 || dayIndex > 31 {
dayIndex = 0 // Default to 0 if out of range
}
summary.HiPerSite[origin][dayIndex-1]++ // Increment hit count for the specific day
summary.HitPerDay[date]++ // Increment total hit count for the date
}
if userAgent != "" {
summary.TopUserAgents[userAgent]++
}
if path != "" {
if idx := strings.IndexAny(path, "?#"); idx != -1 {
path = path[:idx]
}
summary.TopPaths[path]++
}
// Increment unique IPs (assuming IP is the first part of the line)
ipPart := strings.Split(line, "[client:")[1]
if ipPart != "" {
ip := strings.TrimSpace(strings.Split(ipPart, "]")[0])
if _, exists := summary.UniqueIPs[ip]; !exists {
summary.UniqueIPs[ip] = 0
}
summary.UniqueIPs[ip]++ // Increment unique IP count for the day
}
// Check for errors: count if status code is not 1xx or 2xx
statusParts := strings.Fields(line)
if len(statusParts) > 0 {
statusStr := statusParts[len(statusParts)-1]
if len(statusStr) == 3 {
if statusCode := statusStr[0]; statusCode != '1' && statusCode != '2' {
summary.TotalErrors++
} else {
summary.TotalValid++
}
}
}
}
js, err := json.Marshal(summary)
if err != nil {
return "", err
}
return string(js), nil
} else {
return "", errors.New("log file not found")
}
}

View File

@@ -41,6 +41,18 @@ func (m *Manager) StartPlugin(pluginID string) error {
Port: getRandomPortNumber(),
RuntimeConst: *m.Options.SystemConst,
}
// Generate API key if the plugin has permitted endpoints
if len(thisPlugin.Spec.PermittedAPIEndpoints) > 0 {
apiKey, err := m.Options.APIKeyManager.GenerateAPIKey(thisPlugin.Spec.ID, thisPlugin.Spec.PermittedAPIEndpoints)
if err != nil {
return err
}
pluginConfiguration.APIKey = apiKey.APIKey
pluginConfiguration.ZoraxyPort = m.Options.ZoraxyPort
m.Log("Generated API key for plugin "+thisPlugin.Spec.Name, nil)
}
js, _ := json.Marshal(pluginConfiguration)
//Start the plugin with given configuration
@@ -270,6 +282,13 @@ func (m *Manager) StopPlugin(pluginID string) error {
thisPlugin.Enabled = false
thisPlugin.StopAllStaticPathRouters()
thisPlugin.StopDynamicForwardRouter()
//Clean up API key
err = m.Options.APIKeyManager.RevokeAPIKeysForPlugin(thisPlugin.Spec.ID)
if err != nil {
m.Log("Failed to revoke API keys for plugin "+thisPlugin.Spec.Name, err)
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"sync"
"time"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/info/logger"
@@ -42,10 +43,14 @@ type ManagerOptions struct {
/* Runtime */
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
ZoraxyPort int //The port of the Zoraxy instance, used for API calls
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
Database *database.Database `json:"-"`
Logger *logger.Logger `json:"-"`
/* API Key Management */
APIKeyManager *auth.APIKeyManager `json:"-"` //The API key manager for the plugins
/* Development */
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
HotReloadInterval int //The interval for checking the plugin file change, in seconds

View File

@@ -17,7 +17,7 @@ import (
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
@@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
if sniffResult == SniffResultAccept {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {

View File

@@ -145,6 +145,24 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
})
}
// HandleFunc registers a handler function for the given pattern
// The pattern should start with the handler prefix, e.g. /ui/hello
// If the pattern does not start with the handler prefix, it will be prepended with the handler prefix
func (p *PluginUiRouter) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), mux *http.ServeMux) {
// If mux is nil, use the default ServeMux
if mux == nil {
mux = http.DefaultServeMux
}
// Make sure the pattern starts with the handler prefix
if !strings.HasPrefix(pattern, p.HandlerPrefix) {
pattern = p.HandlerPrefix + pattern
}
// Register the handler with the http.ServeMux
mux.HandleFunc(pattern, handler)
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {

View File

@@ -47,6 +47,12 @@ type RuntimeConstantValue struct {
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
type PermittedAPIEndpoint struct {
Method string `json:"method"` //HTTP method for the API endpoint (e.g., GET, POST)
Endpoint string `json:"endpoint"` //The API endpoint that the plugin can access
Reason string `json:"reason"` //The reason why the plugin needs to access this endpoint
}
/*
IntroSpect Payload
@@ -97,7 +103,10 @@ type IntroSpect struct {
/* 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
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, paired with comments describing how the event is used, see Zoraxy documentation for more details
/* API Access Control */
PermittedAPIEndpoints []PermittedAPIEndpoint `json:"permitted_api_endpoints"` //List of API endpoints this plugin can access, and a description of why the plugin needs to access this endpoint
}
/*
@@ -126,8 +135,10 @@ 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
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
APIKey string `json:"api_key,omitempty"` //API key for accessing Zoraxy APIs, if the plugin has permitted endpoints
ZoraxyPort int `json:"zoraxy_port,omitempty"` //The port that Zoraxy is running on, used for making API calls to Zoraxy
//To be expanded
}

View File

@@ -0,0 +1,18 @@
//go:build darwin && amd64
// +build darwin,amd64
package sshprox
import "embed"
/*
Binary embedding for AMD64 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_darwin_amd64
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

Binary file not shown.

View File

@@ -0,0 +1,93 @@
package tlscert
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"time"
)
// GenerateSelfSignedCertificate generates a self-signed ECDSA certificate and saves it to the specified files.
func (m *Manager) GenerateSelfSignedCertificate(cn string, sans []string, certFile string, keyFile string) error {
// Generate private key (ECDSA P-256)
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to generate private key", err)
return err
}
// Create certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: cn, // Common Name for the certificate
Organization: []string{"aroz.org"}, // Organization name
OrganizationalUnit: []string{"Zoraxy"}, // Organizational Unit
Country: []string{"US"}, // Country code
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour), // valid for 1 year
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: sans, // Subject Alternative Names
}
// Create self-signed certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to create certificate", err)
return err
}
// Remove old certificate file if it exists
certPath := filepath.Join(m.CertStore, certFile)
if _, err := os.Stat(certPath); err == nil {
os.Remove(certPath)
}
// Remove old key file if it exists
keyPath := filepath.Join(m.CertStore, keyFile)
if _, err := os.Stat(keyPath); err == nil {
os.Remove(keyPath)
}
// Write certificate to file
certOut, err := os.Create(filepath.Join(m.CertStore, certFile))
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to open cert file for writing: "+certFile, err)
return err
}
defer certOut.Close()
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to write certificate to file: "+certFile, err)
return err
}
// Encode private key to PEM
privBytes, err := x509.MarshalECPrivateKey(privKey)
if err != nil {
m.Logger.PrintAndLog("tls-router", "Unable to marshal ECDSA private key", err)
return err
}
keyOut, err := os.Create(filepath.Join(m.CertStore, keyFile))
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to open key file for writing: "+keyFile, err)
return err
}
defer keyOut.Close()
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to write private key to file: "+keyFile, err)
return err
}
m.Logger.PrintAndLog("tls-router", "Certificate and key generated: "+certFile+", "+keyFile, nil)
return nil
}

352
src/mod/tlscert/handler.go Normal file
View File

@@ -0,0 +1,352 @@
package tlscert
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/utils"
)
// Handle cert remove
func (m *Manager) HandleCertRemove(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "invalid domain given")
return
}
err = m.RemoveCert(domain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
}
}
// Handle download of the selected certificate
func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) {
// get the certificate name
certname, err := utils.GetPara(r, "certname")
if err != nil {
utils.SendErrorResponse(w, "invalid certname given")
return
}
certname = filepath.Base(certname) //prevent path escape
// check if the cert exists
pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key")
priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem")
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
//Zip them and serve them via http download
seeking, _ := utils.GetBool(r, "seek")
if seeking {
//This request only check if the key exists. Do not provide download
utils.SendOK(w)
return
}
//Serve both file in zip
zipTmpFolder := "./tmp/download"
os.MkdirAll(zipTmpFolder, 0775)
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
err := utils.ZipFiles(zipFileName, pubKey, priKey)
if err != nil {
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
return
}
defer os.Remove(zipFileName) // Clean up the zip file after serving
// Serve the zip file
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
w.Header().Set("Content-Type", "application/zip")
http.ServeFile(w, r, zipFileName)
} else {
//Not both key exists
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
return
}
}
// Handle upload of the certificate
func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// get the key type
keytype, err := utils.GetPara(r, "ktype")
overWriteFilename := ""
if err != nil {
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
return
}
// get the domain
domain, err := utils.GetPara(r, "domain")
if err != nil {
//Assume localhost
domain = "default"
}
switch keytype {
case "pub":
overWriteFilename = domain + ".pem"
case "pri":
overWriteFilename = domain + ".key"
default:
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
return
}
// parse multipart form data
err = r.ParseMultipartForm(10 << 20) // 10 MB
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
// get file from form data
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to get file", http.StatusBadRequest)
return
}
defer file.Close()
// create file in upload directory
os.MkdirAll(m.CertStore, 0775)
f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename))
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return
}
defer f.Close()
// copy file contents to destination file
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
//Update cert list
m.UpdateLoadedCertList()
// send response
fmt.Fprintln(w, "File upload successful!")
}
// List all certificates and map all their domains to the cert filename
func (m *Manager) HandleListDomains(w http.ResponseWriter, r *http.Request) {
filenames, err := os.ReadDir(m.CertStore)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
certnameToDomainMap := map[string]string{}
for _, filename := range filenames {
if filename.IsDir() {
continue
}
certFilepath := filepath.Join(m.CertStore, filename.Name())
certBtyes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
m.Logger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
for _, dnsName := range cert.DNSNames {
certnameToDomainMap[dnsName] = certname
}
certnameToDomainMap[cert.Subject.CommonName] = certname
}
}
}
}
requireCompact, _ := utils.GetPara(r, "compact")
if requireCompact == "true" {
result := make(map[string][]string)
for key, value := range certnameToDomainMap {
if _, ok := result[value]; !ok {
result[value] = make([]string, 0)
}
result[value] = append(result[value], key)
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
return
}
js, _ := json.Marshal(certnameToDomainMap)
utils.SendJSONResponse(w, string(js))
}
// Return a list of domains where the certificates covers
func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) {
filenames, err := m.ListCertDomains()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
showDate, _ := utils.GetBool(r, "date")
if showDate {
type CertInfo struct {
Domain string
LastModifiedDate string
ExpireDate string
RemainingDays int
UseDNS bool
}
results := []*CertInfo{}
for _, filename := range filenames {
certFilepath := filepath.Join(m.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath)
if err != nil {
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
return
}
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
expiredIn := 0
if err != nil {
//Unable to load this file
continue
} else {
//Cert loaded. Check its expire time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
duration := cert.NotAfter.Sub(time.Now())
// Convert the duration to days
expiredIn = int(duration.Hours() / 24)
}
}
}
certInfoFilename := filepath.Join(m.CertStore, filename+".json")
useDNSValidation := false //Default to false for HTTP TLS certificates
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
if err == nil {
useDNSValidation = certInfo.UseDNS
}
thisCertInfo := CertInfo{
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
UseDNS: useDNSValidation,
}
results = append(results, &thisCertInfo)
}
// convert ExpireDate to date object and sort asc
sort.Slice(results, func(i, j int) bool {
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
return date1.Before(date2)
})
js, _ := json.Marshal(results)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
return
}
response, err := json.Marshal(filenames)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(response)
}
// Check if the default certificates is correctly setup
func (m *Manager) HandleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
type CheckResult struct {
DefaultPubExists bool
DefaultPriExists bool
}
pub, pri := m.DefaultCertExistsSep()
js, _ := json.Marshal(CheckResult{
pub,
pri,
})
utils.SendJSONResponse(w, string(js))
}
func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Request) {
// Get the common name from the request
cn, err := utils.GetPara(r, "cn")
if err != nil {
utils.SendErrorResponse(w, "Common name not provided")
return
}
domains, err := utils.PostPara(r, "domains")
if err != nil {
//No alias domains provided, use the common name as the only domain
domains = "[]"
}
SANs := []string{}
if err := json.Unmarshal([]byte(domains), &SANs); err != nil {
utils.SendErrorResponse(w, "Invalid domains format: "+err.Error())
return
}
//SANs = append([]string{cn}, SANs...)
priKeyFilename := domainToFilename(cn, ".key")
pubKeyFilename := domainToFilename(cn, ".pem")
// Generate self-signed certificate
err = m.GenerateSelfSignedCertificate(cn, SANs, pubKeyFilename, priKeyFilename)
if err != nil {
utils.SendErrorResponse(w, "Failed to generate self-signed certificate: "+err.Error())
return
}
//Update the certificate store
err = m.UpdateLoadedCertList()
if err != nil {
utils.SendErrorResponse(w, "Failed to update certificate store: "+err.Error())
return
}
utils.SendOK(w)
}

View File

@@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain
}
// Convert a domain name to a filename format
func domainToFilename(domain string, ext string) string {
// Replace wildcard '*' with '_'
domain = strings.TrimSpace(domain)
if strings.HasPrefix(domain, "*") {
domain = "_" + strings.TrimPrefix(domain, "*")
}
// Add .pem extension
ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot
return domain + "." + ext
}
func filenameToDomain(filename string) string {
// Remove the extension
ext := filepath.Ext(filename)
if ext != "" {
filename = strings.TrimSuffix(filename, ext)
}
if strings.HasPrefix(filename, "_") {
filename = "*" + filename[1:]
}
return filename
}

View File

@@ -21,10 +21,10 @@ type CertCache struct {
}
type HostSpecificTlsBehavior struct {
DisableSNI bool //If SNI is enabled for this server name
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
PreferredCertificate string //Preferred certificate for this server name, if empty, use the first matching certificate
DisableSNI bool //If SNI is enabled for this server name
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
PreferredCertificate map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate
}
type Manager struct {
@@ -34,13 +34,12 @@ type Manager struct {
/* External handlers */
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
verbal bool
}
//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) {
func NewManager(certStore string, logger *logger.Logger) (*Manager, error) {
if !utils.FileExists(certStore) {
os.MkdirAll(certStore, 0775)
}
@@ -63,7 +62,6 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
CertStore: certStore,
LoadedCerts: []*CertCache{},
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
verbal: verbal,
Logger: logger,
}
@@ -82,7 +80,7 @@ func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior {
DisableSNI: false,
DisableLegacyCertificateMatching: false,
EnableAutoHTTPS: false,
PreferredCertificate: "",
PreferredCertificate: map[string]string{}, // No preferred certificate, use the first matching certificate
}
}
@@ -90,6 +88,10 @@ func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior
return GetDefaultHostSpecificTlsBehavior(), nil
}
func (m *Manager) SetHostSpecificTlsBehavior(fn func(serverName string) (*HostSpecificTlsBehavior, error)) {
m.hostSpecificTlsBehavior = fn
}
// Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file
@@ -213,13 +215,17 @@ func (m *Manager) GetCertificateByHostname(hostname string) (string, string, err
if err != nil {
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
}
preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname]
if !ok {
preferredCertificate = ""
}
if tlsBehavior.DisableSNI && tlsBehavior.PreferredCertificate != "" &&
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")) &&
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")) {
if tlsBehavior.DisableSNI && preferredCertificate != "" &&
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".pem")) &&
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".key")) {
//User setup a Preferred certificate, use the preferred certificate directly
pubKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")
priKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")
pubKey = filepath.Join(m.CertStore, preferredCertificate+".pem")
priKey = filepath.Join(m.CertStore, preferredCertificate+".key")
} else {
if !tlsBehavior.DisableLegacyCertificateMatching &&
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&

View File

@@ -211,6 +211,10 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
//Skip empty header pairs
if len(headerValuePair) < 2 {
continue
}
//Do not copy Upgrade and Connection headers, it will be handled by the upgrader
if strings.EqualFold(headerValuePair[0], "Upgrade") || strings.EqualFold(headerValuePair[0], "Connection") {
continue

135
src/plugin_api.go Normal file
View File

@@ -0,0 +1,135 @@
package main
import (
"net/http"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/netstat"
)
// Register the APIs for HTTP proxy management functions
func RegisterHTTPProxyRestAPI(authMiddleware *auth.PluginAuthMiddleware) {
/* Reverse Proxy Settings & Status */
authMiddleware.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authMiddleware.HandleFunc("/api/proxy/list", ReverseProxyList)
authMiddleware.HandleFunc("/api/proxy/listTags", ReverseProxyListTags)
authMiddleware.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
/* Reverse proxy upstream (load balance) */
authMiddleware.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
/* Reverse proxy virtual directory */
authMiddleware.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
/* Reverse proxy user-defined header */
authMiddleware.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
/* Reverse proxy auth related */
authMiddleware.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
}
// Register the APIs for redirection rules management functions
func RegisterRedirectionRestAPI(authRouter *auth.PluginAuthMiddleware) {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
}
// Register the APIs for access rules management functions
func RegisterAccessRuleRestAPI(authRouter *auth.PluginAuthMiddleware) {
/* Access Rules Settings & Status */
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
// authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
// authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
// authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
// authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
/* Blacklist */
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
/* Whitelist */
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback)
/* Quick Ban List */
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
}
// Register the APIs for path blocking rules management functions, WIP
func RegisterPathRuleRestAPI(authRouter *auth.PluginAuthMiddleware) {
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
}
// Register the APIs statistic anlysis and uptime monitoring functions
func RegisterStatisticalRestAPI(authRouter *auth.PluginAuthMiddleware) {
/* Traffic Summary */
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
/* Zoraxy Analytic */
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
/* UpTime Monitor */
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
}
// Register the APIs for Stream (TCP / UDP) Proxy management functions
func RegisterStreamProxyRestAPI(authRouter *auth.PluginAuthMiddleware) {
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
}
// Register the APIs for mDNS service management functions
func RegisterMDNSRestAPI(authRouter *auth.PluginAuthMiddleware) {
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
}
// Register the APIs for Static Web Server management functions
func RegisterStaticWebServerRestAPI(authRouter *auth.PluginAuthMiddleware) {
/* Static Web Server Controls */
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
/* File Manager */
if *allowWebFileManager {
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
}
}
func RegisterPluginRestAPI(authRouter *auth.PluginAuthMiddleware) {
authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins)
authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo)
authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups)
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
}
/* Register all the APIs */
func initRestAPI(targetMux *http.ServeMux) {
authMiddleware := auth.NewPluginAuthMiddleware(
auth.PluginMiddlewareOptions{
TargetMux: targetMux,
ApiKeyManager: pluginApiKeyManager,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
},
)
//Register the APIs
RegisterHTTPProxyRestAPI(authMiddleware)
RegisterRedirectionRestAPI(authMiddleware)
RegisterAccessRuleRestAPI(authMiddleware)
RegisterPathRuleRestAPI(authMiddleware)
RegisterStatisticalRestAPI(authMiddleware)
RegisterStreamProxyRestAPI(authMiddleware)
RegisterMDNSRestAPI(authMiddleware)
RegisterStaticWebServerRestAPI(authMiddleware)
RegisterPluginRestAPI(authMiddleware)
}

View File

@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"net"
"net/http"
"path/filepath"
"sort"
@@ -135,12 +136,12 @@ func ReverseProxtInit() {
Load all conf from files
*/
confs, _ := filepath.Glob("./conf/proxy/*.config")
confs, _ := filepath.Glob(CONF_HTTP_PROXY + "/*.config")
for _, conf := range confs {
err := LoadReverseProxyConfig(conf)
if err != nil {
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
return
continue
}
}
@@ -717,6 +718,11 @@ func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) {
return
}
if newTlsConfig.PreferredCertificate == nil {
//No update needed, reuse the current TLS config
newTlsConfig.PreferredCertificate = ept.TlsOptions.PreferredCertificate
}
ept.TlsOptions = newTlsConfig
//Prepare to replace the current routing rule
@@ -951,10 +957,10 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
// List, Update or Remove the exception paths for basic auth.
func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
ep, err := utils.GetPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
@@ -976,6 +982,7 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
return
}
@@ -986,10 +993,9 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
return
}
matchingPrefix, err := utils.PostPara(r, "prefix")
exceptionType, err := utils.PostInt(r, "type")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
exceptionType = 0x00 //Default to paths
}
//Load the target proxy object from router
@@ -999,26 +1005,100 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
return
}
//Check if the prefix starts with /. If not, prepend it
if !strings.HasPrefix(matchingPrefix, "/") {
matchingPrefix = "/" + matchingPrefix
}
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true
break
switch exceptionType {
case 0x00:
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
}
if alreadyExists {
utils.SendErrorResponse(w, "This matching path already exists")
//Check if the prefix starts with /. If not, prepend it
if !strings.HasPrefix(matchingPrefix, "/") {
matchingPrefix = "/" + matchingPrefix
}
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true
break
}
}
if alreadyExists {
utils.SendErrorResponse(w, "This matching path already exists")
return
}
targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
RuleType: dynamicproxy.AuthExceptionType_Paths,
PathPrefix: strings.TrimSpace(matchingPrefix),
})
case 0x01:
matchingCIDR, err := utils.PostPara(r, "cidr")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching CIDR given")
return
}
// Accept CIDR, IP address, or wildcard like 192.168.0.*
matchingCIDR = strings.TrimSpace(matchingCIDR)
isValid := false
// Check if it's a valid CIDR
if _, _, err := net.ParseCIDR(matchingCIDR); err == nil {
isValid = true
} else if ip := net.ParseIP(matchingCIDR); ip != nil {
// Valid IP address
isValid = true
} else if strings.Contains(matchingCIDR, "*") {
// Accept wildcard like 192.168.0.*
parts := strings.Split(matchingCIDR, ".")
if len(parts) == 4 && parts[3] == "*" {
// Check first 3 parts are numbers 0-255
validParts := true
for i := 0; i < 3; i++ {
n, err := strconv.Atoi(parts[i])
if err != nil || n < 0 || n > 255 {
validParts = false
break
}
}
if validParts {
isValid = true
}
}
}
if !isValid {
utils.SendErrorResponse(w, "Invalid CIDR, IP, or wildcard given")
return
}
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.CIDR == matchingCIDR {
alreadyExists = true
break
}
}
if alreadyExists {
utils.SendErrorResponse(w, "This matching CIDR already exists")
return
}
targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
RuleType: dynamicproxy.AuthExceptionType_CIDR,
CIDR: strings.TrimSpace(matchingCIDR),
})
default:
//Invalid exception type given
utils.SendErrorResponse(w, "Invalid exception type given")
return
}
targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix),
})
//Save configs to runtime and file
targetProxy.UpdateToRuntime()
@@ -1035,9 +1115,39 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
return
}
exceptionType, err := utils.PostInt(r, "type")
if err != nil {
exceptionType = 0x00 //Default to paths
}
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
matchingPrefix = ""
}
matchingCIDR, err := utils.PostPara(r, "cidr")
if err != nil {
matchingCIDR = ""
}
var typeToCheck dynamicproxy.AuthExceptionType
switch exceptionType {
case 0x01:
typeToCheck = dynamicproxy.AuthExceptionType_CIDR
//Check if the CIDR is valid
if matchingCIDR == "" {
utils.SendErrorResponse(w, "Invalid matching CIDR given")
return
}
case 0x00:
fallthrough //For backward compatibility
default:
typeToCheck = dynamicproxy.AuthExceptionType_Paths
//Check if the prefix is valid
if matchingPrefix == "" {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
return
}
@@ -1051,10 +1161,22 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false
for _, thisExceptionalRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
matchingExists = true
switch typeToCheck {
case dynamicproxy.AuthExceptionType_CIDR:
if thisExceptionalRule.CIDR != matchingCIDR {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
matchingExists = true
}
case dynamicproxy.AuthExceptionType_Paths:
fallthrough //For backward compatibility
default:
if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
matchingExists = true
}
}
}

View File

@@ -1,15 +1,17 @@
package main
import (
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"log"
"net/http"
"net/netip"
"os"
"runtime"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
@@ -98,8 +100,11 @@ func startupSequence() {
http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect)
})
// Create an API key manager for plugin authentication
pluginApiKeyManager = auth.NewAPIKeyManager()
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger)
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger)
if err != nil {
panic(err)
}
@@ -312,11 +317,18 @@ func startupSequence() {
*/
pluginFolder := *path_plugin
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
ZoraxyAddrPort, err := netip.ParseAddrPort(*webUIPort)
ZoraxyPort := 8000
if err == nil && ZoraxyAddrPort.IsValid() && ZoraxyAddrPort.Port() > 0 {
ZoraxyPort = int(ZoraxyAddrPort.Port())
}
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
PluginDir: pluginFolder,
Database: sysdb,
Logger: SystemWideLogger,
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
APIKeyManager: pluginApiKeyManager,
ZoraxyPort: ZoraxyPort,
CSRFTokenGen: func(r *http.Request) string {
return csrf.Token(r)
},
@@ -366,6 +378,9 @@ func finalSequence() {
//Inject routing rules
registerBuildInRoutingRules()
//Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname
tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname)
}
/* Shutdown Sequence */

View File

@@ -339,11 +339,15 @@
<div class="rpconfig_content" rpcfg="ssl">
<div class="ui segment">
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
<table class="ui celled small compact table Tls_resolve_list">
<div class="ui blue message sni_grey_out_info" style="margin-bottom: 1em; display:none;">
<i class="info circle icon"></i>
Certificate dropdowns are greyed out because SNI is enabled
</div>
<table class="ui celled small compact table sortable Tls_resolve_list">
<thead>
<tr>
<th>Hostname</th>
<th>Resolve to Certificate</th>
<th class="no-sort">Resolve to Certificate</th>
</tr>
</thead>
<tbody>
@@ -359,18 +363,20 @@
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="Tls_EnableLegacyCertificateMatching">
<label>Enable Legacy Certificate Matching<br>
<small>Use legacy filename / hostname matching for loading certificates</small>
<small>Use filename for hostname matching, faster but less accurate</small>
</label>
</div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<div class="ui disabled checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="Tls_EnableAutoHTTPS">
<label>Enable Auto HTTPS<br>
<label>Enable Auto HTTPS (WIP)<br>
<small>Automatically request a certificate for the domain</small>
</label>
</div>
<br>
<div class="ui divider"></div>
<button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button>
<button class="ui basic small button getSelfSignCertBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="yellow lock icon"></i> Generate Self-Signed Certificate</button>
</div>
</div>
<!-- Custom Headers -->
@@ -580,6 +586,28 @@
aliasDomains += `</small><br>`;
}
//Build the sorting value
let destSortValue = subd.ActiveOrigins.map(o => {
// Check if it's an IP address (with optional port)
let upstreamAddr = o.OriginIpOrDomain;
let subpath = "";
if (upstreamAddr.indexOf("/") !== -1) {
let parts = upstreamAddr.split("/");
subpath = parts.slice(1).join("/");
upstreamAddr = parts[0];
}
let ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/;
if (ipPortRegex.test(upstreamAddr)) {
let [ip, port] = upstreamAddr.split(":");
// Convert IP to hex
let hexIp = ip.split('.').map(x => ('00' + parseInt(x).toString(16)).slice(-2)).join('');
let hexPort = port ? (port.length < 5 ? port.padStart(5, '0') : port) : '';
return hexIp + (hexPort ? ':' + hexPort : '') + "/" + subpath;
}
// Otherwise, treat it as a domain name
return upstreamAddr;
}).join(",");
//Build tag list
let tagList = renderTagList(subd);
let tagListEmpty = (subd.Tags.length == 0);
@@ -596,7 +624,7 @@
${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td>
<td data-label="" editable="true" datatype="domain">
<td data-label="" editable="true" datatype="domain" data-sort-value="${destSortValue}" style="word-break: break-all;">
<div class="upstreamList">
${upstreams}
</div>
@@ -747,7 +775,7 @@
let newTlsOption = {
"DisableSNI": !enableSNI,
"DisableLegacyCertificateMatching": !enableLegacyCertificateMatching,
"EnableAutoHTTPS": enableAutoHTTPS
"EnableAutoHTTPS": enableAutoHTTPS,
}
$.cjax({
url: "/api/proxy/setTlsConfig",
@@ -769,6 +797,9 @@
function updateTlsResolveList(uuid){
let editor = $("#httprpEditModalWrapper");
editor.find(".certificateDropdown .ui.dropdown").off("change");
editor.find(".certificateDropdown .ui.dropdown").remove();
//Update the TLS resolve list
$.ajax({
url: "/api/cert/resolve?domain=" + uuid,
@@ -785,17 +816,60 @@
resolveList.append(`
<tr>
<td>${primaryDomain}</td>
<td>${certMap[primaryDomain] || "Fallback Certificate"}</td>
<td class="certificateDropdown" domain="${primaryDomain}">${certMap[primaryDomain] || "Fallback Certificate"}</td>
</tr>
`);
aliasDomains.forEach(alias => {
resolveList.append(`
<tr>
<td>${alias}</td>
<td>${certMap[alias] || "Fallback Certificate"}</td>
<td class="certificateDropdown" domain="${alias}">${certMap[alias] || "Fallback Certificate"}</td>
</tr>
`);
});
//Generate the certificate dropdown
generateCertificateDropdown(function(dropdown) {
let SNIEnabled = editor.find(".Tls_EnableSNI")[0].checked;
editor.find(".certificateDropdown").html(dropdown);
editor.find(".certificateDropdown").each(function() {
let dropdownDomain = $(this).attr("domain");
let selectedCertname = certMap[dropdownDomain];
if (selectedCertname) {
$(this).find(".ui.dropdown").dropdown("set selected", selectedCertname);
}
});
editor.find(".certificateDropdown .ui.dropdown").dropdown({
onChange: function(value, text, $selectedItem) {
console.log("Selected certificate for domain:", $(this).parent().attr("domain"), "Value:", value);
let domain = $(this).parent().attr("domain");
let newCertificateName = value;
$.cjax({
url: "/api/cert/setPreferredCertificate",
method: "POST",
data: {
"domain": domain,
"certname": newCertificateName
},
success: function(data) {
if (data.error !== undefined) {
msgbox(data.error, false, 3000);
} else {
msgbox("Preferred Certificate updated");
}
}
});
}
});
if (SNIEnabled) {
editor.find(".certificateDropdown .ui.dropdown").addClass("disabled");
editor.find(".sni_grey_out_info").show();
}else{
editor.find(".sni_grey_out_info").hide();
}
});
}
});
}
@@ -946,6 +1020,29 @@
renewCertificate(renewDomainKey, false, btn);
}
function generateSelfSignedCertificate(uuid, domains, btn=undefined){
let payload = JSON.stringify(domains);
$.cjax({
url: "/api/cert/selfsign",
data: {
"cn": uuid,
"domains": payload
},
success: function(data){
if (data.error == undefined){
msgbox("Self-Signed Certificate Generated", true);
resyncProxyEditorConfig();
if (typeof(initManagedDomainCertificateList) != undefined){
//Re-init the managed domain certificate list
initManagedDomainCertificateList();
}
}else{
msgbox(data.error, false);
}
}
});
}
/* Tags & Search */
function handleSearchInput(event){
if (event.key == "Escape"){
@@ -1074,6 +1171,28 @@
return subd;
}
// Generate a certificate dropdown for the HTTP Proxy Rule Editor
// so user can pick which certificate they want to use for the current editing hostname
function generateCertificateDropdown(callback){
$.ajax({
url: "/api/cert/list",
method: "GET",
success: function(data) {
let dropdown = $('<div class="ui fluid selection dropdown"></div>');
let menu = $('<div class="menu"></div>');
data.forEach(cert => {
menu.append(`<div class="item" data-value="${cert}">${cert}</div>`);
});
// Add a hidden input to store the selected certificate
dropdown.append('<input type="hidden" name="certificate">');
dropdown.append('<i class="dropdown icon"></i>');
dropdown.append('<div class="default text">Fallback Certificate</div>');
dropdown.append(menu);
callback(dropdown);
}
})
}
//Initialize the http proxy rule editor
function initHttpProxyRuleEditorModal(rulepayload){
let subd = JSON.parse(JSON.stringify(rulepayload));
@@ -1081,6 +1200,9 @@
//Populate all the information in the proxy editor
populateAndBindEventsToHTTPProxyEditor(subd);
//Hide all previously opened editor side-frame wrapper
hideEditorSideWrapper();
//Show the first rpconfig
$("#httprpEditModal .rpconfig_content").hide();
$("#httprpEditModal .rpconfig_content[rpcfg='downstream']").show();
@@ -1175,39 +1297,6 @@
});
editor.find(".downstream_alias_hostname").html(aliasHTML);
//TODO: Move this to SSL TLS section
let enableQuickRequestButton = true;
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
let thisAliasName = subd.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
enableQuickRequestButton = false;
}
if (subd.MatchingDomainAlias != undefined){
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
enableQuickRequestButton = false;
break;
}
}
}
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
if (enableQuickRequestButton){
editor.find(".getCertificateBtn").removeClass("disabled");
}else{
editor.find(".getCertificateBtn").addClass("disabled");
}
editor.find(".getCertificateBtn").off("click").on("click", function(){
requestCertificateForExistingHost(uuid, certificateDomains, this);
});
/* ------------ Upstreams ------------ */
editor.find(".upstream_list").html(renderUpstreamList(subd));
@@ -1237,6 +1326,8 @@
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
editor.find(".editVdirBtn").off("click").on("click", function(){
quickEditVdir(uuid);
//Temporary restore scroll
$("body").css("overflow", "auto");
});
/* ------------ Alias ------------ */
@@ -1335,10 +1426,17 @@
/* ------------ TLS ------------ */
updateTlsResolveList(uuid);
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
if (subd.TlsOptions != null){
//Use the saved settings
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
}else{
//Default settings
editor.find(".Tls_EnableSNI").prop("checked", true);
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", false);
editor.find(".Tls_EnableAutoHTTPS").prop("checked", false);
}
editor.find(".Tls_EnableSNI").off("change").on("change", function() {
saveTlsConfigs(uuid);
});
@@ -1349,6 +1447,45 @@
saveTlsConfigs(uuid);
});
/* Quick access to get certificate for the current host */
let enableQuickRequestButton = true;
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
let thisAliasName = subd.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
enableQuickRequestButton = false;
}
if (subd.MatchingDomainAlias != undefined){
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
enableQuickRequestButton = false;
break;
}
}
}
if (enableQuickRequestButton){
editor.find(".getCertificateBtn").removeClass("disabled");
}else{
editor.find(".getCertificateBtn").addClass("disabled");
}
editor.find(".getCertificateBtn").off("click").on("click", function(){
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
requestCertificateForExistingHost(uuid, certificateDomains, this);
});
// Bind event to self-signed certificate button
editor.find(".getSelfSignCertBtn").off("click").on("click", function() {
generateSelfSignedCertificate(uuid, domains, this);
});
/* ------------ Tags ------------ */
(()=>{
let payload = encodeURIComponent(JSON.stringify({
@@ -1411,7 +1548,6 @@
});
}
/*
Page Initialization Functions
*/
@@ -1436,7 +1572,9 @@
// there is a chance where the user has modified the Vdir
// we need to get the latest setting from server side and
// render it again
updateVdirInProxyEditor();
resyncProxyEditorConfig();
window.scrollTo(0, 0);
$("body").css("overflow", "hidden");
} else {
listProxyEndpoints();
//Reset the tag filter

View File

@@ -151,11 +151,31 @@
dataType: 'json',
success: function(data) {
$('#forwardAuthAddress').val(data.address);
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
if (data.responseHeaders != null) {
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
} else {
$('#forwardAuthResponseHeaders').val("");
}
if (data.responseClientHeaders != null) {
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
} else {
$('#forwardAuthResponseClientHeaders').val("");
}
if (data.requestHeaders != null) {
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
} else {
$('#forwardAuthRequestHeaders').val("");
}
if (data.requestIncludedCookies != null) {
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
} else {
$('#forwardAuthRequestIncludedCookies').val("");
}
if (data.requestExcludedCookies != null) {
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
} else {
$('#forwardAuthRequestExcludedCookies').val("");
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);

View File

@@ -137,7 +137,7 @@
});
function clearStreamProxyAddEditForm(){
$('#streamProxyForm input, #streamProxyForm select').val('');
$('#streamProxyForm').find('input:not([type=checkbox]), select').val('');
$('#streamProxyForm select').dropdown('clear');
$("#streamProxyForm input[name=timeout]").val(10);
$("#streamProxyForm .toggle.checkbox").checkbox("set unchecked");

View File

@@ -125,7 +125,7 @@
<!-- Log Viewer -->
<h3>System Log Viewer</h3>
<p>View and download Zoraxy log</p>
<button class="ui basic button" onclick="launchToolWithSize('snippet/logview.html', 1024, 768);"><i class="ui blue file icon"></i> Open Log Viewer</button>
<a class="ui basic button" href="snippet/logview.html" target="_blank"><i class="ui blue file icon"></i> Open Log Viewer</a>
<div class="ui divider"></div>
<!-- System Information -->
<div id="zoraxyinfo">

View File

@@ -46,24 +46,37 @@
</div>
</div>
<div class="ui divider"></div>
<h3 class="ui header">Authentication Exclusion Paths</h3>
<h3 class="ui header">Authentication Exclusion</h3>
<div class="scrolling content ui form">
<p>Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.</p>
<p>Exclude <b>specific directories which contains the following subpath prefix</b> or <b>IP / CIDR</b> from authentication. Useful if you are hosting services require remote API access.</p>
<table class="ui basic very compacted unstackable celled table">
<thead>
<tr>
<th>Path Prefix</th>
<th>Exception Type</th>
<th>Path Prefix / CIDR</th>
<th>Remove</th>
</tr></thead>
<tbody id="exclusionPaths">
<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
<td colspan="3"><i class="ui green circle check icon"></i> No Exclusion Rule</td>
</tr>
</tbody>
</table>
<div class="field">
<input id="newExclusionPath" type="text" placeholder="/public/api/" autocomplete="off">
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
<div class="fields" style="margin-bottom: 0.4em;">
<div class="field" style="margin-bottom: 0.4em;">
<select class="ui basic fluid dropdown" id="exceptionTypeDropdown" style="margin-right: 1em;">
<option value="path">Path Prefix</option>
<option value="ip">IP / CIDR</option>
</select>
</div>
<div class="field" id="exclusionPathField">
<input id="newExclusionPath" type="text" placeholder="/public/api/" autocomplete="off">
<small>Make sure you add the trailing slash!</small>
</div>
<div class="field" id="exclusionIPField" style="display: none;">
<input id="newExclusionIP" type="text" placeholder="192.168.1.0/24" autocomplete="off">
<small>Enter a valid IP address or CIDR block.</small>
</div>
</div>
<div class="field" >
<button class="ui basic button" onclick="addExceptionPath();"><i class="yellow add icon"></i> Add Exception</button>
@@ -99,6 +112,19 @@
console.log("Unable to load endpoint data from hash")
}
}
// Initialize the dropdown
$('#exceptionTypeDropdown').dropdown({
onChange: function(value, text, $selectedItem) {
if (value === 'ip') {
$('#exclusionPathField').hide();
$('#exclusionIPField').show();
} else {
$('#exclusionPathField').show();
$('#exclusionIPField').hide();
}
}
});
$('#exceptionTypeDropdown').dropdown('set selected', 'path');
function loadBasicAuthCredentials(uuid){
$.ajax({
@@ -161,17 +187,31 @@
}
function addExceptionPath(){
// Retrieve the username and password input values
var newExclusionPathMatchingPrefix = $('#newExclusionPath').val().trim();
if (newExclusionPathMatchingPrefix == ""){
parent.msgbox("Matching prefix cannot be empty!", false, 5000);
return;
let exceptionType = $('#exceptionTypeDropdown').val() == "path" ? 0x0 : 0x1;
let newExclusionPathMatchingPrefix = $('#newExclusionPath').val().trim();
let newExclusionIP = $('#newExclusionIP').val().trim();
if (exceptionType == 0x0){
//Check if the path is empty
if (newExclusionPathMatchingPrefix == ""){
parent.msgbox("Matching prefix cannot be empty!", false, 5000);
return;
}
}else{
//Check if the CIDR is empty
if (newExclusionIP == ""){
parent.msgbox("Matching CIDR cannot be empty!", false, 5000);
return;
}
}
$.cjax({
url: "/api/proxy/auth/exceptions/add",
data:{
"type":exceptionType,
ep: editingEndpoint.ep,
prefix: newExclusionPathMatchingPrefix
prefix: newExclusionPathMatchingPrefix,
cidr: newExclusionIP
},
method: "POST",
success: function(data){
@@ -181,6 +221,7 @@
initExceptionPaths();
parent.msgbox("New exception path added", true);
$('#newExclusionPath').val("");
$('#newExclusionIP').val("");
}
}
});
@@ -188,12 +229,29 @@
function removeExceptionPath(object){
let matchingPrefix = $(object).attr("prefix");
let exceptionType = parseInt($(object).attr("etype"));
if (exceptionType == undefined || matchingPrefix == undefined){
parent.msgbox("Invalid exception path data", false, 5000);
return;
}
let reqPayload = {
"type": exceptionType,
ep: editingEndpoint.ep,
};
if (exceptionType == 0x0){
reqPayload.prefix = matchingPrefix;
}else if (exceptionType == 0x1){
reqPayload.cidr = matchingPrefix;
}else{
parent.msgbox("Unknown exception type", false, 5000);
return;
}
$.cjax({
url: "/api/proxy/auth/exceptions/delete",
data:{
ep: editingEndpoint.ep,
prefix: matchingPrefix
},
data: reqPayload,
method: "POST",
success: function(data){
if (data.error != undefined){
@@ -206,6 +264,17 @@
});
}
function exceptionTypeToString(type){
switch(type){
case 0x0:
return "Path Prefix";
case 0x1:
return "IP or CIDR";
default:
return "Unknown Type";
}
}
//Load exception paths from server
function initExceptionPaths(){
$.get(`/api/proxy/auth/exceptions/list?ptype=${editingEndpoint.ept}&ep=${editingEndpoint.ep}`, function(data){
@@ -214,14 +283,15 @@
}else{
if (data.length == 0){
$("#exclusionPaths").html(` <tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Path Excluded</td>
<td colspan="3"><i class="ui green circle check icon"></i> No Path Excluded</td>
</tr>`);
}else{
$("#exclusionPaths").html("");
data.forEach(function(rule){
$("#exclusionPaths").append(` <tr>
<td>${rule.PathPrefix}</td>
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
<td>${exceptionTypeToString(rule.RuleType)}</td>
<td>${rule.PathPrefix || rule.CIDR }</td>
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" etype="${rule.RuleType}" prefix="${rule.PathPrefix || rule.CIDR}"><i class="ui red times icon"></i></button></td>
</tr>`);
})
}

View File

@@ -1,12 +1,16 @@
<!DOCTYPE html>
<html ng-app="App">
<head>
<title>System Logs</title>
<title>LogView | Zoraxy</title>
<meta charset="UTF-8">
<meta name="theme-color" content="#4b75ff">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="icon" type="image/png" href="../favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script type="text/javascript" src="../script/jquery-3.6.0.min.js"></script>
<script type="text/javascript" src="../script/semantic/semantic.min.js"></script>
<link rel="stylesheet" href="main.css">
<style>
.clickable{
cursor: pointer;
@@ -14,6 +18,15 @@
.clickable:hover{
opacity: 0.7;
}
/* Panel menu */
.logfile_menu_btn{
height: 2.8em !important;
margin-top: 0.4em !important;
margin-right: 0 !important;
}
/* Log file list */
.logfile{
padding-left: 1em !important;
position: relative;
@@ -56,163 +69,508 @@
color:white;
}
body.darkTheme .loglist {
background-color: #1b1c1d;
color: #ffffff;
#errorHighlightWrapper{
background-color: #f9f9f9;
padding: 1em;
border-radius: 0.3em;
max-height:300px;
overflow-y:auto;
margin-bottom:1em;
border: 1px solid #b3b3b3;
}
body.darkTheme .loglist .ui.header .content .sub.header {
color: #bbbbbb;
@media (min-width: 768px) {
#logfileDropdown {
margin-left: 0.4em !important;
}
.logfile_menu_btn{
margin-left:0.4em !important;
}
#logfileDropdown{
margin-left: 0.4em !important;
}
}
body.darkTheme .loglist .ui.divider {
border-color: #333333;
}
@media (max-width: 767px) {
#toggleFullscreenBtn {
display: none !important;
}
body.darkTheme .loglist .ui.accordion .title,
body.darkTheme .loglist .ui.accordion .content {
background-color: #1b1c1d;
color: #ffffff;
}
body.darkTheme .loglist .ui.accordion .title:hover {
background-color: #333333;
}
body.darkTheme .loglist .ui.list .item {
color: #ffffff;
}
body.darkTheme .loglist .ui.list .item .content {
color: #ffffff;
}
body.darkTheme .loglist .ui.list .item .showing {
color: #ffffff;
}
body.darkTheme .loglist .ui.button.filterbtn {
background-color: #333333;
color: #ffffff;
}
body.darkTheme .loglist .ui.button.filterbtn:hover {
background-color: #555555;
}
body.darkTheme .loglist .ui.toggle.checkbox label {
color: #ffffff;
}
body.darkTheme .loglist small {
color: #bbbbbb;
#logrenderWrapper{
margin-left: 0em !important;
margin-right: 0em !important;
}
}
</style>
</head>
<body>
<!-- Currently not darktheme ready -->
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<!-- <script src="../script/darktheme.js"></script> -->
<br>
<div class="ui container">
<div class="ui stackable grid">
<div class="four wide column loglist">
<h3 class="ui header" style="padding-top: 1em;">
<div class="content">
Log View
<div class="sub header">Check System Log in Real Time</div>
</div>
</h3>
<div class="ui divider"></div>
<div id="logList" class="ui accordion">
</div>
<div class="ui divider"></div>
<h5>Filters</h5>
<button style="margin-top: 0.4em;" filter="system" class="ui fluid basic small button filterbtn"><i class="ui blue info circle icon"></i> System</button>
<button style="margin-top: 0.4em;" filter="request" class="ui fluid basic small button filterbtn"><i class="green exchange icon"></i> Request</button>
<button style="margin-top: 0.4em;" filter="error" class="ui fluid basic small button filterbtn"><i class="red exclamation triangle icon"></i> Error</button>
<button style="margin-top: 0.4em;" filter="all" class="ui fluid basic active small button filterbtn">All</button>
<div class="ui divider"></div>
<div class="ui toggle checkbox">
<input type="checkbox" id="enableAutoScroll" onchange="handleAutoScrollTicker(event, this.checked);">
<label>Auto Refresh<br>
<small>Refresh the viewing log every 10 seconds</small></label>
</div>
<div class="ui divider"></div>
<small>Notes: Some log files might be huge. Make sure you have checked the log file size before opening</small>
<div class="ui stackable secondary menu">
<div class="item" style="font-weight: bold;">
Zoraxy LogView
</div>
<div class="twelve wide column">
<textarea id="logrender" spellcheck="false" readonly="true">
← Pick a log file from the left menu to start debugging
</textarea>
<a href="#" onclick="openLogInNewTab();">Open In New Tab</a>
<br><br>
<a class="item active panel_menu_btn" id="logViewMenu">
Log View
</a>
<a class="item panel_menu_btn" id="summaryMenu">
Summary
</a>
<a class="item panel_menu_btn" id="settingsMenu">
Settings
</a>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui stackable secondary menu">
<!-- Filter Dropdown -->
<div class="ui selection dropdown" id="filterDropdown" style="margin-top:0.4em;">
<div class="text">Select Filter</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="all"><i class="filter icon"></i> All</div>
<div class="item" data-value="system"><i class="blue info circle icon"></i> System</div>
<div class="item" data-value="request"><i class="green exchange icon"></i> Request</div>
<div class="item" data-value="error"><i class="red exclamation triangle icon"></i> Error</div>
</div>
</div>
<!-- Log File Dropdown -->
<div class="ui selection dropdown" id="logfileDropdown" style="margin-top: 0.4em; height: 2.8em;">
<div class="text">Select Log File</div>
<i class="dropdown icon"></i>
<div class="menu" id="logfileDropdownMenu">
<!-- Log files will be populated here -->
</div>
</div>
<!-- Download Button -->
<button class="ui icon basic button logfile_menu_btn" id="downloadLogBtn" title="Download Current Log File">
<i class="black download icon"></i>
</button>
<!-- Open in New Tab Button -->
<button class="ui icon basic button logfile_menu_btn" onclick="openLogInNewTab();" title="Open in New Tab">
<i class="external alternate icon"></i>
</button>
</div>
</div>
<br><br>
<!-- Summary View -->
<div id="summary" class="ui container subpanel">
<h3 class="ui header">
Summary
</h3>
<div class="ui divider"></div>
<div class="ui small statistics" style="display: flex; justify-content: center;">
<div class="statistic">
<div class="value" id="successRequestsCount"></div>
<div class="label">Success Requests</div>
</div>
<div class="statistic">
<div class="value" id="errorRequestsCount"></div>
<div class="label">Error Requests</div>
</div>
<div class="statistic">
<div class="value" id="totalRequestsCount"></div>
<div class="label">Total Requests</div>
</div>
</div>
<div class="ui divider"></div>
<!-- Error Highlight Section -->
<h4 class="ui header">Error Highlights</h4>
<div id="errorHighlightWrapper">
<p><i class="ui green circle check icon"></i> No error data loaded</p>
</div>
<div class="ui divider"></div>
<div id="analyzer">
<div class="ui info message">
<div class="header">
No log file selected
</div>
<p>Please select a log file from the dropdown menu to view analysis and summary.</p>
</div>
</div>
</div>
<!-- Log Viewer -->
<div id="logviewer" class="subpanel" style="display:none;">
<div id="logrenderWrapper" class="ui container" style="position:relative;">
<textarea id="logrender" spellcheck="false" readonly="true">
Pick a log file from the menu to start debugging
</textarea>
<!-- Full Screen Button -->
<button id="toggleFullscreenBtn" class="ui icon black button" title="Toggle Full Screen" style="border: 1px solid white; position:absolute; top:0.5em; right:0.5em; z-index:999;">
<i class="expand arrows alternate icon"></i>
</button>
<!-- Scroll to Bottom Button -->
<button id="scrollBottomBtn" class="ui icon black button" title="Scroll to Bottom" style="border: 1px solid white; position:absolute; bottom:0.5em; right:0.5em; z-index:999;">
<i class="angle double down icon"></i>
</button>
<script>
$("#scrollBottomBtn").on("click", function() {
var textarea = document.getElementById('logrender');
textarea.scrollTop = textarea.scrollHeight;
});
</script>
</div>
<script>
// Toggle full screen for logrenderWrapper
$("#toggleFullscreenBtn").on("click", function() {
$("#logrenderWrapper").toggleClass("ui container");
$(this).find("i").toggleClass("expand arrows alternate compress arrows alternate");
});
</script>
<br>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui toggle checkbox">
<input type="checkbox" id="enableAutoScroll" onchange="handleAutoScrollTicker(event, this.checked);">
<label>Auto Refresh<br>
<small>Refresh the viewing log every 3 seconds</small></label>
</div>
<div class="ui divider"></div>
<small>Notes: Some log files might be huge. Make sure you have checked the log file size before opening</small>
</div>
</div>
<!-- Settings -->
<div id="settings" class="ui container subpanel" style="display:none;">
<h3 class="ui header">
Settings
</h3>
</div>
<br>
<div class="ui container">
<div class="ui divider"></div>
<small>Zoraxy LogView - Advance log viewer for Zoraxy log files</small>
<br><br><br>
</div>
</body>
<script>
var currentOpenedLogURL = "";
//LogView Implementation
var currentFilter = "all";
var currentOpenedLogURL = "";
var currentLogFile = "";
var autoscroll = false;
$(".checkbox").checkbox();
/* Menu Subpanel Switch */
$(".subpanel").hide();
$("#logviewer").show();
$(".panel_menu_btn").on("click", function() {
var id = $(this).attr("id");
$(".subpanel").hide();
$(".ui.menu .item").removeClass("active");
$(this).addClass("active");
if (id === "summaryMenu") {
$("#summary").show();
} else if (id === "logViewMenu") {
$("#logviewer").show();
} else if (id === "settingsMenu") {
$("#settings").show();
}
});
/* Log file dropdown */
function populateLogfileDropdown() {
$.get("/api/log/list", function(data){
let $menu = $("#logfileDropdownMenu");
$menu.html("");
for (let [key, value] of Object.entries(data)) {
value.reverse();
value.forEach(file => {
$menu.append(
`<div class="item" data-value="${file.Filename}" data-category="${key}">${file.Title} (${formatBytes(file.Filesize)})</div>`
);
});
}
$('#logfileDropdown').dropdown('refresh');
//let firstItem = $menu.find('.item').first();
//if (firstItem.length) {
// $('#logfileDropdown').dropdown('set selected', firstItem.data('value'));
//}
});
}
$('#logfileDropdown').dropdown({
onChange: function(value, text, $choice) {
if (value) {
openLog(null, $choice.data('category'), value, currentFilter || "all");
loadLogSummary(value);
}
}
});
populateLogfileDropdown();
/* Filter dropdown */
$('#filterDropdown').dropdown({
onChange: function(value) {
currentFilter = value;
if (!currentLogFile) {
return;
}
$(".filterbtn.active").removeClass("active");
$(`.filterbtn[filter="${value}"]`).addClass("active");
openLog(null, null, currentLogFile, currentFilter);
}
});
// Set default filter to "error"
$('#filterDropdown').dropdown('set selected', 'all');
currentFilter = "all";
/* Log download button */
$("#downloadLogBtn").on("click", function() {
if (!currentLogFile) {
alert("Please select a log file first.");
return;
}
if (!currentOpenedLogURL) {
alert("No log file is currently opened.");
return;
}
$.get(currentOpenedLogURL, function(data) {
if (data.error !== undefined) {
alert(data.error);
return;
}
let blob = new Blob([data], {type: "text/plain"});
let url = URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = currentLogFile || "log.txt";
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
});
/* Analyzer */
function loadLogSummary(filename){
$.get("/api/log/summary?file=" + filename, function(data){
if (data.error !== undefined){
$("#analyzer").html("<div class='ui negative message'><div class='header'>Error</div><p>" + data.error + "</p></div>");
return;
}
$("#successRequestsCount").text(data.total_valid || 0);
$("#errorRequestsCount").text(data.total_errors || 0);
$("#totalRequestsCount").text(data.total_requests || 0);
renderSummaryToHTML(data);
});
$.get("/api/log/errors?file=" + encodeURIComponent(filename), function(data) {
renderErrorHighlights(data);
}).fail(function() {
$("#errorHighlightWrapper").html(
`<div class="ui error message">
<div class="header">Failed to load error highlights</div>
</div>`
);
});
}
function renderSummaryToHTML(data){
/* Render summary analysis to #analyzer */
let html = `
<div class="ui stackable grid">
<div class="eight wide column">
<h4 class="ui header">Request Methods</h4>
<canvas id="requestMethodsChart" height="180"></canvas>
</div>
<div class="eight wide column">
<h4 class="ui header">Hits Per Day</h4>
<canvas id="hitsPerDayChart" height="180"></canvas>
</div>
</div>
<div class="ui divider"></div>
<div class="ui stackable grid">
<div class="sixteen wide column">
<h4 class="ui header">Hits Per Site</h4>
<div id="siteCharts"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui stackable grid">
<div class="eight wide column">
<h4 class="ui header">Unique IPs</h4>
<table class="ui celled table">
<thead><tr><th>IP</th><th>Requests</th></tr></thead>
<tbody>
${Object.entries(data.unique_ips)
.sort((a, b) => b[1] - a[1])
.map(([ip, count]) => `<tr><td>${ip}</td><td>${count}</td></tr>`)
.join("")}
</tbody>
</table>
</div>
<div class="eight wide column">
<h4 class="ui header">Top User Agents</h4>
<table class="ui celled table">
<thead><tr><th>User Agent</th><th>Requests</th></tr></thead>
<tbody>
${Object.entries(data.top_user_agents)
.sort((a, b) => b[1] - a[1])
.map(([ua, count]) => `<tr><td style="word-break:break-all;">${ua}</td><td>${count}</td></tr>`)
.join("")}
</tbody>
</table>
</div>
</div>
<div class="ui divider"></div>
<div class="ui stackable grid">
<div class="sixteen wide column">
<h4 class="ui header">Top Paths</h4>
<table class="ui celled table">
<thead><tr><th>Path</th><th>Requests</th></tr></thead>
<tbody>
${Object.entries(data.top_paths)
.sort((a, b) => b[1] - a[1])
.map(([path, count]) => `<tr><td style="word-break:break-all;">${path}</td><td>${count}</td></tr>`)
.join("")}
</tbody>
</table>
</div>
</div>
`;
$("#analyzer").html(html);
// Load Chart.js if not loaded
function loadChartJs(cb) {
if (window.Chart) { cb(); return; }
let s = document.createElement("script");
s.src = "https://cdn.jsdelivr.net/npm/chart.js";
s.onload = cb;
document.head.appendChild(s);
}
// Prepare data for charts
function getDateRangeLabels(hitPerDay) {
let dates = Object.keys(hitPerDay).sort();
if (dates.length === 0) return [];
let start = new Date(dates[0]);
let end = new Date(dates[dates.length - 1]);
let labels = [];
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
let y = d.getFullYear(), m = (d.getMonth() + 1).toString().padStart(2, "0"), day = d.getDate().toString().padStart(2, "0");
labels.push(`${y}-${m}-${day}`);
}
return labels;
}
loadChartJs(function() {
// 1. Pie chart for request methods
let reqMethods = data.request_methods || {};
let reqLabels = Object.keys(reqMethods);
let reqCounts = Object.values(reqMethods);
new Chart(document.getElementById("requestMethodsChart"), {
type: "pie",
data: {
labels: reqLabels,
datasets: [{
data: reqCounts,
backgroundColor: ["#4b75ff", "#21ba45", "#db2828", "#fbbd08", "#b5cc18", "#a333c8"]
}]
},
options: { responsive: true, plugins: { legend: { position: "bottom" } } }
});
// 2. Histogram for hit_per_day
let hitPerDay = data.hit_per_day || {};
let dayLabels = getDateRangeLabels(hitPerDay);
let dayCounts = dayLabels.map(d => hitPerDay[d] || 0);
new Chart(document.getElementById("hitsPerDayChart"), {
type: "bar",
data: {
labels: dayLabels,
datasets: [{
label: "Hits",
data: dayCounts,
backgroundColor: "#4b75ff"
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { x: { ticks: { autoSkip: false, maxRotation: 90, minRotation: 45 } } }
}
});
// 3. Line chart for each site in hit_per_site
let siteChartsDiv = $("#siteCharts");
let siteData = data.hit_per_site || {};
let i = 0;
for (let [site, counts] of Object.entries(siteData)) {
let canvasId = "siteChart_" + i;
siteChartsDiv.append(`
<div style="margin-bottom:2em;">
<b>${site}</b>
<canvas id="${canvasId}" height="100"></canvas>
</div>
`);
// If counts length doesn't match dayLabels, pad left with zeros
let paddedCounts = counts.slice(-dayLabels.length);
while (paddedCounts.length < dayLabels.length) paddedCounts.unshift(0);
new Chart(document.getElementById(canvasId), {
type: "line",
data: {
labels: dayLabels,
datasets: [{
label: "Hits",
data: paddedCounts,
fill: false,
borderColor: "#21ba45",
backgroundColor: "#21ba45",
tension: 0.2
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { x: { ticks: { autoSkip: false, maxRotation: 90, minRotation: 45 } } }
}
});
i++;
}
});
}
function openLog(object, catergory, filename, filter="all"){
$(".logfile.active").removeClass('active');
$(object).addClass("active");
currentLogFile = filename;
currentOpenedLogURL = "/api/log/read?file=" + filename + "&filter=" + filter;
$.get(currentOpenedLogURL, function(data){
if (data.error !== undefined){
alert(data.error);
return;
}
renderLogWithCurrentFilter(data);
});
}
function openLogInNewTab(){
if (currentOpenedLogURL != ""){
window.open(currentOpenedLogURL);
}
}
function openLog(object, catergory, filename){
$(".logfile.active").removeClass('active');
$(object).addClass("active");
currentOpenedLogURL = "/api/log/read?file=" + filename;
$.get(currentOpenedLogURL, function(data){
if (data.error !== undefined){
alert(data.error);
return;
}
renderLogWithCurrentFilter(data);
});
}
function initLogList(){
$("#logList").html("");
$.get("/api/log/list", function(data){
//console.log(data);
for (let [key, value] of Object.entries(data)) {
console.log(key, value);
value.reverse(); //Default value was from oldest to newest
var fileItemList = "";
value.forEach(file => {
fileItemList += `<div class="item clickable logfile" onclick="openLog(this, '${key}','${file.Filename}');">
<i class="file outline icon"></i>
<div class="content">
${file.Title} (${formatBytes(file.Filesize)})
<div class="showing"><i class="green chevron right icon"></i></div>
</div>
</div>`;
})
$("#logList").append(`<div class="title">
<i class="dropdown icon"></i>
${key}
</div>
<div class="content">
<div class="ui list">
${fileItemList}
</div>
</div>`);
}
$(".ui.accordion").accordion();
});
}
initLogList();
function formatBytes(x){
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let l = 0, n = parseInt(x, 10) || 0;
@@ -224,47 +582,11 @@
//Filter the log and render it to text area based on current filter choice
function renderLogWithCurrentFilter(data){
if (currentFilter == "all"){
$("#logrender").val(data);
}else{
let filterLines = data.split("\n");
let filteredLogFile = "";
for (var i = 0; i < filterLines.length; i++){
const thisLine = filterLines[i];
if (currentFilter == "system" && thisLine.indexOf("[system:") >= 0){
filteredLogFile += thisLine + "\n";
}else if (currentFilter == "request" && thisLine.indexOf("[router:") >= 0){
filteredLogFile += thisLine + "\n";
}else if (currentFilter == "error" && thisLine.indexOf(":error]") >= 0){
filteredLogFile += thisLine + "\n";
}
}
$("#logrender").val(filteredLogFile);
}
$("#logrender").val(data);
var textarea = document.getElementById('logrender');
textarea.scrollTop = textarea.scrollHeight;
}
/* Filter related functions */
$(".filterbtn").on("click", function(evt){
//Set filter type
let filterType = $(this).attr("filter");
currentFilter = (filterType);
$(".filterbtn.active").removeClass("active");
$(this).addClass('active');
//Reload the log with filter
if (currentOpenedLogURL != ""){
$.get(currentOpenedLogURL, function(data){
if (data.error !== undefined){
alert(data.error);
return;
}
renderLogWithCurrentFilter(data);
});
}
});
/* Auto scroll function */
setInterval(function(){
if (autoscroll){
@@ -279,9 +601,42 @@
});
}
}
}, 10000);
}, 3000);
function handleAutoScrollTicker(event, checked){
autoscroll = checked;
}
// Error Highlights Rendering
function renderErrorHighlights(errors) {
if (!Array.isArray(errors) || errors.length === 0) {
$("#errorHighlightWrapper").html(
`<div class="ui positive message">
<div class="header">No errors found</div>
<p>This log file contains no errors.</p>
</div>`
);
return;
}
let html = `<div class="ui list">`;
errors.forEach(function(err) {
let [timestamp, method, path, code] = err;
html += `
<div class="item">
<div class="ui ${code.startsWith('5') ? 'red' : 'yellow'} message" style="margin-bottom:0.7em;">
<div class="header">
<span style="font-family:monospace;">[${timestamp}]</span>
<span style="font-family:monospace;">${method}</span>
<span style="margin-left:1em;font-family:monospace;">${path}</span>
<span style="float:right;font-weight:bold;">${code}</span>
</div>
</div>
</div>
`;
});
html += `</div>`;
$("#errorHighlightWrapper").html(html);
//Scroll to bottom of the error highlight
$("#errorHighlightWrapper").scrollTop($("#errorHighlightWrapper")[0].scrollHeight);
}
</script>
</html>

View File

@@ -139,6 +139,26 @@
</tbody>
</table>
<div class="ui divider"></div>
<h4>Plugin IntroSpect Permitted API Endpoints</h4>
<p>The following API endpoints are registered by this plugin and will be accessible by the plugin's API key:</p>
<table class="ui basic celled unstackable table">
<thead>
<tr>
<th>Endpoint</th>
<th>Method</th>
<th>Reason</th>
</tr>
</thead>
<!-- This tbody will be filled by JavaScript -->
<tbody id="plugin_permitted_api_endpoints">
</tbody>
</table>
<p>
Note that the API endpoints are only accessible by the plugin's API key.
If the plugin does not have an API key, it will not be able to access these endpoints.
API keys are generated automatically by Zoraxy when a plugin with permitted API endpoints is enabled.
</p>
<div class="ui divider"></div>
</div>
</div>
</div>
@@ -219,6 +239,22 @@
$("#dynamic_capture_sniffing_path").text(dynamicCaptureSniffingPath);
$("#dynamic_capture_ingress").text(dynamicCaptureIngress);
$("#registered_ui_proxy_path").text(registeredUIProxyPath);
//Update permitted API endpoints
let apiEndpoints = data.Spec.permitted_api_endpoints;
if (apiEndpoints == null || apiEndpoints.length == 0) {
$("#plugin_permitted_api_endpoints").html('<tr><td colspan="3">No API endpoints registered</td></tr>');
} else {
let endpointRows = '';
apiEndpoints.forEach(function(endpoint) {
endpointRows += `<tr>
<td>${endpoint.endpoint}</td>
<td>${endpoint.method}</td>
<td>${endpoint.reason}</td>
</tr>`;
});
$("#plugin_permitted_api_endpoints").html(endpointRows);
}
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,8 @@ func getExcludedDNSProviders() []string {
"exec", //Not a DNS provider
"httpreq", //Not a DNS provider
"hurricane", //Multi-credentials arch
"dnshomede", //Multi-credentials arch
"myaddr", //Multi-credentials arch
"oraclecloud", //Evil company
"acmedns", //Not a DNS provider
"selectelv2", //Not sure why not working with our code generator

24
tools/dns_challenge_update/code-gen/update.sh Normal file → Executable file
View File

@@ -1,18 +1,22 @@
#/bin/bash
#!/bin/bash
repo_url="https://github.com/go-acme/lego"
# Get the latest lego version
version=$(curl -s https://api.github.com/repos/go-acme/lego/releases/latest | grep tag_name | cut -d '"' -f 4)
# Check if the folder "./lego" exists
if [ -d "./lego" ]; then
# If the folder exists, change into it and perform a git pull
echo "Folder './lego' exists. Pulling updates..."
cd "./lego" || exit
git pull
cd ../
# If the folder exists, change into it and perform a git pull
echo "Folder './lego' exists. Pulling updates..."
cd "./lego" || exit
git pull
git switch --detach "$version"
cd ../
else
# If the folder doesn't exist, clone the repository
echo "Folder './lego' does not exist. Cloning the repository..."
git clone "$repo_url" "./lego" || exit
# If the folder doesn't exist, clone the repository
echo "Folder './lego' does not exist. Cloning the repository..."
git clone --branch "$version" "$repo_url" "./lego" || exit
fi
# Run the extract.go to get all the config from lego source code
@@ -25,4 +29,4 @@ sleep 2
# Comment the line below if you dont want to pull everytime update
# This is to help go compiler to not load all the lego source file when compile
#rm -rf ./lego/
echo "Config generated"
echo "Config generated"

5
tools/update_acmedns.sh Normal file → Executable file
View File

@@ -1,4 +1,4 @@
# /bin/sh
#!/bin/sh
# Build the acmedns
echo "Building ACMEDNS"
@@ -7,4 +7,5 @@ cd ../tools/dns_challenge_update/code-gen
cd ../../../
cp ./tools/dns_challenge_update/code-gen/acmedns/acmedns.go ./src/mod/acme/acmedns/acmedns.go
cp ./tools/dns_challenge_update/code-gen/acmedns/providers.json ./src/mod/acme/acmedns/providers.json
cp ./tools/dns_challenge_update/code-gen/acmedns/providers.json ./src/mod/acme/acmedns/providers.json

25
tools/update_geodb.sh Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
cd ../src/mod/geodb
@@ -11,24 +11,25 @@ echo "Updating geodb csv files"
echo "Downloading IPv4 database"
curl -f https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv4.csv -o geoipv4.csv
if [ $? -ne 0 ]; then
echo "Failed to download IPv4 database"
failed=true
echo "Failed to download IPv4 database"
failed=true
else
echo "Successfully downloaded IPv4 database"
echo "Successfully downloaded IPv4 database"
fi
echo "Downloading IPv6 database"
curl -f https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv6.csv -o geoipv6.csv
if [ $? -ne 0 ]; then
echo "Failed to download IPv6 database"
failed=true
echo "Failed to download IPv6 database"
failed=true
else
echo "Successfully downloaded IPv6 database"
echo "Successfully downloaded IPv6 database"
fi
if [ "$failed" = true ]; then
echo "One or more downloads failed. Blocking exit..."
while :; do
read -p "Press [Ctrl+C] to exit..." input
done
fi
echo "One or more downloads failed. Blocking exit..."
while :; do
read -p "Press [Ctrl+C] to exit..." input
done
fi