From 42d06e04078bd472875f10877c94dbd5942ef0c8 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 13 Jun 2024 13:24:28 +0200 Subject: [PATCH 01/49] Set up scripts for enabling/disabling WiFi network interface --- debian-pkg/etc/sudoers.d/tinypilot | 3 ++ .../tinypilot-privileged/scripts/disable-wifi | 17 +++++++++++ .../tinypilot-privileged/scripts/enable-wifi | 30 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100755 debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi create mode 100755 debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi diff --git a/debian-pkg/etc/sudoers.d/tinypilot b/debian-pkg/etc/sudoers.d/tinypilot index 1afd13d3c..1d0c4ace8 100644 --- a/debian-pkg/etc/sudoers.d/tinypilot +++ b/debian-pkg/etc/sudoers.d/tinypilot @@ -1,6 +1,9 @@ tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/change-hostname tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/collect-debug-logs tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/configure-janus +tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/disable-wifi +tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/enable-wifi +tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/read-update-log tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/read-update-log tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/update tinypilot ALL=(ALL) NOPASSWD: /sbin/shutdown diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi new file mode 100755 index 000000000..5ce9869ad --- /dev/null +++ b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi @@ -0,0 +1,17 @@ +#!/bin/bash + +# Exit on first error. +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +readonly SCRIPT_DIR +readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" + +# shellcheck source=lib/markers.sh +. "${SCRIPT_DIR}/lib/markers.sh" + +# Remove any existing automated configuration. +"${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" + +# Block (deactivate) WLAN, which will take effect instantly. +rfkill block wlan diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi new file mode 100755 index 000000000..7a2c1e694 --- /dev/null +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -0,0 +1,30 @@ +#!/bin/bash + +# Exit on first error. +set -e + +# TODO parse these parameters from CLI args. +readonly WIFI_COUNTRY='DE' +readonly WIFI_SSID='my-home-network' +readonly WIFI_PSK='p4ssw0rd' + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +readonly SCRIPT_DIR +readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" +# shellcheck source=lib/markers.sh +. "${SCRIPT_DIR}/lib/markers.sh" + +# Remove any existing automated configuration. +"${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" + +# Write out the new configuration. +{ + echo "${MARKER_START}" + echo "country=${WIFI_COUNTRY}" + echo "$(wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d')" + echo "${MARKER_END}" +} | sudo tee --append "${CONFIG_FILE}" > /dev/null + +# Effectuate changes. +rfkill unblock wifi +wpa_cli -i wlan0 reconfigure From d02832d45ce9268817ca5486458457ea87ea19c7 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 13 Jun 2024 13:26:37 +0200 Subject: [PATCH 02/49] Fix bash style --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 7a2c1e694..8f45e8f5d 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -21,7 +21,7 @@ readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" { echo "${MARKER_START}" echo "country=${WIFI_COUNTRY}" - echo "$(wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d')" + wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d' echo "${MARKER_END}" } | sudo tee --append "${CONFIG_FILE}" > /dev/null From 9bbc056c6e4dcec1804b99b3cdcbd0bcdf1ca0dc Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 13 Jun 2024 13:28:14 +0200 Subject: [PATCH 03/49] Remove redundant entry --- debian-pkg/etc/sudoers.d/tinypilot | 1 - 1 file changed, 1 deletion(-) diff --git a/debian-pkg/etc/sudoers.d/tinypilot b/debian-pkg/etc/sudoers.d/tinypilot index 1d0c4ace8..88ecea9d3 100644 --- a/debian-pkg/etc/sudoers.d/tinypilot +++ b/debian-pkg/etc/sudoers.d/tinypilot @@ -4,7 +4,6 @@ tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/configure-janus tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/disable-wifi tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/enable-wifi tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/read-update-log -tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/read-update-log tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/update tinypilot ALL=(ALL) NOPASSWD: /sbin/shutdown tinypilot ALL=(ALL) NOPASSWD: /usr/sbin/service janus restart From e24545924adb1b82c2d24a7ba7071402eb6288f9 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 13 Jun 2024 13:47:43 +0200 Subject: [PATCH 04/49] Add commentary --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 8f45e8f5d..b34866185 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -21,6 +21,10 @@ readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" { echo "${MARKER_START}" echo "country=${WIFI_COUNTRY}" + # Generate the "network" block of the config. The `wpa_passphrase` command + # hashes the WiFi password so that it is not persisted in clear text. + # However, the command still outputs the original password as a comment line, + # so we strip off that line (which starts with `#psk=`). wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d' echo "${MARKER_END}" } | sudo tee --append "${CONFIG_FILE}" > /dev/null From 76371333af5083e0d52f78de82a2dcbb04fa856d Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 17 Jun 2024 18:34:53 +0200 Subject: [PATCH 05/49] Read WiFi settings from CLI args --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index b34866185..3cec741ce 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -4,9 +4,9 @@ set -e # TODO parse these parameters from CLI args. -readonly WIFI_COUNTRY='DE' -readonly WIFI_SSID='my-home-network' -readonly WIFI_PSK='p4ssw0rd' +readonly WIFI_COUNTRY="$1" +readonly WIFI_SSID="$2" +readonly WIFI_PSK="$3" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly SCRIPT_DIR From 517af0edcc9c2ae528e1ad918c72026a6432a137 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 17 Jun 2024 18:36:32 +0200 Subject: [PATCH 06/49] Add mock scripts --- dev-scripts/mock-scripts/disable-wifi | 1 + dev-scripts/mock-scripts/enable-wifi | 1 + 2 files changed, 2 insertions(+) create mode 100755 dev-scripts/mock-scripts/disable-wifi create mode 100755 dev-scripts/mock-scripts/enable-wifi diff --git a/dev-scripts/mock-scripts/disable-wifi b/dev-scripts/mock-scripts/disable-wifi new file mode 100755 index 000000000..a9bf588e2 --- /dev/null +++ b/dev-scripts/mock-scripts/disable-wifi @@ -0,0 +1 @@ +#!/bin/bash diff --git a/dev-scripts/mock-scripts/enable-wifi b/dev-scripts/mock-scripts/enable-wifi new file mode 100755 index 000000000..a9bf588e2 --- /dev/null +++ b/dev-scripts/mock-scripts/enable-wifi @@ -0,0 +1 @@ +#!/bin/bash From 3cf6e385af12f4f3480fc9fc39e834c06467314b Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 20 Jun 2024 14:12:18 +0200 Subject: [PATCH 07/49] Parse CLI arguments --- .../tinypilot-privileged/scripts/disable-wifi | 38 +++++++- .../tinypilot-privileged/scripts/enable-wifi | 93 +++++++++++++++++-- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi index 5ce9869ad..c77ec7e2e 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi @@ -1,17 +1,45 @@ #!/bin/bash +# +# Disable the WiFi network connection. -# Exit on first error. +# Exit on first failure. set -e +print_help() { + cat < 0 )); do + case "$1" in + --help) + print_help + exit + ;; + *) + >&2 echo "Unknown flag/argument: $1" + >&2 echo "Use the '--help' flag for more information" + exit 1 + ;; + esac +done + +# Echo commands before executing them, by default to stderr. +set -x + +# Exit on unset variable. +set -u + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly SCRIPT_DIR readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" -# shellcheck source=lib/markers.sh -. "${SCRIPT_DIR}/lib/markers.sh" - # Remove any existing automated configuration. "${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" -# Block (deactivate) WLAN, which will take effect instantly. +# Block (deactivate) WLAN. This will take effect instantly. rfkill block wlan diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 3cec741ce..0cf39a3d2 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -1,23 +1,100 @@ #!/bin/bash +# +# Enables a WiFi network connection. -# Exit on first error. +# Exit on first failure. set -e -# TODO parse these parameters from CLI args. -readonly WIFI_COUNTRY="$1" -readonly WIFI_SSID="$2" -readonly WIFI_PSK="$3" +print_help() { + cat < 0 )); do + case "$1" in + --help) + print_help + exit + ;; + --country) + if (( "$#" < 2 )); then + shift + break + fi + WIFI_COUNTRY="$2" + shift # For flag name. + shift # For flag value. + ;; + --ssid) + if (( "$#" < 2 )); then + shift + break + fi + WIFI_SSID="$2" + shift # For flag name. + shift # For flag value. + ;; + --psk) + if (( "$#" < 2 )); then + shift + break + fi + WIFI_PSK="$2" + shift # For flag name. + shift # For flag value. + ;; + *) + >&2 echo "Unknown flag/argument: $1" + >&2 echo "Use the '--help' flag for more information" + exit 1 + ;; + esac +done +readonly WIFI_COUNTRY +readonly WIFI_SSID +readonly WIFI_PSK="${WIFI_PSK:-''}" + +if [[ -z "${WIFI_COUNTRY}" ]]; then + >&2 echo 'Missing argument: COUNTRY' + >&2 echo "Use the '--help' flag for more information" + exit 1 +fi + +if [[ "$(echo -n "${WIFI_COUNTRY}" | wc --bytes)" != 2 ]]; then + >&2 echo 'Invalid argument: COUNTRY' + >&2 echo "Use the '--help' flag for more information" + exit 1 +fi + +if [[ -z "${WIFI_SSID}" ]]; then + >&2 echo 'Missing argument: SSID' + >&2 echo "Use the '--help' flag for more information" + exit 1 +fi + +# Echo commands before executing them, by default to stderr. +set -x + +# Exit on unset variable. +set -u SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly SCRIPT_DIR -readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" -# shellcheck source=lib/markers.sh -. "${SCRIPT_DIR}/lib/markers.sh" # Remove any existing automated configuration. +readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" "${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" # Write out the new configuration. +# shellcheck source=lib/markers.sh +. "${SCRIPT_DIR}/lib/markers.sh" { echo "${MARKER_START}" echo "country=${WIFI_COUNTRY}" From e0602d4527b339063c1c4562261c58020726ced9 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Wed, 26 Jun 2024 19:02:22 +0200 Subject: [PATCH 08/49] Allow to join open networks (no PSK) --- .../tinypilot-privileged/scripts/disable-wifi | 6 +-- .../tinypilot-privileged/scripts/enable-wifi | 47 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi index c77ec7e2e..2d3d71470 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/disable-wifi @@ -9,7 +9,7 @@ print_help() { cat < /dev/null && pwd )" readonly SCRIPT_DIR -readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" +readonly CONFIG_FILE='/etc/wpa_supplicant/wpa_supplicant.conf' # Remove any existing automated configuration. "${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" -# Block (deactivate) WLAN. This will take effect instantly. +# Effectuate changes. This will disable the WiFi connection instantly. rfkill block wlan diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 0cf39a3d2..f9495ac36 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -12,7 +12,8 @@ Enables a WiFi network connection. --help Optional. Display this help and exit. --country COUNTRY A two-digit country code, as of ISO 3166-1 alpha-2. --ssid SSID The name (SSID) of the WiFi network. - --psk PSK Optional. The password for authenticating. + --psk PSK Optional. The password for authenticating. If specified, + it must be 8-63 characters in length. EOF } @@ -59,7 +60,7 @@ while (( "$#" > 0 )); do done readonly WIFI_COUNTRY readonly WIFI_SSID -readonly WIFI_PSK="${WIFI_PSK:-''}" +readonly WIFI_PSK="${WIFI_PSK:-}" if [[ -z "${WIFI_COUNTRY}" ]]; then >&2 echo 'Missing argument: COUNTRY' @@ -67,18 +68,23 @@ if [[ -z "${WIFI_COUNTRY}" ]]; then exit 1 fi -if [[ "$(echo -n "${WIFI_COUNTRY}" | wc --bytes)" != 2 ]]; then - >&2 echo 'Invalid argument: COUNTRY' - >&2 echo "Use the '--help' flag for more information" - exit 1 -fi - if [[ -z "${WIFI_SSID}" ]]; then >&2 echo 'Missing argument: SSID' >&2 echo "Use the '--help' flag for more information" exit 1 fi +# If a password is specified, it has to be 8-63 characters in length. +if [[ -n "${WIFI_PSK}" ]]; then + PSK_LENGTH="$(echo -n "${WIFI_PSK}" | wc --bytes)" + readonly PSK_LENGTH + if (( "${PSK_LENGTH}" < 8 || "${PSK_LENGTH}" > 63 )); then + >&2 echo 'Invalid argument: PSK' + >&2 echo "Use the '--help' flag for more information" + exit 1 + fi +fi + # Echo commands before executing them, by default to stderr. set -x @@ -89,7 +95,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" readonly SCRIPT_DIR # Remove any existing automated configuration. -readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" +readonly CONFIG_FILE='/etc/wpa_supplicant/wpa_supplicant.conf' "${SCRIPT_DIR}/strip-marker-sections" "${CONFIG_FILE}" # Write out the new configuration. @@ -98,11 +104,24 @@ readonly CONFIG_FILE="/etc/wpa_supplicant/wpa_supplicant.conf" { echo "${MARKER_START}" echo "country=${WIFI_COUNTRY}" - # Generate the "network" block of the config. The `wpa_passphrase` command - # hashes the WiFi password so that it is not persisted in clear text. - # However, the command still outputs the original password as a comment line, - # so we strip off that line (which starts with `#psk=`). - wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d' + + # Generate the "network" block of the config. + # - If a password is specified, we use the `wpa_passphrase` command. This + # outputs a complete "network" block, and hashes the password instead of + # storing it in clear text. Note that it still includes the original + # password as comment line in the output, so we have to strip off that line + # (which starts with `#psk=`) + # - If no password is specified, we assemble the "network" block manually. In + # this case, we also have to set `key_mgmt=NONE` to denote an open network. + if [[ -n "${WIFI_PSK}" ]]; then + wpa_passphrase "${WIFI_SSID}" "${WIFI_PSK}" | sed '/^\t#psk=.*/d' + else + echo 'network={' + echo -e "\tssid=\"${WIFI_SSID}\"" + echo -e '\tkey_mgmt=NONE' + echo '}' + fi + echo "${MARKER_END}" } | sudo tee --append "${CONFIG_FILE}" > /dev/null From 7995bd4a0caaffa96969625a8371f185fef42a94 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 27 Jun 2024 15:02:01 +0200 Subject: [PATCH 09/49] Fix declaration of optional flags in help output --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index f9495ac36..8781e617e 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -7,7 +7,7 @@ set -e print_help() { cat < Date: Thu, 27 Jun 2024 15:05:07 +0200 Subject: [PATCH 10/49] Clarify purpose of script --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 8781e617e..73fd8f5e3 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -14,6 +14,12 @@ Enables a WiFi network connection. --ssid SSID The name (SSID) of the WiFi network. --psk PSK Optional. The password for authenticating. If specified, it must be 8-63 characters in length. + +Note: this script only stores the WiFi configuration and triggers its +activation, but it doesn't (and cannot) verify whether the device can actually +connect to the wireless network successfully. In that sense, enabling WiFi +means that the device will *attempt* to connect to the wireless network, and +otherwise fall back to the wired Ethernet connection. EOF } From dece804ee89336a26bb2e10c3cadfbc8dcf806bb Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 27 Jun 2024 15:05:23 +0200 Subject: [PATCH 11/49] =?UTF-8?q?Validate=20length=20of=20=E2=80=94country?= =?UTF-8?q?=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 73fd8f5e3..8054a2cdb 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -74,6 +74,15 @@ if [[ -z "${WIFI_COUNTRY}" ]]; then exit 1 fi +# According to ISO 3166-1 alpha-2, the country code has to contain 2 letters. +COUNTRY_LENGTH="$(echo -n "${WIFI_COUNTRY}" | wc --bytes)" +readonly COUNTRY_LENGTH +if (( "${COUNTRY_LENGTH}" != 2 )); then + >&2 echo 'Invalid argument: COUNTRY' + >&2 echo "Use the '--help' flag for more information" + exit 1 +fi + if [[ -z "${WIFI_SSID}" ]]; then >&2 echo 'Missing argument: SSID' >&2 echo "Use the '--help' flag for more information" From 49c18cd68403c09fc5e045bc0cb4df365e21540e Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 27 Jun 2024 15:05:42 +0200 Subject: [PATCH 12/49] Simplify procedure for writing to file --- debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi index 8054a2cdb..4b615560c 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi +++ b/debian-pkg/opt/tinypilot-privileged/scripts/enable-wifi @@ -138,7 +138,7 @@ readonly CONFIG_FILE='/etc/wpa_supplicant/wpa_supplicant.conf' fi echo "${MARKER_END}" -} | sudo tee --append "${CONFIG_FILE}" > /dev/null +} >> "${CONFIG_FILE}" # Effectuate changes. rfkill unblock wifi From 0933a3a4756e2fc6c722a4761911678c684159e9 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Thu, 27 Jun 2024 15:05:56 +0200 Subject: [PATCH 13/49] Reference real scripts in comment --- dev-scripts/mock-scripts/disable-wifi | 2 ++ dev-scripts/mock-scripts/enable-wifi | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dev-scripts/mock-scripts/disable-wifi b/dev-scripts/mock-scripts/disable-wifi index a9bf588e2..636149d21 100755 --- a/dev-scripts/mock-scripts/disable-wifi +++ b/dev-scripts/mock-scripts/disable-wifi @@ -1 +1,3 @@ #!/bin/bash + +# Mock version of /opt/tinypilot-privileged/scripts/disable-wifi diff --git a/dev-scripts/mock-scripts/enable-wifi b/dev-scripts/mock-scripts/enable-wifi index a9bf588e2..f616e1f55 100755 --- a/dev-scripts/mock-scripts/enable-wifi +++ b/dev-scripts/mock-scripts/enable-wifi @@ -1 +1,3 @@ #!/bin/bash + +# Mock version of /opt/tinypilot-privileged/scripts/enable-wifi From c4feb1f5e21d8e2619ca3e3088fc0d4e26467da7 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 1 Jul 2024 22:46:41 +0200 Subject: [PATCH 14/49] Add script for printing marker section content --- .../scripts/print-marker-section | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100755 debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section new file mode 100755 index 000000000..77e5e2519 --- /dev/null +++ b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section @@ -0,0 +1,89 @@ +#!/bin/bash +# +# Prints the content of a marker sections from a file. +# +# If the target file doesn’t contain marker sections, the script doesn’t output +# anything. +# If the target file contains unmatched/orphaned markers, this script fails. + +# We don’t use `set -x`, because it would output every single iteration of the +# while loop when iterating through the lines of the target file, and hence +# generate a lot of noise. + +# Exit on first failure. +set -e + +# Exit on unset variable. +set -u + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +readonly SCRIPT_DIR +# shellcheck source=lib/markers.sh +. "${SCRIPT_DIR}/lib/markers.sh" + +print_help() { + cat << EOF +Usage: ${0##*/} [--help] TARGET_FILE +Prints the content of a marker sections from a file. + TARGET_FILE Path to file with marker sections. + --help Display this help and exit. +EOF +} + +# Parse command-line arguments. +TARGET_FILE='' +while (( "$#" > 0 )); do + case "$1" in + --help) + print_help + exit + ;; + -*) + echo "Illegal option: $1" >&2 + exit 1 + ;; + *) + TARGET_FILE="$1" + shift + ;; + esac +done +readonly TARGET_FILE + +# Ensure target file is specified. +if [[ -z "${TARGET_FILE}" ]]; then + echo 'Input parameter missing: TARGET_FILE' >&2 + exit 1 +fi + +# Ensure target file exists and is a file. +if [[ ! -f "${TARGET_FILE}" ]]; then + echo "Not a file: ${TARGET_FILE}" >&2 + exit 1 +fi + +# Read the original file line by line, and print all lines that reside between +# the markers to stdout. +is_in_marker_section='false' +while IFS='' read -r line; do + if [[ "${line}" == "${MARKER_END}" ]]; then + if ! "${is_in_marker_section}"; then + echo 'Unmatched end marker' >&2 + exit 1 + fi + is_in_marker_section='false' + continue + fi + if [[ "${line}" == "${MARKER_START}" ]]; then + is_in_marker_section='true' + continue + fi + if "${is_in_marker_section}"; then + echo "${line}" + fi +done < "${TARGET_FILE}" + +if "${is_in_marker_section}"; then + echo 'Unmatched start marker' >&2 + exit 1 +fi From c80a86ce0d2cf76c99c723c2eef7ee7610a255c1 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 1 Jul 2024 22:47:56 +0200 Subject: [PATCH 15/49] Add mock script --- dev-scripts/mock-scripts/print-marker-section | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 dev-scripts/mock-scripts/print-marker-section diff --git a/dev-scripts/mock-scripts/print-marker-section b/dev-scripts/mock-scripts/print-marker-section new file mode 100755 index 000000000..4929d5d22 --- /dev/null +++ b/dev-scripts/mock-scripts/print-marker-section @@ -0,0 +1,3 @@ +#!/bin/bash + +# Mock version of /opt/tinypilot-privileged/scripts/print-marker-section From 393730533a7edee9ff0e53ea1353adb7f1774421 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 1 Jul 2024 23:04:08 +0200 Subject: [PATCH 16/49] Add tests --- ...t-marker-section => print-marker-sections} | 10 +- .../scripts/print-marker-sections.bats | 142 ++++++++++++++++++ ...t-marker-section => print-marker-sections} | 2 +- 3 files changed, 150 insertions(+), 4 deletions(-) rename debian-pkg/opt/tinypilot-privileged/scripts/{print-marker-section => print-marker-sections} (87%) create mode 100644 debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections.bats rename dev-scripts/mock-scripts/{print-marker-section => print-marker-sections} (88%) diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections similarity index 87% rename from debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section rename to debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections index 77e5e2519..ae6290d02 100755 --- a/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-section +++ b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections @@ -62,9 +62,10 @@ if [[ ! -f "${TARGET_FILE}" ]]; then exit 1 fi -# Read the original file line by line, and print all lines that reside between -# the markers to stdout. +# Read the original file line by line, and preserve all lines that reside +# between the start and end markers (i.e., the section contents). is_in_marker_section='false' +section_contents=() while IFS='' read -r line; do if [[ "${line}" == "${MARKER_END}" ]]; then if ! "${is_in_marker_section}"; then @@ -79,7 +80,7 @@ while IFS='' read -r line; do continue fi if "${is_in_marker_section}"; then - echo "${line}" + section_contents+=("${line}") fi done < "${TARGET_FILE}" @@ -87,3 +88,6 @@ if "${is_in_marker_section}"; then echo 'Unmatched start marker' >&2 exit 1 fi + +# Print all lines of the section contents. +printf "%s\n" "${section_contents[@]}" diff --git a/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections.bats b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections.bats new file mode 100644 index 000000000..4c66523b8 --- /dev/null +++ b/debian-pkg/opt/tinypilot-privileged/scripts/print-marker-sections.bats @@ -0,0 +1,142 @@ +#!/bin/bash + +{ + # Silence shellcheck for global bats variables. + # https://github.com/tiny-pilot/tinypilot/issues/1718 + # shellcheck disable=SC2154 + echo "${output}" "${status}" "${lines}" >/dev/null +} + +# Wrapper for invoking the script under test as command. +print-marker-sections() { + bash "${BATS_TEST_DIRNAME}/print-marker-sections" "$@" +} + +prints-help() { #@test + run print-marker-sections --help + expected_output="$(cat << EOF +Usage: print-marker-sections [--help] TARGET_FILE +Prints the content of a marker sections from a file. + TARGET_FILE Path to file with marker sections. + --help Display this help and exit. +EOF + )" + + [[ "${status}" == 0 ]] + [[ "${output}" == "${expected_output}" ]] +} + +rejects-missing-input-arg() { #@test + run print-marker-sections + + [[ "${status}" == 1 ]] + [[ "${output}" == 'Input parameter missing: TARGET_FILE' ]] +} + +rejects-illegal-flag() { #@test + run print-marker-sections --foo + + [[ "${status}" == 1 ]] + [[ "${output}" == 'Illegal option: --foo' ]] +} + +rejects-non-existing-file() { #@test + run print-marker-sections foo-file.txt + + [[ "${status}" == 1 ]] + [[ "${output}" == 'Not a file: foo-file.txt' ]] +} + +rejects-non-file() { #@test + tmp_dir="$(mktemp --directory)" + run print-marker-sections "${tmp_dir}" + + [[ "${status}" == 1 ]] + [[ "${output}" == "Not a file: ${tmp_dir}" ]] +} + +empty-output-if-file-has-no-markers() { #@test + target_file="$(mktemp)" + cat << EOF > "${target_file}" +line 1 +line 2 +line 3 +EOF + run print-marker-sections "${target_file}" + + [[ "${status}" == 0 ]] + [[ "${output}" == "" ]] +} + +prints-marker-section() { #@test + target_file="$(mktemp)" + cat << EOF > "${target_file}" +some line +some other line +# --- AUTOGENERATED BY TINYPILOT - START --- +to be +printed +# --- AUTOGENERATED BY TINYPILOT - END --- +final line +EOF + run print-marker-sections "${target_file}" + expected_output="$(cat << EOF +to be +printed +EOF + )" + + [[ "${status}" == 0 ]] + [[ "${output}" == "${expected_output}" ]] +} + +prints-multiple-marker-sections() { #@test + target_file="$(mktemp)" + cat << EOF > "${target_file}" +some line +some other line +# --- AUTOGENERATED BY TINYPILOT - START --- +to be +# --- AUTOGENERATED BY TINYPILOT - END --- +intermediate line +# --- AUTOGENERATED BY TINYPILOT - START --- +printed +# --- AUTOGENERATED BY TINYPILOT - END --- +final line +EOF + run print-marker-sections "${target_file}" + expected_output="$(cat << EOF +to be +printed +EOF + )" + + [[ "${status}" == 0 ]] + [[ "${output}" == "${expected_output}" ]] +} + +fails-for-unmatched-start-marker() { #@test + target_file="$(mktemp)" + cat << EOF > "${target_file}" +some line +# --- AUTOGENERATED BY TINYPILOT - START --- +to be printed +EOF + run print-marker-sections "${target_file}" + + [[ "${status}" == 1 ]] + [[ "${output}" == "Unmatched start marker" ]] +} + +fails-for-unmatched-end-marker() { #@test + target_file="$(mktemp)" + cat << EOF > "${target_file}" +some line +# --- AUTOGENERATED BY TINYPILOT - END --- +final line +EOF + run print-marker-sections "${target_file}" + + [[ "${status}" == 1 ]] + [[ "${output}" == 'Unmatched end marker' ]] +} diff --git a/dev-scripts/mock-scripts/print-marker-section b/dev-scripts/mock-scripts/print-marker-sections similarity index 88% rename from dev-scripts/mock-scripts/print-marker-section rename to dev-scripts/mock-scripts/print-marker-sections index 4929d5d22..947e0f489 100755 --- a/dev-scripts/mock-scripts/print-marker-section +++ b/dev-scripts/mock-scripts/print-marker-sections @@ -1,3 +1,3 @@ #!/bin/bash -# Mock version of /opt/tinypilot-privileged/scripts/print-marker-section +# Mock version of /opt/tinypilot-privileged/scripts/print-marker-sections From 565a7c946dc41d0ae24d6fa15d2dfe225aa96b2c Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 1 Jul 2024 22:48:52 +0200 Subject: [PATCH 17/49] Add backend endpoints and logic --- app/api.py | 88 ++++++++++ app/network.py | 123 +++++++++++++ app/request_parsers/errors.py | 4 + app/request_parsers/network.py | 48 ++++++ app/request_parsers/network_test.py | 161 ++++++++++++++++++ debian-pkg/etc/sudoers.d/tinypilot | 1 + .../mock-scripts/print-marker-sections | 10 ++ 7 files changed, 435 insertions(+) create mode 100644 app/network.py create mode 100644 app/request_parsers/network.py create mode 100644 app/request_parsers/network_test.py diff --git a/app/api.py b/app/api.py index 503ec8279..b4405a60e 100644 --- a/app/api.py +++ b/app/api.py @@ -6,8 +6,10 @@ import hostname import json_response import local_system +import network import request_parsers.errors import request_parsers.hostname +import request_parsers.network import request_parsers.paste import request_parsers.video_settings import update.launcher @@ -199,6 +201,92 @@ def hostname_set(): return json_response.error(e), 500 +@api_blueprint.route('/network', methods=['GET']) +def network_status(): + """Returns the current network status (i.e., which interfaces are active). + + Returns: + On success, a JSON data structure with the following properties: + ethernet: bool. + wifi: bool + + Example: + { + "ethernet": true, + "wifi": false + } + """ + status = network.status() + return json_response.success({ + 'ethernet': status.ethernet, + 'wifi': status.wifi, + }) + + +@api_blueprint.route('/network/wifi', methods=['GET']) +def network_wifi_get(): + """Returns the current WiFi settings, if present. + + Returns: + On success, a JSON data structure with the following properties: + countryCode: string. + ssid: string. + + Example: + { + "countryCode": "US", + "ssid": "my-network" + } + + Returns an error object on failure. + """ + wifi_settings = network.determine_wifi_settings() + return json_response.success({ + 'countryCode': wifi_settings.country_code, + 'ssid': wifi_settings.ssid, + }) + + +@api_blueprint.route('/network/wifi', methods=['PUT']) +def network_wifi_enable(): + """Enables a wireless network connection. + + Expects a JSON data structure in the request body that contains a country + code, an SSID, and optionally a password; all as strings. Example: + { + "countryCode": "US", + "ssid": "my-network", + "psk": "sup3r-s3cr3t!" + } + + Returns: + Empty response on success, error object otherwise. + """ + try: + wifi_settings = request_parsers.network.parse_wifi_settings( + flask.request) + network.enable_wifi(wifi_settings) + return json_response.success() + except request_parsers.errors.Error as e: + return json_response.error(e), 400 + except network.Error as e: + return json_response.error(e), 500 + + +@api_blueprint.route('/network/wifi', methods=['DELETE']) +def network_wifi_disable(): + """Disables the WiFi network connection. + + Returns: + Empty response on success, error object otherwise. + """ + try: + network.disable_wifi() + return json_response.success() + except network.Error as e: + return json_response.error(e), 500 + + @api_blueprint.route('/status', methods=['GET']) def status_get(): """Checks the status of TinyPilot. diff --git a/app/network.py b/app/network.py new file mode 100644 index 000000000..d034c9cf1 --- /dev/null +++ b/app/network.py @@ -0,0 +1,123 @@ +import dataclasses +import re +import subprocess + +_CONFIG_FILE = '/etc/wpa_supplicant/wpa_supplicant.conf' +_WIFI_COUNTRY_PATTERN = re.compile(r'^\s*country=(.+)$') +_WIFI_SSID_PATTERN = re.compile(r'^\s*ssid="(.+)"$') + + +class Error(Exception): + pass + + +class NetworkError(Error): + pass + + +@dataclasses.dataclass +class NetworkStatus: + ethernet: bool + wifi: bool + + +@dataclasses.dataclass +class WiFiSettings: + country_code: str + ssid: str + psk: str # Optional. + + +def status(): + """Checks the connectivity of the network interfaces. + + Returns: + NetworkStatus + """ + network_status = NetworkStatus(False, False) + try: + with open('/sys/class/net/eth0/operstate', encoding='utf-8') as file: + eth0 = file.read().strip() + network_status.ethernet = eth0 == 'up' + except OSError: + pass # We treat this as if the interface was down altogether. + try: + with open('/sys/class/net/wlan0/operstate', encoding='utf-8') as file: + wlan0 = file.read().strip() + network_status.wifi = wlan0 == 'up' + except OSError: + pass # We treat this as if the interface was down altogether. + return network_status + + +def determine_wifi_settings(): + """Determines the current WiFi settings (if set). + + Returns: + WiFiSettings: if the `ssid` and `country_code` attributes are `None`, + there is no WiFi configuration present. The `psk` property is + always `None` for security reasons. + """ + try: + config_lines = subprocess.check_output([ + 'sudo', '/opt/tinypilot-privileged/scripts/print-marker-sections', + '/etc/wpa_supplicant/wpa_supplicant.conf' + ], + stderr=subprocess.STDOUT, + universal_newlines=True) + except subprocess.CalledProcessError as e: + raise NetworkError(str(e.output).strip()) from e + + wifi = WiFiSettings(None, None, None) + for line in config_lines.split('\n'): + match_country = _WIFI_COUNTRY_PATTERN.search(line.strip()) + if match_country: + wifi.country_code = match_country.group(1) + continue + match_ssid = _WIFI_SSID_PATTERN.search(line.strip()) + if match_ssid: + wifi.ssid = match_ssid.group(1) + continue + return wifi + + +def enable_wifi(wifi_settings): + """Enables a wireless network connection. + + Note: The function is executed in a "fire and forget" manner, to prevent + the HTTP request from failing erratically due to a network interruption. + + Args: + wifi_settings: The new, desired settings. + + Raises: + NetworkError + """ + args = [ + 'sudo', '/opt/tinypilot-privileged/scripts/enable-wifi', '--country', + wifi_settings.country_code, '--ssid', wifi_settings.ssid + ] + if wifi_settings.psk: + args.extend(['--psk', wifi_settings.psk]) + try: + return subprocess.Popen(args) + except subprocess.CalledProcessError as e: + raise NetworkError(str(e.output).strip()) from e + + +def disable_wifi(): + """Removes the WiFi settings and disables the wireless connection. + + Note: The function is executed in a "fire and forget" manner, to prevent + the HTTP request from failing erratically due to a network interruption. + + Raises: + NetworkError + """ + try: + return subprocess.Popen([ + 'sudo', + '/opt/tinypilot-privileged/scripts/disable-wifi', + ]) + except subprocess.CalledProcessError as e: + raise NetworkError(str(e.output).strip()) from e diff --git a/app/request_parsers/errors.py b/app/request_parsers/errors.py index 30c7080a7..05c7d3315 100644 --- a/app/request_parsers/errors.py +++ b/app/request_parsers/errors.py @@ -14,6 +14,10 @@ class InvalidHostnameError(Error): code = 'INVALID_HOSTNAME' +class InvalidWifiSettings(Error): + code = 'INVALID_WIFI_SETTINGS' + + class InvalidVideoSettingError(Error): pass diff --git a/app/request_parsers/network.py b/app/request_parsers/network.py new file mode 100644 index 000000000..6816c8073 --- /dev/null +++ b/app/request_parsers/network.py @@ -0,0 +1,48 @@ +import network +from request_parsers import errors +from request_parsers import json + + +def parse_wifi_settings(request): + """Parses WiFi settings from the request. + + Returns: + WiFiSettings + + Raises: + InvalidWifiSettings + """ + # pylint: disable=unbalanced-tuple-unpacking + ( + country_code, + ssid, + psk, + ) = json.parse_json_body(request, + required_fields=['countryCode', 'ssid', 'psk']) + + if not isinstance(country_code, str): + raise errors.InvalidWifiSettings('The country code is not a string.') + if len(country_code) != 2: + # The ISO 3166-1 alpha-2 standard theoretically allows any 2-digit + # combination, so we don’t have to eagerly restrict this. + raise errors.InvalidWifiSettings( + 'The country code must consist of 2 characters.') + if not country_code.isalpha(): + raise errors.InvalidWifiSettings( + 'The country code must only contain letters.') + + if not isinstance(ssid, str): + raise errors.InvalidWifiSettings('The SSID is not a string.') + if len(ssid) == 0: + raise errors.InvalidWifiSettings('The SSID cannot be empty.') + + if psk is not None: + if not isinstance(psk, str): + raise errors.InvalidWifiSettings('The password is not a string.') + if len(psk) < 8 or len(psk) > 63: + # Note: this constraint is imposed by the WPA2 standard. We need + # to enforce this to prevent underlying commands from failing. + raise errors.InvalidWifiSettings( + 'The password must consist of 8-63 characters.') + + return network.WiFiSettings(country_code.upper(), ssid, psk) diff --git a/app/request_parsers/network_test.py b/app/request_parsers/network_test.py new file mode 100644 index 000000000..6996a221a --- /dev/null +++ b/app/request_parsers/network_test.py @@ -0,0 +1,161 @@ +import unittest +from unittest import mock + +from request_parsers import errors +from request_parsers import network + + +def make_mock_request(json_data): + mock_request = mock.Mock() + mock_request.get_json.return_value = json_data + return mock_request + + +class NetworkValidationTest(unittest.TestCase): + + def test_accepts_valid_wifi_credentials_with_psk(self): + wifi = network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + self.assertEqual('US', wifi.country_code) + self.assertEqual('my-network', wifi.ssid) + self.assertEqual('s3cr3t!!!', wifi.psk) + + def test_accepts_valid_wifi_credentials_without_psk(self): + wifi = network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'DE', + 'ssid': 'SomeWiFiHotspot_123', + 'psk': None + })) + self.assertEqual('DE', wifi.country_code) + self.assertEqual('SomeWiFiHotspot_123', wifi.ssid) + self.assertEqual(None, wifi.psk) + + def test_normalizes_country_code_to_uppercase(self): + wifi = network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'us', + 'ssid': 'SomeWiFiHotspot_123', + 'psk': None + })) + self.assertEqual('US', wifi.country_code) + + def test_rejects_absent_required_fields(self): + with self.assertRaises(errors.MissingFieldError): + network.parse_wifi_settings( + make_mock_request({ + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'psk': 's3cr3t!!!' + })) + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 'my-network' + })) + + def test_rejects_country_code_with_incorrect_type(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 12, + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': None, + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + + def test_rejects_country_code_with_incorrect_length(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'A', + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'ABC', + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + + def test_rejects_country_code_non_alpha(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': '12', + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'A*', + 'ssid': 'my-network', + 'psk': 's3cr3t!!!' + })) + + def test_rejects_ssid_with_incorrect_type(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 123, + 'psk': 's3cr3t!!!' + })) + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': None, + 'psk': 's3cr3t!!!' + })) + + def test_rejects_psk_with_incorrect_type(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 'my-network', + 'psk': 123, + })) + + def test_rejects_ssid_with_incorrect_length(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': '', + 'psk': 's3cr3t!!!' + })) + + def test_rejects_psk_with_incorrect_length(self): + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 'Hotspot123', + 'psk': 'x' * 7 + })) + with self.assertRaises(errors.InvalidWifiSettings): + network.parse_wifi_settings( + make_mock_request({ + 'countryCode': 'US', + 'ssid': 'Hotspot123', + 'psk': 'x' * 64 + })) diff --git a/debian-pkg/etc/sudoers.d/tinypilot b/debian-pkg/etc/sudoers.d/tinypilot index 88ecea9d3..8ef302ed8 100644 --- a/debian-pkg/etc/sudoers.d/tinypilot +++ b/debian-pkg/etc/sudoers.d/tinypilot @@ -3,6 +3,7 @@ tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/collect-debug-lo tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/configure-janus tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/disable-wifi tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/enable-wifi +tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/print-marker-sections /etc/wpa_supplicant/wpa_supplicant.conf tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/read-update-log tinypilot ALL=(ALL) NOPASSWD: /opt/tinypilot-privileged/scripts/update tinypilot ALL=(ALL) NOPASSWD: /sbin/shutdown diff --git a/dev-scripts/mock-scripts/print-marker-sections b/dev-scripts/mock-scripts/print-marker-sections index 947e0f489..e3e073952 100755 --- a/dev-scripts/mock-scripts/print-marker-sections +++ b/dev-scripts/mock-scripts/print-marker-sections @@ -1,3 +1,13 @@ #!/bin/bash # Mock version of /opt/tinypilot-privileged/scripts/print-marker-sections + +if [[ "$1" == '/etc/wpa_supplicant/wpa_supplicant.conf' ]]; then + cat << EOF +country=US +network={ + ssid="TinyPilot Office" + psk=4efa961963891a4adb1f96ed645e96cd096cfb9bf5af086cd46c21c85b5bb98b +} +EOF +fi From 6794c58e70d44d8b0b6d3cc4c38ced4b2f657216 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Mon, 1 Jul 2024 22:49:16 +0200 Subject: [PATCH 18/49] Add WiFi dialog (WIP) --- app/static/css/style.css | 3 +- app/static/js/app.js | 7 + app/static/js/controllers.js | 69 ++++ app/templates/custom-elements/menu-bar.html | 7 + .../custom-elements/wifi-dialog.html | 322 ++++++++++++++++++ app/templates/index.html | 3 + 6 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 app/templates/custom-elements/wifi-dialog.html diff --git a/app/static/css/style.css b/app/static/css/style.css index c3100553b..63a83d960 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -111,7 +111,8 @@ input[type="password"] { box-shadow: inset 0 0 0.2rem 0 rgba(0, 0, 0, 0.15); } -input[type="text"].monospace { +input[type="text"].monospace, +input[type="password"].monospace { font-family: "Overpass Mono", monospace; /* Since the monospace font has different characteristics than the regular font, we need to set the height here explicitly. In the end, the input diff --git a/app/static/js/app.js b/app/static/js/app.js index 86f944322..84667befc 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -320,6 +320,13 @@ menuBar.addEventListener("change-hostname-dialog-requested", () => { document.getElementById("change-hostname-overlay").show(); document.getElementById("change-hostname-dialog").initialize(); }); +menuBar.addEventListener("wifi-dialog-requested", () => { + // Note: we have to call `initialize()` after `show()`, to ensure that the + // dialog is able to focus the main input element. + // See https://github.com/tiny-pilot/tinypilot/issues/1770 + document.getElementById("wifi-overlay").show(); + document.getElementById("wifi-dialog").initialize(); +}); menuBar.addEventListener("fullscreen-requested", () => { document.getElementById("remote-screen").fullscreen = true; }); diff --git a/app/static/js/controllers.js b/app/static/js/controllers.js index f49a33b7f..734e60ce4 100644 --- a/app/static/js/controllers.js +++ b/app/static/js/controllers.js @@ -194,6 +194,75 @@ export async function changeHostname(newHostname) { .then(() => newHostname); } +export async function getNetworkStatus() { + return fetch("/api/network", { + method: "GET", + mode: "same-origin", + cache: "no-cache", + redirect: "error", + }) + .then(processJsonResponse) + .then((response) => { + ["ethernet", "wifi"].forEach((field) => { + // eslint-disable-next-line no-prototype-builtins + if (!response.hasOwnProperty(field)) { + throw new ControllerError(`Missing expected ${field} field`); + } + }); + return response; + }); +} + +export async function getWifiSettings() { + return fetch("/api/network/wifi", { + method: "GET", + mode: "same-origin", + cache: "no-cache", + redirect: "error", + }) + .then(processJsonResponse) + .then((response) => { + ["countryCode", "ssid"].forEach((field) => { + // eslint-disable-next-line no-prototype-builtins + if (!response.hasOwnProperty(field)) { + throw new ControllerError(`Missing expected ${field} field`); + } + }); + return response; + }); +} + +export async function enableWifi(countryCode, ssid, psk) { + return fetch("/api/network/wifi", { + method: "PUT", + mode: "same-origin", + cache: "no-cache", + redirect: "error", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCsrfToken(), + }, + body: JSON.stringify({ countryCode, ssid, psk }), + }) + .then(processJsonResponse) + .then(() => true); +} + +export async function disableWifi() { + return fetch("/api/network/wifi", { + method: "DELETE", + mode: "same-origin", + cache: "no-cache", + redirect: "error", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCsrfToken(), + }, + }) + .then(processJsonResponse) + .then(() => true); +} + export async function checkStatus(baseURL = "") { return fetch(baseURL + "/api/status", { method: "GET", diff --git a/app/templates/custom-elements/menu-bar.html b/app/templates/custom-elements/menu-bar.html index a72aefa2d..a1cc1c147 100644 --- a/app/templates/custom-elements/menu-bar.html +++ b/app/templates/custom-elements/menu-bar.html @@ -229,6 +229,13 @@ >Hostname + diff --git a/app/templates/custom-elements/wifi-dialog.html b/app/templates/custom-elements/wifi-dialog.html index a5570361f..392cddcbd 100644 --- a/app/templates/custom-elements/wifi-dialog.html +++ b/app/templates/custom-elements/wifi-dialog.html @@ -65,10 +65,13 @@

