let libubus = require("ubus"); import { open, readfile } from "fs"; import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } from "common"; let ubus = libubus.connect(); hostapd.data.config = {}; hostapd.data.file_fields = { vlan_file: true, wpa_psk_file: true, accept_mac_file: true, deny_mac_file: true, eap_user_file: true, ca_cert: true, server_cert: true, server_cert2: true, private_key: true, private_key2: true, dh_file: true, eap_sim_db: true, }; function iface_remove(cfg) { if (!cfg || !cfg.bss || !cfg.bss[0] || !cfg.bss[0].ifname) return; hostapd.remove_iface(cfg.bss[0].ifname); for (let bss in cfg.bss) wdev_remove(bss.ifname); } function iface_gen_config(phy, config) { let str = `data: ${join("\n", config.radio.data)} channel=${config.radio.channel} `; for (let i = 0; i < length(config.bss); i++) { let bss = config.bss[i]; let type = i > 0 ? "bss" : "interface"; str += ` ${type}=${bss.ifname} ${join("\n", bss.data)} `; } return str; } function iface_restart(phy, config, old_config) { iface_remove(old_config); iface_remove(config); if (!config.bss || !config.bss[0]) { hostapd.printf(`No bss for phy ${phy}`); return; } let bss = config.bss[0]; let err = wdev_create(phy, bss.ifname, { mode: "ap" }); if (err) hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`); let config_inline = iface_gen_config(phy, config); if (hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`) < 0) { hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`); return; } } function array_to_obj(arr, key, start) { let obj = {}; start ??= 0; for (let i = start; i < length(arr); i++) { let cur = arr[i]; obj[cur[key]] = cur; } return obj; } function find_array_idx(arr, key, val) { for (let i = 0; i < length(arr); i++) if (arr[i][key] == val) return i; return -1; } function bss_reload_psk(bss, config, old_config) { if (is_equal(old_config.hash.wpa_psk_file, config.hash.wpa_psk_file)) return; old_config.hash.wpa_psk_file = config.hash.wpa_psk_file; if (!is_equal(old_config, config)) return; let ret = bss.ctrl("RELOAD_WPA_PSK"); ret ??= "failed"; hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`); } function iface_reload_config(phy, config, old_config) { if (!old_config || !is_equal(old_config.radio, config.radio)) return false; if (is_equal(old_config.bss, config.bss)) return true; if (config.bss[0].ifname != old_config.bss[0].ifname) return false; let iface = hostapd.interfaces[config.bss[0].ifname]; if (!iface) return false; let config_inline = iface_gen_config(phy, config); bss_reload_psk(iface.bss[0], config.bss[0], old_config.bss[0]); if (!is_equal(config.bss[0], old_config.bss[0])) { if (phy_is_fullmac(phy)) return false; hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`); if (iface.bss[0].set_config(config_inline, 0) < 0) { hostapd.printf(`Failed to set config`); return false; } } let bss_list = array_to_obj(iface.bss, "name", 1); let new_cfg = array_to_obj(config.bss, "ifname", 1); let old_cfg = array_to_obj(old_config.bss, "ifname", 1); for (let name in old_cfg) { let bss = bss_list[name]; if (!bss) { hostapd.printf(`bss '${name}' not found`); return false; } if (!new_cfg[name]) { hostapd.printf(`Remove bss '${name}' on phy '${phy}'`); bss.delete(); wdev_remove(name); continue; } let new_cfg_data = new_cfg[name]; delete new_cfg[name]; if (is_equal(old_cfg[name], new_cfg_data)) continue; hostapd.printf(`Reload config for bss '${name}' on phy '${phy}'`); let idx = find_array_idx(config.bss, "ifname", name); if (idx < 0) { hostapd.printf(`bss index not found`); return false; } if (bss.set_config(config_inline, idx) < 0) { hostapd.printf(`Failed to set config`); return false; } } for (let name in new_cfg) { hostapd.printf(`Add bss '${name}' on phy '${phy}'`); let idx = find_array_idx(config.bss, "ifname", name); if (idx < 0) { hostapd.printf(`bss index not found`); return false; } if (iface.add_bss(config_inline, idx) < 0) { hostapd.printf(`Failed to add bss`); return false; } } return true; } function iface_set_config(phy, config) { let old_config = hostapd.data.config[phy]; hostapd.data.config[phy] = config; if (!config) return iface_remove(old_config); let ret = iface_reload_config(phy, config, old_config); if (ret) { hostapd.printf(`Reloaded settings for phy ${phy}`); return 0; } hostapd.printf(`Restart interface for phy ${phy}`); return iface_restart(phy, config, old_config); } function config_add_bss(config, name) { let bss = { ifname: name, data: [], hash: {} }; push(config.bss, bss); return bss; } function iface_load_config(filename) { let f = open(filename, "r"); if (!f) return null; let config = { radio: { data: [] }, bss: [], orig_file: filename, }; let bss; let line; while ((line = trim(f.read("line"))) != null) { let val = split(line, "=", 2); if (!val[0]) continue; if (val[0] == "interface") { bss = config_add_bss(config, val[1]); break; } if (val[0] == "channel") { config.radio.channel = val[1]; continue; } push(config.radio.data, line); } while ((line = trim(f.read("line"))) != null) { let val = split(line, "=", 2); if (!val[0]) continue; if (val[0] == "bss") { bss = config_add_bss(config, val[1]); continue; } if (hostapd.data.file_fields[val[0]]) bss.hash[val[0]] = hostapd.sha1(readfile(val[1])); push(bss.data, line); } f.close(); return config; } let main_obj = { reload: { args: { phy: "", }, call: function(req) { try { let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config); for (let phy_name in phy_list) { let phy = hostapd.data.config[phy_name]; let config = iface_load_config(phy.orig_file); iface_set_config(phy_name, config); } } catch(e) { hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`); return libubus.STATUS_INVALID_ARGUMENT; } return 0; } }, config_set: { args: { phy: "", config: "", prev_config: "", }, call: function(req) { let phy = req.args.phy; let file = req.args.config; let prev_file = req.args.prev_config; if (!phy) return libubus.STATUS_INVALID_ARGUMENT; try { if (prev_file && !hostapd.data.config[phy]) { let config = iface_load_config(prev_file); if (config) config.radio.data = []; hostapd.data.config[phy] = config; } let config = iface_load_config(file); hostapd.printf(`Set new config for phy ${phy}: ${file}`); iface_set_config(phy, config); } catch(e) { hostapd.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`); return libubus.STATUS_INVALID_ARGUMENT; } return { pid: hostapd.getpid() }; } }, config_add: { args: { iface: "", config: "", }, call: function(req) { if (!req.args.iface || !req.args.config) return libubus.STATUS_INVALID_ARGUMENT; if (hostapd.add_iface(`bss_config=${req.args.iface}:${req.args.config}`) < 0) return libubus.STATUS_INVALID_ARGUMENT; return { pid: hostapd.getpid() }; } }, config_remove: { args: { iface: "" }, call: function(req) { if (!req.args.iface) return libubus.STATUS_INVALID_ARGUMENT; hostapd.remove_iface(req.args.iface); return 0; } }, }; hostapd.data.ubus = ubus; hostapd.data.obj = ubus.publish("hostapd", main_obj); function bss_event(type, name, data) { let ubus = hostapd.data.ubus; data ??= {}; data.name = name; hostapd.data.obj.notify(`bss.${type}`, data, null, null, null, -1); ubus.call("service", "event", { type: `hostapd.${name}.${type}`, data: {} }); } return { shutdown: function() { for (let phy in hostapd.data.config) iface_set_config(phy, null); hostapd.ubus.disconnect(); }, bss_add: function(name, obj) { bss_event("add", name); }, bss_reload: function(name, obj, reconf) { bss_event("reload", name, { reconf: reconf != 0 }); }, bss_remove: function(name, obj) { bss_event("remove", name); } };