255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
# Skyweave API Framework
|
|
|
|
## 1. Overview
|
|
|
|
This document describes the refactored Skyweave API framework, a lightweight Lua micro-framework designed for the RVLink MV2400 Roof Unit (COMFAST CF-E5 derivative) running a legacy OpenWrt version with Lua 5.1. It replaces the original monolithic `skyweave.lua` script with a more modular, extensible, and maintainable architecture.
|
|
|
|
The primary goal of this refactoring is to decouple request routing and handling from the business logic of individual API endpoints, making it easier to add, remove, or modify endpoints.
|
|
|
|
## 2. Architecture
|
|
|
|
The framework is structured across several key directories and files:
|
|
|
|
* **/app/cgi_scripts/skyweave.lua** (source): This is the main CGI script. For deployment, it should be placed at **`/www/cgi-bin/skyweave.lua`** and be made executable. It is run by the `uhttpd` web server (with Lua CGI support enabled via `/etc/config/uhttpd`).
|
|
* **Note for Deployment**: Ensure `uhttpd` is configured to handle `.lua` scripts as CGI, and the `lua` interpreter is available at `#!/usr/bin/lua`.
|
|
* **/app/skyweave/** (source): This directory contains all core framework modules. For deployment, this entire directory should be copied to **`/usr/lib/lua/skyweave/`** on the target system.
|
|
* `router.lua`: The routing engine. It maps incoming requests to the appropriate handler functions.
|
|
* `request.lua`: Handles parsing of incoming HTTP requests (method, query, headers, body) into a `request_context` table.
|
|
* `response.lua`: Provides helper functions for sending standardized JSON responses (success and error).
|
|
* `utils.lua`: A collection of common utility functions (e.g., command execution, UCI interaction, string parsing).
|
|
* **/app/skyweave/handlers/**: This directory houses the business logic for individual API endpoints. Each `.lua` file typically groups related handlers.
|
|
* Examples: `network.lua`, `system.lua`, `wifi.lua`.
|
|
* **/app/lib/**: Contains external Lua libraries.
|
|
* `json.lua`: The rxi/json.lua library. For deployment, this file (from `/app/lib/json.lua` in the repository) should be copied to **`/usr/lib/lua/json.lua`** on the target system.
|
|
|
|
### Lua Module Loading & Execution
|
|
|
|
The main CGI script (`/www/cgi-bin/skyweave.lua` after deployment) relies on Lua's standard `package.path` mechanism to find modules. It uses `require` calls like `require("skyweave.router")` and `require("json")`. This requires:
|
|
1. The `skyweave` framework modules to be located at `/usr/lib/lua/skyweave/`.
|
|
2. The `json.lua` library to be located at `/usr/lib/lua/json.lua`.
|
|
3. The OpenWrt Lua interpreter's `package.path` to include `/usr/lib/lua/?.lua` and `/usr/lib/lua/?/init.lua` (which is typical).
|
|
|
|
The script does **not** perform any local `package.path` manipulation.
|
|
|
|
## 3. Key Framework Components
|
|
|
|
### 3.1. Router (`skyweave/router.lua`)
|
|
|
|
The router is responsible for directing incoming requests to the correct handler function. It maintains a routing table where each route is defined by an HTTP method (e.g., "GET", "POST") and a conceptual path key (e.g., "/network_config").
|
|
|
|
A key feature for backward compatibility is its ability to translate query parameters from the original API style (e.g., `?get_config=status` or `?set_config=wifi`) into these internal method/path keys for routing.
|
|
The router now also supports dispatching based on the URL path (via the CGI `PATH_INFO` variable). If a request like `/cgi-bin/skyweave.lua/network/config` is made, and `PATH_INFO` is correctly passed by the uhttpd server as `/network/config`, the router will use this path directly to find a matching route. This `PATH_INFO`-based routing is prioritized over the query parameter method.
|
|
|
|
### 3.2. Request Context (`skyweave/request.lua`)
|
|
|
|
Instead of direct CGI variable manipulation, all handler functions receive a single Lua table called `request_context`. This table contains pre-parsed information about the request:
|
|
|
|
* `request.method`: The HTTP request method (e.g., "GET", "POST").
|
|
* `request.path`: The PATH_INFO component of the URL (if provided by `uhttpd` from the web server configuration).
|
|
* `request.query`: A table of parsed query string parameters.
|
|
* `request.headers`: A table of HTTP headers.
|
|
* `request.raw_body`: The raw request body as a string.
|
|
* `request.body`: If the `Content-Type` is `application/json` and a JSON library is available, this field contains the parsed JSON body as a Lua table. Otherwise, it's `nil`.
|
|
* `request.body_parse_error`: Contains an error message if JSON parsing of the body failed.
|
|
|
|
### 3.3. Response Helpers (`skyweave/response.lua`)
|
|
|
|
To ensure consistent API output, two main helper functions are provided:
|
|
|
|
* `response.json(data_table, http_status_code)`: Sends a successful JSON response. `http_status_code` defaults to 200.
|
|
* `response.error(error_message, http_status_code, internal_error_code)`: Sends a JSON error response. `http_status_code` defaults to 500, and `internal_error_code` defaults to -1.
|
|
|
|
Both functions automatically set the `Content-Type: application/json` header and the CGI `Status:` header (e.g., "Status: 200 OK").
|
|
|
|
## 4. API Endpoints
|
|
|
|
This section describes how to add new API endpoints and how they can be accessed.
|
|
|
|
### 4.1. Create/Update Handler Function
|
|
|
|
1. **Location**: Create a new Lua module or open an existing one in the `/app/skyweave/handlers/` directory (e.g., `/app/skyweave/handlers/myfeature.lua`).
|
|
2. **Structure**:
|
|
```lua
|
|
-- /app/skyweave/handlers/myfeature.lua
|
|
local myfeature_handler = {}
|
|
|
|
-- Handler function
|
|
function myfeature_handler.get_custom_data(request_context)
|
|
-- Access request data via request_context
|
|
local item_id = request_context.query.id
|
|
|
|
if not item_id then
|
|
return { errCode = -100, errMsg = "Item ID is required" }
|
|
end
|
|
|
|
-- Your business logic here...
|
|
local data = { id = item_id, value = "Some data for " .. item_id }
|
|
|
|
return data -- This will be sent as a JSON response
|
|
end
|
|
|
|
return myfeature_handler
|
|
```
|
|
3. **Function Signature**: The handler function must accept a single argument, `request_context`.
|
|
4. **Return Value**:
|
|
* For a successful synchronous response, return a Lua table. This table will be automatically encoded as JSON.
|
|
* To indicate an error handled within the function, return a table with `errCode` and `errMsg` fields.
|
|
* To trigger a 204 No Content response, return `nil`.
|
|
* For asynchronous tasks, see Section 5.
|
|
|
|
### 4.2. Register Route
|
|
|
|
1. **Edit Main Dispatcher**: Open `/app/cgi_scripts/skyweave.lua`.
|
|
2. **Require Handler Module**: Add a `require` statement for your new handler module:
|
|
```lua
|
|
local myfeature_handlers = require("skyweave.handlers.myfeature")
|
|
```
|
|
3. **Add Route**: In the "Register Routes" section, call `router:add_route()`:
|
|
```lua
|
|
-- Example: Map GET ?get_config=custom_data to myfeature_handler.get_custom_data
|
|
router:add_route("GET", "/custom_data", myfeature_handlers.get_custom_data)
|
|
```
|
|
The `path_key` (e.g., `"/custom_data"`) will be matched if the router derives this key either from a `PATH_INFO` like `/custom_data` in the URL (e.g. `/www/cgi-bin/skyweave.lua/custom_data`), or from a query parameter like `?get_config=custom_data` (as per the logic in `determine_route_key_from_request`).
|
|
|
|
## 5. Asynchronous Task Pattern
|
|
|
|
For operations that may take a long time (e.g., restarting services, applying complex configurations), handlers can initiate an asynchronous task. This prevents the HTTP request from timing out.
|
|
|
|
To use this pattern, a handler function should return a special Lua table:
|
|
|
|
```lua
|
|
function network_handler.set_connect_wifi(request_context)
|
|
-- ... (validation and command setup) ...
|
|
local uci_commands = "uci set wireless.wwan.ssid='MyNetwork'; uci commit wireless; wifi"
|
|
|
|
return {
|
|
async = true, -- Required: signals an asynchronous task
|
|
command = uci_commands, -- Required: the shell command to execute
|
|
response = { -- Required: the immediate JSON response
|
|
errCode = 0,
|
|
errMsg = "WiFi connection command accepted. Will apply in background."
|
|
}
|
|
}
|
|
end
|
|
```
|
|
|
|
The framework will:
|
|
1. Immediately send the content of the `response` field as a JSON response to the client (typically with an HTTP 202 Accepted status).
|
|
2. Execute the `command` string in the system shell in the background. The execution is wrapped like this: `(sleep 2; <your_command_here>) >/dev/null 2>&1 &`. The `sleep 2` provides a brief buffer before execution.
|
|
|
|
## 6. Dependencies
|
|
|
|
* **Lua 5.1**
|
|
* **JSON Library**: The framework modules now use `require("json")` to load the JSON library. It is assumed that `lua-cjson` is not available or not preferred. For deployment, the file `/app/lib/json.lua` (which is rxi/json.lua) must be copied to a standard Lua path on the target OpenWrt system and named `json.lua` (e.g., copy `/app/lib/json.lua` to `/usr/lib/lua/json.lua`).
|
|
|
|
## 7. Original Script Analysis Summary
|
|
|
|
The original `skyweave.lua` was a monolithic script handling all API logic. This refactoring introduced:
|
|
* **Decoupled Routing Engine**: Clear separation of routing from handler logic.
|
|
* **Request Abstraction**: Unified `request_context` object.
|
|
* **Standardized Response Helpers**: Consistent JSON output.
|
|
* **Formalized Asynchronous Task Pattern**: Standard way for handlers to perform background tasks.
|
|
* **Modular Code Structure**: Improved organization and maintainability.
|
|
|
|
## 8. Example Refactored Handler: `set_connect_wifi`
|
|
|
|
The `set_connect_wifi` handler in `/app/skyweave/handlers/network.lua` demonstrates using the `request_context.body` for POST data and the asynchronous task return pattern:
|
|
|
|
```lua
|
|
-- In /app/skyweave/handlers/network.lua
|
|
local utils = require("skyweave.utils")
|
|
local network_handler = {}
|
|
|
|
-- ... (other handlers like get_network_config) ...
|
|
|
|
function network_handler.set_connect_wifi(request_context)
|
|
local body = request_context.body
|
|
if not body then
|
|
return { errCode = -29, errMsg = "Request body is missing or not valid JSON.", configDone = false }
|
|
end
|
|
|
|
local ssid = body.ssid
|
|
-- ... (retrieve other params: bssid, encryption_type, key, device from body) ...
|
|
|
|
-- ... (validation logic for ssid, encryption_type, key) ...
|
|
-- Example: if not ssid or ssid == "" then return { errCode = -30, errMsg = "SSID is required." } end
|
|
|
|
local commands = {}
|
|
local function add_cmd(cmd) table.insert(commands, cmd) end
|
|
|
|
-- ... (Build UCI commands as strings using add_cmd) ...
|
|
-- Example: add_cmd("uci set wireless.wwan.ssid='" .. ssid:gsub("'", "'\\''") .. "'")
|
|
add_cmd("uci set wireless.wwan.disabled=0")
|
|
add_cmd("uci set wireless.wwan.mode=sta")
|
|
-- Add all other necessary UCI commands for setting up Wi-Fi connection
|
|
add_cmd("uci commit wireless")
|
|
add_cmd("uci commit network")
|
|
add_cmd("/etc/init.d/network restartall")
|
|
|
|
local command_string = table.concat(commands, "; ")
|
|
|
|
local response_msg = "OK (Configuration accepted. Device will attempt to connect to SSID: " .. ssid .. ")"
|
|
-- ... (append BSSID to response_msg if present) ...
|
|
|
|
return {
|
|
async = true,
|
|
command = command_string,
|
|
response = {
|
|
errCode = 0,
|
|
errMsg = response_msg,
|
|
configDone = true
|
|
}
|
|
}
|
|
end
|
|
|
|
return network_handler
|
|
```
|
|
This example illustrates how to access POST data from `request_context.body` and how to structure the return for an asynchronous command execution.
|
|
|
|
### 4.3. Accessing API Endpoints
|
|
|
|
API endpoints can be accessed in two ways:
|
|
|
|
**A. Path-Based URLs (Recommended for clarity):**
|
|
This method uses the path part of the URL after the script name.
|
|
- Example GET: `curl http://<device_ip>/cgi-bin/skyweave.lua/network_config`
|
|
- Example POST: `curl -X POST -d '{\"ssid\":\"MyNet\"}' http://<device_ip>/cgi-bin/skyweave.lua/connect_wifi`
|
|
This relies on your uhttpd server correctly setting the `PATH_INFO` environment variable (see Section 9: uhttpd Server Configuration).
|
|
|
|
**B. Query Parameter-Based URLs (Legacy/Alternative):**
|
|
This method uses query parameters like `get_config` or `set_config`.
|
|
- Example GET: `curl http://<device_ip>/cgi-bin/skyweave.lua?get_config=network_config`
|
|
- Example POST: `curl -X POST -d '{\"ssid\":\"MyNet\"}' http://<device_ip>/cgi-bin/skyweave.lua?set_config=connect_wifi`
|
|
|
|
## 9. uhttpd Server Configuration for PATH_INFO
|
|
|
|
The path-based URL feature (e.g., `/cgi-bin/skyweave.lua/network/config`) depends on the `uhttpd`
|
|
web server correctly interpreting the part of the URL after the script name as `PATH_INFO`
|
|
and passing it as an environment variable to the `skyweave.lua` script.
|
|
|
|
Standard OpenWrt `uhttpd` configurations for CGI scripts typically support this by default.
|
|
Ensure your `/etc/config/uhttpd` includes:
|
|
```
|
|
config uhttpd main
|
|
# ... other main settings ...
|
|
option cgi_prefix '/cgi-bin'
|
|
|
|
config uhttpd cgi
|
|
list interpreter '.lua=/usr/bin/lua'
|
|
# For OpenWrt 21.02+ or uhttpd versions with rfc_cgi_pathinfo support,
|
|
# explicitly enabling it can ensure compliance, though often not needed:
|
|
# option rfc_cgi_pathinfo '1'
|
|
```
|
|
Usually, no special configuration for `PATH_INFO` itself is required beyond enabling CGI for Lua.
|
|
|
|
**Verifying `PATH_INFO`:**
|
|
If you suspect `PATH_INFO` is not being set, you can temporarily add the following
|
|
at the beginning of your `/www/cgi-bin/skyweave.lua` script (after deployment):
|
|
```lua
|
|
print("Content-Type: text/plain")
|
|
print("")
|
|
print("DEBUG PATH_INFO: " .. (os.getenv("PATH_INFO") or "PATH_INFO is nil"))
|
|
os.exit()
|
|
```
|
|
Then, access a URL like `http://<device_ip>/cgi-bin/skyweave.lua/test/path`. The output
|
|
should show `/test/path`. Remember to remove this debugging code afterwards. |