Retrieving Current WiFi Settings

WiFi Settings

-

Enter WiFi settings to access TinyPilot via a wireless network connection.

+

+ Enter WiFi settings to access TinyPilot via a wireless network connection. +

- No Ethernet: TinyPilot is currently not connected via Ethernet. - If you change your WiFi settings, you may lose access to your TinyPilot device until you reconnect an Ethernet cable. + No Ethernet: TinyPilot is currently not connected via + Ethernet. If you change your WiFi settings, you may lose access to your + TinyPilot device until you reconnect an Ethernet cable.
@@ -172,38 +175,46 @@

Success

]); connectedCallback() { - this.attachShadow({mode: "open"}).appendChild( + this.attachShadow({ mode: "open" }).appendChild( template.content.cloneNode(true) ); this._elements = { - noEthernetWarning: this.shadowRoot.querySelector("#no-ethernet-warning"), + noEthernetWarning: this.shadowRoot.querySelector( + "#no-ethernet-warning" + ), inputError: this.shadowRoot.querySelector("#input-error"), - inputErrorReason: this.shadowRoot.querySelector("#input-error-reason"), + inputErrorReason: this.shadowRoot.querySelector( + "#input-error-reason" + ), ssidInput: this.shadowRoot.querySelector("#ssid-input"), pskInput: this.shadowRoot.querySelector("#psk-input"), - countryCodeInput: this.shadowRoot.querySelector("#country-code-input"), - enableButton: - this.shadowRoot.querySelector("#enable-button"), - disableButton: - this.shadowRoot.querySelector("#disable-button"), + countryCodeInput: this.shadowRoot.querySelector( + "#country-code-input" + ), + enableButton: this.shadowRoot.querySelector("#enable-button"), + disableButton: this.shadowRoot.querySelector("#disable-button"), }; - ;[ + [ this._elements.ssidInput, this._elements.pskInput, this._elements.countryCodeInput, - ].forEach(el => el.addEventListener("input", () => { - this._refreshButtons(); - })); + ].forEach((el) => + el.addEventListener("input", () => { + this._refreshButtons(); + }) + ); this._elements.enableButton.addEventListener("click", () => { this._enable(); }); this._elements.disableButton.addEventListener("click", () => { this._disable(); }); - this.shadowRoot.querySelectorAll(".close-button").forEach(el => el.addEventListener("click", () => { - this.dispatchEvent(new DialogClosedEvent()); - })); + this.shadowRoot.querySelectorAll(".close-button").forEach((el) => + el.addEventListener("click", () => { + this.dispatchEvent(new DialogClosedEvent()); + }) + ); } get _state() { @@ -244,7 +255,10 @@

Success

if (wifiSettings) { this._elements.ssidInput.value = wifiSettings.ssid; this._elements.countryCodeInput.value = wifiSettings.countryCode; - this._initialWiFiSettings = { ...this._initialWiFiSettings, ...wifiSettings }; + this._initialWiFiSettings = { + ...this._initialWiFiSettings, + ...wifiSettings, + }; } this._refreshButtons(); @@ -256,30 +270,34 @@

Success

[ [this._elements.ssidInput, this._initialWiFiSettings.ssid], [this._elements.pskInput, this._initialWiFiSettings.psk], - [this._elements.countryCodeInput, this._initialWiFiSettings.countryCode], + [ + this._elements.countryCodeInput, + this._initialWiFiSettings.countryCode, + ], ].forEach(([el, initialValue]) => { if (el.value !== initialValue) { inputChanged = true; } }); let mandatoryInputMissing = false; - [ - this._elements.ssidInput, - this._elements.countryCodeInput, - ].forEach((el) => { - if (el.value === "") { - mandatoryInputMissing = true; + [this._elements.ssidInput, this._elements.countryCodeInput].forEach( + (el) => { + if (el.value === "") { + mandatoryInputMissing = true; + } } - }); - this._elements.enableButton.disabled = !inputChanged || mandatoryInputMissing; - this._elements.disableButton.disabled = !this._initialWiFiSettings.ssid; + ); + this._elements.enableButton.disabled = + !inputChanged || mandatoryInputMissing; + this._elements.disableButton.disabled = + !this._initialWiFiSettings.ssid; } _enable() { enableWifi( this._elements.countryCodeInput.value, this._elements.ssidInput.value, - this._elements.pskInput.value || null, + this._elements.pskInput.value || null ) .then(() => { this._state = this._states.SUCCESS; From e3a3bcbc52a083e0985d1ef229f5e17b931254a5 Mon Sep 17 00:00:00 2001 From: Jan Heuermann Date: Tue, 2 Jul 2024 22:02:01 +0200 Subject: [PATCH 27/49] Clean ups --- .../custom-elements/wifi-dialog.html | 111 ++++++++---------- 1 file changed, 50 insertions(+), 61 deletions(-) diff --git a/app/templates/custom-elements/wifi-dialog.html b/app/templates/custom-elements/wifi-dialog.html index 392cddcbd..f8faf8595 100644 --- a/app/templates/custom-elements/wifi-dialog.html +++ b/app/templates/custom-elements/wifi-dialog.html @@ -5,27 +5,16 @@ #initializing, #prompt, - #changing, - #success { + #changing { display: none; } - :host([state="initializing"]) #initializing { - display: block; - } - - :host([state="prompt"]) #prompt { - display: block; - } - + :host([state="initializing"]) #initializing, + :host([state="prompt"]) #prompt, :host([state="changing"]) #changing { display: block; } - :host([state="success"]) #success { - display: block; - } - #input-error { margin-top: 1rem; } @@ -137,11 +126,6 @@

WiFi Settings

Applying Changes

- -
-

Success

- -