frugal-iot-server 0.3.7 → 0.3.9

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.
@@ -1,6 +1,7 @@
1
- // Change this version number if want existing installs of PWA to refresh their cache
2
- // TODO-28 - probably make this the same as the app version in frugal-iot-client
3
- const CACHE_NAME = 'frugal-iot-cache-1.3.13';
1
+ // Changing this version is what makes an already installed PWA fetch the app again instead of
2
+ // serving what it cached. It follows the version of frugal-iot-client this release installs, and
3
+ // "npm run prerelease" sets it - so there is normally no reason to edit it by hand.
4
+ const CACHE_NAME = 'frugal-iot-cache-1.3.15';
4
5
  const urlsToCache = [
5
6
  '/',
6
7
  '/index.html',
@@ -19,37 +20,36 @@ const urlsToCache = [
19
20
  self.addEventListener('install', event => {
20
21
  event.waitUntil(
21
22
  caches.open(CACHE_NAME)
22
- .then(cache => {
23
- console.log('Opened cache');
24
- return cache.addAll(urlsToCache);
25
- })
26
- );
27
- });
28
-
29
- self.addEventListener('fetch', event => {
30
- event.respondWith(
31
- caches.match(event.request)
32
- .then(response => {
33
- if (response) {
34
- return response;
35
- }
36
- return fetch(event.request);
37
- })
23
+ .then(cache => Promise.all(urlsToCache.map(url =>
24
+ // 'reload' bypasses the HTTP cache, which serves /node_modules immutable for a day -
25
+ // without it a same-day release caches the library it was meant to replace
26
+ fetch(new Request(url, {cache: 'reload'}))
27
+ .then(response => response.ok
28
+ ? cache.put(url, response)
29
+ : Promise.reject(new Error(url + ' -> ' + response.status))))))
30
+ // Without this the new worker waits until every tab of the origin is closed at once.
31
+ // Reloading does not release the old one, so a release can stay invisible for days.
32
+ .then(() => self.skipWaiting())
38
33
  );
39
34
  });
40
35
 
41
36
  // Delete old caches during activation
42
37
  self.addEventListener('activate', event => {
43
- const cacheWhitelist = [CACHE_NAME];
44
38
  event.waitUntil(
45
- caches.keys().then(cacheNames => {
46
- return Promise.all(
47
- cacheNames.map(cacheName => {
48
- if (cacheWhitelist.indexOf(cacheName) === -1) {
49
- return caches.delete(cacheName);
50
- }
51
- })
52
- );
53
- })
39
+ caches.keys()
40
+ .then(cacheNames => Promise.all(
41
+ cacheNames.filter(cacheName => cacheName !== CACHE_NAME)
42
+ .map(cacheName => caches.delete(cacheName))))
43
+ // Take over the pages that are already open, rather than only ones opened from now on
44
+ .then(() => self.clients.claim())
45
+ );
46
+ });
47
+
48
+ self.addEventListener('fetch', event => {
49
+ event.respondWith(
50
+ // Scoped to CACHE_NAME: a bare caches.match searches every cache in the origin, so a
51
+ // leftover older cache answers first and the version bump achieves nothing
52
+ caches.match(event.request, {cacheName: CACHE_NAME})
53
+ .then(response => response || fetch(event.request))
54
54
  );
55
55
  });
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env zsh
2
+ #
3
+ # Bridge this Pi's broker to a production Frugal IoT server. RUN THIS ON THE PI.
4
+ #
5
+ # After this, the Pi's readings also appear on the production server, and its nodes can be
6
+ # controlled from there - while the Pi carries on recording and serving on its own whenever the
7
+ # link is down. Readings taken during an outage stay on the Pi: production gets a gap rather than
8
+ # a backlog delivered later with the wrong timestamps. INSTALLATION.md step 11 explains why.
9
+ #
10
+ # The relaying is done by the broker, so the server, the logger and the nodes are untouched.
11
+ #
12
+ # Run the companion script on the production server first - "frugal-iot-addbridge-prod" - which
13
+ # creates the account this needs and tells you the password.
14
+ #
15
+ # Run from the server's own directory, the one holding frugal-iot.db.
16
+ #
17
+ # Usage:
18
+ # npx --no frugal-iot-addbridge-pi <org-id> <prod-host> <account>
19
+ # scripts/addbridge-pi.zsh --org myfarm --host prod.example.org --account bridge-northfield
20
+ #
21
+ # Both forms work. The positional one is the one to use with npx, because npm swallows any --flag
22
+ # it does not recognise ("Unknown cli config") and passes only the values through, so
23
+ # "npx ... --org myfarm" reaches this script as just "myfarm". Putting "--" before the flags also
24
+ # works, but the positional form is harder to get wrong.
25
+ #
26
+ # The password is asked for rather than passed on the command line, so it does not end up in your
27
+ # shell history. Give it with --password only for an unattended run.
28
+
29
+ set -euo pipefail
30
+
31
+ SCRIPT_NAME=$0
32
+
33
+ usage() {
34
+ echo "Usage: ${SCRIPT_NAME} <org-id> <prod-host> <account> [password]" >&2
35
+ echo " or: ${SCRIPT_NAME} --org <org-id> --host <prod-host> --account <account> [--port N] [--password PW] [--replace]" >&2
36
+ echo "Example: ${SCRIPT_NAME} myfarm prod.example.org bridge-northfield" >&2
37
+ echo "" >&2
38
+ echo "Through npx, use the positional form: npm swallows unrecognised --flags and passes only" >&2
39
+ echo "their values on, so '--org myfarm' arrives here as just 'myfarm'." >&2
40
+ exit 1
41
+ }
42
+
43
+ ORG_ID=""; PROD_HOST=""; ACCOUNT=""; PASSWORD=""; PROD_PORT=8883; REPLACE=false
44
+ POSITIONAL=()
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case "$1" in
48
+ --org) ORG_ID=${2:-}; shift 2 ;;
49
+ --host) PROD_HOST=${2:-}; shift 2 ;;
50
+ --account) ACCOUNT=${2:-}; shift 2 ;;
51
+ --password) PASSWORD=${2:-}; shift 2 ;;
52
+ --port) PROD_PORT=${2:-}; shift 2 ;;
53
+ --replace) REPLACE=true; shift ;;
54
+ -*) echo "Error: unknown option '$1'" >&2; usage ;;
55
+ *) POSITIONAL+=("$1"); shift ;;
56
+ esac
57
+ done
58
+
59
+ # Fill anything not given as a flag from the positional arguments, in the order the usage line
60
+ # shows. This is what makes the npx form work: npm eats "--org" and hands us a bare "myfarm".
61
+ # Written as "if" rather than "[[ ... ]] && X=Y", whose false case is a non-zero last command and
62
+ # would end the script under "set -e".
63
+ # Fill whichever are still empty, in the order the usage line shows them, consuming the
64
+ # positionals in turn rather than by fixed position - so a half-and-half command line still lands
65
+ # correctly. Done inline, not through a function returning its value: a command substitution runs
66
+ # in a subshell, where the counter would advance and then be thrown away, giving every field the
67
+ # same first positional.
68
+ POS_N=1
69
+ for _field in ORG_ID PROD_HOST ACCOUNT PASSWORD; do
70
+ if [[ -z "${(P)_field}" ]] && (( POS_N <= ${#POSITIONAL} )); then
71
+ typeset -g "${_field}"="${POSITIONAL[POS_N]}"
72
+ POS_N=$((POS_N + 1))
73
+ fi
74
+ done
75
+ unset _field
76
+
77
+ if [[ -z "$ORG_ID" || -z "$PROD_HOST" || -z "$ACCOUNT" ]]; then usage; fi
78
+
79
+ BRIDGE_CONF=/etc/mosquitto/conf.d/frugal-iot-bridge.conf
80
+
81
+ # This Pi has to know the organization too, or its own logger will not be recording what it relays.
82
+ if [[ ! -f "config.d/organizations/${ORG_ID}.yaml" ]]; then
83
+ echo "Error: no config.d/organizations/${ORG_ID}.yaml here." >&2
84
+ echo "Run this from the server's own directory, and check the organization id matches the one" >&2
85
+ echo "used on the production server - it is the first element of every topic, so the two ends" >&2
86
+ echo "must agree exactly." >&2
87
+ exit 1
88
+ fi
89
+
90
+ if [[ -e "$BRIDGE_CONF" && "$REPLACE" != true ]]; then
91
+ echo "Error: ${BRIDGE_CONF} already exists - this Pi is already bridged." >&2
92
+ echo "Add --replace to overwrite it, or edit it by hand. Through npx that flag needs a '--'" >&2
93
+ echo "ahead of it - 'npx --no frugal-iot-addbridge-pi -- --org ... --replace' - or npm eats it." >&2
94
+ exit 1
95
+ fi
96
+
97
+ # The example file ships with the server, so look beside this script rather than guessing a path -
98
+ # that works both for an npm install and for a git checkout.
99
+ EXAMPLE="${SCRIPT_NAME:A:h}/../extras/mosquitto-bridge.conf.example"
100
+ if [[ ! -f "$EXAMPLE" ]]; then
101
+ echo "Error: cannot find mosquitto-bridge.conf.example (looked at ${EXAMPLE})" >&2
102
+ exit 1
103
+ fi
104
+
105
+ if [[ -z "$PASSWORD" ]]; then
106
+ echo -n "Password for ${ACCOUNT} on ${PROD_HOST} (not echoed): "
107
+ read -rs PASSWORD
108
+ echo ""
109
+ if [[ -z "$PASSWORD" ]]; then
110
+ echo "Error: no password given" >&2
111
+ exit 1
112
+ fi
113
+ fi
114
+
115
+ # Check the far end is reachable and its certificate checks out, before writing anything. Getting
116
+ # this wrong is the usual reason a bridge silently fails to connect, and the broker's own log is
117
+ # the only place it says so.
118
+ if command -v openssl >/dev/null; then
119
+ echo "Checking ${PROD_HOST}:${PROD_PORT} ..."
120
+ if echo | openssl s_client -connect "${PROD_HOST}:${PROD_PORT}" -servername "$PROD_HOST" \
121
+ -verify_hostname "$PROD_HOST" -verify_return_error >/dev/null 2>&1; then
122
+ echo " reachable, and the certificate is valid for that name"
123
+ else
124
+ echo "Warning: could not complete a TLS handshake with ${PROD_HOST}:${PROD_PORT}." >&2
125
+ echo " Either the port is not open, or the certificate is not valid for that name." >&2
126
+ echo " Carrying on - the bridge will keep retrying - but it will not connect until that is fixed." >&2
127
+ fi
128
+ fi
129
+
130
+ # Fill in the example. Everything the bridge needs, including why each setting is what it is, is in
131
+ # the comments of that file, so it is copied whole rather than reduced to the active lines.
132
+ TMP=$(mktemp)
133
+ trap 'rm -f "$TMP"' EXIT
134
+ sed -e "s|<prod-host>:8883|${PROD_HOST}:${PROD_PORT}|" \
135
+ -e "s|<bridge-account>|${ACCOUNT}|g" \
136
+ -e "s|<bridge-password>|${PASSWORD}|" \
137
+ -e "s|<org>/|${ORG_ID}/|g" \
138
+ -e "s|^# Replace <org> in all four lines with the organization id - the first element of every topic, and|# The organization here is \"${ORG_ID}\" - the first element of every topic, and|" \
139
+ "$EXAMPLE" > "$TMP"
140
+
141
+ if grep -qE '<(prod-host|bridge-account|bridge-password)>' "$TMP"; then
142
+ echo "Error: some placeholders were not filled in - not installing. This is a bug in this script" >&2
143
+ echo " or in extras/mosquitto-bridge.conf.example." >&2
144
+ exit 1
145
+ fi
146
+
147
+ # Mode 600 because it holds the password. Mosquitto reads its configuration as root, before
148
+ # dropping privileges, so it can still read it - unlike the TLS key, which it reads afterwards.
149
+ echo "Writing ${BRIDGE_CONF} ..."
150
+ sudo install -o root -g root -m 600 "$TMP" "$BRIDGE_CONF"
151
+
152
+ # A restart, not a reload: mosquitto does not pick up bridges on a reload signal. Every node
153
+ # reconnects on its own afterwards.
154
+ echo "Restarting mosquitto (nodes will reconnect on their own) ..."
155
+ sudo systemctl restart mosquitto
156
+ sleep 4
157
+
158
+ if [[ "$(systemctl is-active mosquitto 2>/dev/null)" != "active" ]]; then
159
+ echo "Error: mosquitto did not come back up. Its own log says why:" >&2
160
+ echo " sudo tail -20 /var/log/mosquitto/mosquitto.log" >&2
161
+ echo "To undo this: sudo rm ${BRIDGE_CONF} && sudo systemctl restart mosquitto" >&2
162
+ exit 1
163
+ fi
164
+
165
+ # Did it actually connect? The bridge publishes a retained 1 or 0 here whenever that changes, so
166
+ # this is the real answer rather than the absence of an error.
167
+ STATE=""
168
+ if command -v mosquitto_sub >/dev/null; then
169
+ ORG_PW=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "config.d/organizations/${ORG_ID}.yaml" | head -1 | tr -d '"'"'"'') || true
170
+ for attempt in 1 2 3 4 5; do
171
+ # "|| true" because mosquitto_sub always exits non-zero when -W times out, and under "set -e"
172
+ # that ends this script here - silently, after the restart, without ever printing the verdict
173
+ # below. Likewise "if" rather than "&& break", whose false case is a non-zero last command.
174
+ STATE=$(mosquitto_sub -h localhost -u "$ORG_ID" -P "$ORG_PW" \
175
+ -t "\$SYS/broker/connection/${ACCOUNT}/state" -W 3 2>/dev/null | tail -1) || true
176
+ if [[ "$STATE" == "1" ]]; then break; fi
177
+ sleep 3
178
+ done
179
+ fi
180
+
181
+ echo ""
182
+ if [[ "$STATE" == "1" ]]; then
183
+ echo "Connected. This Pi is now relaying ${ORG_ID} to ${PROD_HOST}."
184
+ elif [[ -z "$STATE" ]]; then
185
+ echo "Installed, but this script could not tell whether it connected."
186
+ echo "Check with: npx --no frugal-iot-diagnostic"
187
+ else
188
+ echo "Installed, but not connected yet (state=${STATE})."
189
+ echo "It keeps retrying, so a wrong password or an unreachable port will show up in the log:"
190
+ echo " sudo tail -20 /var/log/mosquitto/mosquitto.log"
191
+ echo "and the state is reported by: npx --no frugal-iot-diagnostic"
192
+ fi
193
+ echo ""
194
+ echo "The readings this Pi has already recorded stay here - only new ones are relayed."
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env zsh
2
+ #
3
+ # Authorize a Raspberry Pi to bridge into this server. RUN THIS ON THE PRODUCTION SERVER.
4
+ #
5
+ # A bridge relays a Pi's readings to this server, so the same nodes appear on a dashboard here and
6
+ # can be controlled from here, while the Pi carries on working on its own when the link is down.
7
+ # This end of it is one broker account and one access-control rule. The Pi end is a configuration
8
+ # file, written by the companion script - "frugal-iot-addbridge-pi", run on the Pi.
9
+ #
10
+ # Each Pi gets its own account rather than sharing the organization's, for two reasons: the
11
+ # organization's broker password is handed to every browser that logs in, so it is not a secret;
12
+ # and a per-Pi account can be confined to one site's topics and revoked without disturbing anyone.
13
+ #
14
+ # Run from the server's own directory, the one holding frugal-iot.db.
15
+ #
16
+ # Usage:
17
+ # npx --no frugal-iot-addbridge-prod <org-id> <site-name> [password]
18
+ # scripts/addbridge-prod.zsh <org-id> <site-name> [password]
19
+ # Example:
20
+ # npx --no frugal-iot-addbridge-prod myfarm northfield
21
+ #
22
+ # The site name only distinguishes one Pi from another within an organization, and becomes part of
23
+ # the account name ("bridge-northfield"). With no password given, a strong one is generated and
24
+ # printed - it is needed once, to type into the Pi.
25
+ #
26
+ # This assumes the server has already been set up to accept bridges at all: a TLS listener on 8883
27
+ # with a certificate. That is a one-off, and INSTALLATION.md step 11 covers it.
28
+
29
+ set -euo pipefail
30
+
31
+ SCRIPT_NAME=$0
32
+
33
+ usage() {
34
+ echo "Usage: ${SCRIPT_NAME} <org-id> <site-name> [password]" >&2
35
+ echo "Example: ${SCRIPT_NAME} myfarm northfield" >&2
36
+ exit 1
37
+ }
38
+
39
+ if [[ $# -lt 2 || $# -gt 3 ]]; then
40
+ usage
41
+ fi
42
+
43
+ ORG_ID=$1
44
+ SITE=$2
45
+ PASSWORD=${3:-}
46
+
47
+ if [[ ! "$ORG_ID" =~ ^[a-z0-9]{1,10}$ ]]; then
48
+ echo "Error: org id must be 1-10 lower-case letters/digits, got '${ORG_ID}'" >&2
49
+ exit 1
50
+ fi
51
+ # The site name ends up in an account name and in an ACL file, so keep it to something that cannot
52
+ # be mistaken for syntax in either
53
+ if [[ ! "$SITE" =~ ^[a-z0-9-]{1,20}$ ]]; then
54
+ echo "Error: site name must be 1-20 lower-case letters, digits or hyphens, got '${SITE}'" >&2
55
+ exit 1
56
+ fi
57
+
58
+ ACCOUNT="bridge-${SITE}"
59
+ CONFIG_FILE="config.d/organizations/${ORG_ID}.yaml"
60
+
61
+ # The organization has to exist here, or the readings will arrive and nothing will record them:
62
+ # this server's logger subscribes per organization, driven by these files.
63
+ if [[ ! -f "$CONFIG_FILE" ]]; then
64
+ echo "Error: no ${CONFIG_FILE} - organization '${ORG_ID}' is not configured on this server." >&2
65
+ echo "Its readings would arrive at the broker and nothing would record them. Add it first:" >&2
66
+ echo " npx --no frugal-iot-addorganization ${ORG_ID} \"<name>\" <email> <phone> \"<password>\"" >&2
67
+ exit 1
68
+ fi
69
+
70
+ if ! command -v mosquitto_passwd >/dev/null; then
71
+ echo "Error: mosquitto_passwd not found - is mosquitto installed on this machine?" >&2
72
+ exit 1
73
+ fi
74
+
75
+ # Which files the running broker actually uses, rather than where they usually are. Only ".conf"
76
+ # files in conf.d are read, and the last setting wins.
77
+ MOSQ_CONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
78
+ if (( ${#MOSQ_CONFS} == 0 )); then
79
+ echo "Error: no mosquitto configuration found in /etc/mosquitto" >&2
80
+ exit 1
81
+ fi
82
+ # Read one setting out of the broker's configuration. Two things to be careful of: a conf.d file
83
+ # can be root-only (a bridge configuration holds a password, so it should be), which makes grep
84
+ # fail rather than return nothing - and under "set -e" a failing command substitution would end
85
+ # this script silently, before it had printed anything at all. Hence "|| true" and the sudo retry.
86
+ mosq_setting() {
87
+ local out
88
+ out=$(grep -hE "^[[:space:]]*$1[[:space:]]" "${MOSQ_CONFS[@]}" 2>/dev/null | tail -1 | awk '{print $2}') || true
89
+ if [[ -z "$out" ]] && command -v sudo >/dev/null; then
90
+ out=$(sudo grep -hE "^[[:space:]]*$1[[:space:]]" "${MOSQ_CONFS[@]}" 2>/dev/null | tail -1 | awk '{print $2}') || true
91
+ fi
92
+ print -r -- "$out"
93
+ }
94
+ PWFILE=$(mosq_setting password_file)
95
+ ACLFILE=$(mosq_setting acl_file)
96
+
97
+ if [[ -z "$PWFILE" ]]; then
98
+ echo "Error: no password_file in the mosquitto configuration - this broker is not using accounts." >&2
99
+ exit 1
100
+ fi
101
+ if [[ ! -e "$PWFILE" ]]; then
102
+ echo "Error: mosquitto names ${PWFILE} as its password file but it does not exist." >&2
103
+ exit 1
104
+ fi
105
+
106
+ # The account names in the password file. Needs the same sudo retry as the settings above: the
107
+ # password file is normally mode 600 owned by the broker's own user, so reading it as anyone else
108
+ # silently returns nothing - which would look exactly like "this account does not exist yet".
109
+ account_exists() {
110
+ local names
111
+ names=$(cut -d: -f1 "$PWFILE" 2>/dev/null) || true
112
+ if [[ -z "$names" ]] && command -v sudo >/dev/null; then
113
+ names=$(sudo cut -d: -f1 "$PWFILE" 2>/dev/null) || true
114
+ fi
115
+ print -r -- "$names" | grep -qx "$1"
116
+ }
117
+
118
+ # Refuse to rotate the password of a bridge that is presumably working: the Pi holds the old one,
119
+ # and it would stop relaying the moment this changed without anyone touching the Pi.
120
+ if account_exists "$ACCOUNT"; then
121
+ echo "Error: account '${ACCOUNT}' already exists in ${PWFILE}." >&2
122
+ echo "If a Pi is already using it, leave it alone. To give it a new password, run" >&2
123
+ echo "mosquitto_passwd as the owner of that file - as root it rewrites the file owned by root," >&2
124
+ echo "and the broker, which reads it as the mosquitto user, then will not start:" >&2
125
+ echo " sudo -u \"\$(stat -c '%U' ${PWFILE})\" mosquitto_passwd -b ${PWFILE} ${ACCOUNT} '<new password>'" >&2
126
+ echo "and then update /etc/mosquitto/conf.d/frugal-iot-bridge.conf on that Pi to match." >&2
127
+ exit 1
128
+ fi
129
+
130
+ if [[ -z "$PASSWORD" ]]; then
131
+ # Unlike the organization's broker password, this one is never given to a browser - it lives only
132
+ # in a root-owned file on the Pi - so it can and should be a real one.
133
+ # The subshell turns pipefail off: head closes the pipe after 24 characters, tr is killed by
134
+ # SIGPIPE, and with pipefail on that failure would end this script before it printed anything.
135
+ # install-pi.sh's randpw does the same for the same reason.
136
+ PASSWORD=$( set +o pipefail; LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 24 )
137
+ GENERATED=true
138
+ else
139
+ GENERATED=false
140
+ fi
141
+
142
+ # Run mosquitto_passwd as whoever owns the password file, the same way addorganization.zsh does.
143
+ # Running it as root instead rewrites the file owned by root, and mosquitto drops to the mosquitto
144
+ # user *before* reading it - so the broker then cannot read its own password file and refuses to
145
+ # start, with an error that says nothing about ownership. mosquitto_passwd also writes a temporary
146
+ # backup beside the file, so it needs the directory and not only the file.
147
+ MOSQ_PASSWD_CMD=(mosquitto_passwd)
148
+ if [[ ! -w "${PWFILE:h}" || ( -e "$PWFILE" && ! -w "$PWFILE" ) ]]; then
149
+ if command -v sudo >/dev/null; then
150
+ # stat's spelling differs between Linux (-c) and BSD/macOS (-f)
151
+ PWFILE_OWNER=$(stat -c '%U' "$PWFILE" 2>/dev/null || stat -f '%Su' "$PWFILE" 2>/dev/null || true)
152
+ if [[ -n "$PWFILE_OWNER" && "$PWFILE_OWNER" != "$(id -un)" && "$PWFILE_OWNER" != "root" ]]; then
153
+ MOSQ_PASSWD_CMD=(sudo -u "$PWFILE_OWNER" mosquitto_passwd)
154
+ else
155
+ MOSQ_PASSWD_CMD=(sudo mosquitto_passwd)
156
+ fi
157
+ fi
158
+ fi
159
+
160
+ echo "Creating broker account ${ACCOUNT} in ${PWFILE} ..."
161
+ if ! $MOSQ_PASSWD_CMD -b "$PWFILE" "$ACCOUNT" "$PASSWORD"; then
162
+ echo "Error: mosquitto_passwd failed - nothing has been changed." >&2
163
+ exit 1
164
+ fi
165
+
166
+ # The access control rule. Without one the account can connect and reach nothing at all, because a
167
+ # mosquitto acl_file is deny-by-default.
168
+ if [[ -n "$ACLFILE" ]]; then
169
+ if [[ ! -e "$ACLFILE" ]]; then
170
+ echo "Error: mosquitto names ${ACLFILE} as its ACL file but it does not exist." >&2
171
+ echo "The account has been created but cannot reach anything until that is sorted out." >&2
172
+ exit 1
173
+ fi
174
+ if sudo grep -qE "^[[:space:]]*user[[:space:]]+${ACCOUNT}[[:space:]]*$" "$ACLFILE"; then
175
+ echo "ACL rule for ${ACCOUNT} is already in ${ACLFILE} - leaving it alone."
176
+ else
177
+ echo "Adding ACL rule to ${ACLFILE} ..."
178
+ sudo tee -a "$ACLFILE" >/dev/null <<EOF
179
+
180
+ # Pi bridge for site "${SITE}", confined to its organization's topics
181
+ user ${ACCOUNT}
182
+ topic readwrite ${ORG_ID}/#
183
+ EOF
184
+ fi
185
+ else
186
+ echo ""
187
+ echo "NOTE: this broker has no acl_file, so ${ACCOUNT} can reach every topic on it, not just"
188
+ echo " ${ORG_ID}/#. That is how the broker already treats every other account, so this is no"
189
+ echo " worse than what is there - but see INSTALLATION.md step 11 for restricting it."
190
+ fi
191
+
192
+ # password_file and acl_file are both re-read on a reload, so nothing needs to be disconnected.
193
+ echo "Reloading mosquitto ..."
194
+ sudo systemctl reload mosquitto || {
195
+ echo "Warning: reload failed - the account exists but the broker has not re-read the files." >&2
196
+ echo " Try: sudo systemctl restart mosquitto" >&2
197
+ }
198
+
199
+ echo ""
200
+ echo "Done. On the Pi at site '${SITE}', run:"
201
+ echo ""
202
+ echo " npx --no frugal-iot-addbridge-pi ${ORG_ID} <this server's public name> ${ACCOUNT}"
203
+ echo ""
204
+ echo "Positional arguments on purpose: npm swallows any --flag it does not recognise and passes"
205
+ echo "only the value on, so the --org form would arrive at that script as a bare word."
206
+ echo ""
207
+ echo "It will ask for this password:"
208
+ echo ""
209
+ echo " ${PASSWORD}"
210
+ echo ""
211
+ if [[ "$GENERATED" == true ]]; then
212
+ echo "That was generated just now and is not stored anywhere you can read it back - copy it before"
213
+ echo "you lose this output. If it does get lost, set a new one with:"
214
+ echo " sudo -u \"\$(stat -c '%U' ${PWFILE})\" mosquitto_passwd -b ${PWFILE} ${ACCOUNT} '<new password>'"
215
+ echo "and use that on the Pi instead."
216
+ fi
217
+ echo "Fill in the public name yourself - it is deliberately not guessed here, because what this"
218
+ echo "machine calls itself is usually not it. This host answers to \"$(hostname -f 2>/dev/null || hostname)\","
219
+ echo "which is only the right answer if that is also the DNS name the Pi can reach and the name on"
220
+ echo "the TLS certificate. The Pi verifies the certificate against whatever you put here, so a"
221
+ echo "local hostname, or a domain the certificate does not cover, fails to connect."
Binary file