#!/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
