From 0cb312815810951bfe5b14f67cc5b04dd35dff18 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 1 Dec 2023 18:02:01 +0000 Subject: [PATCH 01/15] Revised for predictable iface names --- installers/raspap.sudoers | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 8d3d817c..3d625542 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -1,15 +1,15 @@ www-data ALL=(ALL) NOPASSWD:/sbin/ifdown www-data ALL=(ALL) NOPASSWD:/sbin/ifup www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant.conf -www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-wlan[0-9].conf +www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-wl*.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant.conf -www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant-wlan[0-9].conf -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -iwlan[0-9] -www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/wlan[0-9] -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wlan[0-9] scan_results -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wlan[0-9] scan -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wlan[0-9] reconfigure -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wlan[0-9] select_network [0-9]* +www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant-wl*.conf +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -iwl* +www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/wl* +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan_results +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* reconfigure +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network * www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/hostapddata /etc/hostapd/hostapd.conf www-data ALL=(ALL) NOPASSWD:/bin/systemctl start hostapd.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop hostapd.service @@ -28,9 +28,9 @@ www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/dnsmasq.d/090_*.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/dhcpddata /etc/dhcpcd.conf www-data ALL=(ALL) NOPASSWD:/sbin/shutdown -h now www-data ALL=(ALL) NOPASSWD:/sbin/reboot -www-data ALL=(ALL) NOPASSWD:/sbin/ip link set wlan[0-9] down -www-data ALL=(ALL) NOPASSWD:/sbin/ip link set wlan[0-9] up -www-data ALL=(ALL) NOPASSWD:/sbin/ip -s a f label wlan[0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/ip link set wl* down +www-data ALL=(ALL) NOPASSWD:/sbin/ip link set wl* up +www-data ALL=(ALL) NOPASSWD:/sbin/ip -s a f label wl* www-data ALL=(ALL) NOPASSWD:/sbin/ifup * www-data ALL=(ALL) NOPASSWD:/sbin/ifdown * www-data ALL=(ALL) NOPASSWD:/sbin/iw From 20589bacf35f96a4bfdbdd04f90629d8ab090b37 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 1 Dec 2023 18:03:03 +0000 Subject: [PATCH 02/15] Return wpa_cli failure in status message --- includes/configure_client.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/configure_client.php b/includes/configure_client.php index 41da0716..5199129c 100755 --- a/includes/configure_client.php +++ b/includes/configure_client.php @@ -18,8 +18,12 @@ function DisplayWPAConfig() $result = 0; $iface = escapeshellarg($_SESSION['wifi_client_interface']); $netid = intval($_POST['connect']); - exec('sudo wpa_cli -i ' . $iface . ' select_network ' . $netid); - $status->addMessage('New network selected', 'success'); + $return = shell_exec('sudo wpa_cli -i ' .$iface. ' select_network ' . $netid); + if (trim($return) == "FAIL") { + $status->addMessage('WPA command line client returned failure. Check your adapter.', 'danger'); + } else { + $status->addMessage('New network selected', 'success'); + } } elseif (isset($_POST['wpa_reinit'])) { $status->addMessage('Reinitializing wpa_supplicant', 'info', false); $force_remove = true; From 84dc44d944e8dc12ed7542336ad96dc52d694757 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 14:00:34 +0000 Subject: [PATCH 03/15] Add restricted wpa_cli commands to sudoers --- installers/raspap.sudoers | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 3d625542..a1ce4002 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -9,7 +9,11 @@ www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/wl* www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan_results www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* reconfigure -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network * +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* add_network +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i enable_network [0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network [0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* set_network [0-9] * +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* remove_network [0-9] www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/hostapddata /etc/hostapd/hostapd.conf www-data ALL=(ALL) NOPASSWD:/bin/systemctl start hostapd.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop hostapd.service From b5d861a2b9c0b1f97acd7dc7684bb17e4390c849 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 14:16:15 +0000 Subject: [PATCH 04/15] Add wpa_cli disconnect cmd --- installers/raspap.sudoers | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index a1ce4002..9d38ef73 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -11,6 +11,7 @@ www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* reconfigure www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* add_network www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i enable_network [0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i disconnect [0-9]j www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network [0-9] www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* set_network [0-9] * www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* remove_network [0-9] From 79c61d383bb350be24dee5eb4f8dc65a170bbf58 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 14:31:52 +0000 Subject: [PATCH 05/15] Add wpa_cli cmds to set, enable + remove network --- includes/configure_client.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/includes/configure_client.php b/includes/configure_client.php index 5199129c..b66e81c6 100755 --- a/includes/configure_client.php +++ b/includes/configure_client.php @@ -25,19 +25,25 @@ function DisplayWPAConfig() $status->addMessage('New network selected', 'success'); } } elseif (isset($_POST['wpa_reinit'])) { - $status->addMessage('Reinitializing wpa_supplicant', 'info', false); + $status->addMessage('Attempting to reinitialize wpa_supplicant', 'warning'); $force_remove = true; $result = reinitializeWPA($force_remove); - $status->addMessage($result, 'info'); } elseif (isset($_POST['client_settings'])) { $tmp_networks = $networks; + $iface = escapeshellarg($_SESSION['wifi_client_interface']); if ($wpa_file = fopen('/tmp/wifidata', 'w')) { fwrite($wpa_file, 'ctrl_interface=DIR=' . RASPI_WPA_CTRL_INTERFACE . ' GROUP=netdev' . PHP_EOL); fwrite($wpa_file, 'update_config=1' . PHP_EOL); foreach (array_keys($_POST) as $post) { + if (preg_match('/delete(\d+)/', $post, $post_match)) { + $network = $tmp_networks[$_POST['ssid' . $post_match[1]]]; + $netid = $network['index']; + exec('sudo wpa_cli -i ' . $iface . ' disconnect ' . $netid); + exec('sudo wpa_cli -i ' . $iface . ' remove_network ' . $netid); unset($tmp_networks[$_POST['ssid' . $post_match[1]]]); + } elseif (preg_match('/update(\d+)/', $post, $post_match)) { // NB, multiple protocols are separated with a forward slash ('/') $tmp_networks[$_POST['ssid' . $post_match[1]]] = array( @@ -48,6 +54,22 @@ function DisplayWPAConfig() if (array_key_exists('priority' . $post_match[1], $_POST)) { $tmp_networks[$_POST['ssid' . $post_match[1]]]['priority'] = $_POST['priority' . $post_match[1]]; } + $network = $tmp_networks[$_POST['ssid' . $post_match[1]]]; + $ssid = escapeshellarg('"'.$_POST['ssid' . $post_match[1]].'"'); + $psk = escapeshellarg('"'.$_POST['passphrase' . $post_match[1]].'"'); + $netid = trim(shell_exec("sudo wpa_cli -i $iface add_network")); + if (isset($netid)) { + $commands = [ + "sudo wpa_cli -i $iface set_network $netid ssid $ssid", + "sudo wpa_cli -i $iface set_network $netid psk $psk", + "sudo wpa_cli -i $iface enable_network $netid" + ]; + foreach ($commands as $cmd) { + exec($cmd); + } + } else { + $status->addMessage('Unable to add network with WPA command line client', 'warning'); + } } } From c5ffab83325bf633c9e8bddff16dbd582b96f93f Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 14:32:13 +0000 Subject: [PATCH 06/15] Revise reinitializeWPA method --- includes/wifi_functions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php index 1ef0e225..60b3fd7e 100755 --- a/includes/wifi_functions.php +++ b/includes/wifi_functions.php @@ -62,7 +62,6 @@ function nearbyWifiStations(&$networks, $cached = true) $cacheKey, function () { exec('sudo wpa_cli -i ' .$_SESSION['wifi_client_interface']. ' scan'); sleep(3); - $stdout = shell_exec('sudo wpa_cli -i ' .$_SESSION['wifi_client_interface']. ' scan_results'); return preg_split("/\n/", $stdout); } @@ -183,11 +182,12 @@ function getWifiInterface() function reinitializeWPA($force) { if ($force == true) { - $cmd = escapeshellcmd("sudo /bin/rm /var/run/wpa_supplicant/".$_SESSION['wifi_client_interface']); - $result = exec($cmd); + $cmd = "sudo /bin/rm /var/run/wpa_supplicant/".escapeshellarg($_SESSION['wifi_client_interface']); + $result = shell_exec($cmd); } - $cmd = escapeshellcmd("sudo /sbin/wpa_supplicant -B -Dnl80211,wext -c/etc/wpa_supplicant/wpa_supplicant.conf -i". $_SESSION['wifi_client_interface']); + $cmd = "sudo /sbin/wpa_supplicant -B -Dnl80211,wext -c/etc/wpa_supplicant/wpa_supplicant.conf -i".escapeshellarg($_SESSION['wifi_client_interface']); $result = shell_exec($cmd); + sleep(2); return $result; } From 1bf7a32bd5992e81ce71d81785173e132a1762a2 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 17:28:15 +0000 Subject: [PATCH 07/15] Add wpa_cli list_networks --- installers/raspap.sudoers | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 9d38ef73..d69bba44 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -10,8 +10,9 @@ www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan_results www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* reconfigure www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* add_network +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* list_networks www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i enable_network [0-9] -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i disconnect [0-9]j +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i disconnect [0-9] www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network [0-9] www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* set_network [0-9] * www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* remove_network [0-9] From e71042cb63f452307c9782156046a0e0686dfd34 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 17:32:15 +0000 Subject: [PATCH 08/15] Create setKnownStationsWPA() --- includes/wifi_functions.php | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php index 60b3fd7e..e6bcd4a5 100755 --- a/includes/wifi_functions.php +++ b/includes/wifi_functions.php @@ -235,3 +235,60 @@ function getSignalBars($rssi) return $elem; } +/* + * Parses output of wpa_cli list_networks, compares with known networks + * from wpa_supplicant, and adds with wpa_cli if not found + * + * @param array $networks + */ +function setKnownStationsWPA($networks) +{ + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + $output = shell_exec("sudo wpa_cli -i $iface list_networks"); + $lines = explode("\n", $output); + $header = array_shift($lines); + $wpaCliNetworks = []; + + foreach ($lines as $line) { + $data = explode("\t", trim($line)); + if (!empty($data) && count($data) >= 2) { + $id = $data[0]; + $ssid = $data[1]; + $item = [ + 'id' => $id, + 'ssid' => $ssid + ]; + $wpaCliNetworks[] = $item; + } + } + foreach ($networks as $network) { + $ssid = $network['ssid']; + if (!networkExists($ssid, $wpaCliNetworks)) { + $ssid = escapeshellarg('"'.$network['ssid'].'"'); + $psk = escapeshellarg('"'.$network['passphrase'].'"'); + $netid = trim(shell_exec("sudo wpa_cli -i $iface add_network")); + if (isset($netid) && !isset($known[$netid])) { + $commands = [ + "sudo wpa_cli -i $iface set_network $netid ssid $ssid", + "sudo wpa_cli -i $iface set_network $netid psk $psk", + "sudo wpa_cli -i $iface enable_network $netid" + ]; + foreach ($commands as $cmd) { + exec($cmd); + usleep(1000); + } + } + } + } +} + +function networkExists($ssid, $collection) +{ + foreach ($collection as $network) { + if ($network['ssid'] === $ssid) { + return true; + } + } + return false; +} + From 69b629a59b7974ee1744648672bc245b8be30a81 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 17:32:39 +0000 Subject: [PATCH 09/15] Update woth setKnownStationsWPA($networks) --- includes/configure_client.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/configure_client.php b/includes/configure_client.php index b66e81c6..e07d4bf8 100755 --- a/includes/configure_client.php +++ b/includes/configure_client.php @@ -13,6 +13,7 @@ function DisplayWPAConfig() getWifiInterface(); knownWifiStations($networks); + setKnownStationsWPA($networks); if (isset($_POST['connect'])) { $result = 0; From 781a292b6763933fe2a68bf9141540f61f477151 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 18:15:03 +0000 Subject: [PATCH 10/15] Replace loading-spinner.gif w/ fontawesome circle-notch icon --- app/css/all.css | 27 ++++++++++++++++++++++++--- app/img/loading-spinner.gif | Bin 2052 -> 0 bytes app/img/no-trace-200x200.png | Bin 33043 -> 0 bytes templates/configure_client.php | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) delete mode 100644 app/img/loading-spinner.gif delete mode 100644 app/img/no-trace-200x200.png diff --git a/app/css/all.css b/app/css/all.css index 6553afa5..a607fc5b 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -110,10 +110,31 @@ License: GNU General Public License v3.0 margin-bottom: 0.5em; } -.loading-spinner { - background: url("../../app/img/loading-spinner.gif") no-repeat scroll center center transparent; - min-height: 450px; +#wifiClientContent #wpaConf { + min-height: calc(100vh / 3); +} + +.loading-spinner::before { + position: absolute; + top: 0; + left: 0; width: 100%; + height: calc(100vh / 4); + display: flex; + justify-content: center; + align-items: center; + color: #858796; + content: "\f1ce"; /* Unicode for the circle-notch icon */ + font-family: "Font Awesome 5 Free"; + font-weight: 900; /* Adjust as needed */ + font-size: 54px; /* Adjust icon size as needed */ + animation: spin 1.5s linear infinite; + width: 100%; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } @media (min-width: 576px) { diff --git a/app/img/loading-spinner.gif b/app/img/loading-spinner.gif deleted file mode 100644 index f7e0378a8ca71410b4cbb6baa30de9f9e8cf150d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2052 zcmZXVc~Fzr8pcn)eEG5g1`-xo!Xn0?fJ_7$;g$xJEi8hhNLeItBLZSrM4+fi2wMb2 zWD`*XQe`Wk2(mXsP!JKuttyTLsfdkSRH~rPz1-MwrkT0t-*e`DelySWyvN(ui@BNi z6qJHRB4}%Cd-Ukh^z?Lhceh9+a&&Y=p-?6!CWC{6<>loZ4rgU$1!x@06=I+#CuECz zkSD|50gX~$S$={s7(fKJU!Q#)3*Z&Oh;L&bQ#IDJ!lj?sv;RE5p*ddFFR&d?IWU#h zypXxDB9{q#Y>iJ2*Cvg`yIh#4OJV;S-txF9b#y-CH)U&@zx~VSrLlFS(5CW+`N40@q)q=CyP!MpDrmaE3Y_HdA91@`3th@nv1o+T)JFW z-_Y3fYjev+Jc4ia#v)jK6x7ZHus))o-~i}zhOk%yMS+l^o9JQZQS2?faWeWQ>Y)=n ziXl%DQTLV3m{B=#_9p24(-HjMJ2?k%l+TNQSpy246(<&_8q|8KX0^HbFq%jo&QY=k zw1!^1OAhmJ-3##@p)*&z+hchGW zigCGMoU?xD$pGKC)>vr2m(Y$IYgp5~kDLD^yhBIJ_v0I>J1Z>KjA1wTQiy zg_qmsyVAC;9d47IobO85J)gS9gz@ z8~wMZ*J2HSRh&(c+x6uX3~EQuz@a0G__(K?$*SjLJ=Yhee|yL~-N3(6bE&A0XUep;Nn-xxjK-}hzd?aWl3Mg*dO3ortP z>IfvMBcMV#seTHq-+DFw0dIRYk3Y2I{Pr4ZysJOW&TM`4T04x6SH%GXse#RPT%A^J zz~~UCE;0a`&QWaNlQ+U@)b@cecTPbTxv(lgK7ah+O-bv2>Yjg@fzTjJa*QaJWFHgd zXXX4I0q=~Tb^l4?PBn%Z(D{b3j_#!D;{>mk2Mh%1(|<9&Mc0)ut1IBv4>@K98k}Wm3A8fE`~2^RQwI?$*R9hFpQl>`zI#AQ zPEKbZ;ZPv7DA1ml7?#Z6LO2!1Hr2q`qsCYbbk!L91tVP~cSqGZpW9O%ICrnApMvv% zFkqc~{fm1LRKS)R$BG`Y)CS9pa%kqUUPKZd1{o#l>7db}ELnIpNacXrvwHpB5=TO_)&}U@bi@fx+ z+KG1;PKRM;@zRKgI`l|wyULsMOD8}`Q!$+qgcHka91L<~to!wCOd=t?eU~KRH{}A) z-z{`x?#AXQAh`igm2(|B`#*dhAbL>VBl{xo*!05@DM-TqNmJF*!XT|H4Si`sk#0W= zM;qCZE%&@ci$&G`E+u-D&AzZdd{o*2H!>_p&?UJ8*D)zmy!EtjG2o;dQE_HSt4A5n z^AFAZ;g1yerclY0!EM(>rd;(wvAJGu{)`4A41h_a5fCKKFoy<7ksqVPNa{hX6g~(} z2{6(m9*3p@6%Sn1UYM()GSzN~WkzejRl{e>y=cP%4ylHMjf@|%w84ffS(VUwx*SW0 z4#VjjVTgZCq|5h`I!9dzyQMC!%xwjz9+k;4T_>9JQCGOBDV;V^D1q!TY;5+Tksqeo^B^Kd5DT%7Zt%320%1X1&4|y zc&A`fojP$CV{L)$pzoU=la0vBr?v6~Lt6f8R{!$z`OrkC6f8(i&`XcBi8jkUMn=a_ zbMy(A6PZaxP8u*#!1r5OVjRr2sJfY|h@K~0T6#49{G1KRCd1U1Xt1y$hJA$22l4HS z1*>PvAWutv$%bMtnZvO3WmV&zCGAX|KthcFhCZ!2QIKLJtIQDkQ&4s&c_S`%xc*5j z6dG%chj)KxjDvkmG_-dD4|O!YRwasLw56mq-$pRK)KHK48E}{QKwP zZ>MyKUMw~05~ZxsrI1KiLY`advc|!3NbWh-YS8Q~S*xXpnviAcp1#8V>v8E&r@N?Y zFJMl=Xh=gb3j9mGpUl*X>P6Y5SDj`Y!g!7-0|rwI?q7-GL&$z_{j4@)YdO;x>Q$Z4b?>c?lLd)%QJ6k|rHLHu|D=lL2i|2@I>;;l c@T5Y*-Bh$tyc#7uvPYQi1ybpdR2R7Z4?Bn+fdBvi diff --git a/app/img/no-trace-200x200.png b/app/img/no-trace-200x200.png deleted file mode 100644 index ab1b40e474451726b6b3abbccc8b25f58a2d8c6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33043 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 bPk;dcU&?OuRm`WA00000NkvXXu0mjfm`iWk diff --git a/templates/configure_client.php b/templates/configure_client.php index aeca17bd..45c44265 100755 --- a/templates/configure_client.php +++ b/templates/configure_client.php @@ -24,7 +24,7 @@ -
+
From e0751d6f90056e4f59d484b92883cc92164b667a Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Dec 2023 19:53:16 +0000 Subject: [PATCH 11/15] Use getNetworkIdBySSID() to assign index values --- includes/wifi_functions.php | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php index e6bcd4a5..c48921fa 100755 --- a/includes/wifi_functions.php +++ b/includes/wifi_functions.php @@ -9,10 +9,10 @@ function knownWifiStations(&$networks) { // Find currently configured networks exec(' sudo cat ' . RASPI_WPA_SUPPLICANT_CONFIG, $known_return); - $index = 0; + //$index = 0; foreach ($known_return as $line) { if (preg_match('/network\s*=/', $line)) { - $network = array('visible' => false, 'configured' => true, 'connected' => false, 'index' => $index); + $network = array('visible' => false, 'configured' => true, 'connected' => false, 'index' => null); ++$index; } elseif (isset($network) && $network !== null) { if (preg_match('/^\s*}\s*$/', $line)) { @@ -25,6 +25,8 @@ function knownWifiStations(&$networks) $ssid = trim($lineArr[1], '"'); $ssid = str_replace('P"','',$ssid); $network['ssid'] = $ssid; + $index = getNetworkIdBySSID($ssid); + $network['index'] = $index; break; case 'psk': $network['passkey'] = trim($lineArr[1]); @@ -246,7 +248,7 @@ function setKnownStationsWPA($networks) $iface = escapeshellarg($_SESSION['wifi_client_interface']); $output = shell_exec("sudo wpa_cli -i $iface list_networks"); $lines = explode("\n", $output); - $header = array_shift($lines); + array_shift($lines); $wpaCliNetworks = []; foreach ($lines as $line) { @@ -282,6 +284,28 @@ function setKnownStationsWPA($networks) } } +/* + * Parses wpa_cli list_networks output and returns the id + * of a corresponding network SSID + * + * @param string $ssid + * @return integer id + */ +function getNetworkIdBySSID($ssid) { + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + $cmd = "sudo wpa_cli -i $iface list_networks"; + $output = []; + exec($cmd, $output); + array_shift($output); + foreach ($output as $line) { + $columns = preg_split('/\t/', $line); + if (count($columns) >= 4 && trim($columns[1]) === trim($ssid)) { + return $columns[0]; // return network ID + } + } + return null; +} + function networkExists($ssid, $collection) { foreach ($collection as $network) { From 0a547442a403d63a3add039787bc81cd3a9a3b22 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 3 Dec 2023 07:23:27 +0000 Subject: [PATCH 12/15] Minor: animation spin tweak --- app/css/all.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/css/all.css b/app/css/all.css index a607fc5b..3a095198 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -128,7 +128,7 @@ License: GNU General Public License v3.0 font-family: "Font Awesome 5 Free"; font-weight: 900; /* Adjust as needed */ font-size: 54px; /* Adjust icon size as needed */ - animation: spin 1.5s linear infinite; + animation: spin 1.2s linear infinite; width: 100%; } From 9fe7bad7b1c3fc8d9e43c04a3d098ce253528034 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 4 Dec 2023 08:13:13 +0000 Subject: [PATCH 13/15] Update reinitializeWPA method to use wpa_cli reconfigure --- includes/wifi_functions.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php index c48921fa..2056471f 100755 --- a/includes/wifi_functions.php +++ b/includes/wifi_functions.php @@ -183,13 +183,14 @@ function getWifiInterface() */ function reinitializeWPA($force) { + $iface = escapeshellarg($_SESSION['wifi_client_interface']); if ($force == true) { - $cmd = "sudo /bin/rm /var/run/wpa_supplicant/".escapeshellarg($_SESSION['wifi_client_interface']); + $cmd = "sudo /bin/rm /var/run/wpa_supplicant/$iface"; $result = shell_exec($cmd); } - $cmd = "sudo /sbin/wpa_supplicant -B -Dnl80211,wext -c/etc/wpa_supplicant/wpa_supplicant.conf -i".escapeshellarg($_SESSION['wifi_client_interface']); + $cmd = "sudo wpa_cli -i $iface reconfigure"; $result = shell_exec($cmd); - sleep(2); + sleep(1); return $result; } From 5ca9465e8378b8060d417ca8c2fb98112c7628f1 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 4 Dec 2023 09:00:02 +0000 Subject: [PATCH 14/15] Update sudoers interface patterns --- installers/raspap.sudoers | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index d69bba44..99de3b1d 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -1,21 +1,21 @@ www-data ALL=(ALL) NOPASSWD:/sbin/ifdown www-data ALL=(ALL) NOPASSWD:/sbin/ifup www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant.conf -www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-wl*.conf +www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-[a-zA-Z0-9]*.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant-wl*.conf -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -iwl* -www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/wl* -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan_results -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* scan -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* reconfigure -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* add_network -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* list_networks +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i[a-zA-Z0-9]* +www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/[a-zA-Z0-9]* +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* scan_results +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* scan +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* reconfigure +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* add_network +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* list_networks www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i enable_network [0-9] www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i disconnect [0-9] -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* select_network [0-9] -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* set_network [0-9] * -www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i wl* remove_network [0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* select_network [0-9] +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* set_network [0-9] * +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* remove_network [0-9] www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/hostapddata /etc/hostapd/hostapd.conf www-data ALL=(ALL) NOPASSWD:/bin/systemctl start hostapd.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop hostapd.service From ce62edc6986e6e3ab19fab2d48ab5d0ad3299e65 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 4 Dec 2023 09:20:52 +0000 Subject: [PATCH 15/15] Minor: code cleanup --- includes/configure_client.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/configure_client.php b/includes/configure_client.php index e07d4bf8..d390b8c5 100755 --- a/includes/configure_client.php +++ b/includes/configure_client.php @@ -15,11 +15,13 @@ function DisplayWPAConfig() knownWifiStations($networks); setKnownStationsWPA($networks); + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + if (isset($_POST['connect'])) { - $result = 0; - $iface = escapeshellarg($_SESSION['wifi_client_interface']); $netid = intval($_POST['connect']); - $return = shell_exec('sudo wpa_cli -i ' .$iface. ' select_network ' . $netid); + $cmd = "sudo wpa_cli -i $iface select_network $netid"; + $return = shell_exec($cmd); + sleep(2); if (trim($return) == "FAIL") { $status->addMessage('WPA command line client returned failure. Check your adapter.', 'danger'); } else { @@ -31,7 +33,6 @@ function DisplayWPAConfig() $result = reinitializeWPA($force_remove); } elseif (isset($_POST['client_settings'])) { $tmp_networks = $networks; - $iface = escapeshellarg($_SESSION['wifi_client_interface']); if ($wpa_file = fopen('/tmp/wifidata', 'w')) { fwrite($wpa_file, 'ctrl_interface=DIR=' . RASPI_WPA_CTRL_INTERFACE . ' GROUP=netdev' . PHP_EOL); fwrite($wpa_file, 'update_config=1' . PHP_EOL);