Files
skyweave/README.md
T

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.