refactor #1 + adding documentation
Validate source documentation / generated-wiki (pull_request) Failing after 10s
Validate source documentation / generated-wiki (pull_request) Failing after 10s
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* Handle controller commands, adoption state changes, provisioning results,
|
||||
* firmware-version spoofing, and protocol-safe response diagnostics.
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "inform.h"
|
||||
#include "crypto.h"
|
||||
#include "http.h"
|
||||
#include "wlan.h"
|
||||
#include "state.h"
|
||||
#include "config.h"
|
||||
#include "sysinfo.h"
|
||||
#include "clients.h"
|
||||
#include "lldp.h"
|
||||
#include "inform_internal.h"
|
||||
|
||||
|
||||
static int inform_valid_auth_key(const char *key)
|
||||
{
|
||||
if (!key || strlen(key) != 32)
|
||||
return 0;
|
||||
for (size_t i = 0; i < 32; i++)
|
||||
if (!isxdigit((unsigned char)key[i]))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int protocol_debug_level;
|
||||
|
||||
void inform_set_debug_level(int level)
|
||||
{
|
||||
protocol_debug_level = level < 0 ? 0 : level > 2 ? 2 : level;
|
||||
}
|
||||
|
||||
int inform_debug_level(void)
|
||||
{
|
||||
return protocol_debug_level;
|
||||
}
|
||||
|
||||
static int inform_debug_system_cfg_key(const char *key)
|
||||
{
|
||||
if (!key || (strncmp(key, "aaa.", 4) &&
|
||||
strncmp(key, "wireless.", 9)))
|
||||
return 0;
|
||||
return strstr(key, ".ssid") || strstr(key, ".id") ||
|
||||
strstr(key, ".vap_ind") || strstr(key, ".parent");
|
||||
}
|
||||
|
||||
void inform_log_controller_response(struct json_object *response,
|
||||
const char *raw_json)
|
||||
{
|
||||
if (protocol_debug_level <= 0 || !response)
|
||||
return;
|
||||
|
||||
LOG("Protocol debug: decrypted controller response fields follow");
|
||||
json_object_object_foreach(response, key, value) {
|
||||
LOG("Protocol field: %s type=%s", key,
|
||||
json_type_to_name(json_object_get_type(value)));
|
||||
}
|
||||
|
||||
struct json_object *system_cfg_object;
|
||||
if (json_object_object_get_ex(response, "system_cfg",
|
||||
&system_cfg_object)) {
|
||||
const char *system_cfg = json_object_get_string(system_cfg_object);
|
||||
char *copy = system_cfg ? strdup(system_cfg) : NULL;
|
||||
if (copy) {
|
||||
char *save = NULL;
|
||||
for (char *line = strtok_r(copy, "\n", &save);
|
||||
line; line = strtok_r(NULL, "\n", &save)) {
|
||||
line[strcspn(line, "\r")] = '\0';
|
||||
char *equals = strchr(line, '=');
|
||||
if (!equals)
|
||||
continue;
|
||||
*equals = '\0';
|
||||
if (inform_debug_system_cfg_key(line))
|
||||
LOG("Protocol system_cfg: %s=%s", line, equals + 1);
|
||||
}
|
||||
free(copy);
|
||||
}
|
||||
}
|
||||
|
||||
if (protocol_debug_level >= 2) {
|
||||
LOG("WARNING: full decrypted response may contain credentials");
|
||||
LOG("Protocol response JSON: %s", raw_json ? raw_json : "");
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
sys_stats — CPU and memory of the system
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
The controller shows CPU and RAM in the device view.
|
||||
We read /proc/stat and /proc/meminfo directly.
|
||||
*/
|
||||
static void reboot_openwrt(void)
|
||||
{
|
||||
LOG("Controller requested an OpenWrt reboot");
|
||||
int status = system("/sbin/reboot");
|
||||
if (status != 0)
|
||||
LOG("OpenWrt reboot command failed with status=%d", status);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
Process JSON command from the controller
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
_type == "noop" → do nothing
|
||||
_type == "reboot" → reboot OpenWrt
|
||||
_type == "cmd" → set-adopt / legacy reboot / reset / locate
|
||||
_type == "setstate" → apply radio_table + vap_table via UCI
|
||||
_type == "setparam" → change a single parameter
|
||||
*/
|
||||
void inform_handle_response(openuf_state_t *st,
|
||||
const uf_model_t *model,
|
||||
struct json_object *resp,
|
||||
char *action_out)
|
||||
{
|
||||
struct json_object *v;
|
||||
const char *type = "noop";
|
||||
if (json_object_object_get_ex(resp, "_type", &v) &&
|
||||
json_object_is_type(v, json_type_string))
|
||||
type = json_object_get_string(v);
|
||||
|
||||
LOG("Handling response type: %s", type);
|
||||
|
||||
/* An OpenWrt host cannot install UniFi firmware. Acknowledge the
|
||||
* controller's request by reporting its target version from now on. */
|
||||
if (!strcmp(type, "upgrade")) {
|
||||
if (json_object_object_get_ex(resp, "version", &v) &&
|
||||
json_object_is_type(v, json_type_string)) {
|
||||
const char *version = json_object_get_string(v);
|
||||
size_t len = strlen(version);
|
||||
if (len > 0 && len < sizeof(st->firmware_version)) {
|
||||
snprintf(st->firmware_version,
|
||||
sizeof(st->firmware_version), "%s", version);
|
||||
state_save(st);
|
||||
LOG("Firmware upgrade spoofed; now reporting version=%s",
|
||||
st->firmware_version);
|
||||
strcpy(action_out, "upgrade-spoofed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG("Ignoring upgrade response without a valid version");
|
||||
strcpy(action_out, "upgrade-invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── noop ────────────────────────────────────────────────────── */
|
||||
if (!strcmp(type, "noop")) {
|
||||
strcpy(action_out, "noop");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Modern controllers send reboot as a top-level response type rather
|
||||
* than wrapping it in {"_type":"cmd","cmd":"reboot"}. */
|
||||
if (!strcmp(type, "reboot")) {
|
||||
strcpy(action_out, "reboot");
|
||||
reboot_openwrt();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── setparam ────────────────────────────────────────────────── */
|
||||
if (!strcmp(type, "setparam")) {
|
||||
int received_adoption_key = 0;
|
||||
int applied_system_cfg = 0;
|
||||
|
||||
/* First parse mgmt_cfg used by modern controllers. */
|
||||
if (json_object_object_get_ex(resp, "mgmt_cfg", &v)) {
|
||||
const char *mgmt_cfg = json_object_get_string(v);
|
||||
LOG("Parsing mgmt_cfg: %s", mgmt_cfg);
|
||||
|
||||
/* Parse newline-separated key=value pairs. */
|
||||
char cfg_copy[2048];
|
||||
strncpy(cfg_copy, mgmt_cfg, sizeof(cfg_copy)-1);
|
||||
cfg_copy[sizeof(cfg_copy)-1] = '\0';
|
||||
|
||||
char *line = strtok(cfg_copy, "\n");
|
||||
while (line) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (eq) {
|
||||
*eq = '\0';
|
||||
const char *key = line;
|
||||
const char *val = eq + 1;
|
||||
|
||||
if (!strcmp(key, "authkey"))
|
||||
LOG("mgmt_cfg param: authkey = %.8s...", val);
|
||||
else
|
||||
LOG("mgmt_cfg param: %s = %s", key, val);
|
||||
|
||||
if (!strcmp(key, "authkey")) {
|
||||
if (inform_valid_auth_key(val) &&
|
||||
strcmp(st->authkey, val) != 0) {
|
||||
int replacing_key = st->authkey[0] &&
|
||||
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
|
||||
strncpy(st->authkey, val,
|
||||
sizeof(st->authkey)-1);
|
||||
st->authkey[sizeof(st->authkey)-1] = '\0';
|
||||
received_adoption_key = 1;
|
||||
LOG("%s device key from setparam",
|
||||
replacing_key ? "Replaced" : "Accepted");
|
||||
} else if (!inform_valid_auth_key(val)) {
|
||||
LOG("Ignoring invalid authkey from setparam");
|
||||
}
|
||||
} else if (!strcmp(key, "cfgversion")) {
|
||||
/*
|
||||
* This is the version the controller wants, not proof
|
||||
* that its setstate has been applied locally.
|
||||
*/
|
||||
LOG("Controller requested cfgversion=%s; currently applied=%s",
|
||||
val, st->cfgversion);
|
||||
} else if (!strcmp(key, "use_aes_gcm")) {
|
||||
st->use_aes_gcm = !strcmp(val, "true") ||
|
||||
!strcmp(val, "1");
|
||||
LOG("AES-GCM %s for subsequent inform packets",
|
||||
st->use_aes_gcm ? "enabled" : "disabled");
|
||||
} else if (!strcmp(key, "mgmt_url")) {
|
||||
/* Could save mgmt_url for future use */
|
||||
}
|
||||
/* Other management parameters are currently informational. */
|
||||
}
|
||||
line = strtok(NULL, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
struct json_object *system_cfg_obj;
|
||||
if (json_object_object_get_ex(resp, "system_cfg",
|
||||
&system_cfg_obj)) {
|
||||
const char *system_cfg = json_object_get_string(system_cfg_obj);
|
||||
LOG("Applying legacy system_cfg, length=%zu",
|
||||
strlen(system_cfg));
|
||||
if (wlan_apply_system_cfg(system_cfg, model) == 0) {
|
||||
applied_system_cfg = 1;
|
||||
st->config_applied = true;
|
||||
st->config_schema = OPENUF_CONFIG_SCHEMA;
|
||||
if (json_object_object_get_ex(resp, "cfgversion", &v))
|
||||
snprintf(st->cfgversion, sizeof(st->cfgversion), "%s",
|
||||
json_object_get_string(v));
|
||||
LOG("Legacy system_cfg applied successfully, cfgversion=%s",
|
||||
st->cfgversion);
|
||||
} else {
|
||||
st->config_applied = false;
|
||||
strncpy(st->cfgversion, "0",
|
||||
sizeof(st->cfgversion) - 1);
|
||||
LOG("Legacy system_cfg failed; requesting provisioning retry");
|
||||
}
|
||||
}
|
||||
|
||||
/* Fall back to the direct key/value format used by older controllers. */
|
||||
if (json_object_object_get_ex(resp, "key", &v)) {
|
||||
const char *key = json_object_get_string(v);
|
||||
struct json_object *val_o;
|
||||
if (json_object_object_get_ex(resp, "value", &val_o)) {
|
||||
const char *val = json_object_get_string(val_o);
|
||||
LOG("setparam key=%s val=%s", key, val);
|
||||
if (!strcmp(key, "inform_url"))
|
||||
strncpy(st->inform_url, val, sizeof(st->inform_url)-1);
|
||||
else if (!strcmp(key, "authkey") && inform_valid_auth_key(val) &&
|
||||
strcmp(st->authkey, val) != 0) {
|
||||
int replacing_key = st->authkey[0] &&
|
||||
strcmp(st->authkey, DEFAULT_AUTH_KEY) != 0;
|
||||
strncpy(st->authkey, val, sizeof(st->authkey)-1);
|
||||
st->authkey[sizeof(st->authkey)-1] = '\0';
|
||||
received_adoption_key = 1;
|
||||
LOG("%s device key from direct setparam",
|
||||
replacing_key ? "Replaced" : "Accepted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Modern controllers complete adoption by returning the per-device
|
||||
* key in setparam. Mark the device adopted before its next inform so
|
||||
* both the payload and packet encryption switch to that key.
|
||||
*/
|
||||
if (received_adoption_key) {
|
||||
st->adopted = true;
|
||||
LOG("Adoption completed through setparam; next inform will use the controller key");
|
||||
}
|
||||
|
||||
state_save(st);
|
||||
LOG("State saved after setparam");
|
||||
strcpy(action_out, applied_system_cfg ? "provisioned" :
|
||||
received_adoption_key ? "adopted" : "setparam");
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── cmd ─────────────────────────────────────────────────────── */
|
||||
if (!strcmp(type, "cmd")) {
|
||||
const char *cmd = "";
|
||||
if (json_object_object_get_ex(resp, "cmd", &v))
|
||||
cmd = json_object_get_string(v);
|
||||
|
||||
if (!strcmp(cmd, "set-adopt") || !strcmp(cmd, "adopt")) {
|
||||
if (json_object_object_get_ex(resp, "uri", &v))
|
||||
strncpy(st->inform_url, json_object_get_string(v),
|
||||
sizeof(st->inform_url)-1);
|
||||
if (json_object_object_get_ex(resp, "key", &v))
|
||||
strncpy(st->authkey, json_object_get_string(v),
|
||||
sizeof(st->authkey)-1);
|
||||
st->adopted = true;
|
||||
state_save(st);
|
||||
strcpy(action_out, "adopted");
|
||||
LOG("Adopted successfully. Key: %.8s...", st->authkey);
|
||||
|
||||
} else if (!strcmp(cmd, "reboot")) {
|
||||
strcpy(action_out, "reboot");
|
||||
reboot_openwrt();
|
||||
|
||||
} else if (!strcmp(cmd, "reset")) {
|
||||
strcpy(action_out, "reset");
|
||||
system("rm -f " OPENUF_STATE_FILE);
|
||||
reboot_openwrt();
|
||||
|
||||
} else if (!strcmp(cmd, "locate")) {
|
||||
/* Blink LED — on OpenWrt: echo 1 > /sys/class/leds/.../trigger */
|
||||
strcpy(action_out, "locate");
|
||||
} else {
|
||||
snprintf(action_out, 64, "cmd:%s", cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── setstate — WiFi configuration from the controller ──────────── */
|
||||
if (!strcmp(type, "setstate")) {
|
||||
if (json_object_object_get_ex(resp, "cfgversion", &v))
|
||||
snprintf(st->cfgversion, sizeof(st->cfgversion),
|
||||
"%s", json_object_get_string(v));
|
||||
|
||||
struct json_object *rt = NULL, *vt = NULL;
|
||||
json_object_object_get_ex(resp, "radio_table", &rt);
|
||||
json_object_object_get_ex(resp, "vap_table", &vt);
|
||||
int apply_ok = 0;
|
||||
if (rt || vt) {
|
||||
printf("[openuf] Applying controller WiFi configuration...\n");
|
||||
apply_ok = wlan_apply_config(resp, model) == 0;
|
||||
} else {
|
||||
LOG("setstate contained neither radio_table nor vap_table");
|
||||
}
|
||||
|
||||
st->config_applied = apply_ok;
|
||||
if (apply_ok)
|
||||
st->config_schema = OPENUF_CONFIG_SCHEMA;
|
||||
if (!apply_ok) {
|
||||
strncpy(st->cfgversion, "0", sizeof(st->cfgversion) - 1);
|
||||
LOG("WiFi configuration failed; cfgversion reset so the controller retries");
|
||||
}
|
||||
state_save(st);
|
||||
strcpy(action_out, apply_ok ? "setstate" : "setstate-failed");
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(action_out, 64, "unknown:%s", type);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user