Build and publish release / metadata (push) Canceled after 0s
Build and publish release / create-release (push) Canceled after 0s
Build and publish release / build (push) Canceled after 0s
Build and publish release / publish-release (push) Canceled after 0s
Generate and publish developer Wiki / generated-wiki (push) Failing after 43s
Reviewed-on: #38 Co-authored-by: Koda YeenBean <n122330@gmail.com>
361 lines
15 KiB
C
361 lines
15 KiB
C
/*
|
|
* 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);
|
|
}
|