mirror of
https://github.com/OpenMANET/morse-feed.git
synced 2025-12-10 03:43:06 -06:00
wizard-config: (APP-3299, PR #751) auto-configure client device
This is like a cut-rate DPP: requires TCP/IP connection, and no security on the link. Most of the client/server side is run via a ucode script (wizard-config). Refer to HELP at the top of this for more info about what is going on here. This is to get around the problem of wizards on bridged station devices which we either don't have access to _or_ the problem of wizards which change IPs under you. Client config will only happen on first boot after a reset. Approved-by: Sophronia Koilpillai Approved-by: Evan Benn
This commit is contained in:
parent
a778bef4ac
commit
33769b0e13
@ -45,9 +45,6 @@ _maybe_press_dpp_button() {
|
||||
# Check that the device is in a DPP mode (AP or STA) and tell hostap that the
|
||||
# button is pressed if so.
|
||||
maybe_press_dpp_button() {
|
||||
if [ "$(uci -q get prplmesh.config.enable)" = '1' ]; then
|
||||
return
|
||||
fi
|
||||
config_load wireless
|
||||
config_foreach _maybe_press_dpp_button wifi-iface
|
||||
}
|
||||
|
||||
33
utils/wizard-config/Makefile
Normal file
33
utils/wizard-config/Makefile
Normal file
@ -0,0 +1,33 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
PKG_NAME:=wizard-config
|
||||
PKG_RELEASE=1
|
||||
|
||||
PKG_MAINTAINER:=Morse Micro
|
||||
|
||||
define Package/wizard-config
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=Configure HaLow devices automatically on DHCP client success
|
||||
DEPENDS:=persistent-vars-storage @BUSYBOX_CONFIG_UDHCPC +umdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-uci +ucode-mod-math
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/wizard-config/install
|
||||
$(INSTALL_DIR) $(1)/sbin
|
||||
$(INSTALL_BIN) ./files/sbin/wizard-config $(1)/sbin
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/etc/init.d/wizard-config $(1)/etc/init.d/
|
||||
$(INSTALL_DIR) $(1)/etc/udhcpc.user.d
|
||||
$(INSTALL_BIN) ./files/etc/udhcpc.user.d/wizard-config $(1)/etc/udhcpc.user.d/
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/99-wizard-config $(1)/etc/uci-defaults/
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/wizard-config $(1)/etc/config/
|
||||
$(INSTALL_DIR) $(1)/etc/umdns
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,wizard-config))
|
||||
3
utils/wizard-config/README
Normal file
3
utils/wizard-config/README
Normal file
@ -0,0 +1,3 @@
|
||||
Mechanism to configure client devices after they discover a server device.
|
||||
|
||||
See the HELP in bin/wizard-config for more information.
|
||||
1
utils/wizard-config/files/etc/config/wizard-config
Normal file
1
utils/wizard-config/files/etc/config/wizard-config
Normal file
@ -0,0 +1 @@
|
||||
config main main
|
||||
34
utils/wizard-config/files/etc/init.d/wizard-config
Executable file
34
utils/wizard-config/files/etc/init.d/wizard-config
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
# Must be after uhttpd.
|
||||
START=99
|
||||
|
||||
# We use procd not because we're a legit service - this is a script
|
||||
# that runs only to do reconfiguration - but so we can
|
||||
# easily rerun on changes to wireless/prplmesh config.
|
||||
USE_PROCD=1
|
||||
|
||||
start_service() {
|
||||
if [ "$(uci get wizard-config.main.server)" != 1 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
procd_open_instance wizard-config
|
||||
procd_set_param command wizard-config server
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
# Our ucode script reads these directly, so we tell procd we rely on them
|
||||
# so it knows to restart us if they change.
|
||||
procd_set_param file /etc/config/wireless
|
||||
procd_set_param file /etc/config/prplmesh
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
# Tell wizard-config that we're stopping (i.e. remove umdns stuff).
|
||||
wizard-config serverstop
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger wireless prplmesh
|
||||
}
|
||||
19
utils/wizard-config/files/etc/uci-defaults/99-wizard-config
Normal file
19
utils/wizard-config/files/etc/uci-defaults/99-wizard-config
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ "$(uci -q get wizard-config.main.server)" != "" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Disable jail, as it somehow blocks reading from /etc/umdns
|
||||
# (and appears to occasionally lead to umdns segfaults).
|
||||
uci set umdns.@umdns[0].jail=0
|
||||
|
||||
if [ "$(persistent_vars_storage.sh READ mm_mode)" = sta ]; then
|
||||
uci set wizard-config.main.server=0
|
||||
uci set wizard-config.main.client=1
|
||||
else
|
||||
uci set wizard-config.main.server=1
|
||||
uci set wizard-config.main.client=0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
28
utils/wizard-config/files/etc/udhcpc.user.d/wizard-config
Normal file
28
utils/wizard-config/files/etc/udhcpc.user.d/wizard-config
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
|
||||
# NB this file is usually _sourced_ via /lib/netifd/dhcp.script,
|
||||
# so don't 'exit' or introduce any persistent changes to the shell
|
||||
# environment as it will affect subsequent scripts.
|
||||
|
||||
# Only do anything if we've got a new address.
|
||||
if [ "$1" != bound ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Only attempt to run if configured in uci.
|
||||
if [ "$(uci -q get wizard-config.main.client)" != 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# For now, to reduce potential confusion and security issues, only do this once,
|
||||
# _even if it fails_.
|
||||
WC_TMPDIR="$(mktemp -d -t wizard-config.XXXXXX)"
|
||||
uci -t "$WC_TMPDIR" set wizard-config.main.client=0
|
||||
uci -t "$WC_TMPDIR" commit
|
||||
rm -rf "$WC_TMPDIR"
|
||||
|
||||
echo Attempting to configure automatically... | logger -t wizard-config -p daemon.info
|
||||
# Prefer 'serverid' - i.e. DHCP server - if multiple mDNS results.
|
||||
wizard-config client "${serverid+x}" \
|
||||
2> >(logger -t wizard-config -p daemon.info) \
|
||||
> >(logger -t wizard-config -p daemon.error)
|
||||
585
utils/wizard-config/files/sbin/wizard-config
Executable file
585
utils/wizard-config/files/sbin/wizard-config
Executable file
@ -0,0 +1,585 @@
|
||||
#!/usr/bin/ucode -RS
|
||||
|
||||
const HELP = `
|
||||
wizard_config automatic configurator
|
||||
|
||||
This implements two sides of an automatic configuration process for
|
||||
client devices. The intent is that the primary device (the server)
|
||||
serves a configuration file which other devices (the clients) discover
|
||||
and apply.
|
||||
|
||||
WARNING!!!
|
||||
|
||||
This auto-configure code _relies_ on a HalowLink 1 being in a
|
||||
fresh 'reset to Extender' (aqua) mode to work correctly, and
|
||||
cannot reconfigure the device in an arbitrary way. The code
|
||||
is currently only run in this circumstance.
|
||||
|
||||
It also has some interesting security issues:
|
||||
- credentials for the router are available locally via HTTP
|
||||
(i.e. if you're on the local network, you have access)
|
||||
- if the first DHCP client address is obtained from something other
|
||||
than a HaLowLink, it may discover a _different_ _wizard-config._tcp
|
||||
service via mDNS, allowing someone on the local network to tell it
|
||||
to connect to an entirely different network. To make this less
|
||||
serious, the device will only reconfigure itself the first time
|
||||
it connects to a network.
|
||||
|
||||
So we rely heavily on the local network being secure; this is our
|
||||
general posture, since router passwords are sent in clear-text anyway.
|
||||
|
||||
This is a quick and dirty version of provisioning similar to DPP,
|
||||
and can actually work after a 'normal' DPP connection. Hopefully in the
|
||||
future we have the time to implement this behaviour via DPP.
|
||||
|
||||
END WARNING!!!
|
||||
|
||||
wizard_config serverstart
|
||||
|
||||
=> configure umdns so a _wizard-config._tcp service is advertised
|
||||
and create a /www/wizard_config.json file for our http server.
|
||||
This is usually run via /etc/init.d/wizard-config
|
||||
|
||||
wizard_config serverstop
|
||||
|
||||
=> cleanup: remove the umdns entry for the service.
|
||||
This is usually run via /etc/init.d/wizard-config
|
||||
|
||||
wizard_config client <preferredip>
|
||||
|
||||
=> use umdns to find a _wizard-config._tcp service, preferring
|
||||
preferredip if available. Then make an HTTP request against
|
||||
http://ip:port/wizard_config.json, and interpret this file
|
||||
to mutate (and subsequently reload) the device config.
|
||||
This is usually run by /etc/user.udhcpc.d/wizard-config
|
||||
|
||||
The JSON blob response currently looks something like:
|
||||
|
||||
{
|
||||
"mode": "standard",
|
||||
"ssid": "foobar",
|
||||
"key": "secret123",
|
||||
}
|
||||
|
||||
i.e. a set of simple key/value pairs, where the values are all strings.
|
||||
These values vary slightly depending on what the mode is; see
|
||||
generate_config() and apply_config() for the details. The good news
|
||||
is that because we define both sides of our interaction in this file,
|
||||
the chance for incompatibility is reduced, though of course we do need
|
||||
to take care if different versions interact. I would suggest
|
||||
creating a new TXT record entry if we want to achieve this (i.e.
|
||||
path_v2).
|
||||
|
||||
Currently, this code is somewhat tied to working only for the default
|
||||
Extender configuration for Artini, and as such is essentially limited
|
||||
to mutating the expected starting config in this mode.
|
||||
|
||||
So it\'s easier to test individual parts of the system, it also supports
|
||||
the following commands. These should only be needed for development and
|
||||
testing:
|
||||
|
||||
wizard_config _apply_config "JSONBLOB"
|
||||
wizard_config _find_service_url
|
||||
wizard_config _umdns_configure
|
||||
wizard_config _generate_config
|
||||
|
||||
`;
|
||||
|
||||
|
||||
import { connect } from "ubus";
|
||||
import { cursor } from "uci";
|
||||
import { popen, error, readfile, writefile, unlink } from "fs";
|
||||
import { rand } from "math";
|
||||
|
||||
|
||||
/* mDNS name of the service.
|
||||
*/
|
||||
const SERVICE = "_wizard-config._tcp";
|
||||
|
||||
const WWW_ROOT = "/www";
|
||||
const PATH_V1 = "/wizard-config.json";
|
||||
|
||||
|
||||
/* mDNS service definition.
|
||||
*
|
||||
* This echoes the kind of umdns service that we'd get for free if
|
||||
* we just turned on umdns in the procd script, but unfortunately this
|
||||
* would require our service to be persistent (umdns will only advertise
|
||||
* services where running:true) and there's no reason for us to be persistent
|
||||
* (since uhttpd is doing the actual persistent work here). I considered
|
||||
* extending uhttpd so it could advertise custom services, but this seemed
|
||||
* like a heavier/messier touch despite its architectural purity.
|
||||
*/
|
||||
const UMDNS_CONFIG_LOCATION = "/etc/umdns/wizard-config.json";
|
||||
const UMDNS_CONFIG = {
|
||||
"custom-wizard-config_80": {
|
||||
"service": "_wizard-config._tcp.local",
|
||||
"port": 80,
|
||||
"txt": ["path_v1=" + PATH_V1],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const UMDNS_RETRY_LIMIT = 5;
|
||||
const HTTP_RETRY_LIMIT = 5;
|
||||
|
||||
|
||||
/* Find all services of a particular type via mDNS.
|
||||
*
|
||||
* This will try multiple times, waiting after call umdns update.
|
||||
*
|
||||
* Example ubus browse output:
|
||||
*
|
||||
* # ubus call umdns browse '{"service": "_wizard-config._tcp", "array": true}'
|
||||
* {
|
||||
* "_wizard-config._tcp": {
|
||||
* "halowlink1-f4a1": {
|
||||
* "iface": "br-ahwlan",
|
||||
* "host": "halowlink1-f4a1.local",
|
||||
* "port": 80,
|
||||
* "txt": ["path=/wizard-config.json"]
|
||||
* "ipv4": ["10.11.0.227"],
|
||||
* "ipv6": ["fd82:57:cd72::1", "fe80::9683:c4ff:fe55:f4a2"]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function find_services(ubus, service) {
|
||||
for (let i = 0; i < UMDNS_RETRY_LIMIT; ++i) {
|
||||
const result = ubus.call("umdns", "browse", { service, array: true });
|
||||
if (!result) {
|
||||
warn("umdns browse error: '", ubus.error(), "'\n");
|
||||
} else if (result[service]) {
|
||||
return values(result[service]);
|
||||
}
|
||||
|
||||
// Couldn't find it. Let's try updating and wait for a bit.
|
||||
if (!ubus.call("umdns", "update") && ubus.error()) {
|
||||
warn("umdns update error: '", ubus.error(), "'\n");
|
||||
}
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* Find the 'best' service of those available.
|
||||
*
|
||||
* This is the first one to have the server_ip, or just the first one.
|
||||
*/
|
||||
function get_best_service(services, dhcp_server_ipv4) {
|
||||
for (let service in services) {
|
||||
if (service.ipv4 && index(service.ipv4, dhcp_server_ipv4) != -1) {
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
return services[0];
|
||||
}
|
||||
|
||||
|
||||
/* Extract the HTTP configurator URL from the mDNS record.
|
||||
*/
|
||||
function get_url_from_service(service) {
|
||||
let address;
|
||||
if (service.ipv4 && length(service.ipv4) > 0) {
|
||||
address = service.ipv4[0];
|
||||
} else if (service.ipv6 && length(service.ipv6) > 0) {
|
||||
// TODO: test this codepath.
|
||||
// I have only done IPv4 configuration.
|
||||
address = `[${service.ipv6[0]}]`;
|
||||
}
|
||||
|
||||
let port = service.port;
|
||||
|
||||
let path;
|
||||
for (let txt in service.txt) {
|
||||
txt = split(txt, "=");
|
||||
if (txt[0] === PATH_V1) {
|
||||
path = txt[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (address && port && path) {
|
||||
return `http://${address}:${port}${path}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Fetch config from URL.
|
||||
*
|
||||
* Return a string if failure, a JSON blob on success. This is so we can easily retry.
|
||||
*/
|
||||
function fetch_config_once(url) {
|
||||
// It seems there's no good way to do a popen without passing through the shell (!!!).
|
||||
// We don't have a good way to url encode this, but I think we're pretty safe if we just make
|
||||
// sure there are no single quotes.
|
||||
const proc = popen(`uclient-fetch -T 2 -q -O - '${replace(url, "'", "%27")}'`);
|
||||
if (!proc) {
|
||||
return `Unable to request expected file (${url}): ${error()}\n`;
|
||||
}
|
||||
|
||||
const raw_config = proc.read("all");
|
||||
if (raw_config === null) {
|
||||
return `Unable to read output from URL: ${proc.error()}\n`;
|
||||
}
|
||||
|
||||
const proc_code = proc.close();
|
||||
if (proc_code !== 0) {
|
||||
return `uclient-fetch had error: ${proc_code}\n`;
|
||||
}
|
||||
|
||||
try {
|
||||
return json(raw_config);
|
||||
} catch (e) {
|
||||
return `Configurator gave invalid JSON: ${raw_config}\n"`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Check that the config object has all the noted params as strings.
|
||||
*/
|
||||
function check_config(config, params) {
|
||||
let has_error = false;
|
||||
for (let param in params) {
|
||||
if (type(config[param]) !== "string") {
|
||||
warn("Missing valid config param: ", param, type(param), "\n");
|
||||
has_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
assert(!has_error, "Stopping due to bad config object from service\n");
|
||||
}
|
||||
|
||||
|
||||
function find_morse_device(uci) {
|
||||
const w = uci.get_all("wireless");
|
||||
return filter(keys(w),
|
||||
s => w[s][".type"] === "wifi-device" && w[s]["type"] === "morse" && w[s]["disabled"] !== "1")[0];
|
||||
}
|
||||
|
||||
|
||||
function find_ifaces(uci, device) {
|
||||
const w = uci.get_all("wireless");
|
||||
return filter(keys(w),
|
||||
s => w[s][".type"] === "wifi-iface" && w[s]["device"] === device && w[s]["disabled"] !== "1");
|
||||
}
|
||||
|
||||
|
||||
/* Interpret the json blob, applying the necessary changes for each mode.
|
||||
*/
|
||||
function apply_config (uci, config) {
|
||||
assert(uci.load("wireless"), "Missing wireless config; aborting.\n");
|
||||
assert(uci.load("system"), "Missing system config; aborting.\n");
|
||||
|
||||
const morse_device = find_morse_device(uci);
|
||||
assert(morse_device, "Unable to find morse wifi-device in UCI wireless config.\n");
|
||||
|
||||
const morse_iface = find_ifaces(uci, morse_device)[0];
|
||||
assert(morse_iface, "Unable to find morse wifi-iface in UCI wireless config.\n");
|
||||
|
||||
switch (config.mode) {
|
||||
case 'standard':
|
||||
check_config(config, ["country", "ssid", "key", "encryption"]);
|
||||
uci.set("wireless", morse_device, "country", config.country);
|
||||
uci.set("wireless", morse_iface, "mode", "sta");
|
||||
uci.set("wireless", morse_iface, "ssid", config.ssid);
|
||||
uci.set("wireless", morse_iface, "key", config.key);
|
||||
uci.set("wireless", morse_iface, "encryption", config.encryption);
|
||||
break;
|
||||
case 'mesh11s':
|
||||
check_config(config, ["country", "channel", "mesh_id", "key", "encryption"]);
|
||||
uci.set("wireless", morse_device, "country", config.country);
|
||||
uci.set("wireless", morse_device, "channel", config.channel);
|
||||
uci.set("wireless", morse_iface, "mode", "mesh");
|
||||
uci.set("wireless", morse_iface, "mesh_id", config.mesh_id);
|
||||
uci.set("wireless", morse_iface, "key", config.key);
|
||||
uci.set("wireless", morse_iface, "encryption", config.encryption);
|
||||
uci.set("wireless", morse_iface, "ifname", "mesh0");
|
||||
uci.set("system", "led_halow", "dev", "mesh0");
|
||||
break;
|
||||
case 'prplmesh':
|
||||
check_config(config, ["country", "channel", "ssid", "key", "encryption"]);
|
||||
assert(uci.load("network"), "Device missing uci network config");
|
||||
assert(uci.load("prplmesh"), "Configurator requests prplmesh but prplmesh is not installed.");
|
||||
|
||||
uci.set("wireless", morse_device, "country", config.country);
|
||||
// prplmesh shouldn't require a channel, but currently does due to
|
||||
// a bug with bringing up a HaLow AP and STA at the same time.
|
||||
uci.set("wireless", morse_device, "channel", config.channel);
|
||||
|
||||
uci.set("wireless", morse_iface, "mode", "ap");
|
||||
uci.set("wireless", morse_iface, "ssid", config.ssid);
|
||||
uci.set("wireless", morse_iface, "key", config.key);
|
||||
uci.set("wireless", morse_iface, "encryption", config.encryption);
|
||||
uci.set("wireless", morse_iface, "bss_transition", "1");
|
||||
uci.set("wireless", morse_iface, "multi_ap", "3");
|
||||
uci.set("wireless", morse_iface, "ieee80211k", "1");
|
||||
uci.set("wireless", morse_iface, "ieee80211w", "2");
|
||||
uci.set("wireless", morse_iface, "ifname", "wlan-prpl");
|
||||
|
||||
const backhaul_sta = "default_bh_" + morse_device;
|
||||
uci.set("wireless", backhaul_sta, "wifi-iface");
|
||||
uci.set("wireless", backhaul_sta, "device", morse_device);
|
||||
uci.set("wireless", backhaul_sta, "network", "lan");
|
||||
uci.set("wireless", backhaul_sta, "mode", "sta");
|
||||
uci.set("wireless", backhaul_sta, "ssid", config.ssid);
|
||||
uci.set("wireless", backhaul_sta, "key", config.key);
|
||||
uci.set("wireless", backhaul_sta, "multi_ap", "1");
|
||||
uci.set("wireless", backhaul_sta, "wds", "1");
|
||||
// TODO: we probably should do this, but...
|
||||
// uci.set("wireless", backhaul_sta, "sae_pwe", "1");
|
||||
// uci.set("wireless", backhaul_sta, "encryption", config.encryption);
|
||||
// ...this makes it the same as our WPS thing.
|
||||
uci.set("wireless", backhaul_sta, "encryption", "psk");
|
||||
uci.set("wireless", backhaul_sta, "ifname", "wlan-prpl-1");
|
||||
// Use backhaul link for showing halow info, since that's the one
|
||||
// most likely to indicate that it's working (and traffic from AP
|
||||
// will generally show on the backhaul link anyway).
|
||||
uci.set("system", "led_halow", "dev", "wlan-prpl-1");
|
||||
|
||||
uci.set("prplmesh", "config", "enable", "1");
|
||||
uci.set("prplmesh", "config", "management_mode", "Multi-AP-Agent");
|
||||
uci.set("prplmesh", "config", "master", "0");
|
||||
uci.set("prplmesh", "config", "gateway", "0");
|
||||
uci.set("prplmesh", "config", "wired_backhaul", "0");
|
||||
uci.set("prplmesh", "config", "operating_mode", "WDS-Repeater");
|
||||
|
||||
// prplmesh demands that its bridge is named br-prpl. How annoying.
|
||||
uci.foreach("network", "device", section => {
|
||||
if (section['name'] === 'br-lan') {
|
||||
uci.set("network", section[".name"], "name", "br-prpl");
|
||||
const morse_macaddr = readfile("/sys/class/net/wlan0/address");
|
||||
const suffix = morse_macaddr
|
||||
? substr(rtrim(morse_macaddr), 3)
|
||||
: join(':', map([rand(), rand(), rand(), rand(), rand()], (n) => hexenc(chr(n % 256))));
|
||||
uci.set("network", section[".name"], "macaddr", `f2:${suffix}`);
|
||||
}
|
||||
});
|
||||
uci.set("network", "lan", "device", "br-prpl");
|
||||
|
||||
uci.set("prplmesh", morse_device, "wifi-device");
|
||||
uci.set("prplmesh", morse_device, "hostap_iface", "wlan-prpl");
|
||||
uci.set("prplmesh", morse_device, "sta_iface", "wlan-prpl-1");
|
||||
break;
|
||||
default:
|
||||
die(`Mode not understood: ${config.mode}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* If uci changes have been made, commit and notify.
|
||||
*/
|
||||
function commit_config(ubus, uci) {
|
||||
const config_files = keys(uci.changes());
|
||||
|
||||
if (length(keys(config_files)) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(uci.commit(), "Failed to commit changed config");
|
||||
|
||||
// Tell procd about the changes, as /bin/reload_config does.
|
||||
for (let cf in config_files) {
|
||||
if (!ubus.call("service", "event", { type: "config.change", data: { package: cf } })) {
|
||||
warn(ubus.error(), "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Get the config from the url.
|
||||
*/
|
||||
function fetch_config(url) {
|
||||
let result;
|
||||
for (let i = 0; i < HTTP_RETRY_LIMIT; ++i) {
|
||||
result = fetch_config_once(url);
|
||||
if (type(result) === "object") {
|
||||
return result;
|
||||
}
|
||||
sleep(500);
|
||||
}
|
||||
|
||||
die(`Unable to load config url ${url}; error: ${result}\n`);
|
||||
};
|
||||
|
||||
|
||||
function find_service_url(ubus, dhcp_server_ipv4) {
|
||||
const services = find_services(ubus, SERVICE);
|
||||
assert(services && length(services) > 0, `Unable to find a '${SERVICE}' service; doing nothing\n`);
|
||||
|
||||
const url = get_url_from_service(get_best_service(services, dhcp_server_ipv4));
|
||||
assert(url, `Unable to construct valid wizard config URL from service: ${services}\n`);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
|
||||
/* Write string to fname only if file is not already there with the specified contents.
|
||||
*
|
||||
* Returns false if no write happened.
|
||||
*/
|
||||
function write_if_necessary(fname, string) {
|
||||
const existing = readfile(fname);
|
||||
|
||||
if (existing === string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(writefile(fname, string), `Unable to write to ${fname}: ${error()}\n`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function umdns_configure(ubus, umdns_config) {
|
||||
let requires_reload = true;
|
||||
if (umdns_config) {
|
||||
requires_reload = write_if_necessary(UMDNS_CONFIG_LOCATION, `${umdns_config}`);
|
||||
} else {
|
||||
requires_reload = unlink(UMDNS_CONFIG_LOCATION);
|
||||
}
|
||||
|
||||
if (requires_reload) {
|
||||
ubus.call("umdns", "reload", {});
|
||||
assert(!ubus.error(), `Unable to reload umdns: ${ubus.error()}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function serverstop(ubus) {
|
||||
system(`rm -f ${UMDNS_CONFIG_LOCATION}`);
|
||||
ubus.call("umdns", "reload", {});
|
||||
assert(!ubus.error(), `Unable to reload umdns: ${ubus.error()}\n`);
|
||||
}
|
||||
|
||||
|
||||
function generate_config(uci) {
|
||||
assert(uci.load("wireless"), "Missing wireless config; aborting.\n");
|
||||
const prplmesh_enabled = uci.load("prplmesh") && uci.get("prplmesh", "config", "enable") === "1";
|
||||
|
||||
const morse_device = find_morse_device(uci);
|
||||
assert(morse_device, "Unable to find morse wifi-device in UCI wireless config.\n");
|
||||
|
||||
const morse_ifaces = find_ifaces(uci, morse_device);
|
||||
|
||||
// Prefer 802.11s Mesh Point config if available.
|
||||
for (let iface in morse_ifaces) {
|
||||
if (uci.get("wireless", iface, "mode") === "mesh") {
|
||||
return {
|
||||
mode: "mesh11s",
|
||||
country: uci.get("wireless", morse_device, "country"),
|
||||
channel: uci.get("wireless", morse_device, "channel"),
|
||||
mesh_id: uci.get("wireless", iface, "mesh_id"),
|
||||
key: uci.get("wireless", iface, "key"),
|
||||
encryption: uci.get("wireless", iface, "encryption"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let iface in morse_ifaces) {
|
||||
if (uci.get("wireless", iface, "mode") === "ap") {
|
||||
return {
|
||||
mode: prplmesh_enabled ? "prplmesh" : "standard",
|
||||
country: uci.get("wireless", morse_device, "country"),
|
||||
channel: uci.get("wireless", morse_device, "channel"),
|
||||
ssid: uci.get("wireless", iface, "ssid"),
|
||||
key: uci.get("wireless", iface, "key"),
|
||||
encryption: uci.get("wireless", iface, "encryption"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
die("Unable to generate config; no enabled HaLow AP/mesh wifi-iface.");
|
||||
};
|
||||
|
||||
|
||||
function write_config(config) {
|
||||
write_if_necessary(WWW_ROOT + PATH_V1, `${config}`);
|
||||
};
|
||||
|
||||
|
||||
function check_umdns_running(ubus) {
|
||||
const result = ubus.call("service", "list", { name: "umdns" });
|
||||
const running_instances = filter(
|
||||
values(result?.umdns?.instances ?? {}),
|
||||
instance => instance.running
|
||||
);
|
||||
|
||||
assert(length(running_instances) > 0, "umdns not running; wizard-config cannot work\n");
|
||||
}
|
||||
|
||||
|
||||
/* We expose different 'commands' here to aid in testing various parts
|
||||
* of this script independently. For actual use, only client
|
||||
* and server are needed.
|
||||
*/
|
||||
const COMMANDS = {
|
||||
client: (ubus, uci, dhcp_server_ipv4) => {
|
||||
apply_config(uci, fetch_config(find_service_url(ubus, dhcp_server_ipv4)));
|
||||
commit_config(ubus, uci);
|
||||
},
|
||||
|
||||
server: (ubus, uci) => {
|
||||
try {
|
||||
write_config(generate_config(uci));
|
||||
umdns_configure(ubus, UMDNS_CONFIG);
|
||||
} catch (e) {
|
||||
umdns_configure(ubus, null);
|
||||
}
|
||||
},
|
||||
|
||||
serverstop: (ubus, _uci) => {
|
||||
// Can leave the normal config around, but need to remove the umdns service
|
||||
// config so it stops advertising.
|
||||
serverstop(ubus);
|
||||
},
|
||||
|
||||
// INTERNAL FUNCTIONS for testing
|
||||
|
||||
_apply_config: (_ubus, uci, raw_config) => {
|
||||
// Use standard uci delta_dir so user can see changes.
|
||||
uci = uci.cursor();
|
||||
apply_config(uci, json(raw_config));
|
||||
uci.save();
|
||||
print("Changes are staged. Use uci changes to view.\n");
|
||||
},
|
||||
|
||||
_umdns_configure: (ubus, _uci) => {
|
||||
umdns_configure(ubus, UMDNS_CONFIG);
|
||||
print("Configured. Other devices should see a _wizard-config._tcp service on this device.\n");
|
||||
},
|
||||
|
||||
_generate_config: (_ubus, uci) => {
|
||||
print(generate_config(uci), "\n");
|
||||
},
|
||||
|
||||
_find_service_url: (ubus, _uci, dhcp_server_ipv4) => {
|
||||
print(find_service_url(ubus, dhcp_server_ipv4), "\n");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function main() {
|
||||
const ubus = connect();
|
||||
check_umdns_running(ubus);
|
||||
|
||||
const command = shift(ARGV);
|
||||
if (command in COMMANDS) {
|
||||
COMMANDS[command](ubus, cursor("/etc/config", "/tmp/.wizard_uci"), ...ARGV);
|
||||
} else {
|
||||
if (command) {
|
||||
warn("ERROR: Command not understood: ", command, "\n");
|
||||
} else {
|
||||
warn("ERROR: Must specify a command.\n");
|
||||
}
|
||||
warn(HELP);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
main();
|
||||
Loading…
x
Reference in New Issue
Block a user