diff --git a/docs/plugins/assets/banner.png b/docs/plugins/assets/banner.png new file mode 100644 index 0000000..56f5d1b Binary files /dev/null and b/docs/plugins/assets/banner.png differ diff --git a/docs/plugins/assets/banner.psd b/docs/plugins/assets/banner.psd new file mode 100644 index 0000000..7e30760 Binary files /dev/null and b/docs/plugins/assets/banner.psd differ diff --git a/docs/plugins/assets/logo.png b/docs/plugins/assets/logo.png new file mode 100644 index 0000000..083342a Binary files /dev/null and b/docs/plugins/assets/logo.png differ diff --git a/docs/plugins/assets/logo_white.png b/docs/plugins/assets/logo_white.png new file mode 100644 index 0000000..9557852 Binary files /dev/null and b/docs/plugins/assets/logo_white.png differ diff --git a/docs/plugins/assets/theme.js b/docs/plugins/assets/theme.js new file mode 100644 index 0000000..4338f19 --- /dev/null +++ b/docs/plugins/assets/theme.js @@ -0,0 +1,51 @@ +/* Things to do before body loads */ +function restoreDarkMode(){ + if (localStorage.getItem("darkMode") === "enabled") { + $("html").addClass("is-dark"); + $("html").removeClass("is-white"); + } else { + $("html").removeClass("is-dark"); + $("html").addClass("is-white"); + } +} +restoreDarkMode(); + +function updateElementToTheme(isDarkTheme=false){ + if (!isDarkTheme){ + let whiteSrc = $("#sysicon").attr("white_src"); + $("#sysicon").attr("src", whiteSrc); + $("#darkModeToggle").html(``); + + // Update the rendering text color in the garphs + if (typeof(changeScaleTextColor) != "undefined"){ + changeScaleTextColor("black"); + } + + }else{ + let darkSrc = $("#sysicon").attr("dark_src"); + $("#sysicon").attr("src", darkSrc); + $("#darkModeToggle").html(``); + + // Update the rendering text color in the garphs + if (typeof(changeScaleTextColor) != "undefined"){ + changeScaleTextColor("white"); + } + } +} + +/* Things to do after body loads */ +$(document).ready(function(){ + $("#darkModeToggle").on("click", function() { + $("html").toggleClass("is-dark"); + $("html").toggleClass("is-white"); + if ($("html").hasClass("is-dark")) { + localStorage.setItem("darkMode", "enabled"); + updateElementToTheme(true); + } else { + localStorage.setItem("darkMode", "disabled"); + updateElementToTheme(false); + } + }); + + updateElementToTheme(localStorage.getItem("darkMode") === "enabled"); +}); \ No newline at end of file diff --git a/docs/plugins/build.go b/docs/plugins/build.go new file mode 100644 index 0000000..d69bf13 --- /dev/null +++ b/docs/plugins/build.go @@ -0,0 +1,280 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/yosssi/gohtml" +) + +type FileInfo struct { + Filename string `json:"filename"` + Title string `json:"title"` + Type string `json:"type"` +} + +func build() { + rootDir := "./docs" + outputFile := "./index.json" + + type Folder struct { + Title string `json:"title"` + Path string `json:"path"` + Type string `json:"type"` + Files []interface{} `json:"files,omitempty"` + } + + var buildTree func(path string, d fs.DirEntry) interface{} + buildTree = func(path string, d fs.DirEntry) interface{} { + relativePath, _ := filepath.Rel(rootDir, path) + relativePath = filepath.ToSlash(relativePath) + var title string + if d.IsDir() { + title = filepath.Base(relativePath) + } else { + title = strings.TrimSuffix(filepath.Base(relativePath), filepath.Ext(relativePath)) + } + + //Strip the leader numbers from the title, e.g. 1. Introduction -> Introduction + if strings.Contains(title, ".") { + parts := strings.SplitN(title, ".", 2) + if len(parts) > 1 { + title = strings.TrimSpace(parts[1]) + } + } + // Remove leading numbers and dots + title = strings.TrimLeft(title, "0123456789. ") + // Remove leading spaces + title = strings.TrimLeft(title, " ") + + if d.IsDir() { + folder := Folder{ + Title: title, + Path: relativePath, + Type: "folder", + } + + entries, err := os.ReadDir(path) + if err != nil { + panic(err) + } + + for _, entry := range entries { + if entry.Name() == "img" || entry.Name() == "assets" { + continue + } + if strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/img/") || strings.Contains(filepath.ToSlash(filepath.Join(relativePath, entry.Name())), "/assets/") { + continue + } + child := buildTree(filepath.Join(path, entry.Name()), entry) + if child != nil { + folder.Files = append(folder.Files, child) + } + } + return folder + } else { + ext := filepath.Ext(relativePath) + if ext != ".md" && ext != ".html" && ext != ".txt" { + return nil + } + return FileInfo{ + Filename: relativePath, + Title: title, + Type: "file", + } + } + } + + rootInfo, err := os.Stat(rootDir) + if err != nil { + panic(err) + } + rootFolder := buildTree(rootDir, fs.FileInfoToDirEntry(rootInfo)) + jsonData, err := json.MarshalIndent(rootFolder, "", " ") + if err != nil { + panic(err) + } + + /* For debug purposes, print the JSON structure */ + err = os.WriteFile(outputFile, jsonData, 0644) + if err != nil { + panic(err) + } + + /* For each file in the folder structure, convert markdown to HTML */ + htmlOutputDir := "./html" + os.RemoveAll(htmlOutputDir) // Clear previous HTML output + err = os.MkdirAll(htmlOutputDir, os.ModePerm) + if err != nil { + panic(err) + } + + var processFiles func(interface{}) + processFiles = func(node interface{}) { + switch n := node.(type) { + case FileInfo: + if filepath.Ext(n.Filename) == ".md" { + inputPath := filepath.Join(rootDir, n.Filename) + outputPath := filepath.Join(htmlOutputDir, strings.TrimSuffix(n.Filename, ".md")+".html") + + // Ensure the output directory exists + err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm) + if err != nil { + panic(err) + } + + // Read the markdown file + mdContent, err := os.ReadFile(inputPath) + if err != nil { + panic(err) + } + + // Convert markdown to HTML + docContent := mdToHTML(mdContent) + docContent, err = optimizeCss(docContent) + if err != nil { + panic(err) + } + + // Load the HTML template + templateBytes, err := os.ReadFile("template/documents.html") + if err != nil { + panic(err) + } + + // Generate the side menu HTML + sideMenuHTML, err := generateSideMenu(string(jsonData), n.Title) + if err != nil { + panic(err) + } + + templateBody := string(templateBytes) + // Replace placeholders in the template + htmlContent := strings.ReplaceAll(templateBody, "{{title}}", n.Title+" | Zoraxy Documentation") + htmlContent = strings.ReplaceAll(htmlContent, "{{content}}", string(docContent)) + htmlContent = strings.ReplaceAll(htmlContent, "{{sideMenu}}", sideMenuHTML) + htmlContent = strings.ReplaceAll(htmlContent, "{{root_url}}", *root_url) + //Add more if needed + + //Beautify the HTML content + htmlContent = gohtml.Format(htmlContent) + + // Write the HTML file + err = os.WriteFile(outputPath, []byte(htmlContent), 0644) + if err != nil { + panic(err) + } + + //Check if the .md file directory have an ./img or ./assets folder. If yes, copy the contents to the output directory + imgDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "img") + assetsDir := filepath.Join(rootDir, filepath.Dir(n.Filename), "assets") + if _, err := os.Stat(imgDir); !os.IsNotExist(err) { + err = filepath.Walk(imgDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(imgDir, srcPath) + if err != nil { + return err + } + destPath := filepath.Join(filepath.Dir(outputPath), "img", relPath) + if info.IsDir() { + return os.MkdirAll(destPath, os.ModePerm) + } + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + return os.WriteFile(destPath, data, 0644) + }) + if err != nil { + panic(err) + } + } + if _, err := os.Stat(assetsDir); !os.IsNotExist(err) { + err = filepath.Walk(assetsDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(assetsDir, srcPath) + if err != nil { + return err + } + destPath := filepath.Join(filepath.Dir(outputPath), "assets", relPath) + if info.IsDir() { + return os.MkdirAll(destPath, os.ModePerm) + } + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + return os.WriteFile(destPath, data, 0644) + }) + if err != nil { + panic(err) + } + } + + fmt.Println("Generated HTML:", outputPath) + } + case Folder: + for _, child := range n.Files { + processFiles(child) + } + } + } + + processFiles(rootFolder) + copyOtherRes() +} + +func copyOtherRes() { + srcDir := "./assets" + destDir := "./html/assets" + + err := filepath.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(srcDir, srcPath) + if err != nil { + return err + } + destPath := filepath.Join(destDir, relPath) + if info.IsDir() { + return os.MkdirAll(destPath, os.ModePerm) + } + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + return os.WriteFile(destPath, data, 0644) + }) + if err != nil { + panic(err) + } + +} + +func mdToHTML(md []byte) []byte { + // create markdown parser with extensions + extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock + p := parser.NewWithExtensions(extensions) + doc := p.Parse(md) + + // create HTML renderer with extensions + htmlFlags := html.CommonFlags | html.HrefTargetBlank + opts := html.RendererOptions{ + Flags: htmlFlags, + } + renderer := html.NewRenderer(opts) + + return markdown.Render(doc, renderer) +} diff --git a/docs/plugins/build.sh b/docs/plugins/build.sh new file mode 100644 index 0000000..f78c1ae --- /dev/null +++ b/docs/plugins/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Run the Go program with the specified arguments +./docs.exe -m=build -root=plugins/html/ \ No newline at end of file diff --git a/docs/plugins/cssOptimizer.go b/docs/plugins/cssOptimizer.go new file mode 100644 index 0000000..f73701e --- /dev/null +++ b/docs/plugins/cssOptimizer.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "net/url" + "strings" + + "github.com/PuerkitoBio/goquery" +) + +func optimizeCss(htmlContent []byte) ([]byte, error) { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(htmlContent))) + if err != nil { + return nil, err + } + + originalHTMLContent := string(htmlContent) + // Replace img elements + + doc.Find("img").Each(func(i int, s *goquery.Selection) { + //For each of the image element, replace the parent from p to div + originalParent, err := s.Parent().Html() + if err != nil { + fmt.Println("Error getting parent HTML:", err) + return + } + + src, exists := s.Attr("src") + if !exists { + fmt.Println("No src attribute found for img element") + return + } + encodedSrc := (&url.URL{Path: src}).String() + + //Patch the bug in the parser that converts " />" to "/>" + originalParent = strings.ReplaceAll(originalParent, "/>", " />") + fmt.Println("
%s
", newClass, originalParagraph)) + }) + + //Replace hr with ts-divider + // Replace hr elements outside of code blocks + doc.Find("hr").Each(func(i int, s *goquery.Selection) { + parent := s.Parent() + if parent.Is("code") { + // Skip blocks
+ return
+ }
+
+ // Replace
with
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "
", "")
+ })
+
+ // Add ts-table to all table elements
+ doc.Find("table").Each(func(i int, s *goquery.Selection) {
+ class, exists := s.Attr("class")
+ var newClass string
+ if exists {
+ newClass = fmt.Sprintf("%s ts-table", class)
+ } else {
+ newClass = "ts-table"
+ }
+
+ originalTable, _ := s.Html()
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, originalTable, fmt.Sprintf("%s
", newClass, originalTable))
+ })
+
+ // Replace and -
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "
", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "- ", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+
+ // Replace with
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+
+ // Replace
without class with
+ for {
+ startIndex := strings.Index(originalHTMLContent, "")
+ if startIndex == -1 {
+ break
+ }
+
+ endIndex := strings.Index(originalHTMLContent[startIndex+6:], "
")
+ if endIndex == -1 {
+ break
+ }
+ endIndex += startIndex + 6
+
+ codeSegment := originalHTMLContent[startIndex : endIndex+7] // Include
+ if !strings.Contains(codeSegment, "class=") {
+ replacement := strings.Replace(codeSegment, "", "", 1)
+ replacement = strings.Replace(replacement, "
", "", 1)
+ originalHTMLContent = strings.Replace(originalHTMLContent, codeSegment, replacement, 1)
+ } else {
+ // Skip if already has a class
+ break
+ }
+ }
+
+ //Replace blockquote to
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "", "")
+
+ /*
+ originalHTMLContent = strings.ReplaceAll(originalHTMLContent, "language-xml", "")
+ // Remove class attribute from inside
+ re := regexp.MustCompile(``)
+ originalHTMLContent = re.ReplaceAllString(originalHTMLContent, "")
+ */
+ return []byte(originalHTMLContent), err
+}
diff --git a/docs/plugins/dev.sh b/docs/plugins/dev.sh
new file mode 100644
index 0000000..dbe63f1
--- /dev/null
+++ b/docs/plugins/dev.sh
@@ -0,0 +1,12 @@
+#/bin/bash
+go build
+# Run the Go program with the specified arguments
+./docs.exe -m=build
+
+echo "Running docs in development mode..."
+./docs.exe
+
+# After the docs web server mode terminate, rebuild it with root = plugins/html/
+./docs.exe -m=build -root=plugins/html/
+
+# The doc should always be ready to push to release branch
diff --git a/docs/plugins/diagrams/dynamic_capture.drawio b/docs/plugins/diagrams/dynamic_capture.drawio
new file mode 100644
index 0000000..466c309
--- /dev/null
+++ b/docs/plugins/diagrams/dynamic_capture.drawio
@@ -0,0 +1 @@
+5VrRcps4FP0az2Qf1gMIAX5MnCbNbHcn00xn277syCBjpRhRIcdOvn4lkG2Q1CZxMSGpHzxwBZI49xxd3QsjMF1uLhkqFn/TBGcjz0k2I3A+8rzQn4h/abivDRAGtSFlJKlN7t5wQx6wMjrKuiIJLlsXckozToq2MaZ5jmPesiHG6Lp92Zxm7VELlGLDcBOjzLT+SxK+qK2RF+7t7zFJF9uR3UA98BJtL1ZPUi5QQtcNE3g3AlNGKa+PlpspziR2W1zq+y5+0LqbGMM5f8oNX+6Xk9ldzsLJZXobx5+vspvwT9XLHcpW6oG/UoY292rK/H6LA6OrPMGyK2cEztYLwvFNgWLZuhaOF7YFX2bizBWH5tS242DG8aZhUlO9xHSJORPDOtvWiYJN8caN1Pl67wV3C+2i4QFf2ZByfLrreo+NOFDwPAMqz4DqOlulJH9xqHxvaFABAyoDJJwnp1Ke4izOUFmSuI0L3hD+WUI4hursS6PlfKPQrU7u1UmCykWF+0+BLemKxfhxTeCktTKY8DfghRZ0tzaGM8TJXXs9sUGuRrimRMx4510Qtr3ru5rX6udRdzX1r3W0m5DqKPC0jjhiKeZGRxUDdo99OCn8V02KYFCk8MPJGEbO/ue3XCtCwbjR6LiHMUaM8qKMcYPHKdP3Yuv5sA01sCy2FseD4FiLrRv9urB+CMxA+O4Fjh3151Na6wj2S+kBMtoPoR2SRxh9PEJPhhwpaoK8rkgRTva/txgotsM1GHOVc0bLQqaEOneELnibLSgjaS6pJJyNmTBI9RCRBJ6qhiVJEnn7GcMleUCzqivJmUI+UvWQ8GwEzyVxRHcrTss6k5W9M8qFB6nsJ+hGskDb8e9yzAanQgundLd0lxuZydGbi0F66ADOoRtxLZgBfUd/bLWY6dlNTuZzkqcS7TwpdiMPRDYioexGNy5o6yYKLKGuV+E8ISvqf0PQRgn4lg0BtIB0vNKLuW3C43Q8km65KCV3X5ytnazq7Y3Ybl1o4B70Ss7QgH2aEWxZG8oFKuThapmdxpw20f2AZji7FlApLc8o53QpLshkwxmKv6UVwac0k/eJ3sC8+lk8xKnGdrriGcnxdFf8dTqSgFZzsSR5oM8tsfcb5HhawdfXV5Mnh1fdeT2neJ6ZvnwqSs4wWr740h44bWwGUCo2t+7PZvbxkr2BSCOAPSRxPx+k75QOmG+mRD9EqGUzrlz/BgK+C7SV6qXTOPCEFze9L1nQa7PdVnC17EaPV3AFv0Gy68H2XtTX0Ty04OpHPa8jZoJ1QdkasaQC6XsFH0rE+iA/LBhSRtHIf2FH7y6idmLn2yq9vWYYoINS79ClZChgcujGVqsbQb0AdewXuGZIPkFxjAs5x3ktqj+GJJvOykaOthiGPZaN/rq6/e/7x3+uPn0oNmw9+fhwPvvaxQcWc5JljZw7gThKfGEXmQr9hhstkTcDwb5wranKAu1QYhbs6uWGJjs9F+pOdlZfmwHs5EpEJ2f79ZGMYitcyhkvUJ5kJE8HpcJ98DIU9yTy7IKX5l1LdexowcvqGNi5COdRjOPYJsJZBGW0fg0iDMeaow4t5ATaB0vweFtHq3/NovMJ0YR3W71fHKbugm505080d9o2jTZCHaA7cbr/HrZ24/6jYvDufw==
\ No newline at end of file
diff --git a/docs/plugins/diagrams/dynamic_capture.png b/docs/plugins/diagrams/dynamic_capture.png
new file mode 100644
index 0000000..0083370
Binary files /dev/null and b/docs/plugins/diagrams/dynamic_capture.png differ
diff --git a/docs/plugins/diagrams/plugin_workflow.drawio b/docs/plugins/diagrams/plugin_workflow.drawio
new file mode 100644
index 0000000..d0aabf5
--- /dev/null
+++ b/docs/plugins/diagrams/plugin_workflow.drawio
@@ -0,0 +1 @@
+7Vpbc5s4FP41nuk+lEGI62PtdDfbzLaZOpnN7suOahSsXYy8Qm7s/PpKRoCRcEwTcNykfvCgIyHQdy76zhEjOFmsf2NoOf+DxjgdOXa8HsGzkeMEbiT+pWBTCDzPLwQJI3EhArVgSu6xEtpKuiIxzhsDOaUpJ8umcEazDM94Q4YYo3fNYbc0bT51iRJsCKYzlJrSP0nM54U0dIJafo5JMi+fDHy14AUqB6uV5HMU07sdEXw/ghNGKS+uFusJTiV2JS7Ffb/u6a1ejOGMd7nBTa5BHF3Azx/t65sPk4uLm9m/bx2lja8oXakVq7flmxICRldZjOUs9giO7+aE4+kSzWTvndC5kM35IhUtIC5vSZpOaEqZaGc0E4PGMcrn29tlv3ocZhyv9y4EVPAIs8J0gTnbiCGlTXnFHcqinEgBfFfrx3fVmPmObkI1DimTSKqZa9TEhQLue0AMfngQYdgRROANhqL3w6MIgq4ogqFQBAaIf1OG1punQdkDVKCMgyVW0MSqGrOLlTuYwRlQXaarhGRC9pmuOGbPjhmEp4YZNDC7XuacYbR4drC84NTAcg8HNJzF7yRLEa1ZivKczJq4tAQsHBuM5SBEOxB4LQiUMoZTxMnX5vRtsKgnXFIiHly7eKhpINCQzemKzbC6a5eqHJjIi7SJOGIJ5sZEWy1Vy3684jrsRC9KcTCMLN+L6l/Qjx4PzHtstXbgui9Krb4j4Pft6gf60erD0x5bqX1z7x52IqDTwhZyDVrUDgfLUKKnW/6pGzuImnuGYz86aMHmRDpXH9iggUnhn92iYahZdOR1smjPG8iggUne77d5zj/LLYe3fs84o9OlLAlJzuen4o3GXwSl9xN59WYqtCi7LhFD4pUw+8UAWcDFm0iilCSZdBABm0gP4FiCSmYofac6FiSO5e1jhnNyj75sp5IqWkpr2YLgjUfemdSKmG7FaV5UvOTsjHLhAFTOA/tRW6WTKuS3qK3NJ+FQgQiYCcSLi0RQY69GgtGZBushzT9yJDITGCLdKldu9ebD9NNH2U+Q+J9enX26vjopNxJEvp8NPdJKjm2pZdhiUc5gftR33XaATQJ63WiPsVP3V+Gxnx5txOLZ5kYak20BLyolf0lYLeBUgrO1ArpobXZbl5iR7SajhKcewUAYWk7UVCaMLOdxYQyGwArsffmCsBLLtr26W6MMA8e40iL2EokJzW5JsmJYkonXSiT0AAhBi2sHxyQSTg9E4tW6dlhXZLSY7drSG3d+fj8+b2uPAbYV7ji9toyhfT40bKc8JLGVOx/04FzwoP+wdrzUcuLU3dPbNujmFt4oPu3SnEj23tKMT9UrlynkU3ze1cgngFZL+hC1beiONVTe55iVjOrQ5qfq9qgOgvAEVAdNMlaft+H/VziX8eMcZXFKsuSnHqU2mhzMdVt2XXhkLb6GBD5yrZ1Di0grCLqO37q3fXcdXTvddWF41G0Qmun9+dXV5RY/5Y4vjsgaoTH0u8XFwQ7UzQ3txSvBDXQ22HFzGuygvs86QdUo8gjvQBbRNRg6dhEOHlhGURR6ttNGPZzpX4p1jYuufWCigeOiax7AVC6ZL2mW41PyyZ6Kd26wB/RG8e6oTtlnhj+QU57KEYOrnwx0/9JGC8WBLIUYtbpjed4DjOT0PO9tX5wEajrwK/o+gPOJZv3NeaG4+sN9+P4b
\ No newline at end of file
diff --git a/docs/plugins/diagrams/plugin_workflow.png b/docs/plugins/diagrams/plugin_workflow.png
new file mode 100644
index 0000000..d49fc06
Binary files /dev/null and b/docs/plugins/diagrams/plugin_workflow.png differ
diff --git a/docs/plugins/diagrams/static_capture.drawio b/docs/plugins/diagrams/static_capture.drawio
new file mode 100644
index 0000000..b30eb4f
--- /dev/null
+++ b/docs/plugins/diagrams/static_capture.drawio
@@ -0,0 +1 @@
+5VrRjqM2FP2aSNOHRoAxhMeZzO52pW010qjqbt884AFawMg4k2S/vjaYALa3k0kDpJk8RHABY849x9fHsADrfPeJojL5lUQ4WzhWtFuA+4XjBBbk/yKwbwKe7zSBmKZRE7K7wGP6HcugJaObNMLV4ERGSMbSchgMSVHgkA1iiFKyHZ72TLLhXUsUYy3wGKJMj/6RRixpoivH7+K/4DRO2jvbXtAcyVF7snySKkER2fZC4MMCrCkhrNnKd2ucCexaXJrrPv7g6KFjFBfsmAu+7fPg6aWgfvAp/isMv37OHv2fZSsvKNvIB/6TULTbyy6zfYsDJZsiwqIpawHutknK8GOJQnF0yxPPYwnLM75n803ZKKYM737YW/uAAecOJjlmlN/Wai8IJGySN/ZK7m+7LNgttEkvA66MIZn4+NB0hw3fkPC8ASpHg+oh28RpMTtUrnNpUAENKg0kXES3Qp58L8xQVaXhEBe8S9lXAeESyr1vvSP3O4luvbOXOxGqkhr3DlgcafJWYOW9Ihsa4tdEosPfgxca0G1jFGeIpS/Dbpggl3d4ICnv4CG7wB9m17WVrDXdl1f19a80dOhQOxI7SkMM0RgzraGaAYfHPp0U7nWRwpuTFK4fLOHK6n7uILW8FCx7By37NMbwu8zKGNt7nTJTD7aOC4dQA8Nga0g88MYabO3VfxfWsbqYi++OZ5lRfzullYbgtJS+QEa7PjRD8gqjxyN08L+qFA1jLrpS+EH3u8ZC0d6ux5jPBaOkKoUlVLnDpcKGbEFZGheCSjy3mPKAEFTKTeCtPJCnUSQuv6O4Sr+jp7opwZlSPFL9kPBuAe8FcXhzG0aqxsmK1ilhPINEtOOdR7JAmfEfPGaPU76BU2pazueNdHN0dTVILR3AOnUirhQzoM7ox1aLbs++pJXoIHmuU8ZRCvlGiEq2objOAUuqS9IRd5jnmc2tVkMhrTy99pk4NZ6SjrBJ088QwJCwrmGGAA0gjbcWo8+j8DJeLkRaPqIoN6zKTM3WEYb5w0Ax2zDva7CvsxQXeo2tElSKzU2e3YaM9NH9gp5w9sChklp+IoyRnJ+QiQN3KPw7rgm+Jpm4jrcGnuufIUOMKGwnG5alBV4fVoOtM0lAWYQxuD4w5RzZeQemT1kBdtXR5Oh6qyZvYs/n6H7m97JiFKN89qHds4bYXMDasT6XfzOzJ3N/c0nDgxO4un+/ydQeD+ivqng7KVfLblmn/goKvg2UkWpuXweOeJMz+ZAFnSHbTSuwhtnoeCuw4B24XwcOVwldFc2jRxXF/brBxOOIbrBufiOig8I0dO5XeN6fZh9Uep4XnslUwNUSDP2ca1rxNS35jjfOwOtXUDAUEFTBPH46qzSkvgsZW0Cmdxit6b62gmwrWFuzK0W34FenFLXWwFOdn7pkC9WiNbZUdJd+c+FVZqQ3FNAwSQsm1c05Xyr2TKW19FuPafaVx4rtiE9OmrI9n+dUUnpyAVNmgFB9kTL2h0i6k7zhqilJUeH5tXgO8akrZ4ZFnUltpPsODJJWa/xzyWM1sTx0z39l8nCGL/1gO904vzz4bveBd5Og7it58OEf
\ No newline at end of file
diff --git a/docs/plugins/diagrams/static_capture.png b/docs/plugins/diagrams/static_capture.png
new file mode 100644
index 0000000..0c51c52
Binary files /dev/null and b/docs/plugins/diagrams/static_capture.png differ
diff --git a/docs/plugins/docs/1. Introduction/1. What is Zoraxy Plugin.md b/docs/plugins/docs/1. Introduction/1. What is Zoraxy Plugin.md
new file mode 100644
index 0000000..3e79889
--- /dev/null
+++ b/docs/plugins/docs/1. Introduction/1. What is Zoraxy Plugin.md
@@ -0,0 +1,43 @@
+# What is Zoraxy Plugin?
+
+Last Update: 25/05/2025
+
+---
+
+Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
+
+Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not **as the plugin library and interface are open source under the LGPL license**.
+
+There are two primary types of plugins:
+- **Router plugins**: Involved with connections from HTTP proxy rules.
+- **Utility plugins**: Provide user interfaces for various network features that operate independently of the Zoraxy core.
+
+---
+
+## How plugins are distributed & installed
+Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as `linux_amd64_foobar`, `windows_amd64_foobar.exe`, or `linux_arm64_foobar`.
+
+To manually install a plugin for testing, place the binary file into the `/plugins/{plugin_name}/` folder within your Zoraxy installation directory.
+
+> **Warning:** The binary name inside the folder must match the plugin folder name. For example, the binary should be named `foobar` (or `foobar.exe` on Windows) if placed in the `/plugins/foobar/` folder. Avoid using names like `foobar_plugin.exe`.
+
+For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
+
+---
+
+## Plugin vs Pull Request
+The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
+
+- Designed to handle features that are challenging to integrate directly into the Zoraxy core.
+- Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
+- Allows for frequent updates to specific code components without impacting the core's stability or causing downtime.
+
+---
+
+### When should you add a core PR or a plugin?
+In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
+
+- **Core PR**: If the feature is relevant to most users and enhances Zoraxy's core functionality, consider submitting a core Pull Request (PR).
+- **Plugin**: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
+
+The decision depends on the feature's general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
diff --git a/docs/plugins/docs/1. Introduction/2. Getting Started.md b/docs/plugins/docs/1. Introduction/2. Getting Started.md
new file mode 100644
index 0000000..1d02cab
--- /dev/null
+++ b/docs/plugins/docs/1. Introduction/2. Getting Started.md
@@ -0,0 +1,62 @@
+# Getting Started
+
+Last Update: 25/05/2025
+
+---
+
+To start developing plugins, you will need the following installed on your computer
+
+1. The source code of Zoraxy
+2. Go compiler
+3. VSCode (recommended, or any editor of your choice)
+
+---
+
+## Step 1: Start Zoraxy at least once
+
+If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
+
+```bash
+cd src/
+go mod tidy
+go build
+sudo ./zoraxy
+```
+
+This would allow Zoraxy to generate all the required folder structure on startup.
+
+After the startup process completes, you would see a folder named "plugins" in the working directory of Zoraxy.
+
+---
+
+## Steps 2: Prepare the development environment for Zoraxy Plugin
+
+Next, you will need to think of a name for your plugin. Lets name our new plugin "Lapwing".
+
+**Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.**
+
+Follow the steps below to create the folder structure
+
+### 2.1 Create Plugin Folder
+
+Create a folder with your plugin name in the `plugins` folder. After creating the folder, you would have something like `plugins/Lapwing/`.
+
+### 2.2 Locate and copy Zoraxy Plugin library
+
+Locate the Zoraxy plugin library from the Zoraxy source code. You can find the `zoraxy_plugin` Go module under `src/mod/plugins/zoraxy_plugin`.
+
+Copy the `zoraxy_plugin` folder from the Zoraxy source code mod folder into the your plugin's mod folder. Let assume you use the same mod folder name as Zoraxy as `mod`, then your copied library path should be `plugins/Lapwing/mod/zoraxy_plugin`.
+
+### 2.3 Prepare Go Project structure
+
+Create the `main.go` file for your plugin. In the example above, it would be located at `plugins/Lapwing/main.go`.
+
+Use `go mod init yourdomain.com/foo/plugin_name` to initiate your plugin. By default the `go.mod` file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be `go mod init github.com/your_user_name/Lapwing`.
+
+---
+
+## Steps 3: Open plugin folder in IDE
+
+Now open your preferred IDE or text editor and use your plugin folder as the project folder
+
+Now, you are ready to start developing Zoraxy plugin!
\ No newline at end of file
diff --git a/docs/plugins/docs/1. Introduction/3. Installing Plugin.md b/docs/plugins/docs/1. Introduction/3. Installing Plugin.md
new file mode 100644
index 0000000..cf35531
--- /dev/null
+++ b/docs/plugins/docs/1. Introduction/3. Installing Plugin.md
@@ -0,0 +1,26 @@
+# Installing Plugin
+
+Last Update: 25/05/2025
+
+---
+
+### Install via Plugin Store
+
+(Work in progress)
+
+### Manual Install
+
+The plugin shall be placed inside the `plugins/{{plugin_name}}/` directory where the binary executable name must be matching with the plugin name.
+
+If you are on Linux, also make sure Zoraxy have the execution permission of the plugin. You can use the following command to enable execution of the plugin binary on Linux with the current user (Assume Zoraxy is run by the current user)
+
+```bash
+cd ./plugins/{{plugin_name}}/
+chmod +x ./{{plugin_name}}
+```
+
+
+
+Sometime plugins might come with additional assets other than the binary file. If that is the case, extract all of the plugins content into the folder with the plugin's name.
+
+After the folder structure is ready, restart Zoraxy. If you are using systemd for Zoraxy, use `sudo systemctl restart zoraxy` to restart Zoraxy via systemd service.
\ No newline at end of file
diff --git a/docs/plugins/docs/1. Introduction/4. Enable Plugins.md b/docs/plugins/docs/1. Introduction/4. Enable Plugins.md
new file mode 100644
index 0000000..d1aa702
--- /dev/null
+++ b/docs/plugins/docs/1. Introduction/4. Enable Plugins.md
@@ -0,0 +1,40 @@
+# Enable Plugins
+
+Last Update: 25/05/2025
+
+---
+
+To enable and assign a plugin to a certain HTTP Proxy Rule, you will need to do the following steps
+
+## 1. Create a tag for your HTTP Proxy Rules
+
+Let say you want to enable debugger on some of your HTTP Proxy Rules. You can do that by first creating a tag in the tag editor. In the example below, we will be using the tag "debug". After adding the tag to the HTTP Proxy rule, you will see something like this.
+
+
+
+
+
+---
+
+## 2. Enable Plugin
+
+Click on the "Enable" button on the plugin which you want to enable
+
+
+
+---
+
+## 3. Assign Plugin to HTTP Proxy Rule
+
+Finally, select the tag that you just created in the dropdown menu
+
+
+
+
+
+Afterward, you will see the plugin is attached to the target tag
+
+
+
+It means the plugin is enabled on the HTTP proxy rule
+
diff --git a/docs/plugins/docs/1. Introduction/5. Viewing Plugin Info.md b/docs/plugins/docs/1. Introduction/5. Viewing Plugin Info.md
new file mode 100644
index 0000000..12647db
--- /dev/null
+++ b/docs/plugins/docs/1. Introduction/5. Viewing Plugin Info.md
@@ -0,0 +1,13 @@
+# Viewing Plugin Info
+
+To view plugin information, you can click on the (i) icon in the plugin list.
+
+
+
+Next, a side menu will pop up from the side. Here ,you can see the current Plugin information and runtime values including Working directories and runtime assigned port.
+
+If you are a developer (which you probably is considering you are reading this doc), you can click on the "developer insight" dropdown to show the capture paths registered by this plugin for debug purposes.
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png
new file mode 100644
index 0000000..d32e67b
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png differ
diff --git a/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png
new file mode 100644
index 0000000..40e248d
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png differ
diff --git a/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png
new file mode 100644
index 0000000..4336e78
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png differ
diff --git a/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png
new file mode 100644
index 0000000..7dac9fa
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png differ
diff --git a/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png b/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png
new file mode 100644
index 0000000..cac42bb
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png differ
diff --git a/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png b/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png
new file mode 100644
index 0000000..c63c5e4
Binary files /dev/null and b/docs/plugins/docs/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png differ
diff --git a/docs/plugins/docs/2. Architecture/1. Plugin Architecture.md b/docs/plugins/docs/2. Architecture/1. Plugin Architecture.md
new file mode 100644
index 0000000..8975975
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/1. Plugin Architecture.md
@@ -0,0 +1,17 @@
+# Plugin Architecture
+
+Last Update: 25/05/2025
+
+---
+
+The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
+
+1. Introspect
+2. Configure
+3. Forwarding
+
+The overall flow looks like this.
+
+
+
+This design make sure that the Zoraxy plugins do not depends on platform dependent implementations that uses, for example, unix socket. This also avoided protocol that require complex conversion to and from HTTP request (data structure) like gRPC, while making sure the plugin can be cross compile into different CPU architecture or OS environment with little to no effect on its performance.
diff --git a/docs/plugins/docs/2. Architecture/2. Introspect.md b/docs/plugins/docs/2. Architecture/2. Introspect.md
new file mode 100644
index 0000000..16fa8ab
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/2. Introspect.md
@@ -0,0 +1,101 @@
+# Introspect
+
+Last Update: 25/05/2025
+
+---
+
+Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
+
+**This is a pre-defined structure where the plugin must provide to Zoraxy** when the plugin is being started with the `-introspect` flag.
+
+The introspect structure is defined under the `zoraxy_plugin` library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
+
+```go
+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, see Zoraxy documentation for more details
+}
+```
+
+
+
+The introspect provide Zoraxy the required information to start the plugin and how to interact with it. For more details on what those capture settings are for, see "Capture Mode" section.
+
+---
+
+## Introspect Manual Triggering
+
+To manually test if the introspect return is correct, you can try using the `-introspect` flag on any Zoraxy plugin. You should be able to see an output like so.
+
+```json
+$ ./debugger -introspect
+{
+ "id": "org.aroz.zoraxy.debugger",
+ "name": "Plugin Debugger",
+ "author": "aroz.org",
+ "author_contact": "https://aroz.org",
+ "description": "A debugger for Zoraxy \u003c-\u003e plugin communication pipeline",
+ "url": "https://zoraxy.aroz.org",
+ "type": 0,
+ "version_major": 1,
+ "version_minor": 0,
+ "version_patch": 0,
+ "static_capture_paths": [
+ {
+ "capture_path": "/test_a"
+ },
+ {
+ "capture_path": "/test_b"
+ }
+ ],
+ "static_capture_ingress": "/s_capture",
+ "dynamic_capture_sniff": "/d_sniff",
+ "dynamic_capture_ingress": "/d_capture",
+ "ui_path": "/debug",
+ "subscription_path": "",
+ "subscriptions_events": null
+}
+```
+
diff --git a/docs/plugins/docs/2. Architecture/3. Configure.md b/docs/plugins/docs/2. Architecture/3. Configure.md
new file mode 100644
index 0000000..ff88351
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/3. Configure.md
@@ -0,0 +1,18 @@
+# Configure
+
+Configure or Configure Spec is the `exec` call where Zoraxy start the plugin. The configure spec JSON structure is defined in `zoraxy_plugin` library.
+
+As the time of writing, the `ConfigureSpec` only contains information on some basic info.
+
+```go
+type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+ //To be expanded
+}
+
+```
+
+The `ConfigureSpec` struct will be parsed to JSON and pass to your plugin via the `-configure=(json payload here)`.
+
+In your plugin, you can use the `zoraxy_plugin` library to parse it or parse it manually (if you are developing a plugin with other languages).
\ No newline at end of file
diff --git a/docs/plugins/docs/2. Architecture/4. Capture Modes.md b/docs/plugins/docs/2. Architecture/4. Capture Modes.md
new file mode 100644
index 0000000..de110b6
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/4. Capture Modes.md
@@ -0,0 +1,58 @@
+# Capture Modes
+
+As you can see in the Introspect section, there are two types of capture mode in Zoraxy plugin API.
+
+
+
+- Static Capture Mode
+- Dynamic Capture Mode
+
+
+
+**Notes: When this document mention the term "endpoint", it means a particular sub-path on the plugin side. For example `/capture` or `/sniff`. In actual implementation, this can be a `http.HandleFunc` or `http.Handle` depends on the plugin implementation.**
+
+---
+
+## Static Capture Mode
+
+Static Capture Mode register a static path to Zoraxy, when the plugin is enabled on a certain HTTP proxy rule, all request that matches the static capture registered paths are forwarded to the plugin without asking first. The overall process is shown in the diagram below.
+
+
+
+The main benefit of static capture mode is that the capture paths are stored in radix tree. That means it takes O(logn) time to resolve the path and forward the request. Hence, **this mode is generally faster** if your plugin always listens to a few certain paths for extended functionalities.
+
+---
+
+## Dynamic Capture Mode
+
+Dynamic Capture Mode register two endpoints to Zoraxy.
+
+1. DynamicCaptureSniff - The sniffing endpoint where Zoraxy will first ask if the plugin want to handle this request
+2. DynamicCaptureIngress - The handling endpoint, where if the plugin reply the sniffing with "YES", Zoraxy forward the incoming request to this plugin at this defined endpoint.
+
+The whole process will takes a few request exchange between plugin and Zoraxy core. Since both of them are communicating via the loopback interface, speed should not be too big of a concern here.
+
+The request handling flow is shown in the diagram below.
+
+
+
+Once Zoraxy receive a request from a client that matches one of the HTTP Proxy Rule, Zoraxy will forward the request header to all the plugins that matches the following criteria
+
+1. The plugin is assigned to a tag that is currently attached to the given HTTP Proxy that the request is coming through
+2. The plugin is enabled and running
+3. The plugin has registered its dynamic capture sniffing endpoint in Introspect
+
+Then the plugin `/sniff` endpoint will receive some basic header information about the request, and response with `SniffResultAccpet` or `SniffResultSkip` to accept or reject handling such request. The response are defined in `zoraxy_plugin` as a public type where you can access with `zoraxy_plugin.SniffresultAccept` and `zoraxy_plugin.SniffResultSkip` respectively.
+
+Note that this shall only be used if static capture mode cannot satisfy your needs in implementation the feature you want, as **dynamic capture is way slower than static capture mode**.
+
+---
+
+## Mixing Capture Modes
+
+It is possible for you to mix both Static and Capture modes if that is what you want. A few thing you need to know about mixing both mode in single plugin
+
+1. Static capture mode has higher priority than dynamic capture mode across all plugins. That means if you have a request that matches Plugin A's static capture path and Plugin B's dynamic capture, the request will be first handled by Plugin A
+2. The same plugin can register both static and dynamic capture modes. Similar to item (1), if the request has already captured by your static capture path, Zoraxy will not proceed and forward the request header to your dynamic sniffing endpoint.
+3. In case there is a collision in static capture paths between two plugins, the longest one will have priority. For example, if Plugin A registered `/foo` and Plugin B registered `/foo/bar`, when a request to `/foo/bar/teacat` enter Zoraxy, Plugin B is used for handling such request.
+
diff --git a/docs/plugins/docs/2. Architecture/5. Plugin UI.md b/docs/plugins/docs/2. Architecture/5. Plugin UI.md
new file mode 100644
index 0000000..6fe321c
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/5. Plugin UI.md
@@ -0,0 +1,23 @@
+# Plugin UI
+
+Last Update: 25/05/2025
+
+---
+
+A plugin can optionally expose a Web UI interface for user configuration.
+
+**A plugin must provide a UI, as it is part of the control mechanism of the plugin life cycle. (i.e. Zoraxy use the plugin UI HTTP server to communicate with the plugin for control signals)** As plugin installed via plugin store provides limited ways for a user to configure the plugin, the plugin web UI will be the best way for user to setup your plugin.
+
+## Plugin Web UI Access
+
+If a plugin provide a Web UI endpoint for Zoraxy during the introspect process, a new item will be shown in the Plugins section on Zoraxy side menu. Below is an example of the Web UI of UPnP Port Forwarder plugin.
+
+
+
+
+
+## Front-end Developer Notes
+
+The Web UI is implemented as a reverse proxy and embed in an iframe. So you do not need to handle CORS issues with the web UI (as it will be proxy internally by Zoraxy as exposed as something like a virtual directory mounted website).
+
+However, the plugin web UI is exposed via the path `/plugin.ui/{{plugin_uuid}}/`, for example, `/plugin.ui/org.aroz.zoraxy.plugins.upnp/`. **When developing the plugin web UI, do not use absolute path for any resources used in the HTML file**, unless you are trying to re-use Zoraxy components like css or image elements stored in Zoraxy embedded web file system (e.g. `/img/logo.svg`).
\ No newline at end of file
diff --git a/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md b/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md
new file mode 100644
index 0000000..49fbc2c
--- /dev/null
+++ b/docs/plugins/docs/2. Architecture/6. Compile a Plugin.md
@@ -0,0 +1,15 @@
+# Compile a Plugin
+
+A plugin is basically a go program with a HTTP Server / Listener. The steps required to build a plugin is identical as building a ordinary go program.
+
+```bash
+# Assuming you are currently inside the root folder of your plugin
+go mod tidy
+go build
+
+# Validate if the plugin is correctly build using -introspect flag
+./{{your_plugin_name}} -introspect
+
+# You should see your plugin information printed to STDOUT as JSON string
+```
+
diff --git a/docs/plugins/docs/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png b/docs/plugins/docs/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png
new file mode 100644
index 0000000..d49fc06
Binary files /dev/null and b/docs/plugins/docs/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png differ
diff --git a/docs/plugins/docs/2. Architecture/img/4. Capture Modes/dynamic_capture.png b/docs/plugins/docs/2. Architecture/img/4. Capture Modes/dynamic_capture.png
new file mode 100644
index 0000000..0083370
Binary files /dev/null and b/docs/plugins/docs/2. Architecture/img/4. Capture Modes/dynamic_capture.png differ
diff --git a/docs/plugins/docs/2. Architecture/img/4. Capture Modes/static_capture.png b/docs/plugins/docs/2. Architecture/img/4. Capture Modes/static_capture.png
new file mode 100644
index 0000000..0c51c52
Binary files /dev/null and b/docs/plugins/docs/2. Architecture/img/4. Capture Modes/static_capture.png differ
diff --git a/docs/plugins/docs/2. Architecture/img/5. Plugin UI/image-20250527201750613.png b/docs/plugins/docs/2. Architecture/img/5. Plugin UI/image-20250527201750613.png
new file mode 100644
index 0000000..4792a4e
Binary files /dev/null and b/docs/plugins/docs/2. Architecture/img/5. Plugin UI/image-20250527201750613.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/1. Hello World.md b/docs/plugins/docs/3. Basic Examples/1. Hello World.md
new file mode 100644
index 0000000..efe8db9
--- /dev/null
+++ b/docs/plugins/docs/3. Basic Examples/1. Hello World.md
@@ -0,0 +1,329 @@
+# Hello World!
+
+Last Update: 25/05/2025
+
+---
+
+Let start with a really simple Hello World plugin. This only function of this plugin is to print "Hello World" in the plugin web UI.
+
+---
+
+## 1. Name your plugin
+
+First things first, give your plugin a name. In this example, we are using the name "helloworld".
+
+**Plugin name cannot contain space or special characters**, so you must use a file name that satisfies the requirement. Dont worry, the plugin file name is not the same as the plugin display name in the introspect.
+
+---
+
+## 2. Create the plugin folder
+
+ If your zoraxy root folder do not contains a folder named "plugins", it might implies that your Zoraxy is freshly clone from Github. **You will need to build and run it once to start working on your plugin**, so if you have a newly cloned source code of Zoraxy, do the followings.
+
+```bash
+git clone https://github.com/tobychui/zoraxy
+cd src
+go mod tidy
+go build
+sudo ./zoraxy
+```
+
+Afterward, create a plugin folder under your Zoraxy development environment that is exactly matching your plugin name. In the above example, the folder name should be "helloworld".
+
+```bash
+# Assume you are already inside the src/ folder
+mkdir helloworld
+cd ./helloworld
+```
+
+---
+
+## 3. Create a go project
+
+Similar to any Go project, you can start by creating a `main.go` file. Next, you would want to let the go compiler knows your plugin name so when generating a binary file, it knows what to name it. This can be done via using the `go mod init` command.
+
+```bash
+touch main.go
+go mod init example.com/zoraxy/helloworld
+ls
+# After you are done, you should see the followings
+# go.mod main.go
+```
+
+---
+
+## 4. Copy the Zoraxy plugin lib from Zoraxy source code
+
+ Locate the Zoraxy plugin library from the Zoraxy source code. You can find the `zoraxy_plugin` Go module under `src/mod/plugins/zoraxy_plugin`
+
+Copy the `zoraxy_plugin` folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as `mod`, then your copied library path should be `plugins/helloworld/mod/zoraxy_plugin`
+
+```bash
+mkdir ./mod
+cp -r "mod/plugins/zoraxy_plugin" ./mod/
+ls ./mod/zoraxy_plugin/
+# You should see something like this (might be different in future versions)
+# dev_webserver.go dynamic_router.go embed_webserver.go README.txt static_router.go zoraxy_plugin.go
+```
+
+---
+
+## 5. Create a web resources folder
+
+Lets create a www folder and put all our web resources, we need to create an `index.html` file as our plugin web ui homepage. This can be done by creating a HTML file in the www folder.
+
+```bash
+# Assuming you are currently in the src/plugins/helloworld/ folder
+mkdir www
+cd www
+touch index.html
+```
+
+
+
+And here is an example `index.html` file that uses the Zoraxy internal resources like css and dark theme toggle mechanism. That csrf token template is not straightly needed in this example as helloworld plugin do not make any POST request to Zoraxy webmin interface, but it might come in handy later.
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello World
+
+
+
+
+
+
+
+ Hello World
+ Welcome to your first Zoraxy plugin
+
+
+
+```
+
+
+
+---
+
+## 6. Creating a handler for Introspect
+
+To create a handler for introspect, you can first start your plugin with a few constants.
+
+1. Plugin ID, this must be unique. You can use a domain you own like `com.example.helloworld`
+2. UI Path, for now we uses "/" as this plugin do not have any other endpoints, so we can use the whole root just for web UI
+3. Web root, for trimming off from the embedded web folder so when user can visit your `index.html` by accessing `/` instead of needing to navigate to `/www`
+
+
+
+After you have defined these constant, we can use `plugin.ServeAndRecvSpec` function to handle the handshake between Zoraxy and your plugin.
+
+```go
+const (
+ PLUGIN_ID = "com.example.helloworld"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+func main(){
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.helloworld",
+ Name: "Hello World Plugin",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple hello world plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+}
+```
+
+
+
+**Notes: If some post processing is needed between Introspect and Configure, you can use two seperate function to handle the first start and the second starting of your plugin. The "separated version" of `ServeAndRecvSpec` is defined as ` ServeIntroSpect(pluginSpect *IntroSpect) ` and `RecvConfigureSpec() (*ConfigureSpec, error)`. See `zoraxy_plugin.go` for more information.**
+
+---
+
+## 7. Creating a web server from embedded web fs
+
+After that, we need to create a web server to serve our plugin UI to Zoraxy via HTTP. This can be done via the `http.FileServer` but for simplicity and ease of upgrade, the Zoraxy plugin library provided an easy to use embedded web FS server API for plugin developers.
+
+To use the Zoraxy plugin embedded web server, you first need to embed your web fs into Zoraxy as such.
+
+```go
+import (
+ _ "embed"
+ "fmt"
+
+ plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
+)
+
+//go:embed www/*
+var content embed.FS
+```
+
+Then call to the `NewPluginEmbedUIRouter` to create a new UI router from the embedded Fs.
+
+```go
+// Create a new PluginEmbedUIRouter that will serve the UI from web folder
+// The router will also help to handle the termination of the plugin when
+// a user wants to stop the plugin via Zoraxy Web UI
+embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+```
+
+
+
+Here is the tricky part. since not all platform support cross process signaling, Zoraxy plugin uses HTTP request to request a plugin to shutdown. The `embedWebRouter` object has a function named `RegisterTerminateHandler` where you can easily use this function to register actions that needed to be done before shutdown.
+
+```go
+embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Hello World Plugin Exited")
+}, nil)
+```
+
+**Notes: This is a blocking function. That is why Zoraxy has a build-in timeout context where if the terminate request takes more than 3 seconds, the plugin process will be treated as "freezed" and forcefully terminated. So please make sure the terminate handler complete its shutdown procedures within 3 seconds.**
+
+---
+
+## 8. Register & Serve the Web UI
+
+After you have created a embedded web router, you can register it to the UI PATH as follows.
+
+```go
+// Serve the hello world page in the www folder
+http.Handle(UI_PATH, embedWebRouter.Handler())
+fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+if err != nil {
+ panic(err)
+}
+```
+
+
+
+As this is just the standard golang net/http package, you can of course add more Function Handlers to it based on your needs. There are something that you need to know about adding API endpoints, we will discuss this in the later sections.
+
+---
+
+## 9. Build and Test
+
+After saving the `main.go` file, you can now build your plugin with `go build`. It should generate the plugin in your platform architecture and OS. If you are on Linux, it will be `helloworld` and if you are on Windows, it will be `helloworld.exe`.
+
+After you are done, restart Zoraxy and enable your plugin in the Plugin List. Now you can test and debug your plugin with your HTTP Proxy Rules. All the STDOUT and STDERR of your plugin will be forwarded to the STDOUT of Zoraxy as well as the log file.
+
+
+
+
+
+**Tips**
+
+You can also enable the Developer Option - Plugin Auto Reload function if you are too lazy to restart Zoraxy everytime the plugin binary changed.
+
+
+
+
+
+---
+
+## 10. Full Code
+
+This is the full code of the helloworld plugin main.go file.
+
+```go
+package main
+
+import (
+ "embed"
+ _ "embed"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "com.example.helloworld"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+//go:embed www/*
+var content embed.FS
+
+func main() {
+ // Serve the plugin intro spect
+ // This will print the plugin intro spect and exit if the -introspect flag is provided
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.helloworld",
+ Name: "Hello World Plugin",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple hello world plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+ // The router will also help to handle the termination of the plugin when
+ // a user wants to stop the plugin via Zoraxy Web UI
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Hello World Plugin Exited")
+ }, nil)
+
+ // Serve the hello world page in the www folder
+ http.Handle(UI_PATH, embedWebRouter.Handler())
+ fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+ if err != nil {
+ panic(err)
+ }
+
+}
+
+```
+
diff --git a/docs/plugins/docs/3. Basic Examples/2. RESTful Example.md b/docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
new file mode 100644
index 0000000..5a2a5c8
--- /dev/null
+++ b/docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
@@ -0,0 +1,493 @@
+# RESTful API Call in Web UI
+Last Update: 29/05/2025
+
+---
+
+When developing a UI for your plugin, sometime you might need to make RESTFUL API calls to your plugin backend for setting up something or getting latest information from your plugin. In this example, I will show you how to create a plugin with RESTful api call capabilities with the embedded web server and the custom `cjax` function.
+
+**Notes: This example assumes you have basic understanding on how to use jQuery `ajax` request.**
+
+Lets get started!
+
+---
+
+## 1. Create the plugin folder structures
+
+This step is identical to the Hello World example, where you create a plugin folder with the required go project structure in the folder. Please refer to the Hello World example section 1 to 5 for details.
+
+---
+
+## 2. Create Introspect
+
+This is quite similar to the Hello World example as well, but we are changing some of the IDs to match what we want to do in this plugin.
+
+```go
+runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.restful-example",
+ Name: "Restful Example",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple demo for making RESTful API calls in plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+```
+
+---
+
+## 3. Create an embedded web server with handlers
+
+In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a `http.HandleFunc` to the plugin so our front-end can request and communicate with the backend.
+
+```go
+embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+embedWebRouter.RegisterTerminateHandler(func() {
+ fmt.Println("Restful-example Exited")
+}, nil)
+
+//Register a simple API endpoint that will echo the request body
+// Since we are using the default http.ServeMux, we can register the handler directly with the last
+// parameter as nil
+embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ //Some handler code here
+}, nil)
+```
+
+The `embedWebRouter.HandleFunc` last parameter is the `http.Mux`, where if you have multiple web server listening interface, you can fill in different Mux based on your implementation. On of the examples is that, when you are developing a static web server plugin, where you need a dedicated HTTP listening endpoint that is not the one Zoraxy assigned to your plugin, you need to create two http.Mux and assign one of them for Zoraxy plugin UI purpose.
+
+---
+
+## 4. Modify the front-end HTML file to make request to backend
+
+To make a RESTFUL API to your plugin, **you must use relative path in your request URL**.
+
+Absolute path that start with `/` is only use for accessing Zoraxy resouces. For example, when you access `/img/logo.svg`, Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of `/plugins/your_plugin_name/{your_web_root}/img/logo.svg`.
+
+### Making GET request
+
+Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like `./api/foo/bar` instead. Here is an example on a front-end and back-end implementation of a simple "echo" API.
+
+The API logic is simple: when you make a GET request to the API with `?name=foobar`, it returns `Hello foobar`. Here is the backend implementation in your plugin code.
+
+```go
+embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ // This is a simple echo API that will return the request body as response
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+ }, nil)
+```
+
+And here is the front-end code in your HTML file
+
+```html
+
+Echo Test (HTTP GET)
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### Making POST request
+
+Making POST request is also similar to GET request, except when making the request, you will need pass the CSRF-Token with the payload. This is required due to security reasons (See [#267](https://github.com/tobychui/zoraxy/issues/267) for more details).
+
+Since the CSRF validation is done by Zoraxy, your plugin backend code can be implemented just like an ordinary handler. Here is an example POST handling function that receive a FORM POST and print it in an HTML response.
+
+```go
+embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ for key, values := range r.PostForm {
+ for _, value := range values {
+ // Generate a simple HTML response
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, "%s: %s
", key, value)
+ }
+ }
+}, nil)
+
+```
+
+For the front-end, you will need to use the `$.cjax` function implemented in Zoraxy `utils.js` file. You can include this file by adding these two lines to your HTML file.
+
+```html
+
+
+
+
+Form Post Test (HTTP POST)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+After that, you can call to the `$.cjax` function just like what you would usually do with the `$ajax` function.
+
+```javascript
+// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
+// alternatively you can use $.ajax with CSRF token in headers
+// the header is named "X-CSRF-Token" and the value is taken from the head
+// meta tag content (i.e. )
+$.cjax({
+ url: './api/post',
+ type: 'POST',
+ data: { name: name, age: age, gender: gender },
+ success: function(data) {
+ console.log('Response:', data);
+ $('#postResponseMessage').html(data).show();
+ },
+ error: function(xhr, status, error) {
+ console.error('Error:', error);
+ $('#postResponseMessage').text('An error occurred while processing your request.').show();
+ }
+});
+```
+
+### POST Request with Vanilla JS
+
+It is possible to make POST request with Vanilla JS. Note that you will need to populate the csrf-token field yourself to make the request pass through the plugin UI request router in Zoraxy. Here is a basic example on how it could be done.
+
+```javascript
+fetch('./api/post', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': csrfToken // Include the CSRF token in the headers
+ },
+ body: JSON.stringify({{your_data_here}})
+})
+```
+
+---
+
+## 5. Full Code
+
+Here is the full code of the RESTFUL example for reference.
+
+Front-end (`plugins/restful-example/www/index.html`)
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+ RESTful Example
+
+
+
+
+
+
+
+
+
+ RESTFul API Example
+
+ Echo Test (HTTP GET)
+
+
+
+
+
+
+
+
+
+
+
+ Form Post Test (HTTP POST)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+Backend (`plugins/restful-example/main.go`)
+
+```go
+package main
+
+import (
+ "embed"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "com.example.restful-example"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+//go:embed www/*
+var content embed.FS
+
+func main() {
+ // Serve the plugin intro spect
+ // This will print the plugin intro spect and exit if the -introspect flag is provided
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.restful-example",
+ Name: "Restful Example",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple demo for making RESTful API calls in plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+ // The router will also help to handle the termination of the plugin when
+ // a user wants to stop the plugin via Zoraxy Web UI
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Restful-example Exited")
+ }, nil)
+
+ //Register a simple API endpoint that will echo the request body
+ // Since we are using the default http.ServeMux, we can register the handler directly with the last
+ // parameter as nil
+ embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ // This is a simple echo API that will return the request body as response
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+ }, nil)
+
+ // Here is another example of a POST API endpoint that will echo the form data
+ // This will handle POST requests to /api/post and return the form data as response
+ embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ for key, values := range r.PostForm {
+ for _, value := range values {
+ // Generate a simple HTML response
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, "%s: %s
", key, value)
+ }
+ }
+ }, nil)
+
+ // Serve the restful-example page in the www folder
+ http.Handle(UI_PATH, embedWebRouter.Handler())
+ fmt.Println("Restful-example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+ if err != nil {
+ panic(err)
+ }
+
+}
+
+```
+
+
+
+What you should expect to see if everything is correctly loaded and working in Zoray
+
+
diff --git a/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md b/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
new file mode 100644
index 0000000..46c4d45
--- /dev/null
+++ b/docs/plugins/docs/3. Basic Examples/3. Static Capture Example.md
@@ -0,0 +1,263 @@
+# Static Capture Example
+Last Update: 29/05/2025
+
+---
+
+This example demonstrates how to use static capture in Zoraxy plugins. Static capture allows you to define specific paths that will be intercepted by your plugin, enabling custom handling of requests to those paths.
+
+**Notes: This example assumes you have already read Hello World example.**
+
+---
+
+## 1. Create the plugin folder structure
+
+Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
+
+---
+
+## 2. Define Introspect
+
+The introspect configuration specifies the static capture paths and ingress for your plugin.
+
+```go
+runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.static-capture-example",
+ Name: "Static Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "An example for showing how static capture works in Zoraxy.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ StaticCapturePaths: []plugin.StaticCaptureRule{
+ { CapturePath: "/test_a" },
+ { CapturePath: "/test_b" },
+ },
+ StaticCaptureIngress: "/s_capture",
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+```
+
+Note the `StaticCapturePaths`. These are the paths that you want to capture in your plugin. These paths will be registered to Zoraxy and when a user have request that matches these paths (including subpaths), the request will get forwarded to your plugin. In this example, we are intercepting the `/test_a` and `test_b` sub-path.
+
+We also defined a new value named `StaticCaptureIngress`. This is to tell Zoraxy that "if you receive requests that matches the above Static capture paths, please forward the request to this endpoint". In this example, this plugin asked Zoraxy to forward th HTTP traffic to `/s_capture` if anything is matched.
+
+
+
+---
+
+## 3. Register Static Capture Handlers
+
+Static capture handlers are used to process requests to the defined paths. Similar to ordinary http.HandleFunc, you can register `http.HandleFunc` as follows.
+
+```go
+pathRouter := plugin.NewPathRouter()
+
+pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
+pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
+
+pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String()))
+}))
+
+pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
+```
+
+
+
+The `SetDefaultHandler` is used to handle exceptions where a request is forwarded to your plugin but it cannot be handled by any of your registered path handlers. This is usually an implementation bug on the plugin side and you can add some help message or debug log to this function if needed.
+
+The `RegisterStaticCaptureHandle` is used to register the static capture ingress endpoint, so Zoraxy knows where to forward the HTTP request when it thinks your plugin shall be the one handling the request. In this example, `/s_capture` is used for static capture endpoint.
+
+---
+
+## 4. Implement Handlers
+
+Here are examples of handlers for the captured paths:
+
+### Handler for `/test_a`
+
+```go
+func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String()))
+}
+```
+
+### Handler for `/test_b`
+
+```go
+func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String()))
+}
+```
+
+
+
+When the user request any HTTP Proxy Rule with the matching path, these two handlers will response to the request and return the hardcoded string above. Again, this is just for demonstration purpose and you should implement your functions here.
+
+
+
+---
+
+## 5. Render Debug UI
+
+The debug UI provides a simple interface for testing and inspecting requests.
+
+```go
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+```
+
+This is technically not related to static capturing, but it is really helpful to have a UI to help with printing debug information. You can access the page rendered by this function in the Zoraxy plugin menu. This should be replaced with the embedded web fs used in the Hello world example after the development is completed.
+
+
+
+
+
+## 6. Full Code
+
+Here is the complete code for the static capture example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+
+ plugin "example.com/zoraxy/static-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.static-capture-example"
+ UI_PATH = "/ui"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+func main() {
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: PLUGIN_ID,
+ Name: "Static Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "An example for showing how static capture works in Zoraxy.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+ StaticCapturePaths: []plugin.StaticCaptureRule{
+ { CapturePath: "/test_a" },
+ { CapturePath: "/test_b" },
+ },
+ StaticCaptureIngress: STATIC_CAPTURE_INGRESS,
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
+ pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
+ pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String()))
+ }))
+ pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Static path capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String()))
+}
+
+func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String()))
+}
+
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+```
+
+---
+
+## 7. Expected Output
+
+To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the "debug" tag and assign it to a localhost loopback HTTP proxy rule.
+
+
+
+
+
+
+
+
+
+When the plugin is running, requests to `/test_a` and `/test_b` will be intercepted by their respective handlers. **Requests to other paths will not pass through your plugin and will be handled by the default upstream server set by the HTTP proxy Rule.**
+
+
+
+
+
+Example terminal output for requesting `/test_a`:
+
+```
+This request is captured by A handler!
+Request URI: /test_a
+```
+
+Example output for requesting `/test_b`:
+```
+This request is captured by the B handler!
+Request URI: /test_b
+```
+
+---
+
+Enjoy exploring static capture in Zoraxy!
+
diff --git a/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md b/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md
new file mode 100644
index 0000000..329e72c
--- /dev/null
+++ b/docs/plugins/docs/3. Basic Examples/4. Dynamic Capture Example.md
@@ -0,0 +1,352 @@
+# Dynamic Capture Example
+Last Update: 29/05/2025
+
+---
+
+
+This example demonstrates how to use dynamic capture in Zoraxy plugins. Dynamic capture allows you to intercept requests based on real-time conditions, so you can program your plugin in a way that it can decided if it want to handle the request or not.
+
+**Notes: This example assumes you have already read Hello World and Stataic Capture Example.**
+
+Lets dive in!
+
+---
+
+## 1. Create the plugin folder structure
+
+Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
+
+---
+
+## 2. Define Introspect
+
+The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin.
+
+```go
+runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+```
+
+Note the `DynamicCaptureSniff` and `DynamicCaptureIngress`. These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests.
+
+---
+
+## 3. Register Dynamic Capture Handlers
+
+Dynamic capture handlers are used to process requests that match specific conditions.
+
+```go
+pathRouter := plugin.NewPathRouter()
+pathRouter.SetDebugPrintMode(true)
+
+pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ fmt.Println("Accepting request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultAccpet
+ }
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+})
+
+pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+})
+```
+
+The `RegisterDynamicSniffHandler` evaluates incoming requests, while the `RegisterDynamicCaptureHandle` processes the intercepted requests.
+
+### Sniffing Logic
+
+If a module registered a dynamic capture path, Zoraxy will forward the request headers as `DynamicSniffForwardRequest` (`dsfr`) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and "think" if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
+
+```go
+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"`
+}
+```
+
+You can also use the `GetRequest()` function to get the `*http.Request` object or `GetRequestUUID()` to get a `string` value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
+
+**Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled.**
+
+In the sniffing stage, you can choose to either return `ControlStatusCode_CAPTURED`, where Zoraxy will forward the request to your plugin `DynamicCaptureIngress` endpoint, or `ControlStatusCode_UNHANDLED`, where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server.
+
+### Capture Handling
+
+The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the ` http.Request` by writing to the `http.ResponseWriter`.
+
+If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the `X-Zoraxy-Requestid` header in the HTTP request. This is the same UUID as the one you get from `dsfr.GetRequestUUID()` in the sniffing stage if they are the same request object on Zoraxy side.
+
+The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
+
+```html
+Request URI: /foobar/test
+Request Method: GET
+Request Headers:
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Encoding: gzip, deflate, br, zstd
+(more fileds)
+X-Forwarded-For: 127.0.0.1
+X-Forwarded-Proto: https
+X-Real-Ip: 127.0.0.1
+X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
+```
+
+You can extract the `X-Zoraxy-Requestid` value from the request header and do your matching for implementing your function if needed.
+
+---
+
+## 4. Render Debug UI
+
+This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here.
+
+```go
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+```
+
+
+
+---
+
+## 5. Full Code
+
+Here is the complete code for the dynamic capture example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+
+ plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
+ UI_PATH = "/debug"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+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: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Dynamic Captures
+ */
+ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ //In this example, we want to capture all URI
+ //that start with /foobar and forward it to the dynamic capture handler
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ reqUUID := dsfr.GetRequestUUID()
+ fmt.Println("Accepting request with UUID: " + reqUUID)
+
+ // Print all the values of the request
+ fmt.Println("Method:", dsfr.Method)
+ fmt.Println("Hostname:", dsfr.Hostname)
+ fmt.Println("URL:", dsfr.URL)
+ fmt.Println("Header:")
+ for key, values := range dsfr.Header {
+ for _, value := range values {
+ fmt.Printf(" %s: %s\n", key, value)
+ }
+ }
+ fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
+ fmt.Println("Host:", dsfr.Host)
+ fmt.Println("RequestURI:", dsfr.RequestURI)
+ fmt.Println("Proto:", dsfr.Proto)
+ fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
+ fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
+
+ // We want to handle this request, reply with aSniffResultAccept
+ return plugin.SniffResultAccpet
+ }
+
+ // If the request URI does not match, we skip this request
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+ })
+ pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ // This is the dynamic capture handler where it actually captures and handle the request
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!"))
+
+ // Print all the request info to the response writer
+ w.Write([]byte("\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+ })
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+
+```
+
+---
+
+## 6. Expected Output
+
+To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the "debug" tag and assigning it to a localhost loopback HTTP proxy rule.
+
+When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
+
+If everything is correctly setup, you should see the following page when requesting any URL with prefix `(your_HTTP_proxy_rule_hostname)/foobar`
+
+
+
+
+
+Example terminal output for requesting `/foobar/*`:
+
+```
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
+[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: "Windows"
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
+```
+
+---
+
+Now you know how to develop a plugin in Zoraxy that handles special routings!
\ No newline at end of file
diff --git a/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250527210849767.png b/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250527210849767.png
new file mode 100644
index 0000000..0928b98
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250527210849767.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250530134148399.png b/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250530134148399.png
new file mode 100644
index 0000000..4639a45
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/1. Hello World/image-20250530134148399.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png b/docs/plugins/docs/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png
new file mode 100644
index 0000000..37bddf3
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png
new file mode 100644
index 0000000..dd754e9
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png
new file mode 100644
index 0000000..4134d31
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png
new file mode 100644
index 0000000..2931a3f
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png
new file mode 100644
index 0000000..2212937
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png differ
diff --git a/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png b/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png
new file mode 100644
index 0000000..3c264f2
Binary files /dev/null and b/docs/plugins/docs/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png differ
diff --git a/docs/plugins/docs/index.md b/docs/plugins/docs/index.md
new file mode 100644
index 0000000..76d0d14
--- /dev/null
+++ b/docs/plugins/docs/index.md
@@ -0,0 +1,27 @@
+
+
+
+
+# Index
+
+Welcome to the Zoraxy Plugin Documentation!
+Click on a topic in the side menu to begin navigating through the available resources and guides for developing and managing plugins.
+
+## FAQ
+### What skills do I need for developing a plugin?
+Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
+
+### Will a plugin crash the whole Zoraxy?
+No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
+
+### Can I sell my plugin?
+Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
+
+### How can I add my plugin to the official plugin store?
+To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
+
+## GNU Free Documentation License
+
+This documentation is licensed under the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. You may copy, distribute, and modify this document under the terms of the license.
+
+A copy of the license is available at [https://www.gnu.org/licenses/fdl-1.3.html](https://www.gnu.org/licenses/fdl-1.3.html).
\ No newline at end of file
diff --git a/docs/plugins/docs/zoraxy_plugin API.md b/docs/plugins/docs/zoraxy_plugin API.md
new file mode 100644
index 0000000..9206f75
--- /dev/null
+++ b/docs/plugins/docs/zoraxy_plugin API.md
@@ -0,0 +1,257 @@
+# Zoraxy Plugin APIs
+This API documentation is auto-generated from the Zoraxy plugin source code.
+
+
+
+package zoraxy_plugin // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
+
+
+FUNCTIONS
+
+func ServeIntroSpect(pluginSpect *IntroSpect)
+ 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
+
+
+TYPES
+
+type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+
+}
+ 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
+
+func RecvConfigureSpec() (*ConfigureSpec, error)
+ 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 ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error)
+ ServeAndRecvSpec Function
+
+ This function will serve the intro spect and return the configure spec See
+ the ServeIntroSpect and RecvConfigureSpec for more details
+
+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 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"`
+
+ // Has unexported fields.
+}
+ 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
+
+func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error)
+ DecodeForwardRequestPayload decodes JSON bytes into a
+ DynamicSniffForwardRequest object
+
+func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest
+ GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an
+ http.Request object
+
+func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request
+ GetRequest returns the original http.Request object, for debugging purposes
+
+func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string
+ GetRequestUUID returns the request UUID if this UUID is empty string,
+ that might indicate the request is not coming from the dynamic router
+
+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
+
+ // 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, see Zoraxy documentation for more details
+}
+ IntroSpect Payload
+
+ When the plugin is initialized with -introspect flag, the plugin shell
+ return this payload as JSON and exit
+
+type PathRouter struct {
+ // Has unexported fields.
+}
+
+func NewPathRouter() *PathRouter
+ NewPathRouter creates a new PathRouter
+
+func (p *PathRouter) PrintRequestDebugMessage(r *http.Request)
+
+func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request))
+ RegisterDynamicCaptureHandle register the dynamic capture ingress path with
+ a handler
+
+func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler)
+ 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) RegisterPathHandler(path string, handler http.Handler)
+ RegisterPathHandler registers a handler for a path
+
+func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux)
+ StartStaticCapture starts the static capture ingress
+
+func (p *PathRouter) RemovePathHandler(path string)
+ RemovePathHandler removes a handler for a path
+
+func (p *PathRouter) SetDebugPrintMode(enable bool)
+ SetDebugPrintMode sets the debug print mode
+
+func (p *PathRouter) SetDefaultHandler(handler http.Handler)
+ SetDefaultHandler sets the default handler for the router This handler will
+ be called if no path handler is found
+
+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 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
+ // Has unexported fields.
+}
+
+func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter
+ 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 (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux)
+ Attach the file system UI handler to the target http.ServeMux
+
+func (p *PluginUiDebugRouter) Handler() http.Handler
+ GetHttpHandler returns the http.Handler for the PluginUiRouter
+
+func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
+ 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
+
+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
+ // Has unexported fields.
+}
+
+func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter
+ 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 (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux)
+ Attach the embed UI handler to the target http.ServeMux
+
+func (p *PluginUiRouter) Handler() http.Handler
+ GetHttpHandler returns the http.Handler for the PluginUiRouter
+
+func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
+ 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
+
+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 SniffHandler func(*DynamicSniffForwardRequest) SniffResult
+
+type SniffResult int
+
+const (
+ SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
+ SniffResultSkip // Skip this plugin and let the next plugin handle the request
+)
+type StaticCaptureRule struct {
+ CapturePath string `json:"capture_path"`
+}
+
+type SubscriptionEvent struct {
+ EventName string `json:"event_name"`
+ EventSource string `json:"event_source"`
+ Payload string `json:"payload"` //Payload of the event, can be empty
+}
+
+
diff --git a/docs/plugins/examples.html b/docs/plugins/examples.html
new file mode 100644
index 0000000..daf364c
--- /dev/null
+++ b/docs/plugins/examples.html
@@ -0,0 +1,6 @@
+
+
+ Examples
+ Test
+
+
\ No newline at end of file
diff --git a/docs/plugins/favicon.png b/docs/plugins/favicon.png
new file mode 100644
index 0000000..03345e0
Binary files /dev/null and b/docs/plugins/favicon.png differ
diff --git a/docs/plugins/gen_zoraxy_plugin_doc.sh b/docs/plugins/gen_zoraxy_plugin_doc.sh
new file mode 100644
index 0000000..9e74444
--- /dev/null
+++ b/docs/plugins/gen_zoraxy_plugin_doc.sh
@@ -0,0 +1,23 @@
+#/bin/bash
+
+# Cd into zoraxy plugin directory
+cd ../../src/mod/plugins/zoraxy_plugin/
+
+
+# Add header to the documentation
+echo "# Zoraxy Plugin APIs" > docs.md
+echo "This API documentation is auto-generated from the Zoraxy plugin source code." >> docs.md
+echo "" >> docs.md
+echo "" >> docs.md
+echo "" >> docs.md
+go doc -all >> docs.md
+echo "
" >> docs.md
+
+# Replace // import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" with
+# // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
+sed -i 's|// import "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"|// import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"|g' docs.md
+
+# Move the generated docs to the plugins/html directory
+mv docs.md "../../../../docs/plugins/docs/zoraxy_plugin API.md"
+
+echo "Done generating Zoraxy plugin documentation."
diff --git a/docs/plugins/go.mod b/docs/plugins/go.mod
new file mode 100644
index 0000000..1e6edf7
--- /dev/null
+++ b/docs/plugins/go.mod
@@ -0,0 +1,16 @@
+module imuslab.com/zoraxy/docs
+
+go 1.24.1
+
+require (
+ github.com/PuerkitoBio/goquery v1.10.3
+ github.com/fsnotify/fsnotify v1.9.0
+ github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
+ github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4
+)
+
+require (
+ github.com/andybalholm/cascadia v1.3.3 // indirect
+ golang.org/x/net v0.40.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+)
diff --git a/docs/plugins/go.sum b/docs/plugins/go.sum
new file mode 100644
index 0000000..fef935a
--- /dev/null
+++ b/docs/plugins/go.sum
@@ -0,0 +1,79 @@
+github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
+github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
+github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html b/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
new file mode 100644
index 0000000..a59b780
--- /dev/null
+++ b/docs/plugins/html/1. Introduction/1. What is Zoraxy Plugin.html
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+ What is Zoraxy Plugin | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ What is Zoraxy Plugin?
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system. It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance. The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
+
+
+
+
+ Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not
+
+ as the plugin library and interface are open source under the LGPL license
+
+ .
+
+
+
+
+ There are two primary types of plugins:
+
+
+
+
+
+ Router plugins
+
+ : Involved with connections from HTTP proxy rules.
+
+
+
+
+ Utility plugins
+
+ : Provide user interfaces for various network features that operate independently of the Zoraxy core.
+
+
+
+
+
+ How plugins are distributed & installed
+
+
+
+ Zoraxy plugins are distributed as platform-dependent binaries, tailored to specific operating systems and CPU architectures. These binaries follow a naming convention that includes the operating system, CPU architecture, and plugin name, such as
+
+ linux_amd64_foobar
+
+ ,
+
+ windows_amd64_foobar.exe
+
+ , or
+
+ linux_arm64_foobar
+
+ .
+
+
+
+
+ To manually install a plugin for testing, place the binary file into the
+
+ /plugins/{plugin_name}/
+
+ folder within your Zoraxy installation directory.
+
+
+
+
+
+
+ Warning:
+
+ The binary name inside the folder must match the plugin folder name. For example, the binary should be named
+
+ foobar
+
+ (or
+
+ foobar.exe
+
+ on Windows) if placed in the
+
+ /plugins/foobar/
+
+ folder. Avoid using names like
+
+ foobar_plugin.exe
+
+ .
+
+
+
+
+
+ For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE, with a manager URL (a JSON file) listing all the plugins supported by that store. See the documentation section for more details on how to implement your own plugin store.
+
+
+
+
+ Plugin vs Pull Request
+
+
+
+ The Zoraxy plugin was introduced to address specific use cases that enhance its functionality. It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
+
+
+
+
+ Designed to handle features that are challenging to integrate directly into the Zoraxy core.
+
+
+
+ Caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
+
+
+
+ Allows for frequent updates to specific code components without impacting the core’s stability or causing downtime.
+
+
+
+
+
+ When should you add a core PR or a plugin?
+
+
+
+ In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
+
+
+
+
+
+ Core PR
+
+ : If the feature is relevant to most users and enhances Zoraxy’s core functionality, consider submitting a core Pull Request (PR).
+
+
+
+
+ Plugin
+
+ : If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
+
+
+
+
+ The decision depends on the feature’s general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/1. Introduction/2. Getting Started.html b/docs/plugins/html/1. Introduction/2. Getting Started.html
new file mode 100644
index 0000000..55832a9
--- /dev/null
+++ b/docs/plugins/html/1. Introduction/2. Getting Started.html
@@ -0,0 +1,347 @@
+
+
+
+
+
+
+
+ Getting Started | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Getting Started
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ To start developing plugins, you will need the following installed on your computer
+
+
+
+
+ The source code of Zoraxy
+
+
+ Go compiler
+
+
+ VSCode (recommended, or any editor of your choice)
+
+
+
+
+ Step 1: Start Zoraxy at least once
+
+
+
+ If you have just cloned Zoraxy from the Github repo, use the following to build and run it once.
+
+
+ cd src/
+go mod tidy
+go build
+sudo ./zoraxy
+
+
+
+ This would allow Zoraxy to generate all the required folder structure on startup.
+
+
+
+ After the startup process completes, you would see a folder named “plugins” in the working directory of Zoraxy.
+
+
+
+ Steps 2: Prepare the development environment for Zoraxy Plugin
+
+
+ Next, you will need to think of a name for your plugin. Lets name our new plugin “Lapwing”.
+
+
+
+
+ Notes: Plugin name described in Introspect (will discuss this in later sessions) can contains space, but the folder and compiled binary filename must not contains space and special characters for platform compatibilities reasons.
+
+
+
+
+
+ Follow the steps below to create the folder structure
+
+
+
+ 2.1 Create Plugin Folder
+
+
+
+ Create a folder with your plugin name in the
+
+ plugins
+
+ folder. After creating the folder, you would have something like
+
+ plugins/Lapwing/
+
+ .
+
+
+
+ 2.2 Locate and copy Zoraxy Plugin library
+
+
+
+ Locate the Zoraxy plugin library from the Zoraxy source code. You can find the
+
+ zoraxy_plugin
+
+ Go module under
+
+ src/mod/plugins/zoraxy_plugin
+
+ .
+
+
+
+ Copy the
+
+ zoraxy_plugin
+
+ folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as
+
+ mod
+
+ , then your copied library path should be
+
+ plugins/Lapwing/mod/zoraxy_plugin
+
+ .
+
+
+ 2.3 Prepare Go Project structure
+
+
+
+ Create the
+
+ main.go
+
+ file for your plugin. In the example above, it would be located at
+
+ plugins/Lapwing/main.go
+
+ .
+
+
+
+
+ Use
+
+ go mod init yourdomain.com/foo/plugin_name
+
+ to initiate your plugin. By default the
+
+ go.mod
+
+ file will be automatically generated by the go compiler. Assuming you are developing Lapwing with its source located on Github, this command would be
+
+ go mod init github.com/your_user_name/Lapwing
+
+ .
+
+
+
+
+ Steps 3: Open plugin folder in IDE
+
+
+
+ Now open your preferred IDE or text editor and use your plugin folder as the project folder
+
+
+
+
+ Now, you are ready to start developing Zoraxy plugin!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/1. Introduction/3. Installing Plugin.html b/docs/plugins/html/1. Introduction/3. Installing Plugin.html
new file mode 100644
index 0000000..76228c7
--- /dev/null
+++ b/docs/plugins/html/1. Introduction/3. Installing Plugin.html
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+ Installing Plugin | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Installing Plugin
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+ Install via Plugin Store
+
+
+
+ (Work in progress)
+
+
+
+ Manual Install
+
+
+
+ The plugin shall be placed inside the
+
+ plugins/{{plugin_name}}/
+
+ directory where the binary executable name must be matching with the plugin name.
+
+
+
+
+ If you are on Linux, also make sure Zoraxy have the execution permission of the plugin. You can use the following command to enable execution of the plugin binary on Linux with the current user (Assume Zoraxy is run by the current user)
+
+
+ cd ./plugins/{{plugin_name}}/
+chmod +x ./{{plugin_name}}
+
+
+ Sometime plugins might come with additional assets other than the binary file. If that is the case, extract all of the plugins content into the folder with the plugin’s name.
+
+
+
+ After the folder structure is ready, restart Zoraxy. If you are using systemd for Zoraxy, use
+
+ sudo systemctl restart zoraxy
+
+ to restart Zoraxy via systemd service.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/1. Introduction/4. Enable Plugins.html b/docs/plugins/html/1. Introduction/4. Enable Plugins.html
new file mode 100644
index 0000000..67aad94
--- /dev/null
+++ b/docs/plugins/html/1. Introduction/4. Enable Plugins.html
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+ Enable Plugins | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Enable Plugins
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ To enable and assign a plugin to a certain HTTP Proxy Rule, you will need to do the following steps
+
+
+
+ 1. Create a tag for your HTTP Proxy Rules
+
+
+ Let say you want to enable debugger on some of your HTTP Proxy Rules. You can do that by first creating a tag in the tag editor. In the example below, we will be using the tag “debug”. After adding the tag to the HTTP Proxy rule, you will see something like this.
+
+
+
+
+
+
+
+
+ 2. Enable Plugin
+
+
+ Click on the “Enable” button on the plugin which you want to enable
+
+
+
+
+
+
+
+
+ 3. Assign Plugin to HTTP Proxy Rule
+
+
+
+ Finally, select the tag that you just created in the dropdown menu
+
+
+
+
+
+
+
+
+
+ Afterward, you will see the plugin is attached to the target tag
+
+
+
+
+
+
+
+
+
+ It means the plugin is enabled on the HTTP proxy rule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html b/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
new file mode 100644
index 0000000..84cc2f1
--- /dev/null
+++ b/docs/plugins/html/1. Introduction/5. Viewing Plugin Info.html
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+ Viewing Plugin Info | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Viewing Plugin Info
+
+
+
+ To view plugin information, you can click on the (i) icon in the plugin list.
+
+
+
+
+
+
+
+
+
+ Next, a side menu will pop up from the side. Here ,you can see the current Plugin information and runtime values including Working directories and runtime assigned port.
+
+
+
+ If you are a developer (which you probably is considering you are reading this doc), you can click on the “developer insight” dropdown to show the capture paths registered by this plugin for debug purposes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png
new file mode 100644
index 0000000..d32e67b
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193601017.png differ
diff --git a/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png
new file mode 100644
index 0000000..40e248d
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527193748456.png differ
diff --git a/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png
new file mode 100644
index 0000000..4336e78
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527194052408.png differ
diff --git a/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png
new file mode 100644
index 0000000..7dac9fa
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/3. Enable Plugins/image-20250527195703464.png differ
diff --git a/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png b/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png
new file mode 100644
index 0000000..cac42bb
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171724441.png differ
diff --git a/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png b/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png
new file mode 100644
index 0000000..c63c5e4
Binary files /dev/null and b/docs/plugins/html/1. Introduction/img/5. Viewing Plugin Info/image-20250530171732607.png differ
diff --git a/docs/plugins/html/2. Architecture/1. Plugin Architecture.html b/docs/plugins/html/2. Architecture/1. Plugin Architecture.html
new file mode 100644
index 0000000..ff12afd
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/1. Plugin Architecture.html
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+ Plugin Architecture | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin Architecture
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ The Zoraxy Plugin uses a 3 steps approach to get information from plugin, setup the plugin and forward request to plugin. The name of the steps are partially referred from dbus designs as followings.
+
+
+
+
+ Introspect
+
+
+ Configure
+
+
+ Forwarding
+
+
+
+
+ The overall flow looks like this.
+
+
+
+
+
+
+
+
+
+ This design make sure that the Zoraxy plugins do not depends on platform dependent implementations that uses, for example, unix socket. This also avoided protocol that require complex conversion to and from HTTP request (data structure) like gRPC, while making sure the plugin can be cross compile into different CPU architecture or OS environment with little to no effect on its performance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/2. Introspect.html b/docs/plugins/html/2. Architecture/2. Introspect.html
new file mode 100644
index 0000000..513ca9f
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/2. Introspect.html
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+ Introspect | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Introspect
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ Introspect, similar to the one in dbus design, is used to get the information from plugin when Zoraxy starts (or manually triggered in development mode or force reload plugin list).
+
+
+
+
+
+ This is a pre-defined structure where the plugin must provide to Zoraxy
+
+ when the plugin is being started with the
+
+ -introspect
+
+ flag.
+
+
+
+
+ The introspect structure is defined under the
+
+ zoraxy_plugin
+
+ library, where both Zoraxy and plugin should use. As of writing, the structure of introspect is like this.
+
+
+ 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, see Zoraxy documentation for more details
+}
+
+
+ The introspect provide Zoraxy the required information to start the plugin and how to interact with it. For more details on what those capture settings are for, see “Capture Mode” section.
+
+
+
+ Introspect Manual Triggering
+
+
+
+ To manually test if the introspect return is correct, you can try using the
+
+ -introspect
+
+ flag on any Zoraxy plugin. You should be able to see an output like so.
+
+
+ $ ./debugger -introspect
+{
+ "id": "org.aroz.zoraxy.debugger",
+ "name": "Plugin Debugger",
+ "author": "aroz.org",
+ "author_contact": "https://aroz.org",
+ "description": "A debugger for Zoraxy \u003c-\u003e plugin communication pipeline",
+ "url": "https://zoraxy.aroz.org",
+ "type": 0,
+ "version_major": 1,
+ "version_minor": 0,
+ "version_patch": 0,
+ "static_capture_paths": [
+ {
+ "capture_path": "/test_a"
+ },
+ {
+ "capture_path": "/test_b"
+ }
+ ],
+ "static_capture_ingress": "/s_capture",
+ "dynamic_capture_sniff": "/d_sniff",
+ "dynamic_capture_ingress": "/d_capture",
+ "ui_path": "/debug",
+ "subscription_path": "",
+ "subscriptions_events": null
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/3. Configure.html b/docs/plugins/html/2. Architecture/3. Configure.html
new file mode 100644
index 0000000..d069dec
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/3. Configure.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+ Configure | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Configure
+
+
+
+ Configure or Configure Spec is the
+
+ exec
+
+ call where Zoraxy start the plugin. The configure spec JSON structure is defined in
+
+ zoraxy_plugin
+
+ library.
+
+
+
+
+ As the time of writing, the
+
+ ConfigureSpec
+
+ only contains information on some basic info.
+
+
+ type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+ //To be expanded
+}
+
+
+
+
+ The
+
+ ConfigureSpec
+
+ struct will be parsed to JSON and pass to your plugin via the
+
+ -configure=(json payload here)
+
+ .
+
+
+
+
+ In your plugin, you can use the
+
+ zoraxy_plugin
+
+ library to parse it or parse it manually (if you are developing a plugin with other languages).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/4. Capture Modes.html b/docs/plugins/html/2. Architecture/4. Capture Modes.html
new file mode 100644
index 0000000..3a25716
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/4. Capture Modes.html
@@ -0,0 +1,365 @@
+
+
+
+
+
+
+
+ Capture Modes | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Capture Modes
+
+
+
+ As you can see in the Introspect section, there are two types of capture mode in Zoraxy plugin API.
+
+
+
+
+ Static Capture Mode
+
+
+ Dynamic Capture Mode
+
+
+
+
+ Notes: When this document mention the term “endpoint”, it means a particular sub-path on the plugin side. For example
+
+ /capture
+
+ or
+
+ /sniff
+
+ . In actual implementation, this can be a
+
+ http.HandleFunc
+
+ or
+
+ http.Handle
+
+ depends on the plugin implementation.
+
+
+
+
+ Static Capture Mode
+
+
+
+ Static Capture Mode register a static path to Zoraxy, when the plugin is enabled on a certain HTTP proxy rule, all request that matches the static capture registered paths are forwarded to the plugin without asking first. The overall process is shown in the diagram below.
+
+
+
+
+
+
+
+
+
+ The main benefit of static capture mode is that the capture paths are stored in radix tree. That means it takes O(logn) time to resolve the path and forward the request. Hence,
+
+ this mode is generally faster
+
+ if your plugin always listens to a few certain paths for extended functionalities.
+
+
+
+
+ Dynamic Capture Mode
+
+
+
+ Dynamic Capture Mode register two endpoints to Zoraxy.
+
+
+
+
+ DynamicCaptureSniff - The sniffing endpoint where Zoraxy will first ask if the plugin want to handle this request
+
+
+ DynamicCaptureIngress - The handling endpoint, where if the plugin reply the sniffing with “YES”, Zoraxy forward the incoming request to this plugin at this defined endpoint.
+
+
+
+
+ The whole process will takes a few request exchange between plugin and Zoraxy core. Since both of them are communicating via the loopback interface, speed should not be too big of a concern here.
+
+
+
+
+ The request handling flow is shown in the diagram below.
+
+
+
+
+
+
+
+
+
+ Once Zoraxy receive a request from a client that matches one of the HTTP Proxy Rule, Zoraxy will forward the request header to all the plugins that matches the following criteria
+
+
+
+
+ The plugin is assigned to a tag that is currently attached to the given HTTP Proxy that the request is coming through
+
+
+ The plugin is enabled and running
+
+
+ The plugin has registered its dynamic capture sniffing endpoint in Introspect
+
+
+
+
+ Then the plugin
+
+ /sniff
+
+ endpoint will receive some basic header information about the request, and response with
+
+ SniffResultAccpet
+
+ or
+
+ SniffResultSkip
+
+ to accept or reject handling such request. The response are defined in
+
+ zoraxy_plugin
+
+ as a public type where you can access with
+
+ zoraxy_plugin.SniffresultAccept
+
+ and
+
+ zoraxy_plugin.SniffResultSkip
+
+ respectively.
+
+
+
+
+ Note that this shall only be used if static capture mode cannot satisfy your needs in implementation the feature you want, as
+
+ dynamic capture is way slower than static capture mode
+
+ .
+
+
+
+
+ Mixing Capture Modes
+
+
+
+ It is possible for you to mix both Static and Capture modes if that is what you want. A few thing you need to know about mixing both mode in single plugin
+
+
+
+
+ Static capture mode has higher priority than dynamic capture mode across all plugins. That means if you have a request that matches Plugin A’s static capture path and Plugin B’s dynamic capture, the request will be first handled by Plugin A
+
+
+ The same plugin can register both static and dynamic capture modes. Similar to item (1), if the request has already captured by your static capture path, Zoraxy will not proceed and forward the request header to your dynamic sniffing endpoint.
+
+
+ In case there is a collision in static capture paths between two plugins, the longest one will have priority. For example, if Plugin A registered
+
+ /foo
+
+ and Plugin B registered
+
+ /foo/bar
+
+ , when a request to
+
+ /foo/bar/teacat
+
+ enter Zoraxy, Plugin B is used for handling such request.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/5. Plugin UI.html b/docs/plugins/html/2. Architecture/5. Plugin UI.html
new file mode 100644
index 0000000..a7b45c6
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/5. Plugin UI.html
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+ Plugin UI | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin UI
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+
+ A plugin can optionally expose a Web UI interface for user configuration.
+
+
+
+
+
+ A plugin must provide a UI, as it is part of the control mechanism of the plugin life cycle. (i.e. Zoraxy use the plugin UI HTTP server to communicate with the plugin for control signals)
+
+ As plugin installed via plugin store provides limited ways for a user to configure the plugin, the plugin web UI will be the best way for user to setup your plugin.
+
+
+
+ Plugin Web UI Access
+
+
+
+ If a plugin provide a Web UI endpoint for Zoraxy during the introspect process, a new item will be shown in the Plugins section on Zoraxy side menu. Below is an example of the Web UI of UPnP Port Forwarder plugin.
+
+
+
+
+
+
+
+
+ Front-end Developer Notes
+
+
+
+ The Web UI is implemented as a reverse proxy and embed in an iframe. So you do not need to handle CORS issues with the web UI (as it will be proxy internally by Zoraxy as exposed as something like a virtual directory mounted website).
+
+
+
+
+ However, the plugin web UI is exposed via the path
+
+ /plugin.ui/{{plugin_uuid}}/
+
+ , for example,
+
+ /plugin.ui/org.aroz.zoraxy.plugins.upnp/
+
+ .
+
+ When developing the plugin web UI, do not use absolute path for any resources used in the HTML file
+
+ , unless you are trying to re-use Zoraxy components like css or image elements stored in Zoraxy embedded web file system (e.g.
+
+ /img/logo.svg
+
+ ).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/6. Compile a Plugin.html b/docs/plugins/html/2. Architecture/6. Compile a Plugin.html
new file mode 100644
index 0000000..677947d
--- /dev/null
+++ b/docs/plugins/html/2. Architecture/6. Compile a Plugin.html
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+ Compile a Plugin | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Compile a Plugin
+
+
+
+ A plugin is basically a go program with a HTTP Server / Listener. The steps required to build a plugin is identical as building a ordinary go program.
+
+
+ # Assuming you are currently inside the root folder of your plugin
+go mod tidy
+go build
+
+# Validate if the plugin is correctly build using -introspect flag
+./{{your_plugin_name}} -introspect
+
+# You should see your plugin information printed to STDOUT as JSON string
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png b/docs/plugins/html/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png
new file mode 100644
index 0000000..d49fc06
Binary files /dev/null and b/docs/plugins/html/2. Architecture/img/1. Plugin Architecture/plugin_workflow.png differ
diff --git a/docs/plugins/html/2. Architecture/img/4. Capture Modes/dynamic_capture.png b/docs/plugins/html/2. Architecture/img/4. Capture Modes/dynamic_capture.png
new file mode 100644
index 0000000..0083370
Binary files /dev/null and b/docs/plugins/html/2. Architecture/img/4. Capture Modes/dynamic_capture.png differ
diff --git a/docs/plugins/html/2. Architecture/img/4. Capture Modes/static_capture.png b/docs/plugins/html/2. Architecture/img/4. Capture Modes/static_capture.png
new file mode 100644
index 0000000..0c51c52
Binary files /dev/null and b/docs/plugins/html/2. Architecture/img/4. Capture Modes/static_capture.png differ
diff --git a/docs/plugins/html/2. Architecture/img/5. Plugin UI/image-20250527201750613.png b/docs/plugins/html/2. Architecture/img/5. Plugin UI/image-20250527201750613.png
new file mode 100644
index 0000000..4792a4e
Binary files /dev/null and b/docs/plugins/html/2. Architecture/img/5. Plugin UI/image-20250527201750613.png differ
diff --git a/docs/plugins/html/3. Basic Examples/1. Hello World.html b/docs/plugins/html/3. Basic Examples/1. Hello World.html
new file mode 100644
index 0000000..76ddfa4
--- /dev/null
+++ b/docs/plugins/html/3. Basic Examples/1. Hello World.html
@@ -0,0 +1,668 @@
+
+
+
+
+
+
+
+ Hello World | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello World!
+
+
+
+ Last Update: 25/05/2025
+
+
+
+
+ Let start with a really simple Hello World plugin. This only function of this plugin is to print “Hello World” in the plugin web UI.
+
+
+
+ 1. Name your plugin
+
+
+ First things first, give your plugin a name. In this example, we are using the name “helloworld”.
+
+
+
+
+ Plugin name cannot contain space or special characters
+
+ , so you must use a file name that satisfies the requirement. Dont worry, the plugin file name is not the same as the plugin display name in the introspect.
+
+
+
+
+ 2. Create the plugin folder
+
+
+ If your zoraxy root folder do not contains a folder named “plugins”, it might implies that your Zoraxy is freshly clone from Github.
+
+ You will need to build and run it once to start working on your plugin
+
+ , so if you have a newly cloned source code of Zoraxy, do the followings.
+
+ git clone https://github.com/tobychui/zoraxy
+cd src
+go mod tidy
+go build
+sudo ./zoraxy
+
+
+ Afterward, create a plugin folder under your Zoraxy development environment that is exactly matching your plugin name. In the above example, the folder name should be “helloworld”.
+
+ # Assume you are already inside the src/ folder
+mkdir helloworld
+cd ./helloworld
+
+
+
+ 3. Create a go project
+
+
+
+ Similar to any Go project, you can start by creating a
+
+ main.go
+
+ file. Next, you would want to let the go compiler knows your plugin name so when generating a binary file, it knows what to name it. This can be done via using the
+
+ go mod init
+
+ command.
+
+
+ touch main.go
+go mod init example.com/zoraxy/helloworld
+ls
+# After you are done, you should see the followings
+# go.mod main.go
+
+
+
+ 4. Copy the Zoraxy plugin lib from Zoraxy source code
+
+
+
+ Locate the Zoraxy plugin library from the Zoraxy source code. You can find the
+
+ zoraxy_plugin
+
+ Go module under
+
+ src/mod/plugins/zoraxy_plugin
+
+
+
+
+
+ Copy the
+
+ zoraxy_plugin
+
+ folder from the Zoraxy source code mod folder into the your plugin’s mod folder. Let assume you use the same mod folder name as Zoraxy as
+
+ mod
+
+ , then your copied library path should be
+
+ plugins/helloworld/mod/zoraxy_plugin
+
+
+
+ mkdir ./mod
+cp -r "mod/plugins/zoraxy_plugin" ./mod/
+ls ./mod/zoraxy_plugin/
+# You should see something like this (might be different in future versions)
+# dev_webserver.go dynamic_router.go embed_webserver.go README.txt static_router.go zoraxy_plugin.go
+
+
+
+ 5. Create a web resources folder
+
+
+
+ Lets create a www folder and put all our web resources, we need to create an
+
+ index.html
+
+ file as our plugin web ui homepage. This can be done by creating a HTML file in the www folder.
+
+
+ # Assuming you are currently in the src/plugins/helloworld/ folder
+mkdir www
+cd www
+touch index.html
+
+
+
+ And here is an example
+
+ index.html
+
+ file that uses the Zoraxy internal resources like css and dark theme toggle mechanism. That csrf token template is not straightly needed in this example as helloworld plugin do not make any POST request to Zoraxy webmin interface, but it might come in handy later.
+
+
+ <!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <!-- CSRF token, if your plugin need to make POST request to backend -->
+ <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
+ <link rel="stylesheet" href="/script/semantic/semantic.min.css">
+ <script src="/script/jquery-3.6.0.min.js"></script>
+ <script src="/script/semantic/semantic.min.js"></script>
+ <script src="/script/utils.js"></script>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="/main.css">
+ <title>Hello World</title>
+ <style>
+ body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
+ font-family: Arial, sans-serif;
+ background:none;
+ }
+ </style>
+</head>
+<body>
+ <!-- Dark theme script must be included after body tag-->
+ <link rel="stylesheet" href="/darktheme.css">
+ <script src="/script/darktheme.js"></script>
+ <div style="text-align: center;">
+ <h1>Hello World</h1>
+ <p>Welcome to your first Zoraxy plugin</p>
+ </div>
+</body>
+</html>
+
+
+
+ 6. Creating a handler for Introspect
+
+
+
+ To create a handler for introspect, you can first start your plugin with a few constants.
+
+
+
+
+ Plugin ID, this must be unique. You can use a domain you own like
+
+ com.example.helloworld
+
+
+
+ UI Path, for now we uses “/” as this plugin do not have any other endpoints, so we can use the whole root just for web UI
+
+
+ Web root, for trimming off from the embedded web folder so when user can visit your
+
+ index.html
+
+ by accessing
+
+ /
+
+ instead of needing to navigate to
+
+ /www
+
+
+
+
+
+ After you have defined these constant, we can use
+
+ plugin.ServeAndRecvSpec
+
+ function to handle the handshake between Zoraxy and your plugin.
+
+
+ const (
+ PLUGIN_ID = "com.example.helloworld"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+func main(){
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.helloworld",
+ Name: "Hello World Plugin",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple hello world plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+}
+
+
+
+ Notes: If some post processing is needed between Introspect and Configure, you can use two seperate function to handle the first start and the second starting of your plugin. The “separated version” of
+
+ ServeAndRecvSpec
+
+ is defined as
+
+ ServeIntroSpect(pluginSpect *IntroSpect)
+
+ and
+
+ RecvConfigureSpec() (*ConfigureSpec, error)
+
+ . See
+
+ zoraxy_plugin.go
+
+ for more information.
+
+
+
+
+ 7. Creating a web server from embedded web fs
+
+
+
+ After that, we need to create a web server to serve our plugin UI to Zoraxy via HTTP. This can be done via the
+
+ http.FileServer
+
+ but for simplicity and ease of upgrade, the Zoraxy plugin library provided an easy to use embedded web FS server API for plugin developers.
+
+
+
+
+ To use the Zoraxy plugin embedded web server, you first need to embed your web fs into Zoraxy as such.
+
+
+ import (
+ _ "embed"
+ "fmt"
+
+ plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
+)
+
+//go:embed www/*
+var content embed.FS
+
+
+
+ Then call to the
+
+ NewPluginEmbedUIRouter
+
+ to create a new UI router from the embedded Fs.
+
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+// The router will also help to handle the termination of the plugin when
+// a user wants to stop the plugin via Zoraxy Web UI
+embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+
+
+
+ Here is the tricky part. since not all platform support cross process signaling, Zoraxy plugin uses HTTP request to request a plugin to shutdown. The
+
+ embedWebRouter
+
+ object has a function named
+
+ RegisterTerminateHandler
+
+ where you can easily use this function to register actions that needed to be done before shutdown.
+
+
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Hello World Plugin Exited")
+}, nil)
+
+
+
+ Notes: This is a blocking function. That is why Zoraxy has a build-in timeout context where if the terminate request takes more than 3 seconds, the plugin process will be treated as “freezed” and forcefully terminated. So please make sure the terminate handler complete its shutdown procedures within 3 seconds.
+
+
+
+
+ 8. Register & Serve the Web UI
+
+
+
+ After you have created a embedded web router, you can register it to the UI PATH as follows.
+
+
+ // Serve the hello world page in the www folder
+http.Handle(UI_PATH, embedWebRouter.Handler())
+fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+if err != nil {
+ panic(err)
+}
+
+
+
+ As this is just the standard golang net/http package, you can of course add more Function Handlers to it based on your needs. There are something that you need to know about adding API endpoints, we will discuss this in the later sections.
+
+
+
+
+ 9. Build and Test
+
+
+
+ After saving the
+
+ main.go
+
+ file, you can now build your plugin with
+
+ go build
+
+ . It should generate the plugin in your platform architecture and OS. If you are on Linux, it will be
+
+ helloworld
+
+ and if you are on Windows, it will be
+
+ helloworld.exe
+
+ .
+
+
+
+
+ After you are done, restart Zoraxy and enable your plugin in the Plugin List. Now you can test and debug your plugin with your HTTP Proxy Rules. All the STDOUT and STDERR of your plugin will be forwarded to the STDOUT of Zoraxy as well as the log file.
+
+
+
+
+
+
+
+
+
+
+ Tips
+
+
+
+
+
+ You can also enable the Developer Option - Plugin Auto Reload function if you are too lazy to restart Zoraxy everytime the plugin binary changed.
+
+
+
+
+
+
+
+
+
+ 10. Full Code
+
+
+
+ This is the full code of the helloworld plugin main.go file.
+
+
+ package main
+
+import (
+ "embed"
+ _ "embed"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "com.example.helloworld"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+//go:embed www/*
+var content embed.FS
+
+func main() {
+ // Serve the plugin intro spect
+ // This will print the plugin intro spect and exit if the -introspect flag is provided
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.helloworld",
+ Name: "Hello World Plugin",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple hello world plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+ // The router will also help to handle the termination of the plugin when
+ // a user wants to stop the plugin via Zoraxy Web UI
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Hello World Plugin Exited")
+ }, nil)
+
+ // Serve the hello world page in the www folder
+ http.Handle(UI_PATH, embedWebRouter.Handler())
+ fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+ if err != nil {
+ panic(err)
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/3. Basic Examples/2. RESTful Example.html b/docs/plugins/html/3. Basic Examples/2. RESTful Example.html
new file mode 100644
index 0000000..3ee805b
--- /dev/null
+++ b/docs/plugins/html/3. Basic Examples/2. RESTful Example.html
@@ -0,0 +1,804 @@
+
+
+
+
+
+
+
+ RESTful Example | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ RESTful API Call in Web UI
+
+
+
+ Last Update: 29/05/2025
+
+
+
+
+
+ When developing a UI for your plugin, sometime you might need to make RESTFUL API calls to your plugin backend for setting up something or getting latest information from your plugin. In this example, I will show you how to create a plugin with RESTful api call capabilities with the embedded web server and the custom
+
+ cjax
+
+ function.
+
+
+
+
+
+ Notes: This example assumes you have basic understanding on how to use jQuery
+
+ ajax
+
+ request.
+
+
+
+
+
+ Lets get started!
+
+
+
+
+ 1. Create the plugin folder structures
+
+
+
+ This step is identical to the Hello World example, where you create a plugin folder with the required go project structure in the folder. Please refer to the Hello World example section 1 to 5 for details.
+
+
+
+
+ 2. Create Introspect
+
+
+
+ This is quite similar to the Hello World example as well, but we are changing some of the IDs to match what we want to do in this plugin.
+
+
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.restful-example",
+ Name: "Restful Example",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple demo for making RESTful API calls in plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+
+
+ 3. Create an embedded web server with handlers
+
+
+
+ In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a
+
+ http.HandleFunc
+
+ to the plugin so our front-end can request and communicate with the backend.
+
+
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+embedWebRouter.RegisterTerminateHandler(func() {
+ fmt.Println("Restful-example Exited")
+}, nil)
+
+//Register a simple API endpoint that will echo the request body
+// Since we are using the default http.ServeMux, we can register the handler directly with the last
+// parameter as nil
+embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ //Some handler code here
+}, nil)
+
+
+
+ The
+
+ embedWebRouter.HandleFunc
+
+ last parameter is the
+
+ http.Mux
+
+ , where if you have multiple web server listening interface, you can fill in different Mux based on your implementation. On of the examples is that, when you are developing a static web server plugin, where you need a dedicated HTTP listening endpoint that is not the one Zoraxy assigned to your plugin, you need to create two http.Mux and assign one of them for Zoraxy plugin UI purpose.
+
+
+
+
+ 4. Modify the front-end HTML file to make request to backend
+
+
+
+ To make a RESTFUL API to your plugin,
+
+ you must use relative path in your request URL
+
+ .
+
+
+
+
+ Absolute path that start with
+
+ /
+
+ is only use for accessing Zoraxy resouces. For example, when you access
+
+ /img/logo.svg
+
+ , Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of
+
+ /plugins/your_plugin_name/{your_web_root}/img/logo.svg
+
+ .
+
+
+
+ Making GET request
+
+
+ Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like
+
+ ./api/foo/bar
+
+ instead. Here is an example on a front-end and back-end implementation of a simple “echo” API.
+
+
+
+ The API logic is simple: when you make a GET request to the API with
+
+ ?name=foobar
+
+ , it returns
+
+ Hello foobar
+
+ . Here is the backend implementation in your plugin code.
+
+
+ embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ // This is a simple echo API that will return the request body as response
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+ }, nil)
+
+
+
+ And here is the front-end code in your HTML file
+
+
+ <!-- The example below show how HTTP GET is used with Zoraxy plugin -->
+<h3>Echo Test (HTTP GET)</h3>
+<div class="ui form">
+ <div class="field">
+ <label for="nameInput">Enter your name:</label>
+ <input type="text" id="nameInput" placeholder="Your name">
+ </div>
+ <button class="ui button primary" id="sendRequestButton">Send Request</button>
+ <div class="ui message" id="responseMessage" style="display: none;"></div>
+</div>
+
+<script>
+ document.getElementById('sendRequestButton').addEventListener('click', function() {
+ const name = document.getElementById('nameInput').value;
+ if (name.trim() === "") {
+ alert("Please enter a name.");
+ return;
+ }
+ // Note the relative path is used here!
+ // GET do not require CSRF token, so you can use $.ajax directly
+ // or $.cjax (defined in /script/utils.js) to make GET request
+ $.ajax({
+ url: `./api/echo`,
+ type: 'GET',
+ data: { name: name },
+ success: function(data) {
+ console.log('Response:', data.message);
+ $('#responseMessage').text(data.message).show();
+ },
+ error: function(xhr, status, error) {
+ console.error('Error:', error);
+ $('#responseMessage').text('An error occurred while processing your request.').show();
+ }
+ });
+ });
+</script>
+
+
+ Making POST request
+
+
+
+ Making POST request is also similar to GET request, except when making the request, you will need pass the CSRF-Token with the payload. This is required due to security reasons (See
+
+ #267
+
+ for more details).
+
+
+
+
+ Since the CSRF validation is done by Zoraxy, your plugin backend code can be implemented just like an ordinary handler. Here is an example POST handling function that receive a FORM POST and print it in an HTML response.
+
+
+ embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ for key, values := range r.PostForm {
+ for _, value := range values {
+ // Generate a simple HTML response
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, "%s: %s<br>", key, value)
+ }
+ }
+}, nil)
+
+
+
+
+ For the front-end, you will need to use the
+
+ $.cjax
+
+ function implemented in Zoraxy
+
+ utils.js
+
+ file. You can include this file by adding these two lines to your HTML file.
+
+
+ <script src="/script/jquery-3.6.0.min.js"></script>
+<script src="/script/utils.js"></script>
+<!- More lines here -->
+<!-- The example below shows how form post can be used in plugin -->
+<h3>Form Post Test (HTTP POST)</h3>
+<div class="ui form">
+ <div class="field">
+ <label for="postNameInput">Name:</label>
+ <input type="text" id="postNameInput" placeholder="Your name">
+ </div>
+ <div class="field">
+ <label for="postAgeInput">Age:</label>
+ <input type="number" id="postAgeInput" placeholder="Your age">
+ </div>
+ <div class="field">
+ <label>Gender:</label>
+ <div class="ui checkbox">
+ <input type="checkbox" id="genderMale" name="gender" value="Male">
+ <label for="genderMale">Male</label>
+ </div>
+ <div class="ui checkbox">
+ <input type="checkbox" id="genderFemale" name="gender" value="Female">
+ <label for="genderFemale">Female</label>
+ </div>
+ </div>
+ <button class="ui button primary" id="postRequestButton">Send</button>
+ <div class="ui message" id="postResponseMessage" style="display: none;"></div>
+</div>
+
+
+
+ After that, you can call to the
+
+ $.cjax
+
+ function just like what you would usually do with the
+
+ $ajax
+
+ function.
+
+
+ // .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
+// alternatively you can use $.ajax with CSRF token in headers
+// the header is named "X-CSRF-Token" and the value is taken from the head
+// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
+$.cjax({
+ url: './api/post',
+ type: 'POST',
+ data: { name: name, age: age, gender: gender },
+ success: function(data) {
+ console.log('Response:', data);
+ $('#postResponseMessage').html(data).show();
+ },
+ error: function(xhr, status, error) {
+ console.error('Error:', error);
+ $('#postResponseMessage').text('An error occurred while processing your request.').show();
+ }
+});
+
+
+ POST Request with Vanilla JS
+
+
+
+ It is possible to make POST request with Vanilla JS. Note that you will need to populate the csrf-token field yourself to make the request pass through the plugin UI request router in Zoraxy. Here is a basic example on how it could be done.
+
+
+ fetch('./api/post', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': csrfToken // Include the CSRF token in the headers
+ },
+ body: JSON.stringify({{your_data_here}})
+})
+
+
+
+ 5. Full Code
+
+
+
+ Here is the full code of the RESTFUL example for reference.
+
+
+
+
+ Front-end (
+
+ plugins/restful-example/www/index.html
+
+ )
+
+
+ <!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <!-- CSRF token, if your plugin need to make POST request to backend -->
+ <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
+ <link rel="stylesheet" href="/script/semantic/semantic.min.css">
+ <script src="/script/jquery-3.6.0.min.js"></script>
+ <script src="/script/semantic/semantic.min.js"></script>
+ <script src="/script/utils.js"></script>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="stylesheet" href="/main.css">
+ <title>RESTful Example</title>
+ <style>
+ body {
+ background-color: var(--theme_bg_primary);
+ }
+ </style>
+</head>
+<body>
+ <!-- Dark theme script must be included after body tag-->
+ <link rel="stylesheet" href="/darktheme.css">
+ <script src="/script/darktheme.js"></script>
+ <br>
+ <div class="standardContainer">
+ <div class="ui container">
+ <h1>RESTFul API Example</h1>
+ <!-- The example below show how HTTP GET is used with Zoraxy plugin -->
+ <h3>Echo Test (HTTP GET)</h3>
+ <div class="ui form">
+ <div class="field">
+ <label for="nameInput">Enter your name:</label>
+ <input type="text" id="nameInput" placeholder="Your name">
+ </div>
+ <button class="ui button primary" id="sendRequestButton">Send Request</button>
+ <div class="ui message" id="responseMessage" style="display: none;"></div>
+ </div>
+
+ <script>
+ document.getElementById('sendRequestButton').addEventListener('click', function() {
+ const name = document.getElementById('nameInput').value;
+ if (name.trim() === "") {
+ alert("Please enter a name.");
+ return;
+ }
+ // Note the relative path is used here!
+ // GET do not require CSRF token, so you can use $.ajax directly
+ // or $.cjax (defined in /script/utils.js) to make GET request
+ $.ajax({
+ url: `./api/echo`,
+ type: 'GET',
+ data: { name: name },
+ success: function(data) {
+ console.log('Response:', data.message);
+ $('#responseMessage').text(data.message).show();
+ },
+ error: function(xhr, status, error) {
+ console.error('Error:', error);
+ $('#responseMessage').text('An error occurred while processing your request.').show();
+ }
+ });
+ });
+ </script>
+ <!-- The example below shows how form post can be used in plugin -->
+ <h3>Form Post Test (HTTP POST)</h3>
+ <div class="ui form">
+ <div class="field">
+ <label for="postNameInput">Name:</label>
+ <input type="text" id="postNameInput" placeholder="Your name">
+ </div>
+ <div class="field">
+ <label for="postAgeInput">Age:</label>
+ <input type="number" id="postAgeInput" placeholder="Your age">
+ </div>
+ <div class="field">
+ <label>Gender:</label>
+ <div class="ui checkbox">
+ <input type="checkbox" id="genderMale" name="gender" value="Male">
+ <label for="genderMale">Male</label>
+ </div>
+ <div class="ui checkbox">
+ <input type="checkbox" id="genderFemale" name="gender" value="Female">
+ <label for="genderFemale">Female</label>
+ </div>
+ </div>
+ <button class="ui button primary" id="postRequestButton">Send</button>
+ <div class="ui message" id="postResponseMessage" style="display: none;"></div>
+ </div>
+
+ <script>
+ document.getElementById('postRequestButton').addEventListener('click', function() {
+ const name = document.getElementById('postNameInput').value;
+ const age = document.getElementById('postAgeInput').value;
+ const genderMale = document.getElementById('genderMale').checked;
+ const genderFemale = document.getElementById('genderFemale').checked;
+
+ if (name.trim() === "" || age.trim() === "" || (!genderMale && !genderFemale)) {
+ alert("Please fill out all fields.");
+ return;
+ }
+
+ const gender = genderMale ? "Male" : "Female";
+
+ // .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
+ // alternatively you can use $.ajax with CSRF token in headers
+ // the header is named "X-CSRF-Token" and the value is taken from the head
+ // meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
+ $.cjax({
+ url: './api/post',
+ type: 'POST',
+ data: { name: name, age: age, gender: gender },
+ success: function(data) {
+ console.log('Response:', data);
+ $('#postResponseMessage').html(data).show();
+ },
+ error: function(xhr, status, error) {
+ console.error('Error:', error);
+ $('#postResponseMessage').text('An error occurred while processing your request.').show();
+ }
+ });
+ });
+ </script>
+ </div>
+ </div>
+</body>
+</html>
+
+
+
+ Backend (
+
+ plugins/restful-example/main.go
+
+ )
+
+
+ package main
+
+import (
+ "embed"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "com.example.restful-example"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+//go:embed www/*
+var content embed.FS
+
+func main() {
+ // Serve the plugin intro spect
+ // This will print the plugin intro spect and exit if the -introspect flag is provided
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.restful-example",
+ Name: "Restful Example",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple demo for making RESTful API calls in plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+ // The router will also help to handle the termination of the plugin when
+ // a user wants to stop the plugin via Zoraxy Web UI
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Restful-example Exited")
+ }, nil)
+
+ //Register a simple API endpoint that will echo the request body
+ // Since we are using the default http.ServeMux, we can register the handler directly with the last
+ // parameter as nil
+ embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ // This is a simple echo API that will return the request body as response
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+ }, nil)
+
+ // Here is another example of a POST API endpoint that will echo the form data
+ // This will handle POST requests to /api/post and return the form data as response
+ embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ for key, values := range r.PostForm {
+ for _, value := range values {
+ // Generate a simple HTML response
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, "%s: %s<br>", key, value)
+ }
+ }
+ }, nil)
+
+ // Serve the restful-example page in the www folder
+ http.Handle(UI_PATH, embedWebRouter.Handler())
+ fmt.Println("Restful-example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+ if err != nil {
+ panic(err)
+ }
+
+}
+
+
+
+
+ What you should expect to see if everything is correctly loaded and working in Zoray
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/3. Basic Examples/3. Static Capture Example.html b/docs/plugins/html/3. Basic Examples/3. Static Capture Example.html
new file mode 100644
index 0000000..492bca4
--- /dev/null
+++ b/docs/plugins/html/3. Basic Examples/3. Static Capture Example.html
@@ -0,0 +1,553 @@
+
+
+
+
+
+
+
+ Static Capture Example | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Static Capture Example
+
+
+
+ Last Update: 29/05/2025
+
+
+
+
+
+ This example demonstrates how to use static capture in Zoraxy plugins. Static capture allows you to define specific paths that will be intercepted by your plugin, enabling custom handling of requests to those paths.
+
+
+
+
+
+ Notes: This example assumes you have already read Hello World example.
+
+
+
+
+
+ 1. Create the plugin folder structure
+
+
+
+ Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
+
+
+
+
+ 2. Define Introspect
+
+
+
+ The introspect configuration specifies the static capture paths and ingress for your plugin.
+
+
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.static-capture-example",
+ Name: "Static Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "An example for showing how static capture works in Zoraxy.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ StaticCapturePaths: []plugin.StaticCaptureRule{
+ { CapturePath: "/test_a" },
+ { CapturePath: "/test_b" },
+ },
+ StaticCaptureIngress: "/s_capture",
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+
+
+
+ Note the
+
+ StaticCapturePaths
+
+ . These are the paths that you want to capture in your plugin. These paths will be registered to Zoraxy and when a user have request that matches these paths (including subpaths), the request will get forwarded to your plugin. In this example, we are intercepting the
+
+ /test_a
+
+ and
+
+ test_b
+
+ sub-path.
+
+
+
+ We also defined a new value named
+
+ StaticCaptureIngress
+
+ . This is to tell Zoraxy that “if you receive requests that matches the above Static capture paths, please forward the request to this endpoint”. In this example, this plugin asked Zoraxy to forward th HTTP traffic to
+
+ /s_capture
+
+ if anything is matched.
+
+
+
+ 3. Register Static Capture Handlers
+
+
+
+ Static capture handlers are used to process requests to the defined paths. Similar to ordinary http.HandleFunc, you can register
+
+ http.HandleFunc
+
+ as follows.
+
+
+ pathRouter := plugin.NewPathRouter()
+
+pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
+pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
+
+pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
+}))
+
+pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
+
+
+
+ The
+
+ SetDefaultHandler
+
+ is used to handle exceptions where a request is forwarded to your plugin but it cannot be handled by any of your registered path handlers. This is usually an implementation bug on the plugin side and you can add some help message or debug log to this function if needed.
+
+
+
+
+ The
+
+ RegisterStaticCaptureHandle
+
+ is used to register the static capture ingress endpoint, so Zoraxy knows where to forward the HTTP request when it thinks your plugin shall be the one handling the request. In this example,
+
+ /s_capture
+
+ is used for static capture endpoint.
+
+
+
+
+ 4. Implement Handlers
+
+
+
+ Here are examples of handlers for the captured paths:
+
+
+
+ Handler for
+
+ /test_a
+
+
+ func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
+}
+
+
+ Handler for
+
+ /test_b
+
+
+ func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
+}
+
+
+
+ When the user request any HTTP Proxy Rule with the matching path, these two handlers will response to the request and return the hardcoded string above. Again, this is just for demonstration purpose and you should implement your functions here.
+
+
+
+
+ 5. Render Debug UI
+
+
+
+ The debug UI provides a simple interface for testing and inspecting requests.
+
+
+ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+
+
+
+ This is technically not related to static capturing, but it is really helpful to have a UI to help with printing debug information. You can access the page rendered by this function in the Zoraxy plugin menu. This should be replaced with the embedded web fs used in the Hello world example after the development is completed.
+
+
+
+
+
+
+
+
+ 6. Full Code
+
+
+
+ Here is the complete code for the static capture example:
+
+
+ package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+
+ plugin "example.com/zoraxy/static-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.static-capture-example"
+ UI_PATH = "/ui"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+func main() {
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: PLUGIN_ID,
+ Name: "Static Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "An example for showing how static capture works in Zoraxy.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+ StaticCapturePaths: []plugin.StaticCaptureRule{
+ { CapturePath: "/test_a" },
+ { CapturePath: "/test_b" },
+ },
+ StaticCaptureIngress: STATIC_CAPTURE_INGRESS,
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
+ pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
+ pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
+ }))
+ pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Static path capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
+}
+
+func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
+}
+
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+
+
+
+ 7. Expected Output
+
+
+ To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the “debug” tag and assign it to a localhost loopback HTTP proxy rule.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ When the plugin is running, requests to
+
+ /test_a
+
+ and
+
+ /test_b
+
+ will be intercepted by their respective handlers.
+
+ Requests to other paths will not pass through your plugin and will be handled by the default upstream server set by the HTTP proxy Rule.
+
+
+
+
+
+
+
+
+
+
+ Example terminal output for requesting
+
+ /test_a
+
+ :
+
+
+ This request is captured by A handler!
+Request URI: /test_a
+
+
+
+ Example output for requesting
+
+ /test_b
+
+ :
+
+
+ This request is captured by the B handler!
+Request URI: /test_b
+
+
+
+
+ Enjoy exploring static capture in Zoraxy!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html b/docs/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html
new file mode 100644
index 0000000..35c786e
--- /dev/null
+++ b/docs/plugins/html/3. Basic Examples/4. Dynamic Capture Example.html
@@ -0,0 +1,677 @@
+
+
+
+
+
+
+
+ Dynamic Capture Example | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dynamic Capture Example
+
+
+
+ Last Update: 29/05/2025
+
+
+
+
+
+ This example demonstrates how to use dynamic capture in Zoraxy plugins. Dynamic capture allows you to intercept requests based on real-time conditions, so you can program your plugin in a way that it can decided if it want to handle the request or not.
+
+
+
+
+
+ Notes: This example assumes you have already read Hello World and Stataic Capture Example.
+
+
+
+
+
+ Lets dive in!
+
+
+
+
+ 1. Create the plugin folder structure
+
+
+
+ Follow the same steps as the Hello World example to set up the plugin folder structure. Refer to the Hello World example sections 1 to 5 for details.
+
+
+
+
+ 2. Define Introspect
+
+
+
+ The introspect configuration specifies the dynamic capture sniff and ingress paths for your plugin.
+
+
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+})
+if err != nil {
+ panic(err)
+}
+
+
+
+ Note the
+
+ DynamicCaptureSniff
+
+ and
+
+ DynamicCaptureIngress
+
+ . These paths define the sniffing and capturing behavior for dynamic requests. The sniff path is used to evaluate whether a request should be intercepted, while the ingress path handles the intercepted requests.
+
+
+
+
+ 3. Register Dynamic Capture Handlers
+
+
+
+ Dynamic capture handlers are used to process requests that match specific conditions.
+
+
+ pathRouter := plugin.NewPathRouter()
+pathRouter.SetDebugPrintMode(true)
+
+pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ fmt.Println("Accepting request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultAccpet
+ }
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+})
+
+pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+})
+
+
+
+ The
+
+ RegisterDynamicSniffHandler
+
+ evaluates incoming requests, while the
+
+ RegisterDynamicCaptureHandle
+
+ processes the intercepted requests.
+
+
+
+ Sniffing Logic
+
+
+ If a module registered a dynamic capture path, Zoraxy will forward the request headers as
+
+ DynamicSniffForwardRequest
+
+ (
+
+ dsfr
+
+ ) object to all the plugins that is assigned to this tag. And in each of the plugins, a dedicated logic will take in the object and “think” if they want to handle the request. You can get the following information from the dsfr object by directly accessing the members of it.
+
+ 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"`
+}
+
+
+
+ You can also use the
+
+ GetRequest()
+
+ function to get the
+
+ *http.Request
+
+ object or
+
+ GetRequestUUID()
+
+ to get a
+
+ string
+
+ value that is a UUID corresponding to this request for later matching with the incoming, forwarded request.
+
+
+
+
+
+ Note that since all request will pass through the sniffing function in your plugin, do not implement any blocking logic in your sniffing function, otherwise this will slow down all traffic going through the HTTP proxy rule with the plugin enabled.
+
+
+
+
+
+ In the sniffing stage, you can choose to either return
+
+ ControlStatusCode_CAPTURED
+
+ , where Zoraxy will forward the request to your plugin
+
+ DynamicCaptureIngress
+
+ endpoint, or
+
+ ControlStatusCode_UNHANDLED
+
+ , where Zoraxy will pass on the request to the next dynamic handling plugin or if there are no more plugins to handle the routing, to the upstream server.
+
+
+
+ Capture Handling
+
+
+
+ The capture handling is where Zoraxy formally forward you the HTTP request the client is requesting. In this situation, you must response the request by properly handling the
+
+ http.Request
+
+ by writing to the
+
+ http.ResponseWriter
+
+ .
+
+
+
+
+ If there is a need to match the sniffing to the capture handling logic (Let say you want to design your plugin to run some kind of pre-processing before the actual request came in), you can use the
+
+ X-Zoraxy-Requestid
+
+ header in the HTTP request. This is the same UUID as the one you get from
+
+ dsfr.GetRequestUUID()
+
+ in the sniffing stage if they are the same request object on Zoraxy side.
+
+
+
+
+ The http request that Zoraxy forwards to the plugin capture handling endpoint contains header like these.
+
+
+ Request URI: /foobar/test
+Request Method: GET
+Request Headers:
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Encoding: gzip, deflate, br, zstd
+(more fileds)
+X-Forwarded-For: 127.0.0.1
+X-Forwarded-Proto: https
+X-Real-Ip: 127.0.0.1
+X-Zoraxy-Requestid: d00619b8-f39e-4c04-acd8-c3a6f55b1566
+
+
+
+ You can extract the
+
+ X-Zoraxy-Requestid
+
+ value from the request header and do your matching for implementing your function if needed.
+
+
+
+
+ 4. Render Debug UI
+
+
+
+ This UI is used help validate the management Web UI is correctly shown in Zoraxy webmin interface. You should implement the required management interface for your plugin here.
+
+
+ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+
+
+
+ 5. Full Code
+
+
+
+ Here is the complete code for the dynamic capture example:
+
+
+ package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+
+ plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
+ UI_PATH = "/debug"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+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: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Dynamic Captures
+ */
+ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ //In this example, we want to capture all URI
+ //that start with /foobar and forward it to the dynamic capture handler
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ reqUUID := dsfr.GetRequestUUID()
+ fmt.Println("Accepting request with UUID: " + reqUUID)
+
+ // Print all the values of the request
+ fmt.Println("Method:", dsfr.Method)
+ fmt.Println("Hostname:", dsfr.Hostname)
+ fmt.Println("URL:", dsfr.URL)
+ fmt.Println("Header:")
+ for key, values := range dsfr.Header {
+ for _, value := range values {
+ fmt.Printf(" %s: %s\n", key, value)
+ }
+ }
+ fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
+ fmt.Println("Host:", dsfr.Host)
+ fmt.Println("RequestURI:", dsfr.RequestURI)
+ fmt.Println("Proto:", dsfr.Proto)
+ fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
+ fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
+
+ // We want to handle this request, reply with aSniffResultAccept
+ return plugin.SniffResultAccpet
+ }
+
+ // If the request URI does not match, we skip this request
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+ })
+ pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ // This is the dynamic capture handler where it actually captures and handle the request
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!"))
+
+ // Print all the request info to the response writer
+ w.Write([]byte("\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+ })
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
+
+
+
+
+ 6. Expected Output
+
+
+ To enable the plugin, add the plugin to one of the tags and assign the tag to your HTTP Proxy Rule. Here is an example of assigning the plugin to the “debug” tag and assigning it to a localhost loopback HTTP proxy rule.
+
+
+
+ When the plugin is running, requests matching the sniff conditions will be intercepted and processed by the dynamic capture handler.
+
+
+
+
+ If everything is correctly setup, you should see the following page when requesting any URL with prefix
+
+ (your_HTTP_proxy_rule_hostname)/foobar
+
+
+
+
+
+
+
+
+
+
+ Example terminal output for requesting
+
+ /foobar/*
+
+ :
+
+
+ [2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] URL: /foobar/test
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accepting request with UUID: 8c916c58-0d6a-4d11-a2f0-f29d3d984509
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Dest: document
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Encoding: gzip, deflate, br, zstd
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Cache-Control: max-age=0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-User: ?1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Upgrade-Insecure-Requests: 1
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Priority: u=0, i
+[2025-05-30 20:44:26.143149] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Mobile: ?0
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua: "Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Ch-Ua-Platform: "Windows"
+[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Site: none
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Sec-Fetch-Mode: navigate
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RemoteAddr: [::1]:54522
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Host: a.localhost
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] RequestURI: /foobar/test
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
+[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
+
+
+
+
+ Now you know how to develop a plugin in Zoraxy that handles special routings!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250527210849767.png b/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250527210849767.png
new file mode 100644
index 0000000..0928b98
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250527210849767.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250530134148399.png b/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250530134148399.png
new file mode 100644
index 0000000..4639a45
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/1. Hello World/image-20250530134148399.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png b/docs/plugins/html/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png
new file mode 100644
index 0000000..37bddf3
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/2. RESTful Example/image-20250530153148506.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png
new file mode 100644
index 0000000..dd754e9
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164549527.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png
new file mode 100644
index 0000000..4134d31
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164903842.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png
new file mode 100644
index 0000000..2931a3f
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530164916476.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png
new file mode 100644
index 0000000..2212937
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/3. Static Capture Example/image-20250530165014188.png differ
diff --git a/docs/plugins/html/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png b/docs/plugins/html/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png
new file mode 100644
index 0000000..3c264f2
Binary files /dev/null and b/docs/plugins/html/3. Basic Examples/img/4. Dynamic Capture Example/image-20250530205430254.png differ
diff --git a/docs/plugins/html/assets/banner.png b/docs/plugins/html/assets/banner.png
new file mode 100644
index 0000000..56f5d1b
Binary files /dev/null and b/docs/plugins/html/assets/banner.png differ
diff --git a/docs/plugins/html/assets/banner.psd b/docs/plugins/html/assets/banner.psd
new file mode 100644
index 0000000..7e30760
Binary files /dev/null and b/docs/plugins/html/assets/banner.psd differ
diff --git a/docs/plugins/html/assets/logo.png b/docs/plugins/html/assets/logo.png
new file mode 100644
index 0000000..083342a
Binary files /dev/null and b/docs/plugins/html/assets/logo.png differ
diff --git a/docs/plugins/html/assets/logo_white.png b/docs/plugins/html/assets/logo_white.png
new file mode 100644
index 0000000..9557852
Binary files /dev/null and b/docs/plugins/html/assets/logo_white.png differ
diff --git a/docs/plugins/html/assets/theme.js b/docs/plugins/html/assets/theme.js
new file mode 100644
index 0000000..4338f19
--- /dev/null
+++ b/docs/plugins/html/assets/theme.js
@@ -0,0 +1,51 @@
+/* Things to do before body loads */
+function restoreDarkMode(){
+ if (localStorage.getItem("darkMode") === "enabled") {
+ $("html").addClass("is-dark");
+ $("html").removeClass("is-white");
+ } else {
+ $("html").removeClass("is-dark");
+ $("html").addClass("is-white");
+ }
+}
+restoreDarkMode();
+
+function updateElementToTheme(isDarkTheme=false){
+ if (!isDarkTheme){
+ let whiteSrc = $("#sysicon").attr("white_src");
+ $("#sysicon").attr("src", whiteSrc);
+ $("#darkModeToggle").html(``);
+
+ // Update the rendering text color in the garphs
+ if (typeof(changeScaleTextColor) != "undefined"){
+ changeScaleTextColor("black");
+ }
+
+ }else{
+ let darkSrc = $("#sysicon").attr("dark_src");
+ $("#sysicon").attr("src", darkSrc);
+ $("#darkModeToggle").html(``);
+
+ // Update the rendering text color in the garphs
+ if (typeof(changeScaleTextColor) != "undefined"){
+ changeScaleTextColor("white");
+ }
+ }
+}
+
+/* Things to do after body loads */
+$(document).ready(function(){
+ $("#darkModeToggle").on("click", function() {
+ $("html").toggleClass("is-dark");
+ $("html").toggleClass("is-white");
+ if ($("html").hasClass("is-dark")) {
+ localStorage.setItem("darkMode", "enabled");
+ updateElementToTheme(true);
+ } else {
+ localStorage.setItem("darkMode", "disabled");
+ updateElementToTheme(false);
+ }
+ });
+
+ updateElementToTheme(localStorage.getItem("darkMode") === "enabled");
+});
\ No newline at end of file
diff --git a/docs/plugins/html/index.html b/docs/plugins/html/index.html
new file mode 100644
index 0000000..991edb6
--- /dev/null
+++ b/docs/plugins/html/index.html
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+ index | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Index
+
+
+ Welcome to the Zoraxy Plugin Documentation!
+
+ Click on a topic in the side menu to begin navigating through the available resources and guides for developing and managing plugins.
+
+
+ FAQ
+
+
+ What skills do I need for developing a plugin?
+
+
+
+ Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
+
+
+
+ Will a plugin crash the whole Zoraxy?
+
+
+
+ No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
+
+
+
+ Can I sell my plugin?
+
+
+
+ Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
+
+
+
+ How can I add my plugin to the official plugin store?
+
+
+
+ To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
+
+
+
+ GNU Free Documentation License
+
+
+
+ This documentation is licensed under the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. You may copy, distribute, and modify this document under the terms of the license.
+
+
+
+
+ A copy of the license is available at
+
+ https://www.gnu.org/licenses/fdl-1.3.html
+
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/html/zoraxy_plugin API.html b/docs/plugins/html/zoraxy_plugin API.html
new file mode 100644
index 0000000..cad0901
--- /dev/null
+++ b/docs/plugins/html/zoraxy_plugin API.html
@@ -0,0 +1,448 @@
+
+
+
+
+
+
+
+ zoraxy_plugin API | Zoraxy Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy Plugin APIs
+
+
+
+ This API documentation is auto-generated from the Zoraxy plugin source code.
+
+
+
+package zoraxy_plugin // import "{{your_module_package_name_in_go.mod}}/mod/plugins/zoraxy_plugin"
+
+
+FUNCTIONS
+
+func ServeIntroSpect(pluginSpect *IntroSpect)
+ 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
+
+
+TYPES
+
+type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+
+}
+ 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
+
+func RecvConfigureSpec() (*ConfigureSpec, error)
+ 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 ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error)
+ ServeAndRecvSpec Function
+
+ This function will serve the intro spect and return the configure spec See
+ the ServeIntroSpect and RecvConfigureSpec for more details
+
+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 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"`
+
+ // Has unexported fields.
+}
+ 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
+
+func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error)
+ DecodeForwardRequestPayload decodes JSON bytes into a
+ DynamicSniffForwardRequest object
+
+func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest
+ GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an
+ http.Request object
+
+func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request
+ GetRequest returns the original http.Request object, for debugging purposes
+
+func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string
+ GetRequestUUID returns the request UUID if this UUID is empty string,
+ that might indicate the request is not coming from the dynamic router
+
+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
+
+ // 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, see Zoraxy documentation for more details
+}
+ IntroSpect Payload
+
+ When the plugin is initialized with -introspect flag, the plugin shell
+ return this payload as JSON and exit
+
+type PathRouter struct {
+ // Has unexported fields.
+}
+
+func NewPathRouter() *PathRouter
+ NewPathRouter creates a new PathRouter
+
+func (p *PathRouter) PrintRequestDebugMessage(r *http.Request)
+
+func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request))
+ RegisterDynamicCaptureHandle register the dynamic capture ingress path with
+ a handler
+
+func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler)
+ 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) RegisterPathHandler(path string, handler http.Handler)
+ RegisterPathHandler registers a handler for a path
+
+func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux)
+ StartStaticCapture starts the static capture ingress
+
+func (p *PathRouter) RemovePathHandler(path string)
+ RemovePathHandler removes a handler for a path
+
+func (p *PathRouter) SetDebugPrintMode(enable bool)
+ SetDebugPrintMode sets the debug print mode
+
+func (p *PathRouter) SetDefaultHandler(handler http.Handler)
+ SetDefaultHandler sets the default handler for the router This handler will
+ be called if no path handler is found
+
+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 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
+ // Has unexported fields.
+}
+
+func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter
+ 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 (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux)
+ Attach the file system UI handler to the target http.ServeMux
+
+func (p *PluginUiDebugRouter) Handler() http.Handler
+ GetHttpHandler returns the http.Handler for the PluginUiRouter
+
+func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
+ 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
+
+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
+ // Has unexported fields.
+}
+
+func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter
+ 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 (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux)
+ Attach the embed UI handler to the target http.ServeMux
+
+func (p *PluginUiRouter) Handler() http.Handler
+ GetHttpHandler returns the http.Handler for the PluginUiRouter
+
+func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux)
+ 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
+
+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 SniffHandler func(*DynamicSniffForwardRequest) SniffResult
+
+type SniffResult int
+
+const (
+ SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
+ SniffResultSkip // Skip this plugin and let the next plugin handle the request
+)
+type StaticCaptureRule struct {
+ CapturePath string `json:"capture_path"`
+}
+
+type SubscriptionEvent struct {
+ EventName string `json:"event_name"`
+ EventSource string `json:"event_source"`
+ Payload string `json:"payload"` //Payload of the event, can be empty
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui
+
+ 2025
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/index.html b/docs/plugins/index.html
new file mode 100644
index 0000000..e41980f
--- /dev/null
+++ b/docs/plugins/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Redirecting...
+
+
+ If you are not redirected automatically, follow this link.
+
+
\ No newline at end of file
diff --git a/docs/plugins/index.json b/docs/plugins/index.json
new file mode 100644
index 0000000..5dc0a19
--- /dev/null
+++ b/docs/plugins/index.json
@@ -0,0 +1,113 @@
+{
+ "title": "",
+ "path": ".",
+ "type": "folder",
+ "files": [
+ {
+ "title": "Introduction",
+ "path": "1. Introduction",
+ "type": "folder",
+ "files": [
+ {
+ "filename": "1. Introduction/1. What is Zoraxy Plugin.md",
+ "title": "What is Zoraxy Plugin",
+ "type": "file"
+ },
+ {
+ "filename": "1. Introduction/2. Getting Started.md",
+ "title": "Getting Started",
+ "type": "file"
+ },
+ {
+ "filename": "1. Introduction/3. Installing Plugin.md",
+ "title": "Installing Plugin",
+ "type": "file"
+ },
+ {
+ "filename": "1. Introduction/4. Enable Plugins.md",
+ "title": "Enable Plugins",
+ "type": "file"
+ },
+ {
+ "filename": "1. Introduction/5. Viewing Plugin Info.md",
+ "title": "Viewing Plugin Info",
+ "type": "file"
+ }
+ ]
+ },
+ {
+ "title": "Architecture",
+ "path": "2. Architecture",
+ "type": "folder",
+ "files": [
+ {
+ "filename": "2. Architecture/1. Plugin Architecture.md",
+ "title": "Plugin Architecture",
+ "type": "file"
+ },
+ {
+ "filename": "2. Architecture/2. Introspect.md",
+ "title": "Introspect",
+ "type": "file"
+ },
+ {
+ "filename": "2. Architecture/3. Configure.md",
+ "title": "Configure",
+ "type": "file"
+ },
+ {
+ "filename": "2. Architecture/4. Capture Modes.md",
+ "title": "Capture Modes",
+ "type": "file"
+ },
+ {
+ "filename": "2. Architecture/5. Plugin UI.md",
+ "title": "Plugin UI",
+ "type": "file"
+ },
+ {
+ "filename": "2. Architecture/6. Compile a Plugin.md",
+ "title": "Compile a Plugin",
+ "type": "file"
+ }
+ ]
+ },
+ {
+ "title": "Basic Examples",
+ "path": "3. Basic Examples",
+ "type": "folder",
+ "files": [
+ {
+ "filename": "3. Basic Examples/1. Hello World.md",
+ "title": "Hello World",
+ "type": "file"
+ },
+ {
+ "filename": "3. Basic Examples/2. RESTful Example.md",
+ "title": "RESTful Example",
+ "type": "file"
+ },
+ {
+ "filename": "3. Basic Examples/3. Static Capture Example.md",
+ "title": "Static Capture Example",
+ "type": "file"
+ },
+ {
+ "filename": "3. Basic Examples/4. Dynamic Capture Example.md",
+ "title": "Dynamic Capture Example",
+ "type": "file"
+ }
+ ]
+ },
+ {
+ "filename": "index.md",
+ "title": "index",
+ "type": "file"
+ },
+ {
+ "filename": "zoraxy_plugin API.md",
+ "title": "zoraxy_plugin API",
+ "type": "file"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/plugins/intro.html b/docs/plugins/intro.html
new file mode 100644
index 0000000..e2fe24d
--- /dev/null
+++ b/docs/plugins/intro.html
@@ -0,0 +1,430 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Introduction
+ What is Zoraxy Plugin?
+
+ Zoraxy Plugin is a powerful extension feature designed to enhance the functionality of the Zoraxy system.
+ It provides additional features and capabilities that are not part of the core system, allowing users to customize their experience and optimize performance.
+ The plugin is built to be modular and flexible, enabling users to tailor their Zoraxy environment to meet specific needs.
+
+
+ Zoraxy plugins are distributed as binaries, and developers have the flexibility to choose whether to open source them or not
+ as the plugin library and interface are open source under the LGPL license.
+
+ There are two primary types of plugins: router and utilities. Router-type plugins are involved with connections from HTTP proxy rules,
+ while utility plugins provide user interfaces for various network features that operate independently of the Zoraxy core.
+
+
+
+ How plugins are distributed & installed
+
+ Zoraxy plugins are distributed as platform-dependent binaries, making them tailored to specific operating systems and CPU architectures.
+ These binaries are named following a convention that includes the operating system, CPU architecture, and plugin name, such as linux_amd64_foobar, windows_amd64_foobar.exe, or linux_arm64_foobar.
+
+ To manually install a plugins for testing, simply place the binary file into the /plugins/{plugin_name}/ folder within your Zoraxy installation directory.
+
+
+
+
+
+ Warning: The binary name inside the folder must match the plugin folder name.
+ For example, the binary should be named foobar (or foobar.exe on Windows)
+ if placed in the /plugins/foobar/ folder.
+ Avoid using names like foobar_plugin.exe.
+
+
+
+
+
+ For distribution, a plugin store system is used. The plugin store architecture is similar to the one built into the Arduino IDE,
+ with a manager URL (a JSON file) where all the plugins supported by that store will be listed.
+ See the documentation section for more details on how to implement your own plugin store.
+
+
+
+ Plugin vs Pull Request
+
+ The Zoraxy plugin was introduced to address specific use cases that enhance its functionality.
+ It serves as an extension to the core Zoraxy system, providing additional features and capabilities while maintaining the integrity of the core system.
+
+
+ -
+ It is designed to handle features that are challenging to integrate directly into the Zoraxy core.
+
+ -
+ It caters to scenarios where certain features are only applicable in limited situations, avoiding unnecessary resource consumption for other users.
+
+ -
+ The plugin allows for frequent updates to specific code components without impacting the core's stability or causing downtime.
+
+
+
+ When should you add a core PR or a plugin?
+
+ In certain situations, implementing a feature as a plugin is more reasonable than directly integrating it into the Zoraxy core:
+
+
+ -
+ Core PR: If the feature is relevant to most users and enhances Zoraxy's core functionality, consider submitting a core Pull Request (PR).
+
+ -
+ Plugin: If the feature is targeted at a smaller user base or requires additional dependencies that not all users need, it should be developed as a plugin.
+
+
+
+ The decision depends on the feature's general relevance and its impact on core stability. Plugins offer flexibility without burdening the core.
+
+
+
+
+ FAQ
+
+ What skills do I need for developing a plugin?
+
+ Basic HTML, JavaScript, and CSS skills are required, with Go (Golang) being the preferred backend language. However, any programming language that can be compiled into a binary and provide a web server interface will work.
+
+
+
+
+ Will a plugin crash the whole Zoraxy?
+
+ No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zoraxy will terminate and disable that plugin without affecting the core operations. This is by design to ensure stability.
+
+
+
+
+ Can I sell my plugin?
+
+ Yes, the plugin library and interface design are open source under the LGPL license. You are not required to disclose the source code of your plugin as long as you do not modify the plugin library and use it as-is. For more details on how to comply with the license, refer to the licensing documentation.
+
+
+
+
+ How can I add my plugin to the official plugin store?
+
+ To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
+
+
+
+
+
+ Ready to start developing your own plugin?
+
+ Begin your journey by exploring the plugin development documentation and setting up your development environment.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 介紹
+ Zoraxy 插件是什麼?
+
+ Zoraxy 插件是一個強大的擴展功能,旨在增強 Zoraxy 系統的功能。
+ 它提供了核心系統中沒有的額外功能和能力,允許用戶自定義體驗並優化性能。
+ 該插件被設計為模組化且靈活的,使用戶能夠根據特定需求調整 Zoraxy 環境。
+
+
+ Zoraxy 插件以二進制文件的形式分發,開發者可以選擇是否開源
+ 因為插件庫和接口是基於 LGPL 授權的開源項目。
+
+ 插件主要分為兩種類型:路由器和工具。路由器類型的插件與 HTTP 代理規則的連接有關,
+ 而工具插件則為各種獨立於 Zoraxy 核心運行的網絡功能提供用戶界面。
+
+
+
+ 插件如何分發與安裝
+
+ Zoraxy 插件以平台相關的二進制文件形式分發,這使得它們針對特定的操作系統和 CPU 架構進行了優化。
+ 這些二進制文件的命名遵循一個約定,包括操作系統、CPU 架構和插件名稱,例如 linux_amd64_foobar、windows_amd64_foobar.exe 或 linux_arm64_foobar。
+
+ 要手動安裝插件進行測試,只需將二進制文件放置在 Zoraxy 安裝目錄中的 /plugins/{plugin_name}/ 文件夾內。
+
+
+
+
+
+ 警告: 文件夾內的二進制文件名稱必須與插件文件夾名稱匹配。
+ 例如,二進制文件應命名為 foobar(或在 Windows 上為 foobar.exe),
+ 如果放置在 /plugins/foobar/ 文件夾中。
+ 請避免使用類似 foobar_plugin.exe 的名稱。
+
+
+
+
+
+ 在分發方面,使用了一個插件商店系統。該插件商店的架構類似於 Arduino IDE 中內置的系統,
+ 包括一個管理器 URL(JSON 文件),其中列出了該商店支持的所有插件。
+ 有關如何實現自己的插件商店的更多詳細信息,請參閱文檔部分。
+
+
+
+ 插件與開 Pull Request 的區別
+
+ Zoraxy 插件的引入是為了解決特定的使用案例,從而增強其功能。
+ 它作為 Zoraxy 核心系統的擴展,提供了額外的功能和能力,同時保持核心系統的完整性。
+
+
+ -
+ 它旨在處理難以直接集成到 Zoraxy 核心中的功能。
+
+ -
+ 它適用於某些功能僅在有限情況下適用的場景,避免了對其他用戶造成不必要的資源消耗。
+
+ -
+ 插件允許對特定代碼組件進行頻繁更新,而不影響核心的穩定性或導致停機。
+
+
+
+ 何時應該新增核心 PR 或插件?
+
+ 在某些情況下,將功能實作為插件比直接整合到 Zoraxy 核心更為合理:
+
+
+ -
+ 核心 PR: 如果功能對大多數用戶都有相關性,並且能提升 Zoraxy 的核心功能,則應考慮提交核心的 Pull Request (PR)。
+
+ -
+ 插件: 如果功能僅針對少數用戶群體,或需要額外的依賴項而非所有用戶都需要,則應開發為插件。
+
+
+
+ 決策取決於功能的普遍相關性及對核心穩定性的影響。插件提供了靈活性,且不會對核心造成負擔。
+
+
+
+
+ 常見問題
+
+ 開發插件需要什麼技能?
+
+ 需要基本的 HTML、JavaScript 和 CSS 技能,後端語言首選 Go(Golang)。然而,任何可以編譯成二進制文件並提供 Web 服務器接口的編程語言都可以使用。
+
+
+
+
+ 插件會導致整個 Zoraxy 崩潰嗎?
+
+ 不會。插件在與 Zoraxy 分離的進程中運行。如果插件崩潰,Zoraxy 會終止並禁用該插件,而不會影響核心操作。這是為了確保穩定性而設計的。
+
+
+
+
+ 我可以出售我的插件嗎?
+
+ 可以,插件庫和接口設計是基於 LGPL 授權的開源項目。只要您不修改插件庫並按原樣使用它,就不需要公開插件的源代碼。關於如何遵守許可證的更多詳細信息,請參閱許可證文檔。
+
+
+
+
+ 如何將我的插件添加到官方插件商店?
+
+ 要將您的插件添加到官方插件商店,請在插件存儲庫中提交一個拉取請求(PR)。
+
+
+
+
+
+ 準備好開始開發自己的插件了嗎?
+
+ 通過探索插件開發文檔並設置您的開發環境,開始您的旅程。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Einführung
+ Was ist das Zoraxy Plugin?
+
+ Das Zoraxy Plugin ist eine leistungsstarke Erweiterungsfunktion, die entwickelt wurde, um die Funktionalität des Zoraxy-Systems zu verbessern.
+ Es bietet zusätzliche Funktionen und Fähigkeiten, die nicht Teil des Kernsystems sind, und ermöglicht es den Benutzern, ihre Erfahrung anzupassen und die Leistung zu optimieren.
+ Das Plugin ist modular und flexibel aufgebaut, sodass Benutzer ihre Zoraxy-Umgebung an spezifische Anforderungen anpassen können.
+
+
+ Zoraxy-Plugins werden als Binärdateien verteilt, und Entwickler können wählen, ob sie diese als Open Source veröffentlichen möchten oder nicht,
+ da die Plugin-Bibliothek und die Schnittstelle unter der LGPL-Lizenz Open Source sind.
+
+ Es gibt zwei Haupttypen von Plugins: Router und Utilities. Router-Plugins sind an Verbindungen von HTTP-Proxy-Regeln beteiligt,
+ während Utility-Plugins Benutzeroberflächen für verschiedene Netzwerkfunktionen bereitstellen, die unabhängig vom Zoraxy-Kern arbeiten.
+
+
+
+ Wie Plugins verteilt und installiert werden
+
+ Zoraxy-Plugins werden als plattformabhängige Binärdateien verteilt, die speziell für bestimmte Betriebssysteme und CPU-Architekturen optimiert sind.
+ Diese Binärdateien werden nach einem Konventionsschema benannt, das das Betriebssystem, die CPU-Architektur und den Plugin-Namen umfasst, wie z. B. linux_amd64_foobar, windows_amd64_foobar.exe oder linux_arm64_foobar.
+
+ Um ein Plugin manuell zu installieren und zu testen, legen Sie die Binärdatei einfach im Ordner /plugins/{plugin_name}/ im Zoraxy-Installationsverzeichnis ab.
+
+
+
+
+
+ Warnung: Der Name der Binärdatei im Ordner muss mit dem Plugin-Ordnernamen übereinstimmen.
+ Beispielsweise sollte die Binärdatei foobar (oder foobar.exe unter Windows) heißen,
+ wenn sie im Ordner /plugins/foobar/ abgelegt wird.
+ Vermeiden Sie Namen wie foobar_plugin.exe.
+
+
+
+
+
+ Für die Verteilung wird ein Plugin-Store-System verwendet. Die Architektur des Plugin-Stores ähnelt der des Arduino IDE,
+ mit einer Manager-URL (einer JSON-Datei), in der alle von diesem Store unterstützten Plugins aufgelistet sind.
+ Weitere Details zur Implementierung eines eigenen Plugin-Stores finden Sie im Dokumentationsabschnitt.
+
+
+
+ Plugin vs Pull Request
+
+ Das Zoraxy-Plugin wurde eingeführt, um spezifische Anwendungsfälle zu adressieren, die seine Funktionalität erweitern.
+ Es dient als Erweiterung des Zoraxy-Kernsystems und bietet zusätzliche Funktionen und Fähigkeiten, während die Integrität des Kernsystems erhalten bleibt.
+
+
+ -
+ Es ist darauf ausgelegt, Funktionen zu behandeln, die schwer direkt in den Zoraxy-Kern zu integrieren sind.
+
+ -
+ Es richtet sich an Szenarien, in denen bestimmte Funktionen nur in begrenzten Situationen anwendbar sind, um unnötigen Ressourcenverbrauch für andere Benutzer zu vermeiden.
+
+ -
+ Das Plugin ermöglicht häufige Updates spezifischer Codekomponenten, ohne die Stabilität des Kerns zu beeinträchtigen oder Ausfallzeiten zu verursachen.
+
+
+
+ Wann sollte ein Core-PR oder ein Plugin hinzugefügt werden?
+
+ In bestimmten Situationen ist es sinnvoller, eine Funktion als Plugin zu implementieren, anstatt sie direkt in den Zoraxy-Kern zu integrieren:
+
+
+ -
+ Core-PR: Wenn die Funktion für die meisten Benutzer relevant ist und die Kernfunktionalität von Zoraxy verbessert, sollten Sie einen Core-Pull-Request (PR) in Betracht ziehen.
+
+ -
+ Plugin: Wenn die Funktion auf eine kleinere Benutzerbasis abzielt oder zusätzliche Abhängigkeiten erfordert, die nicht alle Benutzer benötigen, sollte sie als Plugin entwickelt werden.
+
+
+
+ Die Entscheidung hängt von der allgemeinen Relevanz der Funktion und ihrer Auswirkung auf die Stabilität des Kerns ab. Plugins bieten Flexibilität, ohne den Kern zu belasten.
+
+
+
+
+ FAQ
+
+ Welche Fähigkeiten benötige ich, um ein Plugin zu entwickeln?
+
+ Grundlegende Kenntnisse in HTML, JavaScript und CSS sind erforderlich, wobei Go (Golang) die bevorzugte Backend-Sprache ist.
+ Allerdings kann jede Programmiersprache verwendet werden, die in eine Binärdatei kompiliert werden kann und eine Webserver-Schnittstelle bereitstellt.
+
+
+
+
+ Wird ein Plugin das gesamte Zoraxy zum Absturz bringen?
+
+ Nein. Plugins laufen in einem separaten Prozess von Zoraxy. Wenn ein Plugin abstürzt, beendet und deaktiviert Zoraxy dieses Plugin, ohne die Kernoperationen zu beeinträchtigen. Dies ist so konzipiert, um Stabilität zu gewährleisten.
+
+
+
+
+ Kann ich mein Plugin verkaufen?
+
+ Ja, die Plugin-Bibliothek und das Schnittstellendesign sind unter der LGPL-Lizenz Open Source. Sie sind nicht verpflichtet, den Quellcode Ihres Plugins offenzulegen, solange Sie die Plugin-Bibliothek nicht ändern und sie wie vorgesehen verwenden. Weitere Details zur Einhaltung der Lizenz finden Sie in der Lizenzdokumentation.
+
+
+
+
+ Wie kann ich mein Plugin zum offiziellen Plugin-Store hinzufügen?
+
+ Um Ihr Plugin zum offiziellen Plugin-Store hinzuzufügen, öffnen Sie eine Pull-Request (PR) im Plugin-Repository.
+
+
+
+
+
+ Bereit, Ihr eigenes Plugin zu entwickeln?
+
+ Beginnen Sie Ihre Reise, indem Sie die Plugin-Entwicklungsdokumentation erkunden und Ihre Entwicklungsumgebung einrichten.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/plugins/main.go b/docs/plugins/main.go
new file mode 100644
index 0000000..0059aa0
--- /dev/null
+++ b/docs/plugins/main.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+/* Change this before deploying */
+var (
+ mode = flag.String("m", "web", "Mode to run the application: 'web' or 'build'")
+ root_url = flag.String("root", "/html/", "Root URL for the web server")
+
+ webserver_stop_chan = make(chan bool, 1)
+)
+
+func main() {
+
+ flag.Parse()
+
+ if (*root_url)[0] != '/' {
+ *root_url = "/" + *root_url
+ }
+
+ switch *mode {
+ case "build":
+ build()
+ default:
+ go watchDocsChange()
+ fmt.Println("Running in web mode")
+ startWebServerInBackground()
+ select {}
+ }
+}
+
+func startWebServerInBackground() {
+ go func() {
+ http.DefaultServeMux = http.NewServeMux()
+ server := &http.Server{Addr: ":8080", Handler: http.DefaultServeMux}
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ http.FileServer(http.Dir("./")).ServeHTTP(w, r)
+ })
+
+ go func() {
+ <-webserver_stop_chan
+ fmt.Println("Stopping server at :8080")
+ if err := server.Close(); err != nil {
+ log.Println("Error stopping server:", err)
+ }
+ }()
+
+ fmt.Println("Starting server at :8080")
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatal(err)
+ }
+ }()
+}
+
+func watchDocsChange() {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer watcher.Close()
+
+ err = watcher.Add("./docs")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ if event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Remove|fsnotify.Rename) != 0 {
+ log.Println("Change detected in docs folder:", event)
+ webserver_stop_chan <- true
+ time.Sleep(1 * time.Second) // Allow server to stop gracefully
+ build()
+ startWebServerInBackground()
+ log.Println("Static html files regenerated")
+ }
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ log.Println("Watcher error:", err)
+ }
+ }
+}
diff --git a/docs/plugins/menugen.go b/docs/plugins/menugen.go
new file mode 100644
index 0000000..c215832
--- /dev/null
+++ b/docs/plugins/menugen.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strings"
+)
+
+type MenuItem struct {
+ Title string `json:"title"`
+ Path string `json:"path"`
+ Type string `json:"type"`
+ Files []MenuItem `json:"files,omitempty"`
+ Filename string `json:"filename,omitempty"`
+}
+
+func generateSideMenu(doctreeJson string, selectedTitle string) (string, error) {
+ var root MenuItem
+ if err := json.Unmarshal([]byte(doctreeJson), &root); err != nil {
+ return "", err
+ }
+
+ var buffer bytes.Buffer
+ buffer.WriteString(`
+ `)
+
+ return buffer.String(), nil
+}
+
+func writeMenuItem(buffer *bytes.Buffer, item MenuItem, selectedTitle string) {
+ if item.Type == "file" {
+ activeClass := ""
+ if item.Title == selectedTitle {
+ activeClass = " is-active"
+ }
+
+ //Generate the URL for the file
+ filePath := item.Path
+ if item.Filename != "" {
+ filePath = fmt.Sprintf("%s/%s", item.Path, strings.ReplaceAll(item.Filename, ".md", ".html"))
+ }
+ urlPath := filepath.ToSlash(filepath.Clean(*root_url + filePath))
+ buffer.WriteString(fmt.Sprintf(`
+
+
+ %s
+ `, activeClass, urlPath, item.Title))
+ } else if item.Type == "folder" {
+ buffer.WriteString(fmt.Sprintf(`
+
+ %s
+
+ `, item.Title))
+
+ if len(item.Files) > 0 {
+ buffer.WriteString(`
+ `)
+ }
+ }
+}
diff --git a/docs/plugins/start.sh b/docs/plugins/start.sh
new file mode 100644
index 0000000..c334a61
--- /dev/null
+++ b/docs/plugins/start.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+# This script sets up the environment and runs a Go web server for documentation debug purpose
+./docs.exe
\ No newline at end of file
diff --git a/docs/plugins/template/documents.html b/docs/plugins/template/documents.html
new file mode 100644
index 0000000..f552182
--- /dev/null
+++ b/docs/plugins/template/documents.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+ {{title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{sideMenu}}
+
+
+
+
+ {{content}}
+
+
+
+
+
+
+
+
+
+
+
+ Zoraxy © tobychui 2025
+
+
+
+
+
+
+
diff --git a/example/plugins/dynamic-capture-example/go.mod b/example/plugins/dynamic-capture-example/go.mod
new file mode 100644
index 0000000..fa3d2d9
--- /dev/null
+++ b/example/plugins/dynamic-capture-example/go.mod
@@ -0,0 +1,3 @@
+module example.com/zoraxy/dynamic-capture-example
+
+go 1.23.6
diff --git a/example/plugins/dynamic-capture-example/main.go b/example/plugins/dynamic-capture-example/main.go
new file mode 100644
index 0000000..3f9441d
--- /dev/null
+++ b/example/plugins/dynamic-capture-example/main.go
@@ -0,0 +1,131 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+
+ plugin "example.com/zoraxy/dynamic-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.dynamic-capture-example"
+ UI_PATH = "/debug"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+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: "org.aroz.zoraxy.dynamic-capture-example",
+ Name: "Dynamic Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "This is an example plugin for Zoraxy that demonstrates how to use dynamic captures.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ DynamicCaptureSniff: "/d_sniff",
+ DynamicCaptureIngress: "/d_capture",
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Dynamic Captures
+ */
+ pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
+ //In this example, we want to capture all URI
+ //that start with /foobar and forward it to the dynamic capture handler
+ if strings.HasPrefix(dsfr.RequestURI, "/foobar") {
+ reqUUID := dsfr.GetRequestUUID()
+ fmt.Println("Accepting request with UUID: " + reqUUID)
+
+ // Print all the values of the request
+ fmt.Println("Method:", dsfr.Method)
+ fmt.Println("Hostname:", dsfr.Hostname)
+ fmt.Println("URL:", dsfr.URL)
+ fmt.Println("Header:")
+ for key, values := range dsfr.Header {
+ for _, value := range values {
+ fmt.Printf(" %s: %s\n", key, value)
+ }
+ }
+ fmt.Println("RemoteAddr:", dsfr.RemoteAddr)
+ fmt.Println("Host:", dsfr.Host)
+ fmt.Println("RequestURI:", dsfr.RequestURI)
+ fmt.Println("Proto:", dsfr.Proto)
+ fmt.Println("ProtoMajor:", dsfr.ProtoMajor)
+ fmt.Println("ProtoMinor:", dsfr.ProtoMinor)
+
+ // We want to handle this request, reply with aSniffResultAccept
+ return plugin.SniffResultAccpet
+ }
+
+ // If the request URI does not match, we skip this request
+ fmt.Println("Skipping request with UUID: " + dsfr.GetRequestUUID())
+ return plugin.SniffResultSkip
+ })
+ pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
+ // This is the dynamic capture handler where it actually captures and handle the request
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("Welcome to the dynamic capture handler!"))
+
+ // Print all the request info to the response writer
+ w.Write([]byte("\n\nRequest Info:\n"))
+ w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
+ w.Write([]byte("Request Method: " + r.Method + "\n"))
+ w.Write([]byte("Request Headers:\n"))
+ headers := make([]string, 0, len(r.Header))
+ for key := range r.Header {
+ headers = append(headers, key)
+ }
+ sort.Strings(headers)
+ for _, key := range headers {
+ for _, value := range r.Header[key] {
+ w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
+ }
+ }
+ })
+
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Dynamic capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/README.txt b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/README.txt
similarity index 100%
rename from example/plugins/ztnc/mod/zoraxy_plugin/README.txt
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/README.txt
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/dev_webserver.go
similarity index 100%
rename from example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/dev_webserver.go
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/dynamic_router.go
similarity index 100%
rename from example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/dynamic_router.go
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/embed_webserver.go
similarity index 88%
rename from example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/embed_webserver.go
index b64318f..b68b417 100644
--- a/example/plugins/ztnc/mod/zoraxy_plugin/embed_webserver.go
+++ b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/embed_webserver.go
@@ -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 {
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/static_router.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/static_router.go
similarity index 100%
rename from example/plugins/ztnc/mod/zoraxy_plugin/static_router.go
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/static_router.go
diff --git a/example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/zoraxy_plugin.go
similarity index 100%
rename from example/plugins/ztnc/mod/zoraxy_plugin/zoraxy_plugin.go
rename to example/plugins/dynamic-capture-example/mod/zoraxy_plugin/zoraxy_plugin.go
diff --git a/example/plugins/restful-example/go.mod b/example/plugins/restful-example/go.mod
new file mode 100644
index 0000000..6ec4470
--- /dev/null
+++ b/example/plugins/restful-example/go.mod
@@ -0,0 +1,3 @@
+module example.com/zoraxy/restful-example
+
+go 1.23.6
diff --git a/example/plugins/restful-example/icon.png b/example/plugins/restful-example/icon.png
new file mode 100644
index 0000000..69c3e29
Binary files /dev/null and b/example/plugins/restful-example/icon.png differ
diff --git a/example/plugins/restful-example/main.go b/example/plugins/restful-example/main.go
new file mode 100644
index 0000000..94987a7
--- /dev/null
+++ b/example/plugins/restful-example/main.go
@@ -0,0 +1,103 @@
+package main
+
+import (
+ "embed"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "com.example.restful-example"
+ UI_PATH = "/"
+ WEB_ROOT = "/www"
+)
+
+//go:embed www/*
+var content embed.FS
+
+func main() {
+ // Serve the plugin intro spect
+ // This will print the plugin intro spect and exit if the -introspect flag is provided
+ runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
+ ID: "com.example.restful-example",
+ Name: "Restful Example",
+ Author: "foobar",
+ AuthorContact: "admin@example.com",
+ Description: "A simple demo for making RESTful API calls in plugin",
+ URL: "https://example.com",
+ Type: plugin.PluginType_Utilities,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ // As this is a utility plugin, we don't need to capture any traffic
+ // but only serve the UI, so we set the UI (relative to the plugin path) to "/"
+ UIPath: UI_PATH,
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Create a new PluginEmbedUIRouter that will serve the UI from web folder
+ // The router will also help to handle the termination of the plugin when
+ // a user wants to stop the plugin via Zoraxy Web UI
+ embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
+ embedWebRouter.RegisterTerminateHandler(func() {
+ // Do cleanup here if needed
+ fmt.Println("Restful-example Exited")
+ }, nil)
+
+ //Register a simple API endpoint that will echo the request body
+ // Since we are using the default http.ServeMux, we can register the handler directly with the last
+ // parameter as nil
+ embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
+ // This is a simple echo API that will return the request body as response
+ name := r.URL.Query().Get("name")
+ if name == "" {
+ http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+ }, nil)
+
+ // Here is another example of a POST API endpoint that will echo the form data
+ // This will handle POST requests to /api/post and return the form data as response
+ embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "Failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ for key, values := range r.PostForm {
+ for _, value := range values {
+ // Generate a simple HTML response
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprintf(w, "%s: %s
", key, value)
+ }
+ }
+ }, nil)
+
+ // Serve the restful-example page in the www folder
+ http.Handle(UI_PATH, embedWebRouter.Handler())
+ fmt.Println("Restful-example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+ if err != nil {
+ panic(err)
+ }
+
+}
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/README.txt b/example/plugins/restful-example/mod/zoraxy_plugin/README.txt
new file mode 100644
index 0000000..ed8a405
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/README.txt
@@ -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
\ No newline at end of file
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/dev_webserver.go b/example/plugins/restful-example/mod/zoraxy_plugin/dev_webserver.go
new file mode 100644
index 0000000..9bed106
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/dev_webserver.go
@@ -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())
+}
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/restful-example/mod/zoraxy_plugin/dynamic_router.go
new file mode 100644
index 0000000..1dc53ce
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/dynamic_router.go
@@ -0,0 +1,162 @@
+package zoraxy_plugin
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+/*
+
+ Dynamic Path Handler
+
+*/
+
+type SniffResult int
+
+const (
+ SniffResultAccpet 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 == SniffResultAccpet {
+ 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
+}
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/restful-example/mod/zoraxy_plugin/embed_webserver.go
new file mode 100644
index 0000000..b68b417
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/embed_webserver.go
@@ -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())
+}
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/static_router.go b/example/plugins/restful-example/mod/zoraxy_plugin/static_router.go
new file mode 100644
index 0000000..f4abcb7
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/static_router.go
@@ -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)
+
+ }
+}
diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/restful-example/mod/zoraxy_plugin/zoraxy_plugin.go
new file mode 100644
index 0000000..737e928
--- /dev/null
+++ b/example/plugins/restful-example/mod/zoraxy_plugin/zoraxy_plugin.go
@@ -0,0 +1,176 @@
+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
+}
+
+/*
+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, see Zoraxy documentation for more details
+}
+
+/*
+ServeIntroSpect Function
+
+This function will check if the plugin is initialized with -introspect flag,
+if so, it will print the intro spect and exit
+
+Place this function at the beginning of your plugin main function
+*/
+func ServeIntroSpect(pluginSpect *IntroSpect) {
+ if len(os.Args) > 1 && os.Args[1] == "-introspect" {
+ //Print the intro spect and exit
+ jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
+ fmt.Println(string(jsonData))
+ os.Exit(0)
+ }
+}
+
+/*
+ConfigureSpec Payload
+
+Zoraxy will start your plugin with -configure flag,
+the plugin shell read this payload as JSON and configure itself
+by the supplied values like starting a web server at given port
+that listens to 127.0.0.1:port
+*/
+type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+ //To be expanded
+}
+
+/*
+RecvExecuteConfigureSpec Function
+
+This function will read the configure spec from Zoraxy
+and return the ConfigureSpec object
+
+Place this function after ServeIntroSpect function in your plugin main function
+*/
+func RecvConfigureSpec() (*ConfigureSpec, error) {
+ for i, arg := range os.Args {
+ if strings.HasPrefix(arg, "-configure=") {
+ var configSpec ConfigureSpec
+ if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
+ return nil, err
+ }
+ return &configSpec, nil
+ } else if arg == "-configure" {
+ var configSpec ConfigureSpec
+ var nextArg string
+ if len(os.Args) > i+1 {
+ nextArg = os.Args[i+1]
+ if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, fmt.Errorf("No port specified after -configure flag")
+ }
+ return &configSpec, nil
+ }
+ }
+ return nil, fmt.Errorf("No -configure flag found")
+}
+
+/*
+ServeAndRecvSpec Function
+
+This function will serve the intro spect and return the configure spec
+See the ServeIntroSpect and RecvConfigureSpec for more details
+*/
+func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
+ ServeIntroSpect(pluginSpect)
+ return RecvConfigureSpec()
+}
diff --git a/example/plugins/restful-example/www/index.html b/example/plugins/restful-example/www/index.html
new file mode 100644
index 0000000..6f269a5
--- /dev/null
+++ b/example/plugins/restful-example/www/index.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ RESTful Example
+
+
+
+
+
+
+
+
+
+ RESTFul API Example
+
+ Echo Test (HTTP GET)
+
+
+
+
+
+
+
+
+
+
+
+ Form Post Test (HTTP POST)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/plugins/static-capture-example/go.mod b/example/plugins/static-capture-example/go.mod
new file mode 100644
index 0000000..5323b38
--- /dev/null
+++ b/example/plugins/static-capture-example/go.mod
@@ -0,0 +1,3 @@
+module example.com/zoraxy/static-capture-example
+
+go 1.23.6
diff --git a/example/plugins/static-capture-example/main.go b/example/plugins/static-capture-example/main.go
new file mode 100644
index 0000000..f0f42d4
--- /dev/null
+++ b/example/plugins/static-capture-example/main.go
@@ -0,0 +1,107 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "sort"
+ "strconv"
+
+ plugin "example.com/zoraxy/static-capture-example/mod/zoraxy_plugin"
+)
+
+const (
+ PLUGIN_ID = "org.aroz.zoraxy.static-capture-example"
+ UI_PATH = "/ui"
+ STATIC_CAPTURE_INGRESS = "/s_capture"
+)
+
+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: "org.aroz.zoraxy.static-capture-example",
+ Name: "Static Capture Example",
+ Author: "aroz.org",
+ AuthorContact: "https://aroz.org",
+ Description: "An example for showing how static capture works in Zoraxy.",
+ URL: "https://zoraxy.aroz.org",
+ Type: plugin.PluginType_Router,
+ VersionMajor: 1,
+ VersionMinor: 0,
+ VersionPatch: 0,
+
+ StaticCapturePaths: []plugin.StaticCaptureRule{
+ {
+ CapturePath: "/test_a", // This is the path that will be captured by the static capture handler
+ },
+ {
+ CapturePath: "/test_b", // This is another path that will be captured by the static capture handler
+ },
+ },
+ StaticCaptureIngress: "/s_capture", // This is the ingress path for static capture requests
+
+ UIPath: UI_PATH,
+
+ /*
+ SubscriptionPath: "/subept",
+ SubscriptionsEvents: []plugin.SubscriptionEvent{
+ */
+ })
+ if err != nil {
+ //Terminate or enter standalone mode here
+ panic(err)
+ }
+
+ // Setup the path router
+ pathRouter := plugin.NewPathRouter()
+ //pathRouter.SetDebugPrintMode(true)
+
+ /*
+ Static Routers
+ */
+ pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
+ pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
+ pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // In theory this should never be called
+ // except when there is registered static path in Introspect but you don't create a handler for it (usually a mistake)
+ // but just in case the request is not captured by the path handlers
+ // this will be the fallback handler
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the default handler!
Request URI: " + r.URL.String()))
+ }))
+ pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
+
+ // To simplify the example, we will use the default HTTP ServeMux
+ http.HandleFunc(UI_PATH+"/", RenderDebugUI)
+ fmt.Println("Static path capture example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
+ http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
+}
+
+// Handle the captured request
+func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by A handler!
Request URI: " + r.URL.String()))
+}
+
+func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte("This request is captured by the B handler!
Request URI: " + r.URL.String()))
+}
+
+// Render the debug UI
+func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
+
+ headerKeys := make([]string, 0, len(r.Header))
+ for name := range r.Header {
+ headerKeys = append(headerKeys, name)
+ }
+ sort.Strings(headerKeys)
+ for _, name := range headerKeys {
+ values := r.Header[name]
+ for _, value := range values {
+ fmt.Fprintf(w, "%s: %s\n", name, value)
+ }
+ }
+ w.Header().Set("Content-Type", "text/html")
+}
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/README.txt b/example/plugins/static-capture-example/mod/zoraxy_plugin/README.txt
new file mode 100644
index 0000000..ed8a405
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/README.txt
@@ -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
\ No newline at end of file
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/dev_webserver.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/dev_webserver.go
new file mode 100644
index 0000000..9bed106
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/dev_webserver.go
@@ -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())
+}
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/dynamic_router.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/dynamic_router.go
new file mode 100644
index 0000000..1dc53ce
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/dynamic_router.go
@@ -0,0 +1,162 @@
+package zoraxy_plugin
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+/*
+
+ Dynamic Path Handler
+
+*/
+
+type SniffResult int
+
+const (
+ SniffResultAccpet 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 == SniffResultAccpet {
+ 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
+}
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/embed_webserver.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/embed_webserver.go
new file mode 100644
index 0000000..b68b417
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/embed_webserver.go
@@ -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())
+}
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/static_router.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/static_router.go
new file mode 100644
index 0000000..f4abcb7
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/static_router.go
@@ -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)
+
+ }
+}
diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/zoraxy_plugin.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/zoraxy_plugin.go
new file mode 100644
index 0000000..737e928
--- /dev/null
+++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/zoraxy_plugin.go
@@ -0,0 +1,176 @@
+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
+}
+
+/*
+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, see Zoraxy documentation for more details
+}
+
+/*
+ServeIntroSpect Function
+
+This function will check if the plugin is initialized with -introspect flag,
+if so, it will print the intro spect and exit
+
+Place this function at the beginning of your plugin main function
+*/
+func ServeIntroSpect(pluginSpect *IntroSpect) {
+ if len(os.Args) > 1 && os.Args[1] == "-introspect" {
+ //Print the intro spect and exit
+ jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
+ fmt.Println(string(jsonData))
+ os.Exit(0)
+ }
+}
+
+/*
+ConfigureSpec Payload
+
+Zoraxy will start your plugin with -configure flag,
+the plugin shell read this payload as JSON and configure itself
+by the supplied values like starting a web server at given port
+that listens to 127.0.0.1:port
+*/
+type ConfigureSpec struct {
+ Port int `json:"port"` //Port to listen
+ RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
+ //To be expanded
+}
+
+/*
+RecvExecuteConfigureSpec Function
+
+This function will read the configure spec from Zoraxy
+and return the ConfigureSpec object
+
+Place this function after ServeIntroSpect function in your plugin main function
+*/
+func RecvConfigureSpec() (*ConfigureSpec, error) {
+ for i, arg := range os.Args {
+ if strings.HasPrefix(arg, "-configure=") {
+ var configSpec ConfigureSpec
+ if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
+ return nil, err
+ }
+ return &configSpec, nil
+ } else if arg == "-configure" {
+ var configSpec ConfigureSpec
+ var nextArg string
+ if len(os.Args) > i+1 {
+ nextArg = os.Args[i+1]
+ if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, fmt.Errorf("No port specified after -configure flag")
+ }
+ return &configSpec, nil
+ }
+ }
+ return nil, fmt.Errorf("No -configure flag found")
+}
+
+/*
+ServeAndRecvSpec Function
+
+This function will serve the intro spect and return the configure spec
+See the ServeIntroSpect and RecvConfigureSpec for more details
+*/
+func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
+ ServeIntroSpect(pluginSpect)
+ return RecvConfigureSpec()
+}
diff --git a/example/plugins/static-capture-example/ui_info.go b/example/plugins/static-capture-example/ui_info.go
new file mode 100644
index 0000000..6522496
--- /dev/null
+++ b/example/plugins/static-capture-example/ui_info.go
@@ -0,0 +1,5 @@
+package main
+
+import (
+ _ "embed"
+)
diff --git a/example/plugins/ztnc/README.md b/example/plugins/ztnc/README.md
deleted file mode 100644
index a942efd..0000000
--- a/example/plugins/ztnc/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-## Global Area Network Plugin
-
-This plugin implements a user interface for ZeroTier Network Controller in Zoraxy
-
-
-
-
-
-## License
-
-AGPL
\ No newline at end of file
diff --git a/example/plugins/ztnc/go.mod b/example/plugins/ztnc/go.mod
deleted file mode 100644
index aa0cc97..0000000
--- a/example/plugins/ztnc/go.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module aroz.org/zoraxy/ztnc
-
-go 1.23.6
-
-require (
- github.com/boltdb/bolt v1.3.1
- github.com/syndtr/goleveldb v1.0.0
- golang.org/x/sys v0.30.0
-)
-
-require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
diff --git a/example/plugins/ztnc/go.sum b/example/plugins/ztnc/go.sum
deleted file mode 100644
index 875979f..0000000
--- a/example/plugins/ztnc/go.sum
+++ /dev/null
@@ -1,30 +0,0 @@
-github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
-github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
-github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/example/plugins/ztnc/icon.png b/example/plugins/ztnc/icon.png
deleted file mode 100644
index e19e043..0000000
Binary files a/example/plugins/ztnc/icon.png and /dev/null differ
diff --git a/example/plugins/ztnc/icon.psd b/example/plugins/ztnc/icon.psd
deleted file mode 100644
index e8c221b..0000000
Binary files a/example/plugins/ztnc/icon.psd and /dev/null differ
diff --git a/example/plugins/ztnc/main.go b/example/plugins/ztnc/main.go
deleted file mode 100644
index d3182ac..0000000
--- a/example/plugins/ztnc/main.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package main
-
-import (
- "fmt"
- "net/http"
- "strconv"
-
- "embed"
-
- "aroz.org/zoraxy/ztnc/mod/database"
- "aroz.org/zoraxy/ztnc/mod/ganserv"
- plugin "aroz.org/zoraxy/ztnc/mod/zoraxy_plugin"
-)
-
-const (
- PLUGIN_ID = "org.aroz.zoraxy.ztnc"
- UI_RELPATH = "/ui"
- EMBED_FS_ROOT = "/web"
- DB_FILE_PATH = "ztnc.db"
- AUTH_TOKEN_PATH = "./authtoken.secret"
-)
-
-//go:embed web/*
-var content embed.FS
-
-var (
- sysdb *database.Database
- ganManager *ganserv.NetworkManager
-)
-
-func main() {
- // Serve the plugin intro spect
- runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
- ID: PLUGIN_ID,
- Name: "ztnc",
- Author: "aroz.org",
- AuthorContact: "zoraxy.aroz.org",
- Description: "UI for ZeroTier Network Controller",
- URL: "https://zoraxy.aroz.org",
- Type: plugin.PluginType_Utilities,
- VersionMajor: 1,
- VersionMinor: 0,
- VersionPatch: 0,
-
- // As this is a utility plugin, we don't need to capture any traffic
- // but only serve the UI, so we set the UI (relative to the plugin path) to "/ui/" to match the HTTP Handler
- UIPath: UI_RELPATH,
- })
- if err != nil {
- //Terminate or enter standalone mode here
- panic(err)
- }
-
- // Create a new PluginEmbedUIRouter that will serve the UI from web folder
- uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
- uiRouter.EnableDebug = true
-
- // Register the shutdown handler
- uiRouter.RegisterTerminateHandler(func() {
- // Do cleanup here if needed
- if sysdb != nil {
- sysdb.Close()
- }
- fmt.Println("ztnc Exited")
- }, nil)
-
- // This will serve the index.html file embedded in the binary
- targetHandler := uiRouter.Handler()
- http.Handle(UI_RELPATH+"/", targetHandler)
-
- // Start the GAN Network Controller
- err = startGanNetworkController()
- if err != nil {
- panic(err)
- }
-
- // Initiate the API endpoints
- initApiEndpoints()
-
- // Start the HTTP server, only listen to loopback interface
- fmt.Println("Plugin UI server started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port) + UI_RELPATH)
- http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
-}
diff --git a/example/plugins/ztnc/mod/database/database.go b/example/plugins/ztnc/mod/database/database.go
deleted file mode 100644
index bf82ae0..0000000
--- a/example/plugins/ztnc/mod/database/database.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package database
-
-/*
- ArOZ Online Database Access Module
- author: tobychui
-
- This is an improved Object oriented base solution to the original
- aroz online database script.
-*/
-
-import (
- "log"
- "runtime"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbinc"
-)
-
-type Database struct {
- Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
- BackendType dbinc.BackendType
- Backend dbinc.Backend
-}
-
-func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
- if runtime.GOARCH == "riscv64" {
- log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
- }
- return newDatabase(dbfile, backendType)
-}
-
-// Get the recommended backend type for the current system
-func GetRecommendedBackendType() dbinc.BackendType {
- //Check if the system is running on RISCV hardware
- if runtime.GOARCH == "riscv64" {
- //RISCV hardware, currently only support FS emulated database
- return dbinc.BackendFSOnly
- } else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
- //Powerful hardware
- return dbinc.BackendBoltDB
- //return dbinc.BackendLevelDB
- }
-
- //Default to BoltDB, the safest option
- return dbinc.BackendBoltDB
-}
-
-/*
- Create / Drop a table
- Usage:
- err := sysdb.NewTable("MyTable")
- err := sysdb.DropTable("MyTable")
-*/
-
-// Create a new table
-func (d *Database) NewTable(tableName string) error {
- return d.newTable(tableName)
-}
-
-// Check is table exists
-func (d *Database) TableExists(tableName string) bool {
- return d.tableExists(tableName)
-}
-
-// Drop the given table
-func (d *Database) DropTable(tableName string) error {
- return d.dropTable(tableName)
-}
-
-/*
-Write to database with given tablename and key. Example Usage:
-
- type demo struct{
- content string
- }
-
- thisDemo := demo{
- content: "Hello World",
- }
-
-err := sysdb.Write("MyTable", "username/message",thisDemo);
-*/
-func (d *Database) Write(tableName string, key string, value interface{}) error {
- return d.write(tableName, key, value)
-}
-
-/*
- Read from database and assign the content to a given datatype. Example Usage:
-
- type demo struct{
- content string
- }
- thisDemo := new(demo)
- err := sysdb.Read("MyTable", "username/message",&thisDemo);
-*/
-
-func (d *Database) Read(tableName string, key string, assignee interface{}) error {
- return d.read(tableName, key, assignee)
-}
-
-/*
-Check if a key exists in the database table given tablename and key
-
- if sysdb.KeyExists("MyTable", "username/message"){
- log.Println("Key exists")
- }
-*/
-func (d *Database) KeyExists(tableName string, key string) bool {
- return d.keyExists(tableName, key)
-}
-
-/*
-Delete a value from the database table given tablename and key
-
-err := sysdb.Delete("MyTable", "username/message");
-*/
-func (d *Database) Delete(tableName string, key string) error {
- return d.delete(tableName, key)
-}
-
-/*
- //List table example usage
- //Assume the value is stored as a struct named "groupstruct"
-
- entries, err := sysdb.ListTable("test")
- if err != nil {
- panic(err)
- }
- for _, keypairs := range entries{
- log.Println(string(keypairs[0]))
- group := new(groupstruct)
- json.Unmarshal(keypairs[1], &group)
- log.Println(group);
- }
-
-*/
-
-func (d *Database) ListTable(tableName string) ([][][]byte, error) {
- return d.listTable(tableName)
-}
-
-/*
-Close the database connection
-*/
-func (d *Database) Close() {
- d.close()
-}
diff --git a/example/plugins/ztnc/mod/database/database_core.go b/example/plugins/ztnc/mod/database/database_core.go
deleted file mode 100644
index 347b000..0000000
--- a/example/plugins/ztnc/mod/database/database_core.go
+++ /dev/null
@@ -1,70 +0,0 @@
-//go:build !mipsle && !riscv64
-// +build !mipsle,!riscv64
-
-package database
-
-import (
- "errors"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbbolt"
- "aroz.org/zoraxy/ztnc/mod/database/dbinc"
- "aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
-)
-
-func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
- if backendType == dbinc.BackendFSOnly {
- return nil, errors.New("Unsupported backend type for this platform")
- }
-
- if backendType == dbinc.BackendLevelDB {
- db, err := dbleveldb.NewDB(dbfile)
- return &Database{
- Db: nil,
- BackendType: backendType,
- Backend: db,
- }, err
- }
-
- db, err := dbbolt.NewBoltDatabase(dbfile)
- return &Database{
- Db: nil,
- BackendType: backendType,
- Backend: db,
- }, err
-}
-
-func (d *Database) newTable(tableName string) error {
- return d.Backend.NewTable(tableName)
-}
-
-func (d *Database) tableExists(tableName string) bool {
- return d.Backend.TableExists(tableName)
-}
-
-func (d *Database) dropTable(tableName string) error {
- return d.Backend.DropTable(tableName)
-}
-
-func (d *Database) write(tableName string, key string, value interface{}) error {
- return d.Backend.Write(tableName, key, value)
-}
-
-func (d *Database) read(tableName string, key string, assignee interface{}) error {
- return d.Backend.Read(tableName, key, assignee)
-}
-
-func (d *Database) keyExists(tableName string, key string) bool {
- return d.Backend.KeyExists(tableName, key)
-}
-
-func (d *Database) delete(tableName string, key string) error {
- return d.Backend.Delete(tableName, key)
-}
-
-func (d *Database) listTable(tableName string) ([][][]byte, error) {
- return d.Backend.ListTable(tableName)
-}
-
-func (d *Database) close() {
- d.Backend.Close()
-}
diff --git a/example/plugins/ztnc/mod/database/database_openwrt.go b/example/plugins/ztnc/mod/database/database_openwrt.go
deleted file mode 100644
index fd3d8b2..0000000
--- a/example/plugins/ztnc/mod/database/database_openwrt.go
+++ /dev/null
@@ -1,196 +0,0 @@
-//go:build mipsle || riscv64
-// +build mipsle riscv64
-
-package database
-
-import (
- "encoding/json"
- "errors"
- "log"
- "os"
- "path/filepath"
- "strings"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbinc"
-)
-
-/*
- OpenWRT or RISCV backend
-
- For OpenWRT or RISCV platform, we will use the filesystem as the database backend
- as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
- in conditional compilation will create a build error on these platforms
-*/
-
-func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
- dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
- dbRootPath = "fsdb/" + dbRootPath
- err := os.MkdirAll(dbRootPath, 0755)
- if err != nil {
- return nil, err
- }
-
- log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
- return &Database{
- Db: dbRootPath,
- BackendType: dbinc.BackendFSOnly,
- Backend: nil,
- }, nil
-}
-
-func (d *Database) dump(filename string) ([]string, error) {
- //Get all file objects from root
- rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*"))
- if err != nil {
- return []string{}, err
- }
-
- //Filter out the folders
- rootFolders := []string{}
- for _, file := range rootfiles {
- if !isDirectory(file) {
- rootFolders = append(rootFolders, filepath.Base(file))
- }
- }
-
- return rootFolders, nil
-}
-
-func (d *Database) newTable(tableName string) error {
-
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- if !fileExists(tablePath) {
- return os.MkdirAll(tablePath, 0755)
- }
- return nil
-}
-
-func (d *Database) tableExists(tableName string) bool {
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) {
- return false
- }
-
- if !isDirectory(tablePath) {
- return false
- }
-
- return true
-}
-
-func (d *Database) dropTable(tableName string) error {
-
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- if d.tableExists(tableName) {
- return os.RemoveAll(tablePath)
- } else {
- return errors.New("table not exists")
- }
-
-}
-
-func (d *Database) write(tableName string, key string, value interface{}) error {
-
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- js, err := json.Marshal(value)
- if err != nil {
- return err
- }
-
- key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
-
- return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755)
-}
-
-func (d *Database) read(tableName string, key string, assignee interface{}) error {
- if !d.keyExists(tableName, key) {
- return errors.New("key not exists")
- }
-
- key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
-
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- entryPath := filepath.Join(tablePath, key+".entry")
- content, err := os.ReadFile(entryPath)
- if err != nil {
- return err
- }
-
- err = json.Unmarshal(content, &assignee)
- return err
-}
-
-func (d *Database) keyExists(tableName string, key string) bool {
- key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- entryPath := filepath.Join(tablePath, key+".entry")
- return fileExists(entryPath)
-}
-
-func (d *Database) delete(tableName string, key string) error {
-
- if !d.keyExists(tableName, key) {
- return errors.New("key not exists")
- }
- key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- entryPath := filepath.Join(tablePath, key+".entry")
-
- return os.Remove(entryPath)
-}
-
-func (d *Database) listTable(tableName string) ([][][]byte, error) {
- if !d.tableExists(tableName) {
- return [][][]byte{}, errors.New("table not exists")
- }
- tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
- entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry"))
- if err != nil {
- return [][][]byte{}, err
- }
-
- var results [][][]byte = [][][]byte{}
- for _, entry := range entries {
- if !isDirectory(entry) {
- //Read it
- key := filepath.Base(entry)
- key = strings.TrimSuffix(key, filepath.Ext(key))
- key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/")
-
- bkey := []byte(key)
- bval := []byte("")
- c, err := os.ReadFile(entry)
- if err != nil {
- break
- }
-
- bval = c
- results = append(results, [][]byte{bkey, bval})
- }
- }
- return results, nil
-}
-
-func (d *Database) close() {
- //Nothing to close as it is file system
-}
-
-func isDirectory(path string) bool {
- fileInfo, err := os.Stat(path)
- if err != nil {
- return false
- }
-
- return fileInfo.IsDir()
-}
-
-func fileExists(name string) bool {
- _, err := os.Stat(name)
- if err == nil {
- return true
- }
- if errors.Is(err, os.ErrNotExist) {
- return false
- }
- return false
-}
diff --git a/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go b/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go
deleted file mode 100644
index 8cf7ec0..0000000
--- a/example/plugins/ztnc/mod/database/dbbolt/dbbolt.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package dbbolt
-
-import (
- "encoding/json"
- "errors"
-
- "github.com/boltdb/bolt"
-)
-
-type Database struct {
- Db interface{} //This is the bolt database object
-}
-
-func NewBoltDatabase(dbfile string) (*Database, error) {
- db, err := bolt.Open(dbfile, 0600, nil)
- if err != nil {
- return nil, err
- }
-
- return &Database{
- Db: db,
- }, err
-}
-
-// Create a new table
-func (d *Database) NewTable(tableName string) error {
- err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
- _, err := tx.CreateBucketIfNotExists([]byte(tableName))
- if err != nil {
- return err
- }
- return nil
- })
-
- return err
-}
-
-// Check is table exists
-func (d *Database) TableExists(tableName string) bool {
- return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(tableName))
- if b == nil {
- return errors.New("table not exists")
- }
- return nil
- }) == nil
-}
-
-// Drop the given table
-func (d *Database) DropTable(tableName string) error {
- err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
- err := tx.DeleteBucket([]byte(tableName))
- if err != nil {
- return err
- }
- return nil
- })
- return err
-}
-
-// Write to table
-func (d *Database) Write(tableName string, key string, value interface{}) error {
- jsonString, err := json.Marshal(value)
- if err != nil {
- return err
- }
- err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
- _, err := tx.CreateBucketIfNotExists([]byte(tableName))
- if err != nil {
- return err
- }
- b := tx.Bucket([]byte(tableName))
- err = b.Put([]byte(key), jsonString)
- return err
- })
- return err
-}
-
-func (d *Database) Read(tableName string, key string, assignee interface{}) error {
- err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(tableName))
- v := b.Get([]byte(key))
- json.Unmarshal(v, &assignee)
- return nil
- })
- return err
-}
-
-func (d *Database) KeyExists(tableName string, key string) bool {
- resultIsNil := false
- if !d.TableExists(tableName) {
- //Table not exists. Do not proceed accessing key
- //log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
- return false
- }
- err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(tableName))
- v := b.Get([]byte(key))
- if v == nil {
- resultIsNil = true
- }
- return nil
- })
-
- if err != nil {
- return false
- } else {
- if resultIsNil {
- return false
- } else {
- return true
- }
- }
-}
-
-func (d *Database) Delete(tableName string, key string) error {
- err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
- tx.Bucket([]byte(tableName)).Delete([]byte(key))
- return nil
- })
-
- return err
-}
-
-func (d *Database) ListTable(tableName string) ([][][]byte, error) {
- var results [][][]byte
- err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
- b := tx.Bucket([]byte(tableName))
- c := b.Cursor()
-
- for k, v := c.First(); k != nil; k, v = c.Next() {
- results = append(results, [][]byte{k, v})
- }
- return nil
- })
- return results, err
-}
-
-func (d *Database) Close() {
- d.Db.(*bolt.DB).Close()
-}
diff --git a/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go b/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go
deleted file mode 100644
index 05e708a..0000000
--- a/example/plugins/ztnc/mod/database/dbbolt/dbbolt_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package dbbolt_test
-
-import (
- "os"
- "testing"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbbolt"
-)
-
-func TestNewBoltDatabase(t *testing.T) {
- dbfile := "test.db"
- defer os.Remove(dbfile)
-
- db, err := dbbolt.NewBoltDatabase(dbfile)
- if err != nil {
- t.Fatalf("Failed to create new Bolt database: %v", err)
- }
- defer db.Close()
-
- if db.Db == nil {
- t.Fatalf("Expected non-nil database object")
- }
-}
-
-func TestNewTable(t *testing.T) {
- dbfile := "test.db"
- defer os.Remove(dbfile)
-
- db, err := dbbolt.NewBoltDatabase(dbfile)
- if err != nil {
- t.Fatalf("Failed to create new Bolt database: %v", err)
- }
- defer db.Close()
-
- err = db.NewTable("testTable")
- if err != nil {
- t.Fatalf("Failed to create new table: %v", err)
- }
-}
-
-func TestTableExists(t *testing.T) {
- dbfile := "test.db"
- defer os.Remove(dbfile)
-
- db, err := dbbolt.NewBoltDatabase(dbfile)
- if err != nil {
- t.Fatalf("Failed to create new Bolt database: %v", err)
- }
- defer db.Close()
-
- tableName := "testTable"
- err = db.NewTable(tableName)
- if err != nil {
- t.Fatalf("Failed to create new table: %v", err)
- }
-
- exists := db.TableExists(tableName)
- if !exists {
- t.Fatalf("Expected table %s to exist", tableName)
- }
-
- nonExistentTable := "nonExistentTable"
- exists = db.TableExists(nonExistentTable)
- if exists {
- t.Fatalf("Expected table %s to not exist", nonExistentTable)
- }
-}
diff --git a/example/plugins/ztnc/mod/database/dbinc/dbinc.go b/example/plugins/ztnc/mod/database/dbinc/dbinc.go
deleted file mode 100644
index 8e60ba0..0000000
--- a/example/plugins/ztnc/mod/database/dbinc/dbinc.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package dbinc
-
-/*
- dbinc is the interface for all database backend
-*/
-type BackendType int
-
-const (
- BackendBoltDB BackendType = iota //Default backend
- BackendFSOnly //OpenWRT or RISCV backend
- BackendLevelDB //LevelDB backend
-
- BackEndAuto = BackendBoltDB
-)
-
-type Backend interface {
- NewTable(tableName string) error
- TableExists(tableName string) bool
- DropTable(tableName string) error
- Write(tableName string, key string, value interface{}) error
- Read(tableName string, key string, assignee interface{}) error
- KeyExists(tableName string, key string) bool
- Delete(tableName string, key string) error
- ListTable(tableName string) ([][][]byte, error)
- Close()
-}
-
-func (b BackendType) String() string {
- switch b {
- case BackendBoltDB:
- return "BoltDB"
- case BackendFSOnly:
- return "File System Emulated Key-Value Store"
- case BackendLevelDB:
- return "LevelDB"
- default:
- return "Unknown"
- }
-}
diff --git a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go
deleted file mode 100644
index 59b9667..0000000
--- a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package dbleveldb
-
-import (
- "encoding/json"
- "log"
- "path/filepath"
- "strings"
- "sync"
- "time"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbinc"
- "github.com/syndtr/goleveldb/leveldb"
- "github.com/syndtr/goleveldb/leveldb/util"
-)
-
-// Ensure the DB struct implements the Backend interface
-var _ dbinc.Backend = (*DB)(nil)
-
-type DB struct {
- db *leveldb.DB
- Table sync.Map //For emulating table creation
- batch leveldb.Batch //Batch write
- writeFlushTicker *time.Ticker //Ticker for flushing data into disk
- writeFlushStop chan bool //Stop channel for write flush ticker
-}
-
-func NewDB(path string) (*DB, error) {
- //If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
- if filepath.Ext(path) != "" {
- path = strings.ReplaceAll(path, ".", "_")
- }
-
- db, err := leveldb.OpenFile(path, nil)
- if err != nil {
- return nil, err
- }
-
- thisDB := &DB{
- db: db,
- Table: sync.Map{},
- batch: leveldb.Batch{},
- }
-
- //Create a ticker to flush data into disk every 1 seconds
- writeFlushTicker := time.NewTicker(1 * time.Second)
- writeFlushStop := make(chan bool)
- go func() {
- for {
- select {
- case <-writeFlushTicker.C:
- if thisDB.batch.Len() == 0 {
- //No flushing needed
- continue
- }
- err = db.Write(&thisDB.batch, nil)
- if err != nil {
- log.Println("[LevelDB] Failed to flush data into disk: ", err)
- }
- thisDB.batch.Reset()
- case <-writeFlushStop:
- return
- }
- }
- }()
-
- thisDB.writeFlushTicker = writeFlushTicker
- thisDB.writeFlushStop = writeFlushStop
-
- return thisDB, nil
-}
-
-func (d *DB) NewTable(tableName string) error {
- //Create a table entry in the sync.Map
- d.Table.Store(tableName, true)
- return nil
-}
-
-func (d *DB) TableExists(tableName string) bool {
- _, ok := d.Table.Load(tableName)
- return ok
-}
-
-func (d *DB) DropTable(tableName string) error {
- d.Table.Delete(tableName)
- iter := d.db.NewIterator(nil, nil)
- defer iter.Release()
-
- for iter.Next() {
- key := iter.Key()
- if filepath.Dir(string(key)) == tableName {
- err := d.db.Delete(key, nil)
- if err != nil {
- return err
- }
- }
- }
-
- return nil
-}
-
-func (d *DB) Write(tableName string, key string, value interface{}) error {
- data, err := json.Marshal(value)
- if err != nil {
- return err
- }
- d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
- return nil
-}
-
-func (d *DB) Read(tableName string, key string, assignee interface{}) error {
- data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
- if err != nil {
- return err
- }
- return json.Unmarshal(data, assignee)
-}
-
-func (d *DB) KeyExists(tableName string, key string) bool {
- _, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
- return err == nil
-}
-
-func (d *DB) Delete(tableName string, key string) error {
- return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
-}
-
-func (d *DB) ListTable(tableName string) ([][][]byte, error) {
- iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
- defer iter.Release()
-
- var result [][][]byte
- for iter.Next() {
- key := iter.Key()
- //The key contains the table name as prefix. Trim it before returning
- value := iter.Value()
- result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
- }
-
- err := iter.Error()
- if err != nil {
- return nil, err
- }
- return result, nil
-}
-
-func (d *DB) Close() {
- //Write the remaining data in batch back into disk
- d.writeFlushStop <- true
- d.writeFlushTicker.Stop()
- d.db.Write(&d.batch, nil)
- d.db.Close()
-}
diff --git a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go b/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go
deleted file mode 100644
index c091684..0000000
--- a/example/plugins/ztnc/mod/database/dbleveldb/dbleveldb_test.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package dbleveldb_test
-
-import (
- "os"
- "testing"
-
- "aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
-)
-
-func TestNewDB(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-}
-
-func TestNewTable(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-
- err = db.NewTable("testTable")
- if err != nil {
- t.Fatalf("Failed to create new table: %v", err)
- }
-}
-
-func TestTableExists(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-
- db.NewTable("testTable")
- if !db.TableExists("testTable") {
- t.Fatalf("Table should exist")
- }
-}
-
-func TestDropTable(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-
- db.NewTable("testTable")
- err = db.DropTable("testTable")
- if err != nil {
- t.Fatalf("Failed to drop table: %v", err)
- }
-
- if db.TableExists("testTable") {
- t.Fatalf("Table should not exist")
- }
-}
-
-func TestWriteAndRead(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-
- db.NewTable("testTable")
- err = db.Write("testTable", "testKey", "testValue")
- if err != nil {
- t.Fatalf("Failed to write to table: %v", err)
- }
-
- var value string
- err = db.Read("testTable", "testKey", &value)
- if err != nil {
- t.Fatalf("Failed to read from table: %v", err)
- }
-
- if value != "testValue" {
- t.Fatalf("Expected 'testValue', got '%v'", value)
- }
-}
-func TestListTable(t *testing.T) {
- path := "/tmp/testdb"
- defer os.RemoveAll(path)
-
- db, err := dbleveldb.NewDB(path)
- if err != nil {
- t.Fatalf("Failed to create new DB: %v", err)
- }
- defer db.Close()
-
- db.NewTable("testTable")
- err = db.Write("testTable", "testKey1", "testValue1")
- if err != nil {
- t.Fatalf("Failed to write to table: %v", err)
- }
- err = db.Write("testTable", "testKey2", "testValue2")
- if err != nil {
- t.Fatalf("Failed to write to table: %v", err)
- }
-
- result, err := db.ListTable("testTable")
- if err != nil {
- t.Fatalf("Failed to list table: %v", err)
- }
-
- if len(result) != 2 {
- t.Fatalf("Expected 2 entries, got %v", len(result))
- }
-
- expected := map[string]string{
- "testTable/testKey1": "\"testValue1\"",
- "testTable/testKey2": "\"testValue2\"",
- }
-
- for _, entry := range result {
- key := string(entry[0])
- value := string(entry[1])
- if expected[key] != value {
- t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
- }
- }
-}
diff --git a/example/plugins/ztnc/mod/ganserv/authkey.go b/example/plugins/ztnc/mod/ganserv/authkey.go
deleted file mode 100644
index 006e90d..0000000
--- a/example/plugins/ztnc/mod/ganserv/authkey.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package ganserv
-
-import (
- "errors"
- "log"
- "os"
- "runtime"
- "strings"
-)
-
-func TryLoadorAskUserForAuthkey() (string, error) {
- //Check for zt auth token
- value, exists := os.LookupEnv("ZT_AUTH")
- if !exists {
- log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
- } else {
- return value, nil
- }
-
- authKey := ""
- if runtime.GOOS == "windows" {
- if isAdmin() {
- //Read the secret file directly
- b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
- if err == nil {
- log.Println("Zerotier authkey loaded")
- authKey = string(b)
- } else {
- log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
- }
- } else {
- //Elavate the permission to admin
- ak, err := readAuthTokenAsAdmin()
- if err == nil {
- log.Println("Zerotier authkey loaded")
- authKey = ak
- } else {
- log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
- }
- }
-
- } else if runtime.GOOS == "linux" {
- if isAdmin() {
- //Try to read from source using sudo
- ak, err := readAuthTokenAsAdmin()
- if err == nil {
- log.Println("Zerotier authkey loaded")
- authKey = strings.TrimSpace(ak)
- } else {
- log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
- }
- } else {
- //Try read from source
- b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
- if err == nil {
- log.Println("Zerotier authkey loaded")
- authKey = string(b)
- } else {
- log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
- }
- }
-
- } else if runtime.GOOS == "darwin" {
- b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
- if err == nil {
- log.Println("Zerotier authkey loaded")
- authKey = string(b)
- } else {
- log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
- }
- }
-
- authKey = strings.TrimSpace(authKey)
-
- if authKey == "" {
- return "", errors.New("Unable to load authkey from file")
- }
-
- return authKey, nil
-}
diff --git a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go b/example/plugins/ztnc/mod/ganserv/authkeyLinux.go
deleted file mode 100644
index 91ce202..0000000
--- a/example/plugins/ztnc/mod/ganserv/authkeyLinux.go
+++ /dev/null
@@ -1,37 +0,0 @@
-//go:build linux
-// +build linux
-
-package ganserv
-
-import (
- "os"
- "os/exec"
- "os/user"
- "strings"
-
- "aroz.org/zoraxy/ztnc/mod/utils"
-)
-
-func readAuthTokenAsAdmin() (string, error) {
- if utils.FileExists("./conf/authtoken.secret") {
- authKey, err := os.ReadFile("./conf/authtoken.secret")
- if err == nil {
- return strings.TrimSpace(string(authKey)), nil
- }
- }
-
- cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret")
- output, err := cmd.Output()
- if err != nil {
- return "", err
- }
- return string(output), nil
-}
-
-func isAdmin() bool {
- currentUser, err := user.Current()
- if err != nil {
- return false
- }
- return currentUser.Username == "root"
-}
diff --git a/example/plugins/ztnc/mod/ganserv/authkeyWin.go b/example/plugins/ztnc/mod/ganserv/authkeyWin.go
deleted file mode 100644
index ac5c260..0000000
--- a/example/plugins/ztnc/mod/ganserv/authkeyWin.go
+++ /dev/null
@@ -1,62 +0,0 @@
-//go:build windows
-// +build windows
-
-package ganserv
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "syscall"
-
- "aroz.org/zoraxy/ztnc/mod/utils"
- "golang.org/x/sys/windows"
-)
-
-// Use admin permission to read auth token on Windows
-func readAuthTokenAsAdmin() (string, error) {
- //Check if the previous startup already extracted the authkey
- if utils.FileExists("./conf/authtoken.secret") {
- authKey, err := os.ReadFile("./conf/authtoken.secret")
- if err == nil {
- return strings.TrimSpace(string(authKey)), nil
- }
- }
-
- verb := "runas"
- exe := "cmd.exe"
- cwd, _ := os.Getwd()
-
- output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
- os.WriteFile(output, []byte(""), 0775)
- args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
-
- verbPtr, _ := syscall.UTF16PtrFromString(verb)
- exePtr, _ := syscall.UTF16PtrFromString(exe)
- cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
- argPtr, _ := syscall.UTF16PtrFromString(args)
-
- var showCmd int32 = 1 //SW_NORMAL
-
- err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
- if err != nil {
- return "", err
- }
-
- authKey, err := os.ReadFile("./conf/authtoken.secret")
- if err != nil {
- return "", err
- }
-
- return strings.TrimSpace(string(authKey)), nil
-}
-
-// Check if admin on Windows
-func isAdmin() bool {
- _, err := os.Open("\\\\.\\PHYSICALDRIVE0")
- if err != nil {
- return false
- }
- return true
-}
diff --git a/example/plugins/ztnc/mod/ganserv/ganserv.go b/example/plugins/ztnc/mod/ganserv/ganserv.go
deleted file mode 100644
index f81e39b..0000000
--- a/example/plugins/ztnc/mod/ganserv/ganserv.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package ganserv
-
-import (
- "log"
- "net"
-
- "aroz.org/zoraxy/ztnc/mod/database"
-)
-
-/*
- Global Area Network
- Server side implementation
-
- This module do a few things to help manage
- the system GANs
-
- - Provide DHCP assign to client
- - Provide a list of connected nodes in the same VLAN
- - Provide proxy of packet if the target VLAN is online but not reachable
-
- Also provide HTTP Handler functions for management
- - Create Network
- - Update Network Properties (Name / Desc)
- - Delete Network
-
- - Authorize Node
- - Deauthorize Node
- - Set / Get Network Prefered Subnet Mask
- - Handle Node ping
-*/
-
-type Node struct {
- Auth bool //If the node is authorized in this network
- ClientID string //The client ID
- MAC string //The tap MAC this client is using
- Name string //Name of the client in this network
- Description string //Description text
- ManagedIP net.IP //The IP address assigned by this network
- LastSeen int64 //Last time it is seen from this host
- ClientVersion string //Client application version
- PublicIP net.IP //Public IP address as seen from this host
-}
-
-type Network struct {
- UID string //UUID of the network, must be a 16 char random ASCII string
- Name string //Name of the network, ASCII only
- Description string //Description of the network
- CIDR string //The subnet masked use by this network
- Nodes []*Node //The nodes currently attached in this network
-}
-
-type NetworkManagerOptions struct {
- Database *database.Database
- AuthToken string
- ApiPort int
-}
-
-type NetworkMetaData struct {
- Desc string
-}
-
-type MemberMetaData struct {
- Name string
-}
-
-type NetworkManager struct {
- authToken string
- apiPort int
- ControllerID string
- option *NetworkManagerOptions
- networksMetadata map[string]NetworkMetaData
-}
-
-// Create a new GAN manager
-func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
- option.Database.NewTable("ganserv")
-
- //Load network metadata
- networkMeta := map[string]NetworkMetaData{}
- if option.Database.KeyExists("ganserv", "networkmeta") {
- option.Database.Read("ganserv", "networkmeta", &networkMeta)
- }
-
- //Start the zerotier instance if not exists
-
- //Get controller info
- instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
- if err != nil {
- log.Println("ZeroTier connection failed: ", err.Error())
- return &NetworkManager{
- authToken: option.AuthToken,
- apiPort: option.ApiPort,
- ControllerID: "",
- option: option,
- networksMetadata: networkMeta,
- }
- }
-
- return &NetworkManager{
- authToken: option.AuthToken,
- apiPort: option.ApiPort,
- ControllerID: instanceInfo.Address,
- option: option,
- networksMetadata: networkMeta,
- }
-}
-
-func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData {
- md, ok := m.networksMetadata[netid]
- if !ok {
- return &NetworkMetaData{}
- }
-
- return &md
-}
-
-func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) {
- m.networksMetadata[netid] = *meta
- m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata)
-}
-
-func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData {
- thisMemberData := MemberMetaData{}
- m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData)
- return &thisMemberData
-}
-
-func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) {
- m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta)
-}
diff --git a/example/plugins/ztnc/mod/ganserv/handlers.go b/example/plugins/ztnc/mod/ganserv/handlers.go
deleted file mode 100644
index 4ab76da..0000000
--- a/example/plugins/ztnc/mod/ganserv/handlers.go
+++ /dev/null
@@ -1,504 +0,0 @@
-package ganserv
-
-import (
- "encoding/json"
- "net"
- "net/http"
- "regexp"
- "strings"
-
- "aroz.org/zoraxy/ztnc/mod/utils"
-)
-
-func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {
- if m.ControllerID == "" {
- //Node id not exists. Check again
- instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort)
- if err != nil {
- utils.SendErrorResponse(w, "unable to access node id information")
- return
- }
-
- m.ControllerID = instanceInfo.Address
- }
-
- js, _ := json.Marshal(m.ControllerID)
- utils.SendJSONResponse(w, string(js))
-}
-
-func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) {
- networkInfo, err := m.createNetwork()
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- //Network created. Assign it the standard network settings
- err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- // Return the new network ID
- js, _ := json.Marshal(networkInfo.Nwid)
- utils.SendJSONResponse(w, string(js))
-}
-
-func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) {
- networkID, err := utils.PostPara(r, "id")
- if err != nil {
- utils.SendErrorResponse(w, "invalid or empty network id given")
- return
- }
-
- if !m.networkExists(networkID) {
- utils.SendErrorResponse(w, "network id not exists")
- return
- }
-
- err = m.deleteNetwork(networkID)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- }
-
- utils.SendOK(w)
-}
-
-func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) {
- netid, _ := utils.GetPara(r, "netid")
- if netid != "" {
- targetNetInfo, err := m.getNetworkInfoById(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- js, _ := json.Marshal(targetNetInfo)
- utils.SendJSONResponse(w, string(js))
-
- } else {
- // Return the list of networks as JSON
- networkIds, err := m.listNetworkIds()
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- networkInfos := []*NetworkInfo{}
- for _, id := range networkIds {
- thisNetInfo, err := m.getNetworkInfoById(id)
- if err == nil {
- networkInfos = append(networkInfos, thisNetInfo)
- }
- }
-
- js, _ := json.Marshal(networkInfos)
- utils.SendJSONResponse(w, string(js))
- }
-
-}
-
-func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "network id not given")
- return
- }
-
- if !m.networkExists(netid) {
- utils.SendErrorResponse(w, "network not eixsts")
- }
-
- newName, _ := utils.PostPara(r, "name")
- newDesc, _ := utils.PostPara(r, "desc")
- if newName != "" && newDesc != "" {
- //Strip away html from name and desc
- re := regexp.MustCompile("<[^>]*>")
- newName := re.ReplaceAllString(newName, "")
- newDesc := re.ReplaceAllString(newDesc, "")
-
- //Set the new network name and desc
- err = m.setNetworkNameAndDescription(netid, newName, newDesc)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- utils.SendOK(w)
- } else {
- //Get current name and description
- name, desc, err := m.getNetworkNameAndDescription(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- js, _ := json.Marshal([]string{name, desc})
- utils.SendJSONResponse(w, string(js))
- }
-}
-
-func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "netid not given")
- return
- }
-
- targetNetwork, err := m.getNetworkInfoById(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- js, _ := json.Marshal(targetNetwork)
- utils.SendJSONResponse(w, string(js))
-}
-
-func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "netid not given")
- return
- }
- cidr, err := utils.PostPara(r, "cidr")
- if err != nil {
- utils.SendErrorResponse(w, "cidr not given")
- return
- }
- ipstart, err := utils.PostPara(r, "ipstart")
- if err != nil {
- utils.SendErrorResponse(w, "ipstart not given")
- return
- }
- ipend, err := utils.PostPara(r, "ipend")
- if err != nil {
- utils.SendErrorResponse(w, "ipend not given")
- return
- }
-
- //Validate the CIDR is real, the ip range is within the CIDR range
- _, ipnet, err := net.ParseCIDR(cidr)
- if err != nil {
- utils.SendErrorResponse(w, "invalid cidr string given")
- return
- }
-
- startIP := net.ParseIP(ipstart)
- endIP := net.ParseIP(ipend)
- if startIP == nil || endIP == nil {
- utils.SendErrorResponse(w, "invalid start or end ip given")
- return
- }
-
- withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP)
- if !withinRange {
- utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range")
- return
- }
-
- err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr))
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- utils.SendOK(w)
-}
-
-// Handle listing of network members. Set details=true for listing all details
-func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.GetPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "netid is empty")
- return
- }
-
- details, _ := utils.GetPara(r, "detail")
-
- memberIds, err := m.getNetworkMembers(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- if details == "" {
- //Only show client ids
- js, _ := json.Marshal(memberIds)
- utils.SendJSONResponse(w, string(js))
- } else {
- //Show detail members info
- detailMemberInfo := []*MemberInfo{}
- for _, thisMemberId := range memberIds {
- memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId)
- if err == nil {
- detailMemberInfo = append(detailMemberInfo, memInfo)
- }
- }
-
- js, _ := json.Marshal(detailMemberInfo)
- utils.SendJSONResponse(w, string(js))
- }
-}
-
-// Handle Authorization of members
-func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- memberid, err := utils.PostPara(r, "memid")
- if err != nil {
- utils.SendErrorResponse(w, "memid not set")
- return
- }
-
- //Check if the target memeber exists
- if !m.memberExistsInNetwork(netid, memberid) {
- utils.SendErrorResponse(w, "member not exists in given network")
- return
- }
-
- setAuthorized, err := utils.PostPara(r, "auth")
- if err != nil || setAuthorized == "" {
- //Get the member authorization state
- memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- js, _ := json.Marshal(memberInfo.Authorized)
- utils.SendJSONResponse(w, string(js))
- } else if setAuthorized == "true" {
- m.AuthorizeMember(netid, memberid, true)
- } else if setAuthorized == "false" {
- m.AuthorizeMember(netid, memberid, false)
- } else {
- utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized)
- }
-}
-
-// Handle Delete or Add IP for a member in a network
-func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- memberid, err := utils.PostPara(r, "memid")
- if err != nil {
- utils.SendErrorResponse(w, "memid not set")
- return
- }
-
- opr, err := utils.PostPara(r, "opr")
- if err != nil {
- utils.SendErrorResponse(w, "opr not defined")
- return
- }
-
- targetip, _ := utils.PostPara(r, "ip")
-
- memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- if opr == "add" {
- if targetip == "" {
- utils.SendErrorResponse(w, "ip not set")
- return
- }
-
- if !isValidIPAddr(targetip) {
- utils.SendErrorResponse(w, "ip address not valid")
- return
- }
-
- newIpList := append(memberInfo.IPAssignments, targetip)
- err = m.setAssignedIps(netid, memberid, newIpList)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- utils.SendOK(w)
-
- } else if opr == "del" {
- if targetip == "" {
- utils.SendErrorResponse(w, "ip not set")
- return
- }
-
- //Delete user ip from the list
- newIpList := []string{}
- for _, thisIp := range memberInfo.IPAssignments {
- if thisIp != targetip {
- newIpList = append(newIpList, thisIp)
- }
- }
-
- err = m.setAssignedIps(netid, memberid, newIpList)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
- utils.SendOK(w)
- } else if opr == "get" {
- js, _ := json.Marshal(memberInfo.IPAssignments)
- utils.SendJSONResponse(w, string(js))
- } else {
- utils.SendErrorResponse(w, "unsupported opr type: "+opr)
- }
-}
-
-// Handle naming for members
-func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- memberid, err := utils.PostPara(r, "memid")
- if err != nil {
- utils.SendErrorResponse(w, "memid not set")
- return
- }
-
- if !m.memberExistsInNetwork(netid, memberid) {
- utils.SendErrorResponse(w, "target member not exists in given network")
- return
- }
-
- //Read memeber data
- targetMemberData := m.GetMemberMetaData(netid, memberid)
-
- newname, err := utils.PostPara(r, "name")
- if err != nil {
- //Send over the member data
- js, _ := json.Marshal(targetMemberData)
- utils.SendJSONResponse(w, string(js))
- } else {
- //Write member data
- targetMemberData.Name = newname
- m.WriteMemeberMetaData(netid, memberid, targetMemberData)
- utils.SendOK(w)
- }
-}
-
-// Handle delete of a given memver
-func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- memberid, err := utils.PostPara(r, "memid")
- if err != nil {
- utils.SendErrorResponse(w, "memid not set")
- return
- }
-
- //Check if that member is authorized.
- memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
- if err != nil {
- utils.SendErrorResponse(w, "member not exists in given GANet")
- return
- }
-
- if memberInfo.Authorized {
- //Deauthorized this member before deleting
- m.AuthorizeMember(netid, memberid, false)
- }
-
- //Remove the memeber
- err = m.deleteMember(netid, memberid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- utils.SendOK(w)
-}
-
-// Check if a given network id is a network hosted on this zoraxy node
-func (m *NetworkManager) IsLocalGAN(networkId string) bool {
- networks, err := m.listNetworkIds()
- if err != nil {
- return false
- }
-
- for _, network := range networks {
- if network == networkId {
- return true
- }
- }
-
- return false
-}
-
-// Handle server instant joining a given network
-func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- //Check if the target network is a network hosted on this server
- if !m.IsLocalGAN(netid) {
- utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
- return
- }
-
- if m.memberExistsInNetwork(netid, m.ControllerID) {
- utils.SendErrorResponse(w, "controller already inside network")
- return
- }
-
- //Join the network
- err = m.joinNetwork(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- utils.SendOK(w)
-}
-
-// Handle server instant leaving a given network
-func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
- netid, err := utils.PostPara(r, "netid")
- if err != nil {
- utils.SendErrorResponse(w, "net id not set")
- return
- }
-
- //Check if the target network is a network hosted on this server
- if !m.IsLocalGAN(netid) {
- utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
- return
- }
-
- //Leave the network
- err = m.leaveNetwork(netid)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- //Remove it from target network if it is authorized
- err = m.deleteMember(netid, m.ControllerID)
- if err != nil {
- utils.SendErrorResponse(w, err.Error())
- return
- }
-
- utils.SendOK(w)
-}
diff --git a/example/plugins/ztnc/mod/ganserv/network.go b/example/plugins/ztnc/mod/ganserv/network.go
deleted file mode 100644
index 9f4ec73..0000000
--- a/example/plugins/ztnc/mod/ganserv/network.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package ganserv
-
-import (
- "fmt"
- "math/rand"
- "net"
- "time"
-)
-
-//Get a random free IP from the pool
-func (n *Network) GetRandomFreeIP() (net.IP, error) {
- // Get all IP addresses in the subnet
- ips, err := GetAllAddressFromCIDR(n.CIDR)
- if err != nil {
- return nil, err
- }
-
- // Filter out used IPs
- usedIPs := make(map[string]bool)
- for _, node := range n.Nodes {
- usedIPs[node.ManagedIP.String()] = true
- }
- availableIPs := []string{}
- for _, ip := range ips {
- if !usedIPs[ip] {
- availableIPs = append(availableIPs, ip)
- }
- }
-
- // Randomly choose an available IP
- if len(availableIPs) == 0 {
- return nil, fmt.Errorf("no available IP")
- }
- rand.Seed(time.Now().UnixNano())
- randIndex := rand.Intn(len(availableIPs))
- pickedFreeIP := availableIPs[randIndex]
-
- return net.ParseIP(pickedFreeIP), nil
-}
diff --git a/example/plugins/ztnc/mod/ganserv/network_test.go b/example/plugins/ztnc/mod/ganserv/network_test.go
deleted file mode 100644
index 2002b9f..0000000
--- a/example/plugins/ztnc/mod/ganserv/network_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package ganserv_test
-
-import (
- "fmt"
- "net"
- "strconv"
- "testing"
-
- "aroz.org/zoraxy/ztnc/mod/ganserv"
-)
-
-func TestGetRandomFreeIP(t *testing.T) {
- n := ganserv.Network{
- CIDR: "172.16.0.0/12",
- Nodes: []*ganserv.Node{
- {
- Name: "nodeC1",
- ManagedIP: net.ParseIP("172.16.1.142"),
- },
- {
- Name: "nodeC2",
- ManagedIP: net.ParseIP("172.16.5.174"),
- },
- },
- }
-
- // Call the function for 10 times
- for i := 0; i < 10; i++ {
- freeIP, err := n.GetRandomFreeIP()
- fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP)
-
- // Assert that no error occurred
- if err != nil {
- t.Errorf("Unexpected error: %s", err.Error())
- }
-
- // Assert that the returned IP is a valid IPv4 address
- if freeIP.To4() == nil {
- t.Errorf("Invalid IP address format: %s", freeIP.String())
- }
-
- // Assert that the returned IP is not already used by a node
- for _, node := range n.Nodes {
- if freeIP.Equal(node.ManagedIP) {
- t.Errorf("Returned IP is already in use: %s", freeIP.String())
- }
- }
-
- n.Nodes = append(n.Nodes, &ganserv.Node{
- Name: "NodeT" + strconv.Itoa(i),
- ManagedIP: freeIP,
- })
- }
-
-}
diff --git a/example/plugins/ztnc/mod/ganserv/utils.go b/example/plugins/ztnc/mod/ganserv/utils.go
deleted file mode 100644
index 684f597..0000000
--- a/example/plugins/ztnc/mod/ganserv/utils.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package ganserv
-
-import (
- "net"
-)
-
-//Generate all ip address from a CIDR
-func GetAllAddressFromCIDR(cidr string) ([]string, error) {
- ip, ipnet, err := net.ParseCIDR(cidr)
- if err != nil {
- return nil, err
- }
-
- var ips []string
- for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
- ips = append(ips, ip.String())
- }
- // remove network address and broadcast address
- return ips[1 : len(ips)-1], nil
-}
-
-func inc(ip net.IP) {
- for j := len(ip) - 1; j >= 0; j-- {
- ip[j]++
- if ip[j] > 0 {
- break
- }
- }
-}
-
-func isValidIPAddr(ipAddr string) bool {
- ip := net.ParseIP(ipAddr)
- if ip == nil {
- return false
- }
-
- return true
-}
-
-func ipWithinCIDR(ipAddr string, cidr string) bool {
- // Parse the CIDR string
- _, ipNet, err := net.ParseCIDR(cidr)
- if err != nil {
- return false
- }
-
- // Parse the IP address
- ip := net.ParseIP(ipAddr)
- if ip == nil {
- return false
- }
-
- // Check if the IP address is in the CIDR range
- return ipNet.Contains(ip)
-}
diff --git a/example/plugins/ztnc/mod/ganserv/zerotier.go b/example/plugins/ztnc/mod/ganserv/zerotier.go
deleted file mode 100644
index fa1fd0b..0000000
--- a/example/plugins/ztnc/mod/ganserv/zerotier.go
+++ /dev/null
@@ -1,669 +0,0 @@
-package ganserv
-
-import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "os"
- "strconv"
- "strings"
-)
-
-/*
- zerotier.go
-
- This hold the functions that required to communicate with
- a zerotier instance
-
- See more on
- https://docs.zerotier.com/self-hosting/network-controllers/
-
-*/
-
-type NodeInfo struct {
- Address string `json:"address"`
- Clock int64 `json:"clock"`
- Config struct {
- Settings struct {
- AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
- ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
- HomeDir string `json:"homeDir,omitempty"`
- ListeningOn []string `json:"listeningOn,omitempty"`
- PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
- PrimaryPort int `json:"primaryPort,omitempty"`
- SecondaryPort int `json:"secondaryPort,omitempty"`
- SoftwareUpdate string `json:"softwareUpdate,omitempty"`
- SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
- SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
- TertiaryPort int `json:"tertiaryPort,omitempty"`
- } `json:"settings"`
- } `json:"config"`
- Online bool `json:"online"`
- PlanetWorldID int `json:"planetWorldId"`
- PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"`
- PublicIdentity string `json:"publicIdentity"`
- TCPFallbackActive bool `json:"tcpFallbackActive"`
- Version string `json:"version"`
- VersionBuild int `json:"versionBuild"`
- VersionMajor int `json:"versionMajor"`
- VersionMinor int `json:"versionMinor"`
- VersionRev int `json:"versionRev"`
-}
-type ErrResp struct {
- Message string `json:"message"`
-}
-
-type NetworkInfo struct {
- AuthTokens []interface{} `json:"authTokens"`
- AuthorizationEndpoint string `json:"authorizationEndpoint"`
- Capabilities []interface{} `json:"capabilities"`
- ClientID string `json:"clientId"`
- CreationTime int64 `json:"creationTime"`
- DNS []interface{} `json:"dns"`
- EnableBroadcast bool `json:"enableBroadcast"`
- ID string `json:"id"`
- IPAssignmentPools []interface{} `json:"ipAssignmentPools"`
- Mtu int `json:"mtu"`
- MulticastLimit int `json:"multicastLimit"`
- Name string `json:"name"`
- Nwid string `json:"nwid"`
- Objtype string `json:"objtype"`
- Private bool `json:"private"`
- RemoteTraceLevel int `json:"remoteTraceLevel"`
- RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
- Revision int `json:"revision"`
- Routes []interface{} `json:"routes"`
- Rules []struct {
- Not bool `json:"not"`
- Or bool `json:"or"`
- Type string `json:"type"`
- } `json:"rules"`
- RulesSource string `json:"rulesSource"`
- SsoEnabled bool `json:"ssoEnabled"`
- Tags []interface{} `json:"tags"`
- V4AssignMode struct {
- Zt bool `json:"zt"`
- } `json:"v4AssignMode"`
- V6AssignMode struct {
- SixPlane bool `json:"6plane"`
- Rfc4193 bool `json:"rfc4193"`
- Zt bool `json:"zt"`
- } `json:"v6AssignMode"`
-}
-
-type MemberInfo struct {
- ActiveBridge bool `json:"activeBridge"`
- Address string `json:"address"`
- AuthenticationExpiryTime int `json:"authenticationExpiryTime"`
- Authorized bool `json:"authorized"`
- Capabilities []interface{} `json:"capabilities"`
- CreationTime int64 `json:"creationTime"`
- ID string `json:"id"`
- Identity string `json:"identity"`
- IPAssignments []string `json:"ipAssignments"`
- LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"`
- LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"`
- LastAuthorizedTime int `json:"lastAuthorizedTime"`
- LastDeauthorizedTime int `json:"lastDeauthorizedTime"`
- NoAutoAssignIps bool `json:"noAutoAssignIps"`
- Nwid string `json:"nwid"`
- Objtype string `json:"objtype"`
- RemoteTraceLevel int `json:"remoteTraceLevel"`
- RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
- Revision int `json:"revision"`
- SsoExempt bool `json:"ssoExempt"`
- Tags []interface{} `json:"tags"`
- VMajor int `json:"vMajor"`
- VMinor int `json:"vMinor"`
- VProto int `json:"vProto"`
- VRev int `json:"vRev"`
-}
-
-// Get the zerotier node info from local service
-func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
- url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
-
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("X-ZT1-AUTH", token)
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
-
- //Read from zerotier service instance
-
- defer resp.Body.Close()
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- //Parse the payload into struct
- thisInstanceInfo := NodeInfo{}
- err = json.Unmarshal(payload, &thisInstanceInfo)
- if err != nil {
- return nil, err
- }
-
- return &thisInstanceInfo, nil
-}
-
-/*
- Network Functions
-*/
-//Create a zerotier network
-func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
- url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID)
-
- data := []byte(`{}`)
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("X-ZT1-AUTH", m.authToken)
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
-
- defer resp.Body.Close()
-
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- networkInfo := NetworkInfo{}
- err = json.Unmarshal(payload, &networkInfo)
- if err != nil {
- return nil, err
- }
-
- return &networkInfo, nil
-}
-
-// List network details
-func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
- req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("X-Zt1-Auth", m.authToken)
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- thisNetworkInfo := NetworkInfo{}
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- err = json.Unmarshal(payload, &thisNetworkInfo)
- if err != nil {
- return nil, err
- }
-
- return &thisNetworkInfo, nil
-}
-
-func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error {
- payloadBytes, err := json.Marshal(newNetworkInfo)
- if err != nil {
- return err
- }
- payloadBuffer := bytes.NewBuffer(payloadBytes)
-
- // Create the HTTP request
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/"
- req, err := http.NewRequest("POST", url, payloadBuffer)
- if err != nil {
- return err
- }
- req.Header.Set("X-Zt1-Auth", m.authToken)
- req.Header.Set("Content-Type", "application/json")
-
- // Send the HTTP request
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- // Print the response status code
- if resp.StatusCode != 200 {
- return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-// List network IDs
-func (m *NetworkManager) listNetworkIds() ([]string, error) {
- req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
- if err != nil {
- return []string{}, err
- }
- req.Header.Set("X-Zt1-Auth", m.authToken)
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return []string{}, err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return []string{}, errors.New("network error")
- }
-
- networkIds := []string{}
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return []string{}, err
- }
-
- err = json.Unmarshal(payload, &networkIds)
- if err != nil {
- return []string{}, err
- }
-
- return networkIds, nil
-}
-
-// wrapper for checking if a network id exists
-func (m *NetworkManager) networkExists(networkId string) bool {
- networkIds, err := m.listNetworkIds()
- if err != nil {
- return false
- }
-
- for _, thisid := range networkIds {
- if thisid == networkId {
- return true
- }
- }
-
- return false
-}
-
-// delete a network
-func (m *NetworkManager) deleteNetwork(networkID string) error {
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
- client := &http.Client{}
-
- // Create a new DELETE request
- req, err := http.NewRequest("DELETE", url, nil)
- if err != nil {
- return err
- }
-
- // Add the required authorization header
- req.Header.Set("X-Zt1-Auth", m.authToken)
-
- // Send the request and get the response
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
-
- // Close the response body when we're done
- defer resp.Body.Close()
- s, err := io.ReadAll(resp.Body)
- fmt.Println(string(s), err, resp.StatusCode)
-
- // Print the response status code
- if resp.StatusCode != 200 {
- return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-// Configure network
-// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
-func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
- data := map[string]interface{}{
- "ipAssignmentPools": []map[string]string{
- {
- "ipRangeStart": ipRangeStart,
- "ipRangeEnd": ipRangeEnd,
- },
- },
- "routes": []map[string]interface{}{
- {
- "target": routeTarget,
- "via": nil,
- },
- },
- "v4AssignMode": "zt",
- "private": true,
- }
-
- payload, err := json.Marshal(data)
- if err != nil {
- return err
- }
-
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
- if err != nil {
- return err
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-ZT1-AUTH", m.authToken)
-
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
-
- defer resp.Body.Close()
- // Print the response status code
- if resp.StatusCode != 200 {
- return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error {
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid
- data := map[string]interface{}{
- "ipAssignments": newIps,
- }
-
- payload, err := json.Marshal(data)
- if err != nil {
- return err
- }
-
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
- if err != nil {
- return err
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-ZT1-AUTH", m.authToken)
-
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
-
- defer resp.Body.Close()
- // Print the response status code
- if resp.StatusCode != 200 {
- return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error {
- // Convert string to rune slice
- r := []rune(name)
-
- // Loop over runes and remove non-ASCII characters
- for i, v := range r {
- if v > 127 {
- r[i] = ' '
- }
- }
-
- // Convert back to string and trim whitespace
- name = strings.TrimSpace(string(r))
-
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/"
- data := map[string]interface{}{
- "name": name,
- }
-
- payload, err := json.Marshal(data)
- if err != nil {
- return err
- }
-
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
- if err != nil {
- return err
- }
-
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-ZT1-AUTH", m.authToken)
-
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
-
- defer resp.Body.Close()
- // Print the response status code
- if resp.StatusCode != 200 {
- return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- meta := m.GetNetworkMetaData(netid)
- if meta != nil {
- meta.Desc = desc
- m.WriteNetworkMetaData(netid, meta)
- }
-
- return nil
-}
-
-func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) {
- //Get name from network info
- netinfo, err := m.getNetworkInfoById(netid)
- if err != nil {
- return "", "", err
- }
-
- name := netinfo.Name
-
- //Get description from meta
- desc := ""
- networkMeta := m.GetNetworkMetaData(netid)
- if networkMeta != nil {
- desc = networkMeta.Desc
- }
-
- return name, desc, nil
-}
-
-/*
- Member functions
-*/
-
-func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) {
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member"
- reqBody := bytes.NewBuffer([]byte{})
- req, err := http.NewRequest("GET", url, reqBody)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("X-ZT1-AUTH", m.authToken)
-
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
-
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, errors.New("failed to get network members")
- }
-
- memberList := map[string]int{}
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- err = json.Unmarshal(payload, &memberList)
- if err != nil {
- return nil, err
- }
-
- members := make([]string, 0, len(memberList))
- for k := range memberList {
- members = append(members, k)
- }
-
- return members, nil
-}
-
-func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool {
- //Get a list of member
- memberids, err := m.getNetworkMembers(netid)
- if err != nil {
- return false
- }
- for _, thisMemberId := range memberids {
- if thisMemberId == memid {
- return true
- }
- }
-
- return false
-}
-
-// Get a network memeber info by netid and memberid
-func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
- req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("X-Zt1-Auth", m.authToken)
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- thisMemeberInfo := &MemberInfo{}
- payload, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- err = json.Unmarshal(payload, &thisMemeberInfo)
- if err != nil {
- return nil, err
- }
-
- return thisMemeberInfo, nil
-}
-
-// Set the authorization state of a member
-func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
- url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
- payload := []byte(`{"authorized": true}`)
- if !setAuthorized {
- payload = []byte(`{"authorized": false}`)
- }
-
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
- if err != nil {
- return err
- }
- req.Header.Set("X-ZT1-AUTH", m.authToken)
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-// Delete a member from the network
-func (m *NetworkManager) deleteMember(netid string, memid string) error {
- req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
- if err != nil {
- return err
- }
- req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-// Make the host to join a given network
-func (m *NetworkManager) joinNetwork(netid string) error {
- req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
- if err != nil {
- return err
- }
- req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
-
-// Make the host to leave a given network
-func (m *NetworkManager) leaveNetwork(netid string) error {
- req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
- if err != nil {
- return err
- }
- req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != 200 {
- return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
- }
-
- return nil
-}
diff --git a/example/plugins/ztnc/mod/utils/conv.go b/example/plugins/ztnc/mod/utils/conv.go
deleted file mode 100644
index 6adf753..0000000
--- a/example/plugins/ztnc/mod/utils/conv.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package utils
-
-import (
- "archive/zip"
- "io"
- "os"
- "path/filepath"
- "strconv"
- "strings"
-)
-
-func StringToInt64(number string) (int64, error) {
- i, err := strconv.ParseInt(number, 10, 64)
- if err != nil {
- return -1, err
- }
- return i, nil
-}
-
-func Int64ToString(number int64) string {
- convedNumber := strconv.FormatInt(number, 10)
- return convedNumber
-}
-
-func ReplaceSpecialCharacters(filename string) string {
- replacements := map[string]string{
- "#": "%pound%",
- "&": "%amp%",
- "{": "%left_cur%",
- "}": "%right_cur%",
- "\\": "%backslash%",
- "<": "%left_ang%",
- ">": "%right_ang%",
- "*": "%aster%",
- "?": "%quest%",
- " ": "%space%",
- "$": "%dollar%",
- "!": "%exclan%",
- "'": "%sin_q%",
- "\"": "%dou_q%",
- ":": "%colon%",
- "@": "%at%",
- "+": "%plus%",
- "`": "%backtick%",
- "|": "%pipe%",
- "=": "%equal%",
- ".": "_",
- "/": "-",
- }
-
- for char, replacement := range replacements {
- filename = strings.ReplaceAll(filename, char, replacement)
- }
-
- return filename
-}
-
-/* Zip File Handler */
-// zipFiles compresses multiple files into a single zip archive file
-func ZipFiles(filename string, files ...string) error {
- newZipFile, err := os.Create(filename)
- if err != nil {
- return err
- }
- defer newZipFile.Close()
-
- zipWriter := zip.NewWriter(newZipFile)
- defer zipWriter.Close()
-
- for _, file := range files {
- if err := addFileToZip(zipWriter, file); err != nil {
- return err
- }
- }
- return nil
-}
-
-// addFileToZip adds an individual file to a zip archive
-func addFileToZip(zipWriter *zip.Writer, filename string) error {
- fileToZip, err := os.Open(filename)
- if err != nil {
- return err
- }
- defer fileToZip.Close()
-
- info, err := fileToZip.Stat()
- if err != nil {
- return err
- }
-
- header, err := zip.FileInfoHeader(info)
- if err != nil {
- return err
- }
-
- header.Name = filepath.Base(filename)
- header.Method = zip.Deflate
-
- writer, err := zipWriter.CreateHeader(header)
- if err != nil {
- return err
- }
- _, err = io.Copy(writer, fileToZip)
- return err
-}
diff --git a/example/plugins/ztnc/mod/utils/template.go b/example/plugins/ztnc/mod/utils/template.go
deleted file mode 100644
index e5772a8..0000000
--- a/example/plugins/ztnc/mod/utils/template.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package utils
-
-import (
- "net/http"
-)
-
-/*
- Web Template Generator
-
- This is the main system core module that perform function similar to what PHP did.
- To replace part of the content of any file, use {{paramter}} to replace it.
-
-
-*/
-
-func SendHTMLResponse(w http.ResponseWriter, msg string) {
- w.Header().Set("Content-Type", "text/html")
- w.Write([]byte(msg))
-}
diff --git a/example/plugins/ztnc/mod/utils/utils.go b/example/plugins/ztnc/mod/utils/utils.go
deleted file mode 100644
index 2fe1ffd..0000000
--- a/example/plugins/ztnc/mod/utils/utils.go
+++ /dev/null
@@ -1,202 +0,0 @@
-package utils
-
-import (
- "errors"
- "log"
- "net"
- "net/http"
- "os"
- "strconv"
- "strings"
- "time"
-)
-
-/*
- Common
-
- Some commonly used functions in ArozOS
-
-*/
-
-// Response related
-func SendTextResponse(w http.ResponseWriter, msg string) {
- w.Write([]byte(msg))
-}
-
-// Send JSON response, with an extra json header
-func SendJSONResponse(w http.ResponseWriter, json string) {
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(json))
-}
-
-func SendErrorResponse(w http.ResponseWriter, errMsg string) {
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
-}
-
-func SendOK(w http.ResponseWriter) {
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte("\"OK\""))
-}
-
-// Get GET parameter
-func GetPara(r *http.Request, key string) (string, error) {
- // Get first value from the URL query
- value := r.URL.Query().Get(key)
- if len(value) == 0 {
- return "", errors.New("invalid " + key + " given")
- }
- return value, nil
-}
-
-// Get GET paramter as boolean, accept 1 or true
-func GetBool(r *http.Request, key string) (bool, error) {
- x, err := GetPara(r, key)
- if err != nil {
- return false, err
- }
-
- // Convert to lowercase and trim spaces just once to compare
- switch strings.ToLower(strings.TrimSpace(x)) {
- case "1", "true", "on":
- return true, nil
- case "0", "false", "off":
- return false, nil
- }
-
- return false, errors.New("invalid boolean given")
-}
-
-// Get POST parameter
-func PostPara(r *http.Request, key string) (string, error) {
- // Try to parse the form
- if err := r.ParseForm(); err != nil {
- return "", err
- }
- // Get first value from the form
- x := r.Form.Get(key)
- if len(x) == 0 {
- return "", errors.New("invalid " + key + " given")
- }
- return x, nil
-}
-
-// Get POST paramter as boolean, accept 1 or true
-func PostBool(r *http.Request, key string) (bool, error) {
- x, err := PostPara(r, key)
- if err != nil {
- return false, err
- }
-
- // Convert to lowercase and trim spaces just once to compare
- switch strings.ToLower(strings.TrimSpace(x)) {
- case "1", "true", "on":
- return true, nil
- case "0", "false", "off":
- return false, nil
- }
-
- return false, errors.New("invalid boolean given")
-}
-
-// Get POST paramter as int
-func PostInt(r *http.Request, key string) (int, error) {
- x, err := PostPara(r, key)
- if err != nil {
- return 0, err
- }
-
- x = strings.TrimSpace(x)
- rx, err := strconv.Atoi(x)
- if err != nil {
- return 0, err
- }
-
- return rx, nil
-}
-
-func FileExists(filename string) bool {
- _, err := os.Stat(filename)
- if err == nil {
- // File exists
- return true
- } else if errors.Is(err, os.ErrNotExist) {
- // File does not exist
- return false
- }
- // Some other error
- return false
-}
-
-func IsDir(path string) bool {
- if !FileExists(path) {
- return false
- }
- fi, err := os.Stat(path)
- if err != nil {
- log.Fatal(err)
- return false
- }
- switch mode := fi.Mode(); {
- case mode.IsDir():
- return true
- case mode.IsRegular():
- return false
- }
- return false
-}
-
-func TimeToString(targetTime time.Time) string {
- return targetTime.Format("2006-01-02 15:04:05")
-}
-
-// Check if given string in a given slice
-func StringInArray(arr []string, str string) bool {
- for _, a := range arr {
- if a == str {
- return true
- }
- }
- return false
-}
-
-func StringInArrayIgnoreCase(arr []string, str string) bool {
- smallArray := []string{}
- for _, item := range arr {
- smallArray = append(smallArray, strings.ToLower(item))
- }
-
- return StringInArray(smallArray, strings.ToLower(str))
-}
-
-// Validate if the listening address is correct
-func ValidateListeningAddress(address string) bool {
- // Check if the address starts with a colon, indicating it's just a port
- if strings.HasPrefix(address, ":") {
- return true
- }
-
- // Split the address into host and port parts
- host, port, err := net.SplitHostPort(address)
- if err != nil {
- // Try to parse it as just a port
- if _, err := strconv.Atoi(address); err == nil {
- return false // It's just a port number
- }
- return false // It's an invalid address
- }
-
- // Check if the port part is a valid number
- if _, err := strconv.Atoi(port); err != nil {
- return false
- }
-
- // Check if the host part is a valid IP address or empty (indicating any IP)
- if host != "" {
- if net.ParseIP(host) == nil {
- return false
- }
- }
-
- return true
-}
\ No newline at end of file
diff --git a/example/plugins/ztnc/start.go b/example/plugins/ztnc/start.go
deleted file mode 100644
index 1090031..0000000
--- a/example/plugins/ztnc/start.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package main
-
-import (
- "fmt"
- "net/http"
- "os"
-
- "aroz.org/zoraxy/ztnc/mod/database"
- "aroz.org/zoraxy/ztnc/mod/database/dbinc"
- "aroz.org/zoraxy/ztnc/mod/ganserv"
- "aroz.org/zoraxy/ztnc/mod/utils"
-)
-
-func startGanNetworkController() error {
- fmt.Println("Starting ZeroTier Network Controller")
- //Create a new database
- var err error
- sysdb, err = database.NewDatabase(DB_FILE_PATH, dbinc.BackendBoltDB)
- if err != nil {
- return err
- }
-
- //Initiate the GAN server manager
- usingZtAuthToken := ""
- ztAPIPort := 9993
-
- if utils.FileExists(AUTH_TOKEN_PATH) {
- authToken, err := os.ReadFile(AUTH_TOKEN_PATH)
- if err != nil {
- fmt.Println("Error reading auth config file:", err)
- return err
- }
- usingZtAuthToken = string(authToken)
- fmt.Println("Loaded ZeroTier Auth Token from file")
- }
-
- if usingZtAuthToken == "" {
- usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
- if err != nil {
- fmt.Println("Error getting ZeroTier Auth Token:", err)
- }
- }
-
- ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
- AuthToken: usingZtAuthToken,
- ApiPort: ztAPIPort,
- Database: sysdb,
- })
-
- return nil
-}
-
-func initApiEndpoints() {
- //UI_RELPATH must be the same as the one in the plugin intro spect
- // as Zoraxy plugin UI proxy will only forward the UI path to your plugin
- http.HandleFunc(UI_RELPATH+"/api/gan/network/info", ganManager.HandleGetNodeID)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/add", ganManager.HandleAddNetwork)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/remove", ganManager.HandleRemoveNetwork)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/list", ganManager.HandleListNetwork)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/name", ganManager.HandleNetworkNaming)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/setRange", ganManager.HandleSetRanges)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/join", ganManager.HandleServerJoinNetwork)
- http.HandleFunc(UI_RELPATH+"/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
- http.HandleFunc(UI_RELPATH+"/api/gan/members/list", ganManager.HandleMemberList)
- http.HandleFunc(UI_RELPATH+"/api/gan/members/ip", ganManager.HandleMemberIP)
- http.HandleFunc(UI_RELPATH+"/api/gan/members/name", ganManager.HandleMemberNaming)
- http.HandleFunc(UI_RELPATH+"/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
- http.HandleFunc(UI_RELPATH+"/api/gan/members/delete", ganManager.HandleMemberDelete)
-}
diff --git a/example/plugins/ztnc/web/details.html b/example/plugins/ztnc/web/details.html
deleted file mode 100644
index 766644c..0000000
--- a/example/plugins/ztnc/web/details.html
+++ /dev/null
@@ -1,752 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Settings
-
-
-
-
- IPv4 Auto-Assign
-
-
-
-
-
-
-
-
-
- Custom IP Range
- Manual IP Range Configuration. The IP range must be within the selected CIDR range.
-
Use Utilities > IP to CIDR tool
if you are not too familiar with CIDR notations.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Members
- To join this network using command line, type sudo zerotier-cli join
on your device terminal
-
-
-
-
-
-
-
-
- Auth
- Address
- Name
- Managed IP
- Authorized Since
- Version
- Remove
-
-
-
-
-
-
-
-
-
-
- Add Controller as Member
- Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.
-
-
-
-
-
diff --git a/example/plugins/ztnc/web/index.html b/example/plugins/ztnc/web/index.html
deleted file mode 100644
index 3753ed4..0000000
--- a/example/plugins/ztnc/web/index.html
+++ /dev/null
@@ -1,267 +0,0 @@
-
-
-
-
-
-
-
-
- Global Area Network | Zoraxy
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Global Area Network
- Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region
-
-
-
-
-
-
- Network Controller ID
-
-
-
-
-
-
- 0
- Networks
-
-
-
-
-
- 0
- Connected Nodes
-
-
-
-
-
-
-
-
-
-
-
-
- Network ID
- Name
- Description
- Subnet (Assign Range)
- Nodes
- Actions
-
-
-
-
- No Global Area Network Found on this host
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file