frugal-iot-server 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +148 -0
- package/config.d/schema/modules.yaml +103 -0
- package/extras/frugaliot-ap-watchdog +46 -0
- package/extras/frugaliot-ap-watchdog.service +8 -0
- package/extras/frugaliot-ap-watchdog.timer +9 -0
- package/extras/mosquitto.conf +19 -0
- package/frugal-iot-createdb.sql +5 -0
- package/frugal-iot-server.js +18 -5
- package/lib/dynsec-plan.js +30 -0
- package/lib/schema-columns.js +31 -0
- package/package.json +6 -3
- package/public/service-worker.js +1 -1
- package/scripts/addbridge-prod.zsh +44 -7
- package/scripts/diagnostic.zsh +20 -4
- package/scripts/init.zsh +5 -0
- package/scripts/install-pi.sh +202 -2
- package/scripts/rebuild-dynsec.js +39 -12
package/INSTALLATION.md
CHANGED
|
@@ -217,6 +217,15 @@ organization's web login - twice each, not echoed, and pressing Enter has one ge
|
|
|
217
217
|
and `--yes` asks nothing at all, which is what to use over ssh with no terminal. A password you
|
|
218
218
|
chose is not written to the log; a generated one has to be, or you would have no way of knowing it.
|
|
219
219
|
|
|
220
|
+
It also asks for the password of **the login you are using on the Pi** (`pi`, normally), and gives
|
|
221
|
+
that same password to the setup network the Pi raises if it ever loses its WiFi - see
|
|
222
|
+
[Moving the Pi to another WiFi network](#moving-the-pi-to-another-wifi-network). Then whoever moves
|
|
223
|
+
the Pi needs to know only one password. Linux keeps only a scrambled (hashed) form of a login
|
|
224
|
+
password, so the installer cannot read it; it checks that what you type matches, so a typing slip
|
|
225
|
+
does not give the network a password nobody knows. Press Enter instead to give the network a password
|
|
226
|
+
of its own, or pass `--ap-password`. WiFi needs 8 to 63 characters, so a shorter login password
|
|
227
|
+
cannot be reused. `--no-setup-ap` leaves the setup network out.
|
|
228
|
+
|
|
220
229
|
The organization's *broker* credential is no longer among them: it is generated and never shown,
|
|
221
230
|
because nothing on the Pi needs you to know it. Nodes are issued their own credential when they
|
|
222
231
|
enrol, and browsers derive their own when they log in. `--broker-password` still sets it, for an
|
|
@@ -462,6 +471,14 @@ sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
|
|
|
462
471
|
(Everything in `/etc/mosquitto/conf.d/` is read in addition to the packaged
|
|
463
472
|
`/etc/mosquitto/mosquitto.conf`, which keeps its own settings for logging and persistence.)
|
|
464
473
|
|
|
474
|
+
It also says how long the broker holds messages for a node that is asleep. Nodes keep their
|
|
475
|
+
session with the broker across a sleep, so a setting changed on the dashboard while a node sleeps
|
|
476
|
+
is held and handed over when it wakes - up to `max_queued_messages` (1000) per node, for up to
|
|
477
|
+
`persistent_client_expiration` (14 days) of not reconnecting, after which the node simply
|
|
478
|
+
subscribes afresh. Holding them across a broker restart relies on `persistence true` in the
|
|
479
|
+
packaged `mosquitto.conf`, which Raspberry Pi OS and Debian both ship; check with
|
|
480
|
+
`grep persistence /etc/mosquitto/mosquitto.conf`.
|
|
481
|
+
|
|
465
482
|
That file also turns off the broker's per-connection logging, because a node on a weak signal
|
|
466
483
|
reconnects constantly and each of those lines is a write to the SD card. Nothing is lost — the
|
|
467
484
|
readings themselves are recorded by the server, and everything else the broker says, including why
|
|
@@ -701,6 +718,50 @@ journalctl -u frugaliot -f
|
|
|
701
718
|
Reboot the Pi (`sudo reboot`), wait a couple of minutes, and check `http://frugaliot.local:8080`
|
|
702
719
|
still answers. Your server is now installed.
|
|
703
720
|
|
|
721
|
+
### 8a. A setup network, for when the Pi loses its WiFi
|
|
722
|
+
|
|
723
|
+
A Pi on WiFi that is moved to another site, or whose router is replaced, cannot be reached, and
|
|
724
|
+
changing its WiFi needs a login. [comitup](https://davesteele.github.io/comitup/) fixes that: when
|
|
725
|
+
no saved network is in range, the Pi raises a network of its own, `frugaliot-bridge-setup`. Its page
|
|
726
|
+
lists the WiFi networks the Pi can see and takes the password for one.
|
|
727
|
+
[Moving the Pi to another WiFi network](#moving-the-pi-to-another-wifi-network) is how it is used.
|
|
728
|
+
|
|
729
|
+
**Write comitup's configuration before installing it.** Installing the package starts it at once,
|
|
730
|
+
and without this file it uses its defaults: a network called `comitup-<nnn>` with **no password**,
|
|
731
|
+
raised straight away if the Pi has no WiFi connection at that moment.
|
|
732
|
+
|
|
733
|
+
```
|
|
734
|
+
sudo install -m 600 /dev/null /etc/comitup.conf
|
|
735
|
+
sudo nano /etc/comitup.conf
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Put these two lines in it. The password is normally the one for the `pi` login, so that whoever moves
|
|
739
|
+
the Pi needs only one. WiFi needs 8 to 63 characters.
|
|
740
|
+
|
|
741
|
+
```
|
|
742
|
+
ap_name: frugaliot-bridge-setup
|
|
743
|
+
ap_password: <the pi login password>
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Do not call it `frugaliot`. comitup announces its name on the network too, and announcing the Pi's own
|
|
747
|
+
name a second time stops `frugaliot.local` resolving for up to 15 minutes after each reconnect.
|
|
748
|
+
|
|
749
|
+
Then install comitup, and the watchdog that covers two ways comitup alone can strand a Pi (the
|
|
750
|
+
comments in `extras/frugaliot-ap-watchdog` give both):
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
sudo apt-get install -y -o Dpkg::Options::=--force-confold comitup
|
|
754
|
+
sudo install -m 755 node_modules/frugal-iot-server/extras/frugaliot-ap-watchdog /usr/local/sbin/
|
|
755
|
+
sudo install -m 644 node_modules/frugal-iot-server/extras/frugaliot-ap-watchdog.service \
|
|
756
|
+
node_modules/frugal-iot-server/extras/frugaliot-ap-watchdog.timer /etc/systemd/system/
|
|
757
|
+
sudo systemctl daemon-reload
|
|
758
|
+
sudo systemctl enable --now frugaliot-ap-watchdog.timer
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`--force-confold` keeps the `/etc/comitup.conf` you just wrote, instead of stopping to ask about the
|
|
762
|
+
one the package ships. `systemctl is-active comitup` should say `active`. The Pi stays on its WiFi;
|
|
763
|
+
nothing changes until that WiFi goes away.
|
|
764
|
+
|
|
704
765
|
### 9. Point your sensor nodes at the Pi
|
|
705
766
|
|
|
706
767
|
A node is flashed with an **enrolment secret**,
|
|
@@ -1138,6 +1199,89 @@ and read the **Wear on the SD card** section, which reports how much has been wr
|
|
|
1138
1199
|
whether the machine is swapping, and whether each of the settings above is in force. Run it twice a
|
|
1139
1200
|
few days apart — the interesting number is the rate, not the total.
|
|
1140
1201
|
|
|
1202
|
+
## Moving the Pi to another WiFi network
|
|
1203
|
+
|
|
1204
|
+
This is how to give the Pi a new WiFi network when you cannot log in to it, because it no longer
|
|
1205
|
+
finds the old one: it has moved to another site, or the router has been replaced. It needs the setup
|
|
1206
|
+
network from [step 8a](#8a-a-setup-network-for-when-the-pi-loses-its-wifi), which the installer sets
|
|
1207
|
+
up.
|
|
1208
|
+
|
|
1209
|
+
**You need:**
|
|
1210
|
+
|
|
1211
|
+
* a phone or laptop;
|
|
1212
|
+
* the setup network's password - the `pi` login's password, unless the installer said otherwise;
|
|
1213
|
+
* the name and password of the new WiFi network.
|
|
1214
|
+
|
|
1215
|
+
**Then:**
|
|
1216
|
+
|
|
1217
|
+
1. Power the Pi up where it is going, and give it about three minutes. It tries every network it
|
|
1218
|
+
has saved first, and each one that is not there costs it about half a minute.
|
|
1219
|
+
2. On the phone or laptop, join the network **`frugaliot-bridge-setup`** with that password. A
|
|
1220
|
+
"sign in to network" page should open by itself. If it does not, browse to `http://10.41.0.1`.
|
|
1221
|
+
3. Choose the new network from the list, and type its password. The setup network disappears as the
|
|
1222
|
+
Pi leaves it to join the new one.
|
|
1223
|
+
4. Join the phone or laptop to the new network too, and check the dashboard at
|
|
1224
|
+
`http://frugaliot.local:8080`.
|
|
1225
|
+
5. **Tell the phone or laptop to forget `frugaliot-bridge-setup`.** Devices rejoin a network they know
|
|
1226
|
+
whenever it appears. While anything is connected to the setup network, the Pi stops looking for
|
|
1227
|
+
its real WiFi. It gives up waiting after 15 minutes, so nothing is stranded for good, but a phone
|
|
1228
|
+
left in the house can delay the Pi's recovery after every power cut.
|
|
1229
|
+
|
|
1230
|
+
**If the setup network comes back a minute or so later**, the Pi could not join the network you
|
|
1231
|
+
chose, usually because the password was mistyped. Join the setup network again and choose it again.
|
|
1232
|
+
|
|
1233
|
+
**If `frugaliot.local` does not answer on the new network**, find the Pi's address in the router's
|
|
1234
|
+
list of connected devices and use that instead, e.g. `http://192.168.1.107:8080`. Some range
|
|
1235
|
+
extenders, and possibly phone hotspots, pass ordinary traffic but not the lookups that make `.local`
|
|
1236
|
+
names work.
|
|
1237
|
+
|
|
1238
|
+
**If the page will not do it**, log in while you are joined to the setup network, and set the WiFi
|
|
1239
|
+
by hand as in [step 2](#2-log-in-over-the-network):
|
|
1240
|
+
|
|
1241
|
+
```
|
|
1242
|
+
ssh pi@10.41.0.1
|
|
1243
|
+
sudo nmtui
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
**A phone hotspot as the new network** works, with two things to know on an iPhone: turn on
|
|
1247
|
+
*Maximise Compatibility*, and keep the *Personal Hotspot* screen open while the Pi joins. Without
|
|
1248
|
+
that, the iPhone often ignores a new device.
|
|
1249
|
+
|
|
1250
|
+
### What it does by itself
|
|
1251
|
+
|
|
1252
|
+
Nobody needs to be there for any of this:
|
|
1253
|
+
|
|
1254
|
+
* **After a power cut**, the Pi usually boots faster than the router. It raises the setup network,
|
|
1255
|
+
tries its saved networks again every three to six minutes, and rejoins the router once it is up.
|
|
1256
|
+
* **If a device sits on the setup network**, the Pi stops retrying while it is there - so that you
|
|
1257
|
+
are not cut off while you are using the page. After 15 minutes it retries regardless.
|
|
1258
|
+
* **A network that connects but hands out no address** counts as a failure, and the Pi moves on.
|
|
1259
|
+
Without that, a range extender whose router is switched off holds the Pi for ever, because the WiFi
|
|
1260
|
+
connection itself succeeds.
|
|
1261
|
+
|
|
1262
|
+
### Deleting networks the Pi no longer needs
|
|
1263
|
+
|
|
1264
|
+
Every network it has saved costs the Pi about half a minute each time it searches, so remove the
|
|
1265
|
+
ones it will not see again. This needs a login - the setup page cannot do it:
|
|
1266
|
+
|
|
1267
|
+
```
|
|
1268
|
+
nmcli connection show
|
|
1269
|
+
sudo nmcli connection delete "<name>"
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
or `sudo nmtui`, then *Edit a connection*. Leave `frugaliot-bridge-setup-0000`, which is the setup
|
|
1273
|
+
network itself.
|
|
1274
|
+
|
|
1275
|
+
### Changing the setup network's password
|
|
1276
|
+
|
|
1277
|
+
Changing the `pi` login's password later does **not** change the setup network's. To change that,
|
|
1278
|
+
edit `ap_password` in `/etc/comitup.conf` (`sudo nano /etc/comitup.conf`), then
|
|
1279
|
+
`sudo systemctl restart comitup`. The password matters because anyone who has it can point the Pi
|
|
1280
|
+
at a network of their choosing, and the Pi stops talking to your sensors until that is undone.
|
|
1281
|
+
|
|
1282
|
+
What it has been doing is in `sudo tail -50 /var/log/comitup.log`, and the watchdog's part in
|
|
1283
|
+
`journalctl -u frugaliot-ap-watchdog`.
|
|
1284
|
+
|
|
1141
1285
|
## When something does not work
|
|
1142
1286
|
|
|
1143
1287
|
From your install directory:
|
|
@@ -1258,6 +1402,10 @@ install these three, because none of them can simply be copied into place:
|
|
|
1258
1402
|
| `extras/aclfile` | `frugal-iot-addbridge-prod` appends a rule per Pi. Copying over it deletes them, and a mosquitto `acl_file` is deny-by-default — so every bridge goes on logging in and reaches nothing at all. |
|
|
1259
1403
|
| `extras/frugaliot.service` | written for user `pi` in `/home/pi/frugal-iot`; the installer rewrites `User`, `WorkingDirectory` and `ExecStart` for wherever this actually went. |
|
|
1260
1404
|
|
|
1405
|
+
The setup network's watchdog, `extras/frugaliot-ap-watchdog` and its `.service` and `.timer`, *can* be
|
|
1406
|
+
copied as they are; step 8a has the commands. On a Pi installed before the setup network existed,
|
|
1407
|
+
re-running the installer adds it, and asks once for the `pi` login's password to give it.
|
|
1408
|
+
|
|
1261
1409
|
The installer will not overwrite an ACL file that has per-bridge rules in it. If this release adds a
|
|
1262
1410
|
rule to a broker that has them, it says which lines are missing and leaves the file alone for you to
|
|
1263
1411
|
add them by hand, then `sudo systemctl reload mosquitto`.
|
|
@@ -136,6 +136,109 @@ carousel:
|
|
|
136
136
|
name: Display carousel
|
|
137
137
|
topics:
|
|
138
138
|
- leaf: select
|
|
139
|
+
# The lcd_ht example's control - formats another node's temperature and humidity for an LCD.
|
|
140
|
+
# Its inputs are wired to the remote node, its output to this node's lcd/message.
|
|
141
|
+
control_lcd_ht:
|
|
142
|
+
summary: false # its readings are another node's, shown on that node's card
|
|
143
|
+
name: LCD SHT
|
|
144
|
+
topics:
|
|
145
|
+
- leaf: temperature
|
|
146
|
+
leaf_from: controlfloat
|
|
147
|
+
name: Temperature
|
|
148
|
+
min: -40
|
|
149
|
+
max: 85
|
|
150
|
+
color: "#ff0000"
|
|
151
|
+
- leaf: humidity
|
|
152
|
+
leaf_from: controlfloat
|
|
153
|
+
name: Humidity
|
|
154
|
+
color: "#0000ff"
|
|
155
|
+
- leaf: message
|
|
156
|
+
leaf_from: controltext
|
|
157
|
+
name: Message
|
|
158
|
+
rw: r
|
|
159
|
+
# The OLED display controls. Each input is wired to whatever reading it should show, often another
|
|
160
|
+
# node's, so like the LCD they stay off the summary. Ranges and colours match the firmware.
|
|
161
|
+
# control_oled_gps is in the gps example.
|
|
162
|
+
control_oled_gps:
|
|
163
|
+
summary: false # a display, not a reading
|
|
164
|
+
name: OLED GPS
|
|
165
|
+
topics:
|
|
166
|
+
- leaf: latitude
|
|
167
|
+
leaf_from: controlfloat
|
|
168
|
+
name: Latitude
|
|
169
|
+
min: -90
|
|
170
|
+
max: 90
|
|
171
|
+
width: 10
|
|
172
|
+
color: "#0000ff"
|
|
173
|
+
- leaf: longitude
|
|
174
|
+
leaf_from: controlfloat
|
|
175
|
+
name: Longitude
|
|
176
|
+
min: -180
|
|
177
|
+
max: 180
|
|
178
|
+
width: 11
|
|
179
|
+
color: "#0000ff"
|
|
180
|
+
- leaf: altitude
|
|
181
|
+
leaf_from: controlfloat
|
|
182
|
+
name: Altitude
|
|
183
|
+
min: -500
|
|
184
|
+
max: 9000
|
|
185
|
+
width: 6
|
|
186
|
+
color: "#008000"
|
|
187
|
+
- leaf: speed
|
|
188
|
+
leaf_from: controlfloat
|
|
189
|
+
name: Speed
|
|
190
|
+
max: 999
|
|
191
|
+
color: "#ffa500"
|
|
192
|
+
- leaf: course
|
|
193
|
+
leaf_from: controlfloat
|
|
194
|
+
name: Course
|
|
195
|
+
max: 360
|
|
196
|
+
- leaf: satellites
|
|
197
|
+
leaf_from: controlfloat
|
|
198
|
+
name: Satellites
|
|
199
|
+
max: 32
|
|
200
|
+
- leaf: hdop
|
|
201
|
+
leaf_from: controlfloat
|
|
202
|
+
name: HDOP
|
|
203
|
+
max: 50
|
|
204
|
+
# In the sht, loramesher and remotedisplay examples, and in frugal-iot-demo
|
|
205
|
+
control_oled_ht:
|
|
206
|
+
summary: false # a display, not a reading
|
|
207
|
+
name: OLED HT
|
|
208
|
+
topics:
|
|
209
|
+
- leaf: temperature
|
|
210
|
+
leaf_from: controlfloat
|
|
211
|
+
name: Temperature
|
|
212
|
+
max: 50
|
|
213
|
+
color: "#ff0000"
|
|
214
|
+
- leaf: humidity
|
|
215
|
+
leaf_from: controlfloat
|
|
216
|
+
name: Humidity
|
|
217
|
+
color: "#0000ff"
|
|
218
|
+
- leaf: battery
|
|
219
|
+
leaf_from: controlfloat
|
|
220
|
+
name: Battery
|
|
221
|
+
max: 5000
|
|
222
|
+
color: "#00ff00"
|
|
223
|
+
# Library control - src/control/oled_loramesher.cpp. The mesh status it shows is read directly, not wired.
|
|
224
|
+
control_oled_loramesher:
|
|
225
|
+
summary: false # a display, not a reading
|
|
226
|
+
name: OLED LoRaMesher
|
|
227
|
+
topics:
|
|
228
|
+
- leaf: battery
|
|
229
|
+
leaf_from: controlfloat
|
|
230
|
+
name: Battery
|
|
231
|
+
max: 5000
|
|
232
|
+
color: "#00ff00"
|
|
233
|
+
# Library control - src/control/oled_single.cpp. One reading, unit and label set in the sketch.
|
|
234
|
+
control_oled_single:
|
|
235
|
+
summary: false # a display, not a reading
|
|
236
|
+
name: OLED
|
|
237
|
+
topics:
|
|
238
|
+
- leaf: value
|
|
239
|
+
leaf_from: controlfloat
|
|
240
|
+
name: Value
|
|
241
|
+
max: 10000
|
|
139
242
|
controlblinken:
|
|
140
243
|
name: "Control Blinken"
|
|
141
244
|
topics:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/zsh
|
|
2
|
+
# Two guards for comitup, which raises the frugaliot-bridge-setup access point when no known WiFi
|
|
3
|
+
# is in range. Installed to /usr/local/sbin by install-pi.sh, and run every minute by
|
|
4
|
+
# frugaliot-ap-watchdog.timer. Argument: how many seconds the access point may stay up.
|
|
5
|
+
#
|
|
6
|
+
# 1. A WiFi network that gives no IPv4 address must count as FAILED. NetworkManager's default,
|
|
7
|
+
# ipv4.may-fail yes, together with the link-local IPv6 comitup sets, makes any network that
|
|
8
|
+
# completes the WiFi handshake "connected" - so a range extender that outlives its router, with
|
|
9
|
+
# no DHCP behind it, holds the Pi for ever and the access point never appears. comitup creates a
|
|
10
|
+
# new profile for each network chosen on its page, so this is applied every minute, not once.
|
|
11
|
+
#
|
|
12
|
+
# 2. Restart comitup when it has held the access point for too long. comitup only retries the saved
|
|
13
|
+
# networks while NOBODY is connected to its access point, and a phone or laptop that joined it
|
|
14
|
+
# once rejoins by itself whenever it appears - so after a power cut it could hold the Pi in AP
|
|
15
|
+
# mode for ever, with the real WiFi back and the bridge down. On a restart comitup tries every
|
|
16
|
+
# saved network before raising the access point again.
|
|
17
|
+
|
|
18
|
+
LIMIT=${1:-900}
|
|
19
|
+
MARK=/run/frugaliot-ap-since # /run is emptied at boot, which is what we want
|
|
20
|
+
|
|
21
|
+
for UUID in ${(f)"$(nmcli -t -f UUID,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1)"}; do
|
|
22
|
+
SETTINGS=("${(@f)$(nmcli -g 802-11-wireless.mode,ipv4.may-fail,connection.id connection show $UUID)}")
|
|
23
|
+
if [[ $SETTINGS[1] != ap && $SETTINGS[2] == yes ]]; then
|
|
24
|
+
echo "Requiring IPv4 on WiFi profile '$SETTINGS[3]'"
|
|
25
|
+
nmcli connection modify $UUID ipv4.may-fail no
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
WIFI=$(nmcli -t -f DEVICE,TYPE device | awk -F: '$2 == "wifi" { print $1; exit }')
|
|
30
|
+
if [[ -z $WIFI || "$(iw dev $WIFI info 2>/dev/null)" != *"type AP"* ]]; then
|
|
31
|
+
rm -f $MARK
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
NOW=$(date +%s)
|
|
36
|
+
if [[ ! -f $MARK ]]; then
|
|
37
|
+
echo $NOW > $MARK
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
SINCE=$(<$MARK)
|
|
42
|
+
if (( NOW - SINCE >= LIMIT )); then
|
|
43
|
+
echo "In AP mode for $(( NOW - SINCE ))s (limit ${LIMIT}s) - restarting comitup to retry saved networks"
|
|
44
|
+
rm -f $MARK
|
|
45
|
+
systemctl restart comitup
|
|
46
|
+
fi
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Frugal IoT: stop the fallback access point holding the Pi for too long
|
|
3
|
+
After=comitup.service
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=oneshot
|
|
7
|
+
# Seconds the frugaliot-bridge-setup access point may stay up before comitup is made to retry
|
|
8
|
+
ExecStart=/usr/local/sbin/frugaliot-ap-watchdog 900
|
package/extras/mosquitto.conf
CHANGED
|
@@ -38,6 +38,25 @@ acl_file /etc/mosquitto/aclfile
|
|
|
38
38
|
plugin PLUGIN_PATH_SET_BY_INSTALLER
|
|
39
39
|
plugin_opt_config_file /var/lib/mosquitto/dynamic-security.json
|
|
40
40
|
|
|
41
|
+
# Messages held for nodes that are asleep. A node keeps a persistent session across a sleep, and the
|
|
42
|
+
# broker holds every QoS 1 message for its subscriptions until it reconnects - which is how a setting
|
|
43
|
+
# changed on the dashboard, or sent by another node, reaches a node that was asleep when it was sent.
|
|
44
|
+
# (The dashboard and the nodes send "set/" commands at QoS 1 for exactly this reason.)
|
|
45
|
+
#
|
|
46
|
+
# Persistence itself - which keeps those sessions, and the retained values, across a broker restart -
|
|
47
|
+
# is NOT set here: the packaged /etc/mosquitto/mosquitto.conf already has "persistence true", and
|
|
48
|
+
# repeating its settings in this file can stop the broker starting.
|
|
49
|
+
#
|
|
50
|
+
# At most this many held messages per node, beyond those in flight; once full, newer ones are dropped.
|
|
51
|
+
# Plenty for a node that sleeps for minutes. A node that sleeps for a day and has wired inputs from
|
|
52
|
+
# busy nodes can exceed it, but it resubscribes on waking, which brings each topic's latest retained
|
|
53
|
+
# value anyway. This is the broker's own default, set here so the limit is visible.
|
|
54
|
+
max_queued_messages 1000
|
|
55
|
+
# A session nobody has reconnected to in this long is discarded, with whatever it holds - otherwise a
|
|
56
|
+
# node taken out of service keeps up to max_queued_messages on the broker forever. A node that comes
|
|
57
|
+
# back after longer simply finds no session, and subscribes afresh.
|
|
58
|
+
persistent_client_expiration 14d
|
|
59
|
+
|
|
41
60
|
# A node on a weak WiFi signal reconnects often, and each connection and disconnection would be a
|
|
42
61
|
# line in /var/log/mosquitto/mosquitto.log. On an SD card those writes wear the card out, and
|
|
43
62
|
# nothing is lost by dropping them, since the readings themselves are recorded by the server.
|
package/frugal-iot-createdb.sql
CHANGED
|
@@ -80,6 +80,11 @@ CREATE TABLE IF NOT EXISTS `bridges` (
|
|
|
80
80
|
`token` TEXT NOT NULL,
|
|
81
81
|
`created_at` INTEGER NOT NULL,
|
|
82
82
|
`last_pull` INTEGER,
|
|
83
|
+
-- Where the bridge's broker password lives, and so whether frugal-iot-rebuild-dynsec may set it:
|
|
84
|
+
-- 'derived' (issued by the dashboard), 'password_file' (frugal-iot-addbridge-prod), or NULL for a
|
|
85
|
+
-- row from before this was recorded. See bridgesToSync in lib/dynsec-plan.js. Added after the
|
|
86
|
+
-- table first shipped, so lib/schema-columns.js adds it to an existing database.
|
|
87
|
+
`credential` TEXT,
|
|
83
88
|
UNIQUE(`org`, `site`)
|
|
84
89
|
);
|
|
85
90
|
|
package/frugal-iot-server.js
CHANGED
|
@@ -114,6 +114,7 @@ import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode,
|
|
|
114
114
|
makeAttemptLog, setGrant, clearGrant, readGrants } from './lib/enrol.js';
|
|
115
115
|
import { deleteRetained } from './lib/retained.js';
|
|
116
116
|
import { replicaFor, bridgeForToken, noteBridgePull, startReplica } from './lib/replica.js';
|
|
117
|
+
import { addMissingColumns } from './lib/schema-columns.js';
|
|
117
118
|
import { createLoggerClient } from './lib/logger-client.js';
|
|
118
119
|
import { createPushManager } from './lib/farm-platform-push.js';
|
|
119
120
|
import { APIError } from './lib/api-errors.js';
|
|
@@ -578,7 +579,8 @@ function execSqlStart(cb) {
|
|
|
578
579
|
cb(err);
|
|
579
580
|
} else {
|
|
580
581
|
console.log("Exec-ed starting SQL");
|
|
581
|
-
|
|
582
|
+
// A column added to an existing table, which CREATE TABLE IF NOT EXISTS cannot do
|
|
583
|
+
addMissingColumns(db, (err2) => (err2 ? cb(err2) : cb(null, db)));
|
|
582
584
|
}
|
|
583
585
|
});
|
|
584
586
|
}
|
|
@@ -1649,9 +1651,16 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
1649
1651
|
}
|
|
1650
1652
|
const now = Date.now();
|
|
1651
1653
|
const token = crypto.randomBytes(32).toString('base64url');
|
|
1652
|
-
|
|
1654
|
+
// Whether this re-issues a bridge that exists, because then the Pi already has a bridge
|
|
1655
|
+
// configuration, and the command it is given has to say --replace or it refuses
|
|
1656
|
+
db.get('SELECT 1 AS found FROM bridges WHERE org = ? AND site = ?', [org, site], (gerr, existing) => {
|
|
1657
|
+
if (gerr) { return res.status(500).json({ error: gerr.message }); }
|
|
1658
|
+
// credential 'derived' is what lets frugal-iot-rebuild-dynsec recreate this account,
|
|
1659
|
+
// and tells it apart from one authorized with addbridge-prod - see bridgesToSync
|
|
1660
|
+
db.run(`INSERT INTO bridges (org, site, token, created_at, credential) VALUES (?, ?, ?, ?, 'derived')
|
|
1653
1661
|
ON CONFLICT(org, site) DO UPDATE SET token = excluded.token,
|
|
1654
|
-
created_at = excluded.created_at
|
|
1662
|
+
created_at = excluded.created_at,
|
|
1663
|
+
credential = excluded.credential`,
|
|
1655
1664
|
[org, site, token, now], (err) => {
|
|
1656
1665
|
if (err) { return res.status(500).json({ error: err.message }); }
|
|
1657
1666
|
syncBridge(config, { org, site, createdAt: now }, (serr, result) => {
|
|
@@ -1669,11 +1678,15 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
|
|
|
1669
1678
|
replica_token: token,
|
|
1670
1679
|
// The command to run on the Pi. Neither secret is in it on purpose: that
|
|
1671
1680
|
// script prompts for both precisely so they stay out of the Pi's shell history,
|
|
1672
|
-
// and putting them here would undo that.
|
|
1673
|
-
|
|
1681
|
+
// and putting them here would undo that. Re-issuing needs --replace, which npm
|
|
1682
|
+
// would swallow without the "--" ahead of it - hence the flag form there.
|
|
1683
|
+
command: existing
|
|
1684
|
+
? `npx --no frugal-iot-addbridge-pi -- --org ${org} --host <this-server-fqdn> --account ${result.username} --replace`
|
|
1685
|
+
: `npx --no frugal-iot-addbridge-pi ${org} <this-server-fqdn> ${result.username}`,
|
|
1674
1686
|
});
|
|
1675
1687
|
});
|
|
1676
1688
|
});
|
|
1689
|
+
});
|
|
1677
1690
|
},
|
|
1678
1691
|
);
|
|
1679
1692
|
app.post('/node_reset/:org',
|
package/lib/dynsec-plan.js
CHANGED
|
@@ -298,6 +298,36 @@ export function deriveBridgePassword(userSecret, site, createdAt) {
|
|
|
298
298
|
.digest('base64url').slice(0, 22);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/*
|
|
302
|
+
* Which bridges a rebuild may create or re-password broker accounts for. A bridge's password comes
|
|
303
|
+
* from one of two places, recorded in bridges.credential, and only one of them is ours to set:
|
|
304
|
+
*
|
|
305
|
+
* 'derived' issued by the dashboard (POST /bridge_add) from user_secret and created_at, so
|
|
306
|
+
* it can be recreated exactly - always applied.
|
|
307
|
+
* 'password_file' authorized with frugal-iot-addbridge-prod: a random password in mosquitto's
|
|
308
|
+
* password file, which only the Pi holds. Creating a plugin client of that name
|
|
309
|
+
* REPLACES it, because the plugin's answer for a client it knows is final - which
|
|
310
|
+
* is how a rebuild during the 2.0.3 release cut a working bridge off.
|
|
311
|
+
* null a row from before that was recorded. Applied only if the plugin already has
|
|
312
|
+
* the client, because then its password is the derived one already; otherwise
|
|
313
|
+
* there is no telling, and replacing a working credential is the worse mistake.
|
|
314
|
+
*
|
|
315
|
+
* Pure, so it is tested without a broker. clients is the plugin's list of client names.
|
|
316
|
+
*/
|
|
317
|
+
export function bridgesToSync(bridges, clients) {
|
|
318
|
+
const apply = [];
|
|
319
|
+
const skipped = [];
|
|
320
|
+
for (const b of bridges) {
|
|
321
|
+
const account = names.bridgeClient(b.site);
|
|
322
|
+
if (b.credential === 'derived' || (b.credential == null && clients.includes(account))) {
|
|
323
|
+
apply.push(b);
|
|
324
|
+
} else {
|
|
325
|
+
skipped.push({ ...b, account, reason: b.credential === 'password_file' ? 'password_file' : 'unknown' });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { apply, skipped };
|
|
329
|
+
}
|
|
330
|
+
|
|
301
331
|
// The server's administrative account for one organization. Derived, like the logger's, with its
|
|
302
332
|
// own label so the two can never collide.
|
|
303
333
|
export function deriveOrgAdminPassword(userSecret, org) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Columns added to a table after it first shipped.
|
|
3
|
+
*
|
|
4
|
+
* frugal-iot-createdb.sql is all CREATE TABLE IF NOT EXISTS, which creates a new table whole but
|
|
5
|
+
* cannot add a column to one that exists already - so a database from an earlier release would
|
|
6
|
+
* otherwise never get it, and the first INSERT naming it would fail. Each is added here, guarded on
|
|
7
|
+
* its absence, every time the server starts. ADD COLUMN only: anything that changes a constraint
|
|
8
|
+
* needs the table rebuilt, which is what scripts/init.zsh step 3a does for permissions.project.
|
|
9
|
+
*/
|
|
10
|
+
const ADDED = [
|
|
11
|
+
{ table: 'bridges', column: 'credential', type: 'TEXT' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function addMissingColumns(db, cb) {
|
|
15
|
+
let i = 0;
|
|
16
|
+
const next = (err) => {
|
|
17
|
+
if (err) return cb(err);
|
|
18
|
+
if (i >= ADDED.length) return cb(null);
|
|
19
|
+
const { table, column, type } = ADDED[i++];
|
|
20
|
+
db.all(`PRAGMA table_info(${table})`, [], (e, cols) => {
|
|
21
|
+
if (e) return cb(e);
|
|
22
|
+
// No such table means CREATE TABLE has not run yet, or it is a table this database never had
|
|
23
|
+
if (!cols.length || cols.some((c) => c.name === column)) return next();
|
|
24
|
+
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`, [], (e2) => {
|
|
25
|
+
if (!e2) console.log(`Database: added ${table}.${column}`);
|
|
26
|
+
next(e2);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
next();
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"cookie-parser": "^1.4.7",
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"express-session": "^1.19.0",
|
|
15
|
-
"frugal-iot-client": "^2.0.
|
|
16
|
-
"frugal-iot-logger": "^2.0.
|
|
15
|
+
"frugal-iot-client": "^2.0.5",
|
|
16
|
+
"frugal-iot-logger": "^2.0.5",
|
|
17
17
|
"hash-wasm": "^4.12.0",
|
|
18
18
|
"js-yaml": "^4.2.0",
|
|
19
19
|
"morgan": "^1.11.0",
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
"extras/aclfile",
|
|
57
57
|
"extras/mosquitto-bridge.conf.example",
|
|
58
58
|
"extras/frugaliot.service",
|
|
59
|
+
"extras/frugaliot-ap-watchdog",
|
|
60
|
+
"extras/frugaliot-ap-watchdog.service",
|
|
61
|
+
"extras/frugaliot-ap-watchdog.timer",
|
|
59
62
|
"API.md",
|
|
60
63
|
"INSTALLATION.md"
|
|
61
64
|
],
|
|
@@ -86,5 +89,5 @@
|
|
|
86
89
|
"check-files": "node scripts/check-published-files.js"
|
|
87
90
|
},
|
|
88
91
|
"type": "module",
|
|
89
|
-
"version": "2.0.
|
|
92
|
+
"version": "2.0.5"
|
|
90
93
|
}
|
package/public/service-worker.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// it, an installed browser keeps serving the files it already cached - and since the list now covers
|
|
7
7
|
// the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
|
|
8
8
|
// needs this bumped, or it does not reach anyone who already has the old one.
|
|
9
|
-
const CACHE_NAME = 'frugal-iot-cache-2.0.
|
|
9
|
+
const CACHE_NAME = 'frugal-iot-cache-2.0.5';
|
|
10
10
|
/*
|
|
11
11
|
* Everything the app needs, so that a phone on a poor or expensive link fetches it once.
|
|
12
12
|
*
|
|
@@ -72,6 +72,29 @@ if ! command -v mosquitto_passwd >/dev/null; then
|
|
|
72
72
|
exit 1
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# A bridge issued from the dashboard has its account in the dynamic security plugin, and the
|
|
76
|
+
# plugin's answer for a client it knows is final - so a password-file entry made here for the same
|
|
77
|
+
# name would simply be ignored, and the Pi given it refused. The same trap the other way round cut a
|
|
78
|
+
# working bridge off in 2.0.3 (see bridgesToSync in lib/dynsec-plan.js). Node, not sqlite3, for the
|
|
79
|
+
# reason given at the replica token below.
|
|
80
|
+
BRIDGE_CREDENTIAL=$(node -e '
|
|
81
|
+
const sqlite3 = require("sqlite3");
|
|
82
|
+
const [org, site] = process.argv.slice(1);
|
|
83
|
+
const db = new sqlite3.Database("./frugal-iot.db", sqlite3.OPEN_READONLY, (err) => {
|
|
84
|
+
if (err) process.exit(0);
|
|
85
|
+
db.get("SELECT * FROM bridges WHERE org = ? AND site = ?", [org, site], (e, row) => {
|
|
86
|
+
process.stdout.write((row && row.credential) || "");
|
|
87
|
+
db.close();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
' "$ORG_ID" "$SITE" 2>/dev/null) || true
|
|
91
|
+
if [[ "$BRIDGE_CREDENTIAL" == derived ]]; then
|
|
92
|
+
echo "Error: bridge '${SITE}' was issued from the dashboard, so its account is in the broker's" >&2
|
|
93
|
+
echo "dynamic security plugin, which would ignore a password-file entry made here." >&2
|
|
94
|
+
echo "To give its Pi new credentials, re-issue it on the dashboard's Bridges card instead." >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
75
98
|
# Which files the running broker actually uses, rather than where they usually are. Only ".conf"
|
|
76
99
|
# files in conf.d are read, and the last setting wins.
|
|
77
100
|
MOSQ_CONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
|
|
@@ -263,15 +286,24 @@ const token = randomBytes(32).toString("base64url");
|
|
|
263
286
|
const db = new sqlite3.Database("./frugal-iot.db", (err) => {
|
|
264
287
|
if (err) { console.error(err.message); process.exit(1); }
|
|
265
288
|
db.run(`CREATE TABLE IF NOT EXISTS bridges (org TEXT NOT NULL, site TEXT NOT NULL,
|
|
266
|
-
token TEXT NOT NULL, created_at INTEGER NOT NULL, last_pull INTEGER,
|
|
289
|
+
token TEXT NOT NULL, created_at INTEGER NOT NULL, last_pull INTEGER, credential TEXT,
|
|
267
290
|
UNIQUE(org, site))`, () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
// An existing table may predate the credential column, if the server has not been restarted
|
|
292
|
+
// on this release yet - it adds it at startup, so do the same here
|
|
293
|
+
db.all("PRAGMA table_info(bridges)", [], (pe, cols) => {
|
|
294
|
+
const add = (pe || cols.some((c) => c.name === "credential")) ? "SELECT 1" : "ALTER TABLE bridges ADD COLUMN credential TEXT";
|
|
295
|
+
db.run(add, [], () => {
|
|
296
|
+
// "password_file" tells frugal-iot-rebuild-dynsec this account is not its to set
|
|
297
|
+
db.run(`INSERT INTO bridges (org, site, token, created_at, credential) VALUES (?, ?, ?, ?, 'password_file')
|
|
298
|
+
ON CONFLICT(org, site) DO UPDATE SET token = excluded.token, created_at = excluded.created_at,
|
|
299
|
+
credential = excluded.credential`,
|
|
300
|
+
[org, site, token, Date.now()], (e) => {
|
|
301
|
+
if (e) { console.error(e.message); process.exit(1); }
|
|
302
|
+
process.stdout.write(token);
|
|
303
|
+
db.close();
|
|
304
|
+
});
|
|
274
305
|
});
|
|
306
|
+
});
|
|
275
307
|
});
|
|
276
308
|
});
|
|
277
309
|
' "$ORG_ID" "$SITE") || true
|
|
@@ -317,6 +349,11 @@ echo "Then, on the Pi at site '${SITE}':"
|
|
|
317
349
|
echo ""
|
|
318
350
|
echo " npx --no frugal-iot-addbridge-pi ${ORG_ID} <fqdn-from-1-above> ${ACCOUNT}"
|
|
319
351
|
echo ""
|
|
352
|
+
echo "If that Pi already has a bridge configured, to anywhere, it needs --replace, which"
|
|
353
|
+
echo "npm swallows unless a \"--\" comes first:"
|
|
354
|
+
echo ""
|
|
355
|
+
echo " npx --no frugal-iot-addbridge-pi -- --org ${ORG_ID} --host <fqdn-from-1-above> --account ${ACCOUNT} --replace"
|
|
356
|
+
echo ""
|
|
320
357
|
if [[ -n "$REPLICA_TOKEN" ]]; then
|
|
321
358
|
echo "It asks for the password from 2, then for the replica token from 3. Neither is given on the"
|
|
322
359
|
echo "command line on purpose, so that neither ends up in that Pi's shell history."
|
package/scripts/diagnostic.zsh
CHANGED
|
@@ -496,15 +496,31 @@ section "Bridge to a production server (optional)"
|
|
|
496
496
|
# extras/mosquitto-bridge.conf.example. It is configured by a "connection" line in one of
|
|
497
497
|
# mosquitto's own config files, so most installations have none, which is not a problem.
|
|
498
498
|
BRIDGECONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
# The bridge's own file is 600 root, because it holds the bridge's password - so read as an ordinary
|
|
500
|
+
# user it yields nothing, which looked exactly like "no bridge configured" on a Pi whose bridge was
|
|
501
|
+
# configured and failing. Read each file with sudo when it cannot be read plainly, and say so when
|
|
502
|
+
# even that fails rather than report an answer that was never looked for.
|
|
503
|
+
BRIDGETEXT=""; BRIDGEUNREAD=()
|
|
504
|
+
for f in $BRIDGECONFS; do
|
|
505
|
+
if [[ -r $f ]]; then
|
|
506
|
+
BRIDGETEXT+="$(<$f)"$'\n'
|
|
507
|
+
elif have sudo && t=$(sudo -n cat $f 2>/dev/null); then
|
|
508
|
+
BRIDGETEXT+="$t"$'\n'
|
|
509
|
+
else
|
|
510
|
+
BRIDGEUNREAD+=($f)
|
|
511
|
+
fi
|
|
512
|
+
done
|
|
513
|
+
BRIDGENAMES=$(print -r -- "$BRIDGETEXT" | grep -E '^[[:space:]]*connection[[:space:]]' | awk '{print $2}')
|
|
514
|
+
if [[ -z "$BRIDGENAMES" ]] && (( ${#BRIDGEUNREAD} )); then
|
|
515
|
+
item "cannot tell whether a bridge is configured: not readable as $(id -un) - ${(j:, :)BRIDGEUNREAD}"
|
|
516
|
+
item " sudo npx --no frugal-iot-diagnostic reads them"
|
|
517
|
+
elif [[ -z "$BRIDGENAMES" ]]; then
|
|
502
518
|
item "no bridge configured - readings stay on this Pi"
|
|
503
519
|
else
|
|
504
520
|
for b in ${(f)BRIDGENAMES}; do item "configured bridge: $b"; done
|
|
505
521
|
# What it is set to relay and where. remote_password is deliberately not among these.
|
|
506
522
|
item "its settings:"
|
|
507
|
-
grep -
|
|
523
|
+
print -r -- "$BRIDGETEXT" | grep -E '^[[:space:]]*(address|remote_username|remote_clientid|topic|cleansession|restart_timeout|notifications)[[:space:]]' | sed 's/^/ /'
|
|
508
524
|
# Whether it is actually up, rather than what the configuration hoped for. With "notifications"
|
|
509
525
|
# left on, the bridge publishes a retained 1 or 0 here each time it connects or drops.
|
|
510
526
|
if have mosquitto_sub; then
|
package/scripts/init.zsh
CHANGED
|
@@ -255,6 +255,11 @@ check_installed() {
|
|
|
255
255
|
check_installed "${PKG}/extras/mosquitto.conf" /etc/mosquitto/conf.d/frugal-iot.conf plugin
|
|
256
256
|
check_installed "${PKG}/extras/aclfile" /etc/mosquitto/aclfile subset
|
|
257
257
|
check_installed "${PKG}/extras/frugaliot.service" /etc/systemd/system/frugaliot.service service
|
|
258
|
+
# Installed as they are, so compared exactly. Absent on a machine with no WiFi, which install-pi.sh
|
|
259
|
+
# leaves without a setup network - reported as "nothing to compare", which is what it is.
|
|
260
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog" /usr/local/sbin/frugaliot-ap-watchdog exact
|
|
261
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog.service" /etc/systemd/system/frugaliot-ap-watchdog.service exact
|
|
262
|
+
check_installed "${PKG}/extras/frugaliot-ap-watchdog.timer" /etc/systemd/system/frugaliot-ap-watchdog.timer exact
|
|
258
263
|
|
|
259
264
|
if (( ${#DIFFER_TO} )); then
|
|
260
265
|
echo
|
package/scripts/install-pi.sh
CHANGED
|
@@ -25,13 +25,18 @@
|
|
|
25
25
|
# workstation, not here. It prints the settings to give them.
|
|
26
26
|
# * HTTPS, which over-the-air firmware updates to ESP32 need - INSTALLATION.md step 10
|
|
27
27
|
# * reboot. A reboot is worth doing afterwards if the upgrade brought a new kernel.
|
|
28
|
+
#
|
|
29
|
+
# It does set up what happens when the Pi later LOSES its WiFi - moved to another site, or the router
|
|
30
|
+
# replaced: a network of its own, frugaliot-bridge-setup, whose page sets a new one. Otherwise a Pi
|
|
31
|
+
# on WiFi is unreachable at exactly the moment it needs its WiFi changed. INSTALLATION.md, "Moving the
|
|
32
|
+
# Pi to another WiFi network", is the operator's side of it.
|
|
28
33
|
|
|
29
34
|
set -Eeuo pipefail
|
|
30
35
|
|
|
31
36
|
# ---------------------------------------------------------------- settings and arguments
|
|
32
37
|
|
|
33
38
|
ORG_ID=""; ORG_NAME=""; ORG_EMAIL=""; ORG_PHONE=""
|
|
34
|
-
BROKER_PW=""; SUPER_PW=""; LOGIN_PW=""; BROKER_HOST=""
|
|
39
|
+
BROKER_PW=""; SUPER_PW=""; LOGIN_PW=""; BROKER_HOST=""; AP_PW=""; SETUP_AP=1
|
|
35
40
|
INSTALL_DIR=""; RUN_USER=""; ASSUME_YES=0; DRY_RUN=0; RANDOM_PASSWORDS=0
|
|
36
41
|
|
|
37
42
|
usage() {
|
|
@@ -52,6 +57,12 @@ Usage: bash install-pi.sh [options]
|
|
|
52
57
|
thing from the broker credential above and should not be the same string.
|
|
53
58
|
Either of the two logins, if not given, is asked for - and generated if you
|
|
54
59
|
just press Enter, or if there is no terminal to ask at.
|
|
60
|
+
--ap-password password of the frugaliot-bridge-setup network this Pi raises when it cannot
|
|
61
|
+
find its WiFi, 8 to 63 characters. If not given you are asked for the
|
|
62
|
+
password of this Pi's login, and that is used - so whoever moves the Pi
|
|
63
|
+
needs to know only one. Generated if there is no terminal to ask at.
|
|
64
|
+
--no-setup-ap do not set that network up. Leaves a Pi that loses its WiFi reachable only
|
|
65
|
+
by plugging in a screen and keyboard, or an Ethernet cable.
|
|
55
66
|
--broker-host <host> what the browser and the nodes should call this Pi.
|
|
56
67
|
Default: this Pi's hostname with ".local". Use an IP address if you will
|
|
57
68
|
view the dashboard on Android, which cannot resolve ".local" names.
|
|
@@ -76,6 +87,8 @@ while (( $# )); do
|
|
|
76
87
|
--superuser-password) SUPER_PW="${2:-}"; shift 2 ;;
|
|
77
88
|
--login-password) LOGIN_PW="${2:-}"; shift 2 ;;
|
|
78
89
|
--broker-host) BROKER_HOST="${2:-}"; shift 2 ;;
|
|
90
|
+
--ap-password) AP_PW="${2:-}"; shift 2 ;;
|
|
91
|
+
--no-setup-ap) SETUP_AP=0; shift ;;
|
|
79
92
|
--dir) INSTALL_DIR="${2:-}"; shift 2 ;;
|
|
80
93
|
--user) RUN_USER="${2:-}"; shift 2 ;;
|
|
81
94
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
@@ -265,6 +278,91 @@ ask() { # ask <variable name> <prompt> [generated]
|
|
|
265
278
|
[[ -n "${!var}" ]] || { echo "That cannot be empty" >&2; return 1; }
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
# ---------------------------------------------------------------- the setup network
|
|
282
|
+
# What this Pi raises, through comitup, when no saved WiFi network is in range. Not "frugaliot":
|
|
283
|
+
# comitup also announces its name over mDNS, and announcing the Pi's own hostname a second time made
|
|
284
|
+
# avahi refuse the real one - frugaliot.local stopped resolving for up to 15 minutes after every
|
|
285
|
+
# reconnect.
|
|
286
|
+
AP_NAME="frugaliot-bridge-setup"
|
|
287
|
+
COMITUP_CONF=/etc/comitup.conf
|
|
288
|
+
AP_SOURCE="" # login, own, given or kept - which password the network ended up with, for the summary
|
|
289
|
+
|
|
290
|
+
# Why there can be no setup network on this machine, or nothing if there can be
|
|
291
|
+
ap_unavailable() {
|
|
292
|
+
command -v nmcli >/dev/null 2>&1 || { echo "NetworkManager is not installed"; return 0; }
|
|
293
|
+
systemctl is-active --quiet NetworkManager 2>/dev/null || { echo "NetworkManager is not running"; return 0; }
|
|
294
|
+
nmcli -t -f TYPE device 2>/dev/null | grep -qx wifi || echo "this machine has no WiFi"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Is "$1" the password of RUN_USER's login? 0 yes, 1 no, 2 cannot tell.
|
|
298
|
+
# Linux keeps only a one-way hash of a password, in /etc/shadow, so there is nothing to read back -
|
|
299
|
+
# instead what was typed is hashed the same way, with the same salt, and the two compared. Without
|
|
300
|
+
# that a typing slip would give the network a password nobody knows. libcrypt through ctypes because
|
|
301
|
+
# Python 3.13 removed its crypt module; both values go to python on stdin, since "ps" shows arguments.
|
|
302
|
+
login_password_matches() {
|
|
303
|
+
local stored rc=0
|
|
304
|
+
stored="$(sudo_ getent shadow "$RUN_USER" 2>/dev/null | cut -d: -f2 || true)"
|
|
305
|
+
[[ "$stored" == \$* ]] || return 2 # locked ("!"), no password ("*"), or unreadable
|
|
306
|
+
printf '%s\n%s' "$stored" "$1" | python3 -c '
|
|
307
|
+
import ctypes, sys
|
|
308
|
+
stored, typed = sys.stdin.read().split("\n", 1)
|
|
309
|
+
try:
|
|
310
|
+
lib = ctypes.CDLL("libcrypt.so.1")
|
|
311
|
+
except OSError:
|
|
312
|
+
sys.exit(2)
|
|
313
|
+
lib.crypt.restype = ctypes.c_char_p
|
|
314
|
+
lib.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
|
315
|
+
result = lib.crypt(typed.encode(), stored.encode())
|
|
316
|
+
sys.exit(0 if result and result.decode() == stored else 1)
|
|
317
|
+
' || rc=$?
|
|
318
|
+
(( rc <= 1 )) || rc=2 # 127 is no python3 at all, which is also "cannot tell"
|
|
319
|
+
return $rc
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
wifi_length_ok() { (( ${#1} >= 8 && ${#1} <= 63 )); } # what WPA accepts
|
|
323
|
+
|
|
324
|
+
ask_ap_password() {
|
|
325
|
+
if [[ -n "$AP_PW" ]]; then
|
|
326
|
+
AP_SOURCE=given
|
|
327
|
+
return 0
|
|
328
|
+
fi
|
|
329
|
+
if (( RANDOM_PASSWORDS )) || (( ASSUME_YES )) || [[ ! -t 0 ]]; then
|
|
330
|
+
AP_PW="$(randpw)"; GENERATED+=(AP_PW); AP_SOURCE=own
|
|
331
|
+
info "generated a password for the ${AP_NAME} network$( [[ -t 0 ]] || echo ' (no terminal to ask at)' )"
|
|
332
|
+
return 0
|
|
333
|
+
fi
|
|
334
|
+
local typed rc tries=0
|
|
335
|
+
printf ' When this Pi cannot find its WiFi it raises a network called %s,\n' "$AP_NAME" > /dev/tty
|
|
336
|
+
printf ' whose password is that of the %s login - so whoever moves it needs to know only one.\n' "$RUN_USER" > /dev/tty
|
|
337
|
+
while (( tries < 3 )); do
|
|
338
|
+
printf ' Password for the %s login\n (or press Enter to give that network a password of its own): ' "$RUN_USER" > /dev/tty
|
|
339
|
+
read -rs typed < /dev/tty; printf '\n' > /dev/tty
|
|
340
|
+
[[ -n "$typed" ]] || break
|
|
341
|
+
rc=0; login_password_matches "$typed" || rc=$?
|
|
342
|
+
case $rc in
|
|
343
|
+
0)
|
|
344
|
+
if wifi_length_ok "$typed"; then
|
|
345
|
+
AP_PW="$typed"; AP_SOURCE=login
|
|
346
|
+
info "the ${AP_NAME} network will have the password of the ${RUN_USER} login"
|
|
347
|
+
return 0
|
|
348
|
+
fi
|
|
349
|
+
printf ' That is right, but a WiFi password must be 8 to 63 characters, so this\n network needs one of its own.\n' > /dev/tty
|
|
350
|
+
break ;;
|
|
351
|
+
1) printf ' That is not the password for %s - try again.\n' "$RUN_USER" > /dev/tty ;;
|
|
352
|
+
*) printf ' That cannot be checked against the %s login here, so this network gets\n a password of its own.\n' "$RUN_USER" > /dev/tty
|
|
353
|
+
break ;;
|
|
354
|
+
esac
|
|
355
|
+
tries=$((tries+1))
|
|
356
|
+
done
|
|
357
|
+
AP_SOURCE=own
|
|
358
|
+
while true; do
|
|
359
|
+
ask_password AP_PW "the ${AP_NAME} network (8 to 63 characters)"
|
|
360
|
+
wifi_length_ok "$AP_PW" && return 0
|
|
361
|
+
printf ' A WiFi password must be 8 to 63 characters - try again.\n' > /dev/tty
|
|
362
|
+
AP_PW=""
|
|
363
|
+
done
|
|
364
|
+
}
|
|
365
|
+
|
|
268
366
|
echo "Frugal IoT server installation"
|
|
269
367
|
(( DRY_RUN )) && echo "Dry run - nothing will be changed" || echo "Log: ${LOG}"
|
|
270
368
|
|
|
@@ -375,6 +473,21 @@ if [[ -z "$BROKER_PW" ]]; then
|
|
|
375
473
|
fi
|
|
376
474
|
[[ -n "$BROKER_HOST" ]] || BROKER_HOST="$(hostname).local"
|
|
377
475
|
|
|
476
|
+
# The setup network: whether it can be had here, and whether a previous run already gave it a
|
|
477
|
+
# password - re-running this should not ask again. Read without a pipe into "grep -q", which under
|
|
478
|
+
# pipefail can report a match as a failure when grep closes the pipe early.
|
|
479
|
+
if (( SETUP_AP )); then AP_SKIP="$(ap_unavailable)"; else AP_SKIP="--no-setup-ap was given"; fi
|
|
480
|
+
if [[ -n "$AP_PW" ]] && ! wifi_length_ok "$AP_PW"; then
|
|
481
|
+
echo "--ap-password must be 8 to 63 characters, which is what WiFi accepts" >&2; exit 1
|
|
482
|
+
fi
|
|
483
|
+
AP_KEEP=0
|
|
484
|
+
if [[ -z "$AP_SKIP" && -z "$AP_PW" ]]; then
|
|
485
|
+
COMITUP_NOW="$(read_maybe_root "$COMITUP_CONF")"
|
|
486
|
+
grep -q '^ap_password:' <<< "$COMITUP_NOW" && { AP_KEEP=1; AP_SOURCE=kept; }
|
|
487
|
+
fi
|
|
488
|
+
# Not in a dry run: it would mean typing the login password for a run that then does nothing with it
|
|
489
|
+
if [[ -z "$AP_SKIP" ]] && (( ! AP_KEEP )) && (( ! DRY_RUN )); then ask_ap_password; fi
|
|
490
|
+
|
|
378
491
|
info "install into: ${INSTALL_DIR} (owned by ${RUN_USER})"
|
|
379
492
|
info "organization: ${ORG_ID} (${ORG_NAME})"
|
|
380
493
|
info "broker URL: ws://${BROKER_HOST}:9012"
|
|
@@ -391,6 +504,13 @@ $( (( NEEDS_SWAP )) && echo " * add a 2G swap file, because this board has unde
|
|
|
391
504
|
* install and configure mosquitto, with listeners on 1883 and 9012
|
|
392
505
|
* create the superuser login and the ${ORG_ID} organization, and test the broker
|
|
393
506
|
* install and start the frugaliot service, running as ${RUN_USER}
|
|
507
|
+
$( if [[ -n "$AP_SKIP" ]]; then
|
|
508
|
+
echo " * no setup network: ${AP_SKIP}"
|
|
509
|
+
else
|
|
510
|
+
echo " * install comitup and the watchdog that keeps it honest, so that with no saved WiFi in"
|
|
511
|
+
echo " range this Pi raises a network called ${AP_NAME} whose page sets a new one."
|
|
512
|
+
echo " Its password: $( (( AP_KEEP )) && echo "kept, from ${COMITUP_CONF}" || { [[ -n "$AP_PW" ]] && echo "the one given" || echo "that of the ${RUN_USER} login, asked for"; } )"
|
|
513
|
+
fi )
|
|
394
514
|
|
|
395
515
|
Passwords it would use. A generated one is different on every run, so pass it or type it if you
|
|
396
516
|
want this exact set. All of them are shown here because a dry run writes no log; a real run prints
|
|
@@ -413,7 +533,9 @@ UPGRADE_PLAN="$(sudo_ apt-get -s full-upgrade 2>/dev/null | awk '/^Inst /{print
|
|
|
413
533
|
BOOT_BEFORE="$(boot_fingerprint)"
|
|
414
534
|
# No reboot here even if this brings a new kernel, so that the run can continue unattended. Whether
|
|
415
535
|
# one is needed is worked out below and said plainly in the summary.
|
|
416
|
-
|
|
536
|
+
# --force-confold: a package whose shipped configuration changed keeps the one on this machine
|
|
537
|
+
# rather than stopping to ask. /etc/comitup.conf, written below, is one such.
|
|
538
|
+
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y -qq -o Dpkg::Options::=--force-confold
|
|
417
539
|
if [[ -n "$UPGRADE_PLAN" ]]; then
|
|
418
540
|
info "upgraded: $(echo "$UPGRADE_PLAN" | wc -l | tr -d ' ') package(s)"
|
|
419
541
|
else
|
|
@@ -861,6 +983,66 @@ else
|
|
|
861
983
|
fi
|
|
862
984
|
systemctl is-enabled --quiet frugaliot && ok "will start again at boot"
|
|
863
985
|
|
|
986
|
+
step "A setup network, for when this Pi cannot find its WiFi"
|
|
987
|
+
# Moved to another site, or the router replaced, a Pi on WiFi is unreachable - and changing its WiFi
|
|
988
|
+
# needs a login. comitup raises a network of its own whenever no saved one is in range; its page
|
|
989
|
+
# lists the networks it can see and takes the password for one. The watchdog covers two ways comitup
|
|
990
|
+
# alone was seen to strand a Pi - see the comments in extras/frugaliot-ap-watchdog.
|
|
991
|
+
if [[ -n "$AP_SKIP" ]]; then
|
|
992
|
+
info "not set up: ${AP_SKIP}"
|
|
993
|
+
else
|
|
994
|
+
# The configuration goes in BEFORE the package, because installing it starts comitup at once - and
|
|
995
|
+
# without this file it uses its defaults, a network called comitup-<nnn> with NO password, raised
|
|
996
|
+
# straight away on a Pi that has no WiFi connection at that moment. Root 600: it holds a password,
|
|
997
|
+
# by default the login's, and comitup runs as root.
|
|
998
|
+
if (( AP_KEEP )); then
|
|
999
|
+
skip "${COMITUP_CONF} already has a password"
|
|
1000
|
+
else
|
|
1001
|
+
sudo_ install -o root -g root -m 600 /dev/null "$COMITUP_CONF"
|
|
1002
|
+
printf '# Written by install-pi.sh: the network this Pi raises when no saved WiFi is in range.\n# After changing either line: sudo systemctl restart comitup\nap_name: %s\nap_password: %s\n' \
|
|
1003
|
+
"$AP_NAME" "$AP_PW" | sudo_ tee "$COMITUP_CONF" >/dev/null
|
|
1004
|
+
ok "${COMITUP_CONF} written"
|
|
1005
|
+
fi
|
|
1006
|
+
if dpkg -s comitup >/dev/null 2>&1; then
|
|
1007
|
+
skip "comitup already installed"
|
|
1008
|
+
# A restart while connected keeps the connection; it only matters for the next time it is lost
|
|
1009
|
+
(( AP_KEEP )) || { sudo_ systemctl restart comitup; ok "comitup restarted to read the new password"; }
|
|
1010
|
+
else
|
|
1011
|
+
# --force-confold: the package ships its own /etc/comitup.conf, and would otherwise stop to ask
|
|
1012
|
+
# which to keep
|
|
1013
|
+
sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq -o Dpkg::Options::=--force-confold comitup
|
|
1014
|
+
ok "comitup installed"
|
|
1015
|
+
fi
|
|
1016
|
+
|
|
1017
|
+
# From the installed package, not ${INSTALL_DIR}/extras - frugal-iot-init never overwrites its
|
|
1018
|
+
# copy, so a release that changed one of these would otherwise never reach the system
|
|
1019
|
+
install_extra() { # install_extra <file in extras/> <destination> <mode>; fails if already current
|
|
1020
|
+
local src="${PKG_DIR}/extras/$1"
|
|
1021
|
+
[[ -f "$src" ]] || src="${INSTALL_DIR}/extras/$1"
|
|
1022
|
+
[[ -f "$src" ]] || { echo "Expected extras/$1 in ${PKG_DIR} or ${INSTALL_DIR}" >&2; exit 1; }
|
|
1023
|
+
cmp -s "$src" "$2" && return 1
|
|
1024
|
+
sudo_ install -o root -g root -m "$3" "$src" "$2"
|
|
1025
|
+
}
|
|
1026
|
+
WD_CHANGED=0
|
|
1027
|
+
install_extra frugaliot-ap-watchdog /usr/local/sbin/frugaliot-ap-watchdog 755 && WD_CHANGED=1 || true
|
|
1028
|
+
install_extra frugaliot-ap-watchdog.service /etc/systemd/system/frugaliot-ap-watchdog.service 644 && WD_CHANGED=1 || true
|
|
1029
|
+
install_extra frugaliot-ap-watchdog.timer /etc/systemd/system/frugaliot-ap-watchdog.timer 644 && WD_CHANGED=1 || true
|
|
1030
|
+
if (( WD_CHANGED )); then
|
|
1031
|
+
sudo_ systemctl daemon-reload
|
|
1032
|
+
ok "watchdog installed"
|
|
1033
|
+
else
|
|
1034
|
+
skip "watchdog already current"
|
|
1035
|
+
fi
|
|
1036
|
+
sudo_ systemctl enable -q --now frugaliot-ap-watchdog.timer
|
|
1037
|
+
|
|
1038
|
+
if systemctl is-active --quiet comitup; then
|
|
1039
|
+
ok "comitup running: with no saved WiFi in range, this Pi raises ${AP_NAME}"
|
|
1040
|
+
else
|
|
1041
|
+
warn "comitup is not running, so this Pi will NOT raise ${AP_NAME} if it loses its WiFi."
|
|
1042
|
+
warn " Why: sudo tail -20 /var/log/comitup.log"
|
|
1043
|
+
fi
|
|
1044
|
+
fi
|
|
1045
|
+
|
|
864
1046
|
# ---------------------------------------------------------------- what is left to do
|
|
865
1047
|
|
|
866
1048
|
IP_ADDR="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
|
@@ -883,6 +1065,22 @@ else
|
|
|
883
1065
|
installed here is already running."
|
|
884
1066
|
fi
|
|
885
1067
|
|
|
1068
|
+
if [[ -n "$AP_SKIP" ]]; then
|
|
1069
|
+
AP_TEXT=" If this Pi loses its WiFi there is no setup network to fix it with (${AP_SKIP}).
|
|
1070
|
+
It can then be reached only with a screen and keyboard, or an Ethernet cable."
|
|
1071
|
+
else
|
|
1072
|
+
case "$AP_SOURCE" in
|
|
1073
|
+
login) AP_PW_TEXT="the password of the ${RUN_USER} login" ;;
|
|
1074
|
+
kept) AP_PW_TEXT="unchanged - see ${COMITUP_CONF}" ;;
|
|
1075
|
+
given) AP_PW_TEXT="the one you gave" ;;
|
|
1076
|
+
*) AP_PW_TEXT="$(shown AP_PW)" ;;
|
|
1077
|
+
esac
|
|
1078
|
+
AP_TEXT=" If this Pi cannot find its WiFi - moved, or a new router - it raises a network
|
|
1079
|
+
called ${AP_NAME}. Join it from a phone or laptop; the password is
|
|
1080
|
+
${AP_PW_TEXT}. A page opens to choose the new WiFi.
|
|
1081
|
+
INSTALLATION.md, \"Moving the Pi to another WiFi network\", has the rest."
|
|
1082
|
+
fi
|
|
1083
|
+
|
|
886
1084
|
cat <<SUMMARY
|
|
887
1085
|
|
|
888
1086
|
=======================================================================
|
|
@@ -939,6 +1137,8 @@ ${REBOOT_TEXT}
|
|
|
939
1137
|
network, which is all an offline installation needs. What you do not get is over-the-air
|
|
940
1138
|
firmware updates, since an ESP32 requires HTTPS to fetch them.
|
|
941
1139
|
|
|
1140
|
+
${AP_TEXT}
|
|
1141
|
+
|
|
942
1142
|
If anything looks wrong:
|
|
943
1143
|
cd ${INSTALL_DIR} && npx --no frugal-iot-diagnostic
|
|
944
1144
|
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
applyRolesAndGroups, applyUser, applyNode, applyLogger, applyOrgAdmin, applyBridge,
|
|
25
25
|
checkRolesAndGroups,
|
|
26
26
|
} from '../lib/dynsec-sync.js';
|
|
27
|
-
import { names } from '../lib/dynsec-plan.js';
|
|
27
|
+
import { names, bridgesToSync } from '../lib/dynsec-plan.js';
|
|
28
28
|
|
|
29
29
|
const userClientName = names.userClient;
|
|
30
30
|
|
|
@@ -90,10 +90,14 @@ new MqttLogger().readYamlConfig('.', (err, config) => {
|
|
|
90
90
|
const want = names.loggerClient(org);
|
|
91
91
|
if (!clients.includes(want)) differences.push(`client missing: ${want}`);
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
// Only a bridge whose password this server derives can be missing - see
|
|
94
|
+
// bridgesToSync for why the others are left alone rather than reported
|
|
95
|
+
const { apply, skipped } = bridgesToSync(bridges, clients);
|
|
96
|
+
for (const b of apply) {
|
|
94
97
|
const want = names.bridgeClient(b.site);
|
|
95
98
|
if (!clients.includes(want)) differences.push(`client missing: ${want}`);
|
|
96
99
|
}
|
|
100
|
+
reportSkippedBridges(skipped);
|
|
97
101
|
report(differences);
|
|
98
102
|
dynsec.end(() => process.exit(differences.length ? 3 : 0));
|
|
99
103
|
});
|
|
@@ -118,9 +122,10 @@ new MqttLogger().readYamlConfig('.', (err, config) => {
|
|
|
118
122
|
syncOrgAdmins(dynsec, orgs, userSecret, (e7, j) => {
|
|
119
123
|
if (e7) return fail(`Syncing admin accounts: ${e7.message}`);
|
|
120
124
|
console.log(`${j} server admin account(s)`);
|
|
121
|
-
syncBridges(dynsec, bridges, userSecret, (e8, b) => {
|
|
125
|
+
syncBridges(dynsec, bridges, userSecret, (e8, b, skipped) => {
|
|
122
126
|
if (e8) return fail(`Syncing bridge accounts: ${e8.message}`);
|
|
123
127
|
console.log(`${b} bridge account(s)`);
|
|
128
|
+
reportSkippedBridges(skipped);
|
|
124
129
|
dynsec.end(() => process.exit(0));
|
|
125
130
|
});
|
|
126
131
|
});
|
|
@@ -146,19 +151,41 @@ function readBridges(db, cb) {
|
|
|
146
151
|
db.all("SELECT name FROM sqlite_master WHERE type='table' AND name='bridges'", [], (err, t) => {
|
|
147
152
|
if (err) return cb(err);
|
|
148
153
|
if (!t.length) return cb(null, []);
|
|
149
|
-
|
|
154
|
+
// "*" and not a column list: credential is absent from a database the server has not yet
|
|
155
|
+
// started on since it was added, and a missing column must read as null, not fail the query
|
|
156
|
+
db.all('SELECT * FROM bridges', [], (e, rows) => cb(e, rows || []));
|
|
150
157
|
});
|
|
151
158
|
}
|
|
152
159
|
|
|
160
|
+
// Only the bridges whose password this server derives - see bridgesToSync in lib/dynsec-plan.js.
|
|
161
|
+
// A bridge authorized with frugal-iot-addbridge-prod has its own password in mosquitto's password
|
|
162
|
+
// file, and creating a plugin client of the same name would silently replace it.
|
|
153
163
|
function syncBridges(dynsec, bridges, userSecret, cb) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
(
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
dynsec.listClients((err, clients) => {
|
|
165
|
+
if (err) return cb(err);
|
|
166
|
+
const { apply, skipped } = bridgesToSync(bridges, clients);
|
|
167
|
+
let i = 0;
|
|
168
|
+
const next = () => {
|
|
169
|
+
if (i >= apply.length) return cb(null, apply.length, skipped);
|
|
170
|
+
const b = apply[i++];
|
|
171
|
+
applyBridge(dynsec, { org: b.org, site: b.site, createdAt: b.created_at, userSecret },
|
|
172
|
+
(e) => (e ? cb(e) : next()));
|
|
173
|
+
};
|
|
174
|
+
next();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function reportSkippedBridges(skipped) {
|
|
179
|
+
for (const b of skipped) {
|
|
180
|
+
if (b.reason === 'password_file') {
|
|
181
|
+
console.log(` ${b.account}: left alone - authorized with frugal-iot-addbridge-prod, so its password`);
|
|
182
|
+
console.log(' is in mosquitto\'s password file. Re-issue it on the dashboard\'s Bridges card to move it here.');
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` ${b.account}: left alone - no account for it here, and nothing records whether its password`);
|
|
185
|
+
console.log(' is in mosquitto\'s password file. If its Pi is connected, it is, and nothing is wrong;');
|
|
186
|
+
console.log(' if not, re-issue it on the dashboard\'s Bridges card.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
function report(differences) {
|