hostapd: do not store data in object prototype
[openwrt/staging/hauke.git] / package / network / services / hostapd / files / hostapd.uc
1 let libubus = require("ubus");
2 import { open, readfile } from "fs";
3 import { wdev_create, wdev_remove, is_equal, vlist_new, phy_is_fullmac } from "common";
4
5 let ubus = libubus.connect();
6
7 hostapd.data.config = {};
8
9 hostapd.data.file_fields = {
10 vlan_file: true,
11 wpa_psk_file: true,
12 accept_mac_file: true,
13 deny_mac_file: true,
14 eap_user_file: true,
15 ca_cert: true,
16 server_cert: true,
17 server_cert2: true,
18 private_key: true,
19 private_key2: true,
20 dh_file: true,
21 eap_sim_db: true,
22 };
23
24 function iface_remove(cfg)
25 {
26 if (!cfg || !cfg.bss || !cfg.bss[0] || !cfg.bss[0].ifname)
27 return;
28
29 hostapd.remove_iface(cfg.bss[0].ifname);
30 for (let bss in cfg.bss)
31 wdev_remove(bss.ifname);
32 }
33
34 function iface_gen_config(phy, config)
35 {
36 let str = `data:
37 ${join("\n", config.radio.data)}
38 channel=${config.radio.channel}
39 `;
40
41 for (let i = 0; i < length(config.bss); i++) {
42 let bss = config.bss[i];
43 let type = i > 0 ? "bss" : "interface";
44
45 str += `
46 ${type}=${bss.ifname}
47 ${join("\n", bss.data)}
48 `;
49 }
50
51 return str;
52 }
53
54 function iface_restart(phy, config, old_config)
55 {
56 iface_remove(old_config);
57 iface_remove(config);
58
59 if (!config.bss || !config.bss[0]) {
60 hostapd.printf(`No bss for phy ${phy}`);
61 return;
62 }
63
64 let bss = config.bss[0];
65 let err = wdev_create(phy, bss.ifname, { mode: "ap" });
66 if (err)
67 hostapd.printf(`Failed to create ${bss.ifname} on phy ${phy}: ${err}`);
68 let config_inline = iface_gen_config(phy, config);
69
70 let ubus = hostapd.data.ubus;
71 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: true });
72 if (hostapd.add_iface(`bss_config=${bss.ifname}:${config_inline}`) < 0)
73 hostapd.printf(`hostapd.add_iface failed for phy ${phy} ifname=${bss.ifname}`);
74 ubus.call("wpa_supplicant", "phy_set_state", { phy: phy, stop: false });
75 }
76
77 function array_to_obj(arr, key, start)
78 {
79 let obj = {};
80
81 start ??= 0;
82 for (let i = start; i < length(arr); i++) {
83 let cur = arr[i];
84 obj[cur[key]] = cur;
85 }
86
87 return obj;
88 }
89
90 function find_array_idx(arr, key, val)
91 {
92 for (let i = 0; i < length(arr); i++)
93 if (arr[i][key] == val)
94 return i;
95
96 return -1;
97 }
98
99 function bss_reload_psk(bss, config, old_config)
100 {
101 if (is_equal(old_config.hash.wpa_psk_file, config.hash.wpa_psk_file))
102 return;
103
104 old_config.hash.wpa_psk_file = config.hash.wpa_psk_file;
105 if (!is_equal(old_config, config))
106 return;
107
108 let ret = bss.ctrl("RELOAD_WPA_PSK");
109 ret ??= "failed";
110
111 hostapd.printf(`Reload WPA PSK file for bss ${config.ifname}: ${ret}`);
112 }
113
114 function iface_reload_config(phy, config, old_config)
115 {
116 if (!old_config || !is_equal(old_config.radio, config.radio))
117 return false;
118
119 if (is_equal(old_config.bss, config.bss))
120 return true;
121
122 if (!old_config.bss || !old_config.bss[0])
123 return false;
124
125 if (config.bss[0].ifname != old_config.bss[0].ifname)
126 return false;
127
128 let iface_name = config.bss[0].ifname;
129 let iface = hostapd.interfaces[iface_name];
130 if (!iface)
131 return false;
132
133 let first_bss = hostapd.bss[iface_name];
134 if (!first_bss)
135 return false;
136
137 let config_inline = iface_gen_config(phy, config);
138
139 bss_reload_psk(first_bss, config.bss[0], old_config.bss[0]);
140 if (!is_equal(config.bss[0], old_config.bss[0])) {
141 if (phy_is_fullmac(phy))
142 return false;
143
144 if (config.bss[0].bssid != old_config.bss[0].bssid)
145 return false;
146
147 hostapd.printf(`Reload config for bss '${config.bss[0].ifname}' on phy '${phy}'`);
148 if (first_bss.set_config(config_inline, 0) < 0) {
149 hostapd.printf(`Failed to set config`);
150 return false;
151 }
152 }
153
154 let new_cfg = array_to_obj(config.bss, "ifname", 1);
155 let old_cfg = array_to_obj(old_config.bss, "ifname", 1);
156
157 for (let name in old_cfg) {
158 let bss = hostapd.bss[name];
159 if (!bss) {
160 hostapd.printf(`bss '${name}' not found`);
161 return false;
162 }
163
164 if (!new_cfg[name]) {
165 hostapd.printf(`Remove bss '${name}' on phy '${phy}'`);
166 bss.delete();
167 wdev_remove(name);
168 continue;
169 }
170
171 let new_cfg_data = new_cfg[name];
172 delete new_cfg[name];
173
174 if (is_equal(old_cfg[name], new_cfg_data))
175 continue;
176
177 hostapd.printf(`Reload config for bss '${name}' on phy '${phy}'`);
178 let idx = find_array_idx(config.bss, "ifname", name);
179 if (idx < 0) {
180 hostapd.printf(`bss index not found`);
181 return false;
182 }
183
184 if (bss.set_config(config_inline, idx) < 0) {
185 hostapd.printf(`Failed to set config`);
186 return false;
187 }
188 }
189
190 for (let name in new_cfg) {
191 hostapd.printf(`Add bss '${name}' on phy '${phy}'`);
192
193 let idx = find_array_idx(config.bss, "ifname", name);
194 if (idx < 0) {
195 hostapd.printf(`bss index not found`);
196 return false;
197 }
198
199 if (iface.add_bss(config_inline, idx) < 0) {
200 hostapd.printf(`Failed to add bss`);
201 return false;
202 }
203 }
204
205 return true;
206 }
207
208 function iface_set_config(phy, config)
209 {
210 let old_config = hostapd.data.config[phy];
211
212 hostapd.data.config[phy] = config;
213
214 if (!config)
215 return iface_remove(old_config);
216
217 let ret = iface_reload_config(phy, config, old_config);
218 if (ret) {
219 hostapd.printf(`Reloaded settings for phy ${phy}`);
220 return 0;
221 }
222
223 hostapd.printf(`Restart interface for phy ${phy}`);
224 return iface_restart(phy, config, old_config);
225 }
226
227 function config_add_bss(config, name)
228 {
229 let bss = {
230 ifname: name,
231 data: [],
232 hash: {}
233 };
234
235 push(config.bss, bss);
236
237 return bss;
238 }
239
240 function iface_load_config(filename)
241 {
242 let f = open(filename, "r");
243 if (!f)
244 return null;
245
246 let config = {
247 radio: {
248 data: []
249 },
250 bss: [],
251 orig_file: filename,
252 };
253
254 let bss;
255 let line;
256 while ((line = trim(f.read("line"))) != null) {
257 let val = split(line, "=", 2);
258 if (!val[0])
259 continue;
260
261 if (val[0] == "interface") {
262 bss = config_add_bss(config, val[1]);
263 break;
264 }
265
266 if (val[0] == "channel") {
267 config.radio.channel = val[1];
268 continue;
269 }
270
271 push(config.radio.data, line);
272 }
273
274 while ((line = trim(f.read("line"))) != null) {
275 let val = split(line, "=", 2);
276 if (!val[0])
277 continue;
278
279 if (val[0] == "bssid")
280 bss.bssid = val[1];
281
282 if (val[0] == "bss") {
283 bss = config_add_bss(config, val[1]);
284 continue;
285 }
286
287 if (hostapd.data.file_fields[val[0]])
288 bss.hash[val[0]] = hostapd.sha1(readfile(val[1]));
289
290 push(bss.data, line);
291 }
292 f.close();
293
294 return config;
295 }
296
297
298
299 let main_obj = {
300 reload: {
301 args: {
302 phy: "",
303 },
304 call: function(req) {
305 try {
306 let phy_list = req.args.phy ? [ req.args.phy ] : keys(hostapd.data.config);
307 for (let phy_name in phy_list) {
308 let phy = hostapd.data.config[phy_name];
309 let config = iface_load_config(phy.orig_file);
310 iface_set_config(phy_name, config);
311 }
312 } catch(e) {
313 hostapd.printf(`Error reloading config: ${e}\n${e.stacktrace[0].context}`);
314 return libubus.STATUS_INVALID_ARGUMENT;
315 }
316
317 return 0;
318 }
319 },
320 apsta_state: {
321 args: {
322 phy: "",
323 up: true,
324 frequency: 0,
325 sec_chan_offset: 0,
326 csa: true,
327 csa_count: 0,
328 },
329 call: function(req) {
330 if (req.args.up == null || !req.args.phy)
331 return libubus.STATUS_INVALID_ARGUMENT;
332
333 let phy = req.args.phy;
334 let config = hostapd.data.config[phy];
335 if (!config || !config.bss || !config.bss[0] || !config.bss[0].ifname)
336 return 0;
337
338 let iface = hostapd.interfaces[config.bss[0].ifname];
339 if (!iface)
340 return 0;
341
342 if (!req.args.up) {
343 iface.stop();
344 return 0;
345 }
346
347 let freq = req.args.frequency;
348 if (!freq)
349 return libubus.STATUS_INVALID_ARGUMENT;
350
351 let sec_offset = req.args.sec_chan_offset;
352 if (sec_offset != -1 && sec_offset != 1)
353 sec_offset = 0;
354
355 let width = 0;
356 for (let line in config.radio.data) {
357 if (!sec_offset && match(line, /^ht_capab=.*HT40/)) {
358 sec_offset = null; // auto-detect
359 continue;
360 }
361
362 let val = match(line, /^(vht_oper_chwidth|he_oper_chwidth)=(\d+)/);
363 if (!val)
364 continue;
365
366 val = int(val[2]);
367 if (val > width)
368 width = val;
369 }
370
371 if (freq < 4000)
372 width = 0;
373
374 let freq_info = hostapd.freq_info(freq, sec_offset, width);
375 if (!freq_info)
376 return libubus.STATUS_UNKNOWN_ERROR;
377
378 let ret;
379 if (req.args.csa) {
380 freq_info.csa_count = req.args.csa_count ?? 10;
381 ret = iface.switch_channel(freq_info);
382 } else {
383 iface.stop();
384 ret = iface.start(freq_info);
385 }
386 if (!ret)
387 return libubus.STATUS_UNKNOWN_ERROR;
388
389 return 0;
390 }
391 },
392 config_set: {
393 args: {
394 phy: "",
395 config: "",
396 prev_config: "",
397 },
398 call: function(req) {
399 let phy = req.args.phy;
400 let file = req.args.config;
401 let prev_file = req.args.prev_config;
402
403 if (!phy)
404 return libubus.STATUS_INVALID_ARGUMENT;
405
406 try {
407 if (prev_file && !hostapd.data.config[phy]) {
408 let config = iface_load_config(prev_file);
409 if (config)
410 config.radio.data = [];
411 hostapd.data.config[phy] = config;
412 }
413
414 let config = iface_load_config(file);
415
416 hostapd.printf(`Set new config for phy ${phy}: ${file}`);
417 iface_set_config(phy, config);
418 } catch(e) {
419 hostapd.printf(`Error loading config: ${e}\n${e.stacktrace[0].context}`);
420 return libubus.STATUS_INVALID_ARGUMENT;
421 }
422
423 return {
424 pid: hostapd.getpid()
425 };
426 }
427 },
428 config_add: {
429 args: {
430 iface: "",
431 config: "",
432 },
433 call: function(req) {
434 if (!req.args.iface || !req.args.config)
435 return libubus.STATUS_INVALID_ARGUMENT;
436
437 if (hostapd.add_iface(`bss_config=${req.args.iface}:${req.args.config}`) < 0)
438 return libubus.STATUS_INVALID_ARGUMENT;
439
440 return {
441 pid: hostapd.getpid()
442 };
443 }
444 },
445 config_remove: {
446 args: {
447 iface: ""
448 },
449 call: function(req) {
450 if (!req.args.iface)
451 return libubus.STATUS_INVALID_ARGUMENT;
452
453 hostapd.remove_iface(req.args.iface);
454 return 0;
455 }
456 },
457 };
458
459 hostapd.data.ubus = ubus;
460 hostapd.data.obj = ubus.publish("hostapd", main_obj);
461
462 function bss_event(type, name, data) {
463 let ubus = hostapd.data.ubus;
464
465 data ??= {};
466 data.name = name;
467 hostapd.data.obj.notify(`bss.${type}`, data, null, null, null, -1);
468 ubus.call("service", "event", { type: `hostapd.${name}.${type}`, data: {} });
469 }
470
471 return {
472 shutdown: function() {
473 for (let phy in hostapd.data.config)
474 iface_set_config(phy, null);
475 hostapd.ubus.disconnect();
476 },
477 bss_add: function(name, obj) {
478 bss_event("add", name);
479 },
480 bss_reload: function(name, obj, reconf) {
481 bss_event("reload", name, { reconf: reconf != 0 });
482 },
483 bss_remove: function(name, obj) {
484 bss_event("remove", name);
485 }
486 };