panama-osd read the wrong brightnessctl field, showing the hardware max instead of a percentage on any backlight device. panama-doctor called three sibling scripts by bare name with nothing on PATH, making three health checks permanently and falsely report broken; its repair actions also reused the short probe timeout, so a slow-but- successful restart was reported as failed. panama-wifi-qr left the cleartext passphrase temp file behind on its failure path (the RETURN trap doesn't fire on exit), and its nmcli parsing broke on connection names containing a colon or backslash -- verified against a real NetworkManager profile. panama-power-profile's set command always returned success regardless of whether the write actually took. panama-keyring's daemon-origin check picked whichever gnome-keyring-daemon process happened to enumerate first in /proc, defeating the exact dual-daemon scenario it exists to detect; it now resolves the PID that actually owns the Secret Service D-Bus name. gnf aborted before running a firmware update whenever the metadata was already current (a non-error exit under set -e), and its flatpak update lacked the -y its own docs promise. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
147 lines
6.2 KiB
Bash
Executable File
147 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# A QR code for a saved Wi-Fi network, so a guest can join by pointing a camera.
|
|
#
|
|
# GNOME's Wi-Fi panel has this and it is the single most-used thing in it.
|
|
# The payload is the de-facto WIFI: URI that Android and iOS both scan:
|
|
#
|
|
# WIFI:T:WPA;S:<ssid>;P:<passphrase>;H:<hidden>;;
|
|
#
|
|
# HANDLING THE PASSPHRASE
|
|
#
|
|
# This image contains the network password in machine-readable form. Anyone who
|
|
# can read the file can read the password, so:
|
|
#
|
|
# * it is written under XDG_RUNTIME_DIR, which is 0700 and on tmpfs, so it
|
|
# never reaches disk and disappears at logout -- not /tmp, which is shared;
|
|
# * it is created with umask 077;
|
|
# * the passphrase is never printed, never passed as an argument (argv is
|
|
# world-readable via /proc), and never appears in an error message.
|
|
#
|
|
# It is piped to qrencode on stdin for that last reason.
|
|
#
|
|
# Usage:
|
|
# panama-wifi-qr list -> {"networks":[{"name","ssid","shareable"}]}
|
|
# panama-wifi-qr qr <name> -> {"path":"/run/user/…/….png"}
|
|
|
|
set -uo pipefail
|
|
|
|
# The payload file (created in cmd_qr) is tracked at script scope so it can be
|
|
# removed no matter how the script exits -- success, an emit_error exit 0, or a
|
|
# signal -- rather than only on a clean function return.
|
|
payload_file=""
|
|
|
|
cleanup_payload() {
|
|
[[ -n $payload_file ]] && rm -f -- "$payload_file"
|
|
}
|
|
trap cleanup_payload EXIT
|
|
|
|
emit_error() {
|
|
printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
|
|
exit 0
|
|
}
|
|
|
|
command -v nmcli >/dev/null 2>&1 || emit_error 'NetworkManager is not available'
|
|
command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn'
|
|
|
|
cmd_list() {
|
|
local rows=() name type ssid psk
|
|
while IFS= read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
|
|
# nmcli's terse mode backslash-escapes ':' and '\' WITHIN a field so a
|
|
# combined NAME,TYPE line stays splittable -- but a plain awk -F:
|
|
# doesn't know that, so a name containing either character (e.g.
|
|
# "Cafe: Guest") gets split in the wrong place and TYPE no longer
|
|
# lines up, silently dropping the connection from this list. Querying
|
|
# one field at a time with escaping turned off (-e no) sidesteps the
|
|
# problem entirely: there's nothing to split, so each value comes
|
|
# back exactly as stored.
|
|
type="$(nmcli -e no -g connection.type connection show "$name" 2>/dev/null)"
|
|
[[ "$type" == "802-11-wireless" ]] || continue
|
|
|
|
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
|
[[ -n "$ssid" ]] || ssid="$name"
|
|
|
|
# Only networks whose passphrase this user can actually read are
|
|
# shareable. An enterprise network has no passphrase to share at all,
|
|
# and a QR code for one would simply not work.
|
|
psk="$(nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)"
|
|
|
|
rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \
|
|
--argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \
|
|
'{name: $name, ssid: $ssid, shareable: $shareable}')")
|
|
done < <(nmcli -e no -t -f NAME connection show 2>/dev/null)
|
|
|
|
if [[ ${#rows[@]} -eq 0 ]]; then
|
|
printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n'
|
|
return 0
|
|
fi
|
|
printf '{"networks":[%s],"path":"","error":""}\n' "$(IFS=,; printf '%s' "${rows[*]}")"
|
|
}
|
|
|
|
# The WIFI: URI reserves \ ; , : and ", each escaped with a backslash. An SSID
|
|
# containing a semicolon would otherwise terminate the field early and produce a
|
|
# QR code for a different network entirely.
|
|
#
|
|
# Trailing newlines are stripped as well. nmcli terminates every value with one,
|
|
# and left in place it lands INSIDE the payload -- the code still decodes here,
|
|
# but a newline in the middle of a WIFI: URI is not something every phone's
|
|
# scanner tolerates, and the failure would look like "the QR code just does not
|
|
# work on my phone".
|
|
escape_field() {
|
|
sed -e 's/\\/\\\\/g' -e 's/;/\\;/g' -e 's/,/\\,/g' -e 's/:/\\:/g' -e 's/"/\\"/g' \
|
|
| tr -d '\n'
|
|
}
|
|
|
|
cmd_qr() {
|
|
local name="${1:-}"
|
|
[[ -n "$name" ]] || emit_error 'no network named'
|
|
|
|
local ssid hidden psk_file out_dir out_file
|
|
# -e no here too: $name is the literal connection name (list emits it
|
|
# un-escaped -- see cmd_list), and nmcli's terse escaping is a one-way
|
|
# transform on VALUES, not something connection-show lookups expect on
|
|
# their NAME argument. Escaping $ssid/$psk here would feed escape_field
|
|
# an already-escaped value below and double-escape it.
|
|
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
|
[[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"."
|
|
|
|
hidden="$(nmcli -e no -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)"
|
|
[[ "$hidden" == "yes" ]] && hidden=true || hidden=false
|
|
|
|
out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama"
|
|
umask 077
|
|
mkdir -p "$out_dir" 2>/dev/null || emit_error 'could not create the runtime directory'
|
|
chmod 700 "$out_dir" 2>/dev/null || true
|
|
|
|
# Named after the connection, hashed, so repeated shares reuse one file
|
|
# instead of accumulating images of the password.
|
|
out_file="$out_dir/wifi-$(printf '%s' "$name" | sha256sum | cut -c1-16).png"
|
|
|
|
# Built in a file rather than a variable that could be echoed, and piped to
|
|
# qrencode on stdin so the passphrase never appears in argv.
|
|
payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file'
|
|
|
|
{
|
|
printf 'WIFI:T:WPA;S:'
|
|
printf '%s' "$ssid" | escape_field
|
|
printf ';P:'
|
|
nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field
|
|
printf ';H:%s;;' "$hidden"
|
|
} >"$payload_file"
|
|
|
|
if ! qrencode -o "$out_file" -s 8 -m 2 -l M <"$payload_file" 2>/dev/null; then
|
|
emit_error "Could not generate a QR code for \"$name\"."
|
|
fi
|
|
chmod 600 "$out_file" 2>/dev/null || true
|
|
|
|
jq -cn --arg path "$out_file" '{networks: [], path: $path, error: ""}'
|
|
}
|
|
|
|
case "${1:-list}" in
|
|
list) cmd_list ;;
|
|
qr) shift; cmd_qr "${1:-}" ;;
|
|
*) printf 'usage: panama-wifi-qr [list|qr <name>]\n' >&2; exit 2 ;;
|
|
esac
|