mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-26 17:31:45 +02:00
Added restful-api plugin
- Added restful-api example plugin - Added more plugin docs - Added zoraxy_plugin HandleFunc API
This commit is contained in:
@ -6,7 +6,7 @@ Last Update: 25/05/2025
|
||||
|
||||
A plugin can optionally expose a Web UI interface for user configuration.
|
||||
|
||||
**It is generally recommended that a plugin have such UI exposed for easy configurations.** 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.
|
||||
**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
|
||||
|
||||
|
@ -121,6 +121,8 @@ And here is an example `index.html` file that uses the Zoraxy internal resources
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 6. Creating a handler for Introspect
|
||||
@ -240,6 +242,8 @@ After saving the `main.go` file, you can now build your plugin with `go build`.
|
||||
|
||||
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**
|
||||
|
493
docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
Normal file
493
docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
Normal file
@ -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
|
||||
<!-- 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](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<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.
|
||||
|
||||
```html
|
||||
<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.
|
||||
|
||||
```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. <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.
|
||||
|
||||
```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
|
||||
<!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`)
|
||||
|
||||
```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
|
||||
|
||||

|
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
@ -1,3 +1,7 @@
|
||||
<div class="ts-image is-rounded">
|
||||
<img src="../assets/banner.png" alt="Zoraxy Plugin Banner" style="width: 100%; height: auto; border-radius: 0.5rem;">
|
||||
</div>
|
||||
|
||||
# Index
|
||||
|
||||
Welcome to the Zoraxy Plugin Documentation!
|
||||
@ -14,4 +18,10 @@ No. Plugins operate in a separate process from Zoraxy. If a plugin crashes, Zora
|
||||
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.
|
||||
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).
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
@ -158,7 +161,7 @@
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
It is generally recommended that a plugin have such UI exposed for easy configurations.
|
||||
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)
|
||||
</span>
|
||||
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.
|
||||
</p>
|
||||
|
@ -131,6 +131,9 @@
|
||||
<a class="item is-active" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
@ -526,6 +529,11 @@ if err != nil {
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/1. Hello World/image-20250530134148399.png" alt="image-20250530134148399" />
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
|
787
docs/plugins/html/3. Basic Examples/2. RESTful Example.html
Normal file
787
docs/plugins/html/3. Basic Examples/2. RESTful Example.html
Normal file
@ -0,0 +1,787 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="is-white">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>
|
||||
RESTful Example | Zoraxy Documentation
|
||||
</title>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js" integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<!-- css -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/5.0.2/tocas.min.js"></script>
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<!-- Code highlight -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
|
||||
<!-- additional languages -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/go.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/c.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/javascript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/xml.min.js"></script>
|
||||
<script>
|
||||
hljs.highlightAll();
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/tomorrow-night-bright.css">
|
||||
<style>
|
||||
#msgbox{
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
code{
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/html/assets/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ts-content">
|
||||
<div class="ts-container">
|
||||
<div style="float: right;">
|
||||
<button class="ts-button is-icon" id="darkModeToggle">
|
||||
<span class="ts-icon is-moon-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ts-tab is-pilled">
|
||||
<a href="" class="item" style="user-select: none;">
|
||||
<img id="sysicon" class="ts-image" style="height: 30px" white_src="/html/assets/logo.png" dark_src="/html/assets/logo_white.png" src="/html/assets/logo.png"></img>
|
||||
</a>
|
||||
<a href="#!" class="is-active item">
|
||||
Documents
|
||||
</a>
|
||||
<a href="#!" class="item">
|
||||
Examples
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-divider"></div>
|
||||
<div>
|
||||
<div class="has-padded">
|
||||
<div class="ts-grid mobile:is-stacked">
|
||||
<div class="column is-4-wide">
|
||||
<div class="ts-box">
|
||||
<div class="ts-menu is-end-icon">
|
||||
<a class="item">
|
||||
Introduction
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/1. Introduction/1. What is Zoraxy Plugin.html">
|
||||
What is Zoraxy Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/2. Getting Started.html">
|
||||
Getting Started
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/3. Installing Plugin.html">
|
||||
Installing Plugin
|
||||
</a>
|
||||
<a class="item" href="/html/1. Introduction/4. Enable Plugins.html">
|
||||
Enable Plugins
|
||||
</a>
|
||||
</div>
|
||||
<a class="item">
|
||||
Architecture
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/2. Architecture/1. Plugin Architecture.html">
|
||||
Plugin Architecture
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/2. Introspect.html">
|
||||
Introspect
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/3. Configure.html">
|
||||
Configure
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/4. Capture Modes.html">
|
||||
Capture Modes
|
||||
</a>
|
||||
<a class="item" href="/html/2. Architecture/5. Plugin UI.html">
|
||||
Plugin UI
|
||||
</a>
|
||||
</div>
|
||||
<a class="item">
|
||||
Basic Examples
|
||||
<span class="ts-icon is-caret-down-icon"></span>
|
||||
</a>
|
||||
<div class="ts-menu is-dense is-small is-horizontally-padded">
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item is-active" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item" href="/html/index.html">
|
||||
index
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-wide">
|
||||
<div class="ts-box">
|
||||
<div class="ts-container is-padded has-top-padded-large">
|
||||
<h1 id="restful-api-call-in-web-ui">
|
||||
RESTful API Call in Web UI
|
||||
</h1>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Last Update: 29/05/2025
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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
|
||||
<span class="ts-text is-code">
|
||||
cjax
|
||||
</span>
|
||||
function.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
<span class="ts-text is-heavy">
|
||||
Notes: This example assumes you have basic understanding on how to use jQuery
|
||||
<span class="ts-text is-code">
|
||||
ajax
|
||||
</span>
|
||||
request.
|
||||
</span>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Lets get started!
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="1-create-the-plugin-folder-structures">
|
||||
1. Create the plugin folder structures
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="2-create-introspect">
|
||||
2. Create Introspect
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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)
|
||||
}
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="3-create-an-embedded-web-server-with-handlers">
|
||||
3. Create an embedded web server with handlers
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a
|
||||
<span class="ts-text is-code">
|
||||
http.HandleFunc
|
||||
</span>
|
||||
to the plugin so our front-end can request and communicate with the backend.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The
|
||||
<span class="ts-text is-code">
|
||||
embedWebRouter.HandleFunc
|
||||
</span>
|
||||
last parameter is the
|
||||
<span class="ts-text is-code">
|
||||
http.Mux
|
||||
</span>
|
||||
, 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.
|
||||
</p>
|
||||
</p>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="4-modify-the-front-end-html-file-to-make-request-to-backend">
|
||||
4. Modify the front-end HTML file to make request to backend
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
To make a RESTFUL API to your plugin,
|
||||
<span class="ts-text is-heavy">
|
||||
you must use relative path in your request URL
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Absolute path that start with
|
||||
<span class="ts-text is-code">
|
||||
/
|
||||
</span>
|
||||
is only use for accessing Zoraxy resouces. For example, when you access
|
||||
<span class="ts-text is-code">
|
||||
/img/logo.svg
|
||||
</span>
|
||||
, Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of
|
||||
<span class="ts-text is-code">
|
||||
/plugins/your_plugin_name/{your_web_root}/img/logo.svg
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
<h3 id="making-get-request">
|
||||
Making GET request
|
||||
</h3>
|
||||
<p>
|
||||
Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like
|
||||
<span class="ts-text is-code">
|
||||
./api/foo/bar
|
||||
</span>
|
||||
instead. Here is an example on a front-end and back-end implementation of a simple “echo” API.
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
The API logic is simple: when you make a GET request to the API with
|
||||
<span class="ts-text is-code">
|
||||
?name=foobar
|
||||
</span>
|
||||
, it returns
|
||||
<span class="ts-text is-code">
|
||||
Hello foobar
|
||||
</span>
|
||||
. Here is the backend implementation in your plugin code.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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)
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
And here is the front-end code in your HTML file
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><!-- 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>
|
||||
</code></pre>
|
||||
<h3 id="making-post-request">
|
||||
Making POST request
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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
|
||||
<a href="https://github.com/tobychui/zoraxy/issues/267" target="_blank">
|
||||
#267
|
||||
</a>
|
||||
for more details).
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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<br>", key, value)
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
For the front-end, you will need to use the
|
||||
<span class="ts-text is-code">
|
||||
$.cjax
|
||||
</span>
|
||||
function implemented in Zoraxy
|
||||
<span class="ts-text is-code">
|
||||
utils.js
|
||||
</span>
|
||||
file. You can include this file by adding these two lines to your HTML file.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-html"><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>
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
After that, you can call to the
|
||||
<span class="ts-text is-code">
|
||||
$.cjax
|
||||
</span>
|
||||
function just like what you would usually do with the
|
||||
<span class="ts-text is-code">
|
||||
$ajax
|
||||
</span>
|
||||
function.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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. <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();
|
||||
}
|
||||
});
|
||||
</code></pre>
|
||||
<h3 id="post-request-with-vanilla-js">
|
||||
POST Request with Vanilla JS
|
||||
</h3>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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}})
|
||||
})
|
||||
</code></pre>
|
||||
<div class="ts-divider has-top-spaced-large"></div>
|
||||
<h2 id="5-full-code">
|
||||
5. Full Code
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Here is the full code of the RESTFUL example for reference.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Front-end (
|
||||
<span class="ts-text is-code">
|
||||
plugins/restful-example/www/index.html
|
||||
</span>
|
||||
)
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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>
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
Backend (
|
||||
<span class="ts-text is-code">
|
||||
plugins/restful-example/main.go
|
||||
</span>
|
||||
)
|
||||
</p>
|
||||
</p>
|
||||
<pre><code class="language-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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</code></pre>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
What you should expect to see if everything is correctly loaded and working in Zoray
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<div class="ts-image is-rounded" style="max-width: 800px">
|
||||
<img src="img/2. RESTful Example/image-20250530153148506.png" alt="image-20250530153148506" />
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ts-container">
|
||||
<div class="ts-divider"></div>
|
||||
<div class="ts-content">
|
||||
<div class="ts-text">
|
||||
Zoraxy © tobychui
|
||||
<span class="thisyear">
|
||||
2025
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(".thisyear").text(new Date().getFullYear());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
@ -131,6 +131,9 @@
|
||||
<a class="item" href="/html/3. Basic Examples/1. Hello World.html">
|
||||
Hello World
|
||||
</a>
|
||||
<a class="item" href="/html/3. Basic Examples/2. RESTful Example.html">
|
||||
RESTful Example
|
||||
</a>
|
||||
</div>
|
||||
<a class="item is-active" href="/html/index.html">
|
||||
index
|
||||
@ -141,6 +144,9 @@
|
||||
<div class="column is-12-wide">
|
||||
<div class="ts-box">
|
||||
<div class="ts-container is-padded has-top-padded-large">
|
||||
<div class="ts-image is-rounded">
|
||||
<img src="../assets/banner.png" alt="Zoraxy Plugin Banner" style="width: 100%; height: auto; border-radius: 0.5rem;">
|
||||
</div>
|
||||
<h1 id="index">
|
||||
Index
|
||||
</h1>
|
||||
@ -184,6 +190,23 @@
|
||||
To add your plugin to the official plugin store, open a pull request (PR) in the plugin repository.
|
||||
</p>
|
||||
</p>
|
||||
<h2 id="gnu-free-documentation-license">
|
||||
GNU Free Documentation License
|
||||
</h2>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
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.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
<p class="ts-text">
|
||||
A copy of the license is available at
|
||||
<a href="https://www.gnu.org/licenses/fdl-1.3.html" target="_blank">
|
||||
https://www.gnu.org/licenses/fdl-1.3.html
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="refresh" content="0; url=html/1.%20Introduction/1.%20Getting%20Started.html#!" />
|
||||
<meta http-equiv="refresh" content="0; url=html/" />
|
||||
<title>Redirecting...</title>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -71,6 +71,11 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -41,8 +41,8 @@ func main() {
|
||||
|
||||
func startWebServerInBackground() {
|
||||
go func() {
|
||||
server := &http.Server{Addr: ":8080", Handler: http.DefaultServeMux}
|
||||
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)
|
||||
})
|
||||
|
Reference in New Issue
Block a user