From 575876406caf4c17b8ee5fdc8bee09a086dada07 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 17 Jun 2025 06:59:14 -0700 Subject: [PATCH 001/122] Update blocklistProviders, extract names from json --- includes/functions.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index f4e26eca..f1b90b9e 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -570,8 +570,13 @@ function dnsServers() function blocklistProviders() { - $data = json_decode(file_get_contents("./config/blocklists.json")); - return (array) $data; + $raw = json_decode(file_get_contents("./config/blocklists.json"), true); + $result = []; + + foreach ($raw as $group => $entries) { + $result[$group] = array_keys($entries); + } + return $result; } function optionsForSelect($options) From 2aaf1eca072390d302829c51b448a9457f5d1134 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 17 Jun 2025 07:01:17 -0700 Subject: [PATCH 002/122] Refactor ajax handler, separate config + logic --- ajax/adblock/update_blocklist.php | 77 +++++++++++++++---------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/ajax/adblock/update_blocklist.php b/ajax/adblock/update_blocklist.php index f21ed4cd..6d363930 100644 --- a/ajax/adblock/update_blocklist.php +++ b/ajax/adblock/update_blocklist.php @@ -5,50 +5,45 @@ require_once '../../includes/session.php'; require_once '../../includes/config.php'; require_once '../../includes/authenticate.php'; +define('BLOCKLISTS_FILE', __DIR__ . '/../../config/blocklists.json'); + if (isset($_POST['blocklist_id'])) { - $blocklist_id = escapeshellcmd($_POST['blocklist_id']); + $blocklist_id = $_POST['blocklist_id']; + $json = file_get_contents(BLOCKLISTS_FILE); + $allLists = json_decode($json, true); - switch ($blocklist_id) { - case "StevenBlack/hosts \(default\)": - $list_url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"; - $dest_file = "hostnames.txt"; - break; - case "badmojr/1Hosts \(Mini\)": - $list_url = "https://badmojr.github.io/1Hosts/mini/hosts.txt"; - $dest_file = "hostnames.txt"; - break; - case "badmojr/1Hosts \(Lite\)": - $list_url = "https://badmojr.github.io/1Hosts/Lite/hosts.txt"; - $dest_file = "hostnames.txt"; - break; - case "badmojr/1Hosts \(Pro\)": - $list_url = "https://badmojr.github.io/1Hosts/Pro/hosts.txt"; - $dest_file = "hostnames.txt"; - break; - case "badmojr/1Hosts \(Xtra\)": - $list_url = "https://badmojr.github.io/1Hosts/Xtra/hosts.txt"; - $dest_file = "hostnames.txt"; - break; - case "oisd/big \(default\)": - $list_url = "https://big.oisd.nl/dnsmasq"; - $dest_file = "domains.txt"; - break; - case "oisd/small": - $list_url = "https://small.oisd.nl/dnsmasq"; - $dest_file = "domains.txt"; - break; - case "oisd/nsfw": - $list_url = "https://nsfw.oisd.nl/dnsmasq"; - $dest_file = "domains.txt"; - break; + if ($allLists === null) { + echo json_encode([ + 'return' => 3, + 'output' => ['Failed to parse blocklists.json'] + ]); + exit; } - $blocklist = $list_url . $dest_file; - $dest = substr($dest_file, 0, strrpos($dest_file, ".")); + $flatList = flattenList($allLists); + + if (!isset($flatList[$blocklist_id])) { + echo json_encode(['return' => 1, 'output' => ['Invalid blocklist ID']]); + exit; + } + + $list_url = escapeshellcmd($flatList[$blocklist_id]['list_url']); + $dest_file = escapeshellcmd($flatList[$blocklist_id]['dest_file']); + $dest = pathinfo($dest_file, PATHINFO_FILENAME); + + exec("sudo /etc/raspap/adblock/update_blocklist.sh $list_url $dest_file " . RASPI_ADBLOCK_LISTPATH, $output, $return_var); + echo json_encode(['return' => $return_var, 'output' => $output, 'list' => $dest]); - exec("sudo /etc/raspap/adblock/update_blocklist.sh $list_url $dest_file " .RASPI_ADBLOCK_LISTPATH, $return); - $jsonData = ['return'=>$return,'list'=>$dest]; - echo json_encode($jsonData); } else { - $jsonData = ['return'=>2,'output'=>['Error getting data']]; - echo json_encode($jsonData); + echo json_encode(['return' => 2, 'output' => ['No blocklist ID provided']]); } + +function flattenList(array $grouped): array { + $flat = []; + foreach ($grouped as $group) { + foreach ($group as $name => $meta) { + $flat[$name] = $meta; + } + } + return $flat; +} + From e9742a52526647ea34ac2222553335e73f1072c5 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 17 Jun 2025 07:02:29 -0700 Subject: [PATCH 003/122] Enhance/update blocklists w/ nested metadata --- config/blocklists.json | 56 +++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/config/blocklists.json b/config/blocklists.json index 7138c451..1a3a5e45 100644 --- a/config/blocklists.json +++ b/config/blocklists.json @@ -1,17 +1,45 @@ { - "StevenBlack/hosts": [ - "StevenBlack/hosts (default)" - ], - "badmojr/hosts": [ - "badmojr/1Hosts (Mini)", - "badmojr/1Hosts (Lite)", - "badmojr/1Hosts (Pro)", - "badmojr/1Hosts (Xtra)" - ], - "OISD/domains": [ - "oisd/big (default)", - "oisd/small", - "oisd/nsfw" - ] + "StevenBlack/hosts": { + "StevenBlack/hosts (default)": { + "list_url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + "dest_file": "hostnames.txt" + } + }, + "HaGeZi/hosts": { + "hagezi/hosts (Light)": { + "list_url": "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/light.txt", + "dest_file": "hostnames.txt" + }, + "hagezi/hosts (Normal)": { + "list_url": "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/multi.txt", + "dest_file": "hostnames.txt" + }, + "hagezi/hosts (Pro)": { + "list_url": "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/pro.txt", + "dest_file": "hostnames.txt" + }, + "hagezi/hosts (Pro++)": { + "list_url": "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/pro.plus.txt", + "dest_file": "hostnames.txt" + }, + "hagezi/hosts (Ultimate)": { + "list_url": "https://raw.githubusercontent.com/hagezi/dns-blocklists/main/hosts/ultimate.txt", + "dest_file": "hostnames.txt" + } + }, + "OISD/domains": { + "oisd/big (default)": { + "list_url": "https://big.oisd.nl/dnsmasq", + "dest_file": "domains.txt" + }, + "oisd/small": { + "list_url": "https://small.oisd.nl/dnsmasq", + "dest_file": "domains.txt" + }, + "oisd/nsfw": { + "list_url": "https://nsfw.oisd.nl/dnsmasq", + "dest_file": "domains.txt" + } + } } From 116704c59b7cccbe8da5f569bc2baf65242c5a19 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 18 Jun 2025 00:10:46 -0700 Subject: [PATCH 004/122] Replace hardcoded path w/ $scriptPath, add file check --- ajax/adblock/update_blocklist.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ajax/adblock/update_blocklist.php b/ajax/adblock/update_blocklist.php index 6d363930..458369ab 100644 --- a/ajax/adblock/update_blocklist.php +++ b/ajax/adblock/update_blocklist.php @@ -29,9 +29,21 @@ if (isset($_POST['blocklist_id'])) { $list_url = escapeshellcmd($flatList[$blocklist_id]['list_url']); $dest_file = escapeshellcmd($flatList[$blocklist_id]['dest_file']); $dest = pathinfo($dest_file, PATHINFO_FILENAME); + $scriptPath = RASPI_CONFIG . '/adblock/update_blocklist.sh'; - exec("sudo /etc/raspap/adblock/update_blocklist.sh $list_url $dest_file " . RASPI_ADBLOCK_LISTPATH, $output, $return_var); - echo json_encode(['return' => $return_var, 'output' => $output, 'list' => $dest]); + if (!file_exists($scriptPath)) { + echo json_encode([ + 'return' => 5, + 'output' => ["Update script not found: $scriptPath"] + ]); + exit; + } + exec("sudo $scriptPath $list_url $dest_file " . RASPI_ADBLOCK_LISTPATH, $output, $return_var); + echo json_encode([ + 'return' => $return_var, + 'output' => $output, + 'list' => $dest + ]); } else { echo json_encode(['return' => 2, 'output' => ['No blocklist ID provided']]); From c0f496cf0770452f5aecff887bea3158f0c9549a Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 18 Jun 2025 00:31:47 -0700 Subject: [PATCH 005/122] Revise updateBlocklist() w/ more robust error handling --- app/js/custom.js | 72 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/app/js/custom.js b/app/js/custom.js index d95b5405..f4fc74ff 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -730,22 +730,68 @@ function setHardwareModeTooltip() { * Interface elements are updated to indicate current progress, status. */ function updateBlocklist() { - var opt = $('#cbxblocklist option:selected'); - var blocklist_id = opt.val(); - var csrfToken = $('meta[name=csrf_token]').attr('content'); - if (blocklist_id == '') { return; } - $('#cbxblocklist-status').find('i').removeClass('fas fa-check').addClass('fas fa-cog fa-spin'); - $('#cbxblocklist-status').removeClass('check-hidden').addClass('check-progress'); - $.post('ajax/adblock/update_blocklist.php',{ 'blocklist_id':blocklist_id, 'csrf_token': csrfToken},function(data){ - var jsonData = JSON.parse(data); - if (jsonData['return'] == '0') { - $('#cbxblocklist-status').find('i').removeClass('fas fa-cog fa-spin').addClass('fas fa-check'); - $('#cbxblocklist-status').removeClass('check-progress').addClass('check-updated').delay(500).animate({ opacity: 1 }, 700); - $('#blocklist-'+jsonData['list']).text("Just now"); + const opt = $('#cbxblocklist option:selected'); + const blocklist_id = opt.val(); + const csrfToken = $('meta[name=csrf_token]').attr('content'); + + if (blocklist_id === '') return; + + const statusIcon = $('#cbxblocklist-status').find('i'); + const statusWrapper = $('#cbxblocklist-status'); + + statusIcon.removeClass('fa-check fa-exclamation-triangle').addClass('fa-cog fa-spin'); + statusWrapper.removeClass('check-hidden check-error check-updated').addClass('check-progress'); + + $.post('ajax/adblock/update_blocklist.php', { + 'blocklist_id': blocklist_id, + 'csrf_token': csrfToken + }, function (data) { + let jsonData; + try { + jsonData = JSON.parse(data); + } catch (e) { + showError("Unexpected server response."); + return; } - }) + const resultCode = jsonData['return']; + const output = jsonData['output']?.join('\n') || ''; + + switch (resultCode) { + case 0: + statusIcon.removeClass('fa-cog fa-spin').addClass('fa-check'); + statusWrapper.removeClass('check-progress').addClass('check-updated').delay(500).animate({ opacity: 1 }, 700); + $('#blocklist-' + jsonData['list']).text("Just now"); + break; + case 1: + showError("Invalid blocklist."); + break; + case 2: + showError("No blocklist provided."); + break; + case 3: + showError("Could not parse blocklists.json."); + break; + case 4: + showError("blocklists.json file not found."); + break; + case 5: + showError("Update script not found."); + break; + default: + showError("Unknown error occurred."); + } + }).fail(function (jqXHR, textStatus, errorThrown) { + showError(`AJAX request failed: ${textStatus}`); + }); + + function showError(message) { + statusIcon.removeClass('fa-cog fa-spin').addClass('fa-exclamation-triangle'); + statusWrapper.removeClass('check-progress').addClass('check-error'); + alert("Blocklist update failed:\n\n" + message); + } } + function clearBlocklistStatus() { $('#cbxblocklist-status').removeClass('check-updated').addClass('check-hidden'); } From f802c825f6a67dcf93911dc2bcc7aa76f5bb96b0 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 18 Jun 2025 00:33:37 -0700 Subject: [PATCH 006/122] Minor: whitespace --- app/js/custom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/js/custom.js b/app/js/custom.js index f4fc74ff..3e8a7c58 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -791,7 +791,6 @@ function updateBlocklist() { } } - function clearBlocklistStatus() { $('#cbxblocklist-status').removeClass('check-updated').addClass('check-hidden'); } From f514f5a12ef0c34853b5370ef55d630b499f977d Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 21 Jun 2025 01:19:17 -0700 Subject: [PATCH 007/122] Sanitize user input w/ $validLocales, add detectBrowserLocale() --- includes/locale.php | 148 +++++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 90 deletions(-) diff --git a/includes/locale.php b/includes/locale.php index 9f83b5a6..17ab03e5 100755 --- a/includes/locale.php +++ b/includes/locale.php @@ -1,107 +1,43 @@ = 2) { - $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); - switch ($lang) { - case "de": - $locale = "de_DE.UTF-8"; - break; - case "fr": - $locale = "fr_FR.UTF-8"; - break; - case "it": - $locale = "it_IT.UTF-8"; - break; - case "pt": - $locale = "pt_BR.UTF-8"; - break; - case "sv": - $locale = "sv_SE.UTF-8"; - break; - case "nl": - $locale = "nl_NL.UTF-8"; - break; - case "zh": - if ($_SERVER['HTTP_ACCEPT_LANGUAGE'] == 'zh_TW') { - $locale = "zh_TW.UTF-8"; - } else { - $locale = "zh_CN.UTF-8"; - } - break; - case "cs": - $locale = "cs_CZ.UTF-8"; - break; - case "ru": - $locale = "ru_RU.UTF-8"; - break; - case "es": - $locale = "es_MX.UTF-8"; - break; - case "fi": - $locale = "fi_FI.UTF-8"; - break; - case "da": - $locale = "da_DK.UTF-8"; - break; - case "tr": - $locale = "tr_TR.UTF-8"; - break; - case "id": - $locale = "id_ID.UTF-8"; - break; - case "ko": - $locale = "ko_KR.UTF-8"; - break; - case "ja": - $locale = "ja_JP.UTF-8"; - break; - case "vi": - $locale = "vi_VN.UTF-8"; - break; - case "el": - $locale = "el_GR.UTF-8"; - break; - case "pl": - $locale = "pl_PL.UTF-8"; - break; - case "sk": - $locale = "sk_SK.UTF-8"; - break; - default: - $locale = "en_GB.UTF-8"; - break; - } - $_SESSION['locale'] = $locale; +// Set locale from POST, if provided and valid +$validLocales = array_keys(getLocales()); +if (!empty($_POST['locale']) && in_array($_POST['locale'], $validLocales, true)) { + $_SESSION['locale'] = $_POST['locale']; } -// Note: the associated locale must be installed on the RPi -// Use: 'sudo raspi-configure' and select 'Localisation Options' - -// activate the locale setting -if (!empty($_SESSION['locale'])) { - putenv("LANG=" . $_SESSION['locale']); - setlocale(LC_ALL, $_SESSION['locale']); +// Set locale from browser detection, if not already set +if (empty($_SESSION['locale'])) { + $_SESSION['locale'] = detectBrowserLocale(); } + +// Enforce only valid locale values in session +if (!in_array($_SESSION['locale'], $validLocales, true)) { + $_SESSION['locale'] = 'en_GB.UTF-8'; +} + +// Apply locale settings +putenv("LANG=" . escapeshellarg($_SESSION['locale'])); +setlocale(LC_ALL, $_SESSION['locale']); bindtextdomain(LOCALE_DOMAIN, LOCALE_ROOT); bind_textdomain_codeset(LOCALE_DOMAIN, 'UTF-8'); - textdomain(LOCALE_DOMAIN); -function getLocales() +function getLocales(): array { - $arrLocales = array( + return [ 'en_GB.UTF-8' => 'English', 'cs_CZ.UTF-8' => 'Čeština', 'zh_TW.UTF-8' => '正體中文 (Chinese traditional)', @@ -125,6 +61,38 @@ function getLocales() 'sv_SE.UTF-8' => 'Svenska', 'tr_TR.UTF-8' => 'Türkçe', 'vi_VN.UTF-8' => 'Tiếng Việt (Vietnamese)' - ); - return $arrLocales; + ]; } + +function detectBrowserLocale(): string +{ + if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) || strlen($_SERVER['HTTP_ACCEPT_LANGUAGE']) < 2) { + return 'en_GB.UTF-8'; + } + + $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); + return match ($lang) { + 'de' => 'de_DE.UTF-8', + 'fr' => 'fr_FR.UTF-8', + 'it' => 'it_IT.UTF-8', + 'pt' => 'pt_BR.UTF-8', + 'sv' => 'sv_SE.UTF-8', + 'nl' => 'nl_NL.UTF-8', + 'zh' => ($_SERVER['HTTP_ACCEPT_LANGUAGE'] === 'zh_TW') ? 'zh_TW.UTF-8' : 'zh_CN.UTF-8', + 'cs' => 'cs_CZ.UTF-8', + 'ru' => 'ru_RU.UTF-8', + 'es' => 'es_MX.UTF-8', + 'fi' => 'fi_FI.UTF-8', + 'da' => 'da_DK.UTF-8', + 'tr' => 'tr_TR.UTF-8', + 'id' => 'id_ID.UTF-8', + 'ko' => 'ko_KR.UTF-8', + 'ja' => 'ja_JP.UTF-8', + 'vi' => 'vi_VN.UTF-8', + 'el' => 'el_GR.UTF-8', + 'pl' => 'pl_PL.UTF-8', + 'sk' => 'sk_SK.UTF-8', + default => 'en_GB.UTF-8', + }; +} + From 72d7028e25317afe6de5aa2278acb811baef3bc7 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 21 Jun 2025 01:54:47 -0700 Subject: [PATCH 008/122] Fetch disk storage from system->usedDisk, create getResourceStatus() --- includes/system.php | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/includes/system.php b/includes/system.php index d37a5450..343e1aab 100755 --- a/includes/system.php +++ b/includes/system.php @@ -91,10 +91,16 @@ function DisplaySystem(&$extraFooterScripts) // memory use $memused = $system->usedMemory(); - $memStatus = getMemStatus($memused); + $memStatus = getResourceStatus($memused); $memused_status = $memStatus['status']; $memused_led = $memStatus['led']; + // disk storage use + $diskused = $system->usedDisk(); + $diskStatus = getResourceStatus($diskused); + $diskused_status = $diskStatus['status']; + $diskused_led = $diskStatus['led']; + // cpu load $cpuload = $system->systemLoadPercentage(); $cpuload_status = getCPULoadStatus($cpuload); @@ -138,6 +144,9 @@ function DisplaySystem(&$extraFooterScripts) "memused", "memused_status", "memused_led", + "diskused", + "diskused_status", + "diskused_led", "cpuload", "cpuload_status", "cputemp", @@ -150,25 +159,25 @@ function DisplaySystem(&$extraFooterScripts) )); } -function getMemStatus($memused): array +function getResourceStatus($used): array { - $memused_status = "primary"; - $memused_led = ""; + $used_status = "primary"; + $used_led = ""; - if ($memused > 90) { - $memused_status = "danger"; - $memused_led = "service-status-down"; - } elseif ($memused > 75) { - $memused_status = "warning"; - $memused_led = "service-status-warn"; - } elseif ($memused > 0) { - $memused_status = "success"; - $memused_led = "service-status-up"; + if ($used > 90) { + $used_status = "danger"; + $used_led = "service-status-down"; + } elseif ($used > 75) { + $used_status = "warning"; + $used_led = "service-status-warn"; + } elseif ($used > 0) { + $used_status = "success"; + $used_led = "service-status-up"; } return [ - 'status' => $memused_status, - 'led' => $memused_led + 'status' => $used_status, + 'led' => $used_led ]; } From 3b35f5a0c64b0bf4538fa9808a7fbdd2e1ef44b0 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 21 Jun 2025 01:55:22 -0700 Subject: [PATCH 009/122] Add usedDisk() public method --- src/RaspAP/System/Sysinfo.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/RaspAP/System/Sysinfo.php b/src/RaspAP/System/Sysinfo.php index d8391944..0e636709 100755 --- a/src/RaspAP/System/Sysinfo.php +++ b/src/RaspAP/System/Sysinfo.php @@ -46,12 +46,18 @@ class Sysinfo return $systime; } - public function usedMemory() + public function usedMemory(): int { $used = shell_exec("free -m | awk 'NR==2{ total=$2 ; used=$3 } END { print used/total*100}'"); return floor(intval($used)); } + public function usedDisk(): int + { + $output = shell_exec("df -h / | awk 'NR==2 {print $5}'"); + return intval(str_replace('%', '', trim($output))); + } + public function processorCount() { $procs = shell_exec("nproc --all"); From c222f9cd4f3ea7b8f9abdef23f23f69c49f6b6f9 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 21 Jun 2025 01:55:53 -0700 Subject: [PATCH 010/122] Add storage used progress bar to system stats --- templates/system/basic.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/system/basic.php b/templates/system/basic.php index c3d4cba0..01728528 100644 --- a/templates/system/basic.php +++ b/templates/system/basic.php @@ -40,6 +40,13 @@ include('includes/sysstats.php'); style="width: %">% +
+
+
% +
+
Date: Sat, 21 Jun 2025 01:57:41 -0700 Subject: [PATCH 011/122] Update en_US locale + compile .mo --- locale/en_US/LC_MESSAGES/messages.mo | Bin 65119 -> 65161 bytes locale/en_US/LC_MESSAGES/messages.po | 3 +++ 2 files changed, 3 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index e7a5c6f2b08284aa178717e5ebe407bda49584c4..039abaa18cf40e8b04f2d23c530aef1fe778fb23 100644 GIT binary patch delta 13110 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 delta 13070 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 W9<->@#-cwAj^227LG{MZOaBJ|K?J1$ diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index 95a970d4..70a5c706 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -1064,6 +1064,9 @@ msgstr "System Time" msgid "Memory Used" msgstr "Memory Used" +msgid "Storage Used" +msgstr "Storage Used" + msgid "CPU Load" msgstr "CPU Load" From eb53c46c336384d78336b021adea94d9257e1d67 Mon Sep 17 00:00:00 2001 From: Sam Hsu Date: Tue, 24 Jun 2025 00:27:38 -0700 Subject: [PATCH 012/122] fix: mitigate UUF vulnerability by escaping entity with escapeshellarg --- ajax/networking/get_wgkey.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ajax/networking/get_wgkey.php b/ajax/networking/get_wgkey.php index 52e3d2de..5096e574 100644 --- a/ajax/networking/get_wgkey.php +++ b/ajax/networking/get_wgkey.php @@ -5,7 +5,7 @@ require_once '../../includes/session.php'; require_once '../../includes/config.php'; require_once '../../includes/authenticate.php'; -$entity = escapeshellcmd($_POST['entity']); +$entity = escapeshellarg($_POST['entity']); if (isset($entity)) { From 58229148900ba91c6e0932d4977e39eea3b6802e Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 25 Jun 2025 01:39:44 -0700 Subject: [PATCH 013/122] Update release version --- README.md | 2 +- includes/defaults.php | 2 +- index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 849e2f38..72c43bb1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![RaspAP Custom OS images](https://github.com/user-attachments/assets/e871adf1-123c-450b-94eb-80a185c242cc) -[![Release 3.3.5](https://img.shields.io/badge/release-v3.3.5-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) +[![Release 3.3.6](https://img.shields.io/badge/release-v3.3.6-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. diff --git a/includes/defaults.php b/includes/defaults.php index 0eaa05b6..33bbc68e 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) { $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel', - 'RASPI_VERSION' => '3.3.5', + 'RASPI_VERSION' => '3.3.6', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', diff --git a/index.php b/index.php index e4884dc5..2b244391 100755 --- a/index.php +++ b/index.php @@ -14,7 +14,7 @@ * @author Lawrence Yau * @author Bill Zimmerman * @license GNU General Public License, version 3 (GPL-3.0) - * @version 3.3.5 + * @version 3.3.6 * @link https://github.com/RaspAP/raspap-webgui/ * @link https://raspap.com/ * @see http://sirlagz.net/2013/02/08/raspap-webgui/ From eb8d81e590aa8d00930709a0712370b1e12f06ce Mon Sep 17 00:00:00 2001 From: Bill Zimmerman Date: Sun, 29 Jun 2025 21:16:23 +0200 Subject: [PATCH 014/122] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72c43bb1..fab84586 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. -RaspAP has been featured by [PC World](https://www.pcwelt.de/article/1789512/raspberry-pi-als-wlan-router.html), [Adafruit](https://blog.adafruit.com/2016/06/24/raspap-wifi-configuration-portal-piday-raspberrypi-raspberry_pi/), [Raspberry Pi Weekly](https://www.raspberrypi.org/weekly/commander/), and [Awesome Raspberry Pi](https://project-awesome.org/thibmaek/awesome-raspberry-pi) and implemented in [countless projects](https://github.com/RaspAP/raspap-awesome#projects). +RaspAP has been featured by [PC World](https://www.pcwelt.de/article/1789512/raspberry-pi-als-wlan-router.html), [MSN](https://www.msn.com/en-us/news/technology/4-reasons-i-installed-raspap-on-my-raspberry-pi/ar-AA1GLHdE), [Adafruit](https://blog.adafruit.com/2016/06/24/raspap-wifi-configuration-portal-piday-raspberrypi-raspberry_pi/), [Raspberry Pi Weekly](https://www.raspberrypi.org/weekly/commander/), and [Awesome Raspberry Pi](https://project-awesome.org/thibmaek/awesome-raspberry-pi) and implemented in [countless projects](https://github.com/RaspAP/raspap-awesome#projects). We hope you enjoy using RaspAP as much as we do creating it. Tell us how you use this with [your own projects](https://github.com/raspap/raspap-awesome). From 6de96dad14e7e1c671310ede46c07cc67087dadd Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 2 Jul 2025 23:26:46 -0700 Subject: [PATCH 015/122] Update translations + compile .mo files --- locale/de_DE/LC_MESSAGES/messages.mo | Bin 35961 -> 40392 bytes locale/de_DE/LC_MESSAGES/messages.po | 1054 +++++++++++++++++++++- locale/es_MX/LC_MESSAGES/messages.mo | Bin 24484 -> 32298 bytes locale/es_MX/LC_MESSAGES/messages.po | 1205 ++++++++++++++++++++++++-- locale/ko_KR/LC_MESSAGES/messages.mo | Bin 62647 -> 76737 bytes locale/ko_KR/LC_MESSAGES/messages.po | 361 +++++++- 6 files changed, 2489 insertions(+), 131 deletions(-) diff --git a/locale/de_DE/LC_MESSAGES/messages.mo b/locale/de_DE/LC_MESSAGES/messages.mo index a88e0cb626e37d63bbcaff0037bfcb3c43970174..f1b886d51a8d7f888239db3129bdb48c3e712f7b 100644 GIT binary patch delta 15265 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 delta 10857 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 Qt0SlHij-L1T}$iy6XRCG@Bjb+ diff --git a/locale/de_DE/LC_MESSAGES/messages.po b/locale/de_DE/LC_MESSAGES/messages.po index ad46bfa4..91ad6efa 100644 --- a/locale/de_DE/LC_MESSAGES/messages.po +++ b/locale/de_DE/LC_MESSAGES/messages.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: raspap\n" "Report-Msgid-Bugs-To: Bill Zimmerman \n" "POT-Creation-Date: 2017-10-19 08:56+0000\n" -"PO-Revision-Date: 2023-06-22 05:56\n" +"PO-Revision-Date: 2025-07-03 05:45\n" "Last-Translator: Bill Zimmerman \n" "Language-Team: German\n" "Language: de_DE\n" @@ -24,8 +24,8 @@ msgstr "RaspAP Wifi Konfigurationsportal" msgid "Toggle navigation" msgstr "Navigation umschalten" -msgid "RaspAP Wifi Portal" -msgstr "RaspAP WLAN Portal" +msgid "RaspAP Admin Panel" +msgstr "" msgid "Dashboard" msgstr "Übersicht" @@ -36,11 +36,11 @@ msgstr "WLAN Client" msgid "Hotspot" msgstr "Hotspot" -msgid "Memory Use" -msgstr "Arbeitsspeichernutzung" +msgid "Mem Use" +msgstr "RAM Nutzung" -msgid "CPU Temp" -msgstr "CPU Temp" +msgid "CPU" +msgstr "CPU" msgid "Networking" msgstr "Netzwerk" @@ -97,6 +97,69 @@ msgstr "Neues Passwort" msgid "Repeat new password" msgstr "Wiederholung neues Passwort" +msgid "Please provide a valid username." +msgstr "Geben Sie einen gültigen Nutzernamen an." + +msgid "Please provide a valid password." +msgstr "Geben Sie ein gültiges Passwort an." + +msgid "Please enter your old password." +msgstr "Geben Sie Ihr altes Passwort ein." + +msgid "Please enter a new password." +msgstr "Geben Sie Ihr neues Passwort ein." + +msgid "Please re-enter your new password." +msgstr "Wiederholen Sie Ihr neues Passwort." + +msgid "Avatar" +msgstr "" + +msgid "Click or tap to upload a new user avatar." +msgstr "" + +msgid "Image files of type JPG, GIF or PNG are accepted. Max file size: 2 MB." +msgstr "" + +msgid "Reset avatar" +msgstr "" + +msgid "Enable limited privilege user" +msgstr "" + +msgid "This option enables a non-admin user who can access RaspAP's management interface, but has limited ability to modify the existing configuration. This user becomes active when the current admin user is logged-out." +msgstr "" + +msgid "Admin login failed. Please try again." +msgstr "" + +msgid "Limited privilege user mode enabled" +msgstr "" + +msgid "Failed to enable limited privilege user mode" +msgstr "" + +msgid "Logout and enable limited user mode" +msgstr "" + +msgid "Limited user login" +msgstr "" + +msgid "Limited user password" +msgstr "" + +msgid "This action will save the limited user's credentials and logout the current admin user. Save and enable limited privilege mode?" +msgstr "Dieser Vorgang wird die Anmeldedaten des beschränkten Nutzers speichern und den aktuellen Admin-Nutzer abmelden. Speichern und Begrenzten-Berechtigungsmodus aktivieren?" + +msgid "Save and logout" +msgstr "Speichern und abmelden" + +msgid "Admin credentials updated successfully" +msgstr "Admin-Anmeldedaten erfolgreich geändert" + +msgid "Limited user credentials updated successfully" +msgstr "" + #: includes/configure_client.php msgid "Client settings" msgstr "Client Einstellungen" @@ -161,8 +224,8 @@ msgstr "Hinweis: WEP access points erscheinen als 'Offen'. Rasp msgid "No Wifi stations found" msgstr "Keine WLAN-Sender gefunden" -msgid "Reinitializing wpa_supplicant" -msgstr "Reinitialisierung von wpa_supplicant" +msgid "Reinitialized wpa_supplicant. Choose Rescan." +msgstr "" msgid "Click 'Rescan' to search for nearby Wifi stations." msgstr "Auf „Rescan“ klicken, um nach WLAN-Sendern in der Nähe zu suchen." @@ -231,8 +294,8 @@ msgstr "Frequenz" msgid "Link Quality" msgstr "Verbindungsqualität" -msgid "Information provided by ip and iw and from system" -msgstr "Die Informationen, die von der IP und IW sowie vom System bereitgestellt werden" +msgid "Information provided by raspap.system" +msgstr "" msgid "No MAC Address Found" msgstr "Keine MAC Addresse gefunden" @@ -270,6 +333,9 @@ msgstr "Verbundene Geräte" msgid "Client: Ethernet cable" msgstr "Client: Ethernet-Kabel" +msgid "Current status" +msgstr "" + msgid "Ethernet" msgstr "Ethernet" @@ -282,6 +348,44 @@ msgstr "Smartphone" msgid "WiFi" msgstr "WLAN" +msgid "Repeater" +msgstr "" + +msgid "Tethering" +msgstr "" + +msgid "Cellular" +msgstr "" + +msgid "AP" +msgstr "" + +msgid "Bridged" +msgstr "" + +msgid "Adblock" +msgstr "" + +msgid "VPN" +msgstr "" + +msgid "Netmask" +msgstr "" + +msgid "5G" +msgstr "" + +msgid "2.4G" +msgstr "" + +msgid "%d WLAN %s" +msgstr "" + +msgid "client" +msgid_plural "clients" +msgstr[0] "" +msgstr[1] "" + msgid "Mobile Data Client" msgstr "Mobile-Daten-Client" @@ -321,6 +425,22 @@ msgstr "Signalstärke" msgid "No Client device or not yet configured" msgstr "Kein Clientgerät oder noch nicht konfiguriert" +msgid "No Client device found" +msgstr "" + +#: includes/footer.php +msgid "Created by the %s" +msgstr "" + +msgid "RaspAP Team" +msgstr "" + +msgid "Get Insiders" +msgstr "" + +msgid "Thanks for being an Insider" +msgstr "" + #: includes/dhcp.php msgid "DHCP server settings" msgstr "DHCP-Servereinstellungen" @@ -463,6 +583,54 @@ msgstr "Diese Option fügt dhcp-host-Einträge zur dnsmasq-Konfigur msgid "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." msgstr "Dies schaltet die Option gateway/nogateway für diese Schnittstelle in der DHCPCD-Konfiguration um." +msgid "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." +msgstr "" + +msgid "Disable wpa_supplicant dhcp hook for this interface" +msgstr "" + +msgid "If you manage wireless connections with wpa_supplicant itself, the hook may create unwanted connection events. This option disables the hook." +msgstr "" + +msgid "Please provide a valid IP Address." +msgstr "Geben Sie eine gültige IP-Adresse an." + +msgid "Please provide a valid Default gateway." +msgstr "Geben Sie ein gültiges Standard-Gateway an." + +msgid "Please provide a valid Starting IP Address." +msgstr "Geben Sie eine gültige Start-IP-Adresse an." + +msgid "Please provide a valid Ending IP Address." +msgstr "Geben Sie eine gültige End-IP-Adresse an." + +msgid "Please provide a valid Lease Time." +msgstr "Geben Sie eine gültige Vergabe-Zeit an." + +msgid "Invalid interface name." +msgstr "Ungültiger Schnittstellen-Name." + +msgid "Invalid static IP address." +msgstr "Ungültige statische IP-Adresse." + +msgid "Invalid default gateway." +msgstr "Ungültiges Standard-Gateway." + +msgid "Invalid DHCP range start." +msgstr "Ungültiger Anfang des DHCP-Bereichs." + +msgid "Invalid DHCP range end." +msgstr "Ungültiges Ende des DHCP-Bereichs." + +msgid "Invalid DHCP lease time, not a number." +msgstr "Ungültige DHCP-Vergabe-Zeit, keine Zahl." + +msgid "Unknown DHCP lease time unit." +msgstr "Unbekannte DHCP-Vergabe-Zeit Einheit." + +msgid "Invalid metric value, not a number." +msgstr "" + #: includes/hostapd.php msgid "Basic" msgstr "Basis" @@ -485,8 +653,8 @@ msgstr "Sicherheitstyp" msgid "Encryption Type" msgstr "Verschlüsselungstyp" -msgid "PSK" -msgstr "PSK" +msgid "Pre-shared key (PSK)" +msgstr "Vorher vereinbarter Schlüssel (PSK)" msgid "Advanced settings" msgstr "Erweiterte Einstellungen" @@ -521,11 +689,26 @@ msgstr "Unbekanntes interface" msgid "Country code must be blank or two characters" msgstr "Ländercode muss leer bleiben oder mit zwei Buchstaben angegeben werden" -msgid "Wifi Hotspot settings saved" -msgstr "Wifi Hotspot Einstellungen gespeichert" +msgid "DHCP configuration for %s enabled." +msgstr "DHCP-Konfiguration für %s ist aktiviert." -msgid "Unable to save wifi hotspot settings" -msgstr "WiFi Hotspot Einstellungen konnten nicht gespeichert werden" +msgid "DHCP configuration for %s added." +msgstr "DHCP-Konfiguration für %s hinzugefügt." + +msgid "DHCP configuration for %s updated." +msgstr "DHCP-Konfiguration für %s geändert." + +msgid "Interface %s has no default settings." +msgstr "Schnittstelle %s hat keine Standard-Einstellungen." + +msgid "Configure settings in DHCP Server before starting AP." +msgstr "" + +msgid "Wifi hotspot settings saved." +msgstr "WLAN-Hotspot-Einstellungen gespeichert." + +msgid "Unable to save WiFi hotspot settings." +msgstr "Konnte WLAN-Hotspot-Einstellungen nicht speichern." msgid "Start hotspot" msgstr "Hotspot starten" @@ -548,6 +731,9 @@ msgstr "AP-Modus von WiFI-Client" msgid "Bridged AP mode" msgstr "Bridge-AP-Modus" +msgid "WiFi repeater mode" +msgstr "WLAN-Verstärker Modus" + msgid "Hide SSID in broadcast" msgstr "SSID in der Übertragung ausblenden" @@ -588,31 +774,31 @@ msgid "WPA and WPA2" msgstr "WPA und WPA2" msgid "WPA2 and WPA3-Personal (transitional mode)" -msgstr "WPA2- und WPA3-Personal (Übergangsmodus)" +msgstr "WPA2 und WPA3-Personal (Übergangsmodus)" msgid "WPA3-Personal (required)" msgstr "WPA3-Personal (erforderlich)" msgid "Enabled (for supported clients)" -msgstr "Aktiviert (für unterstützte Clients)" +msgstr "Aktiviert (für unterstützte Geräte)" msgid "Required (for supported clients)" -msgstr "Erforderlich (für unterstützte Clients)" +msgstr "Erforderlich (für unterstützte Geräte)" msgid "802.11w extends strong cryptographic protection to a select set of robust management frames, including Deauthentication, Disassociation and certain categories of Action Management frames. Collectively, this is known as Management Frame Protection (MFP)." -msgstr "802.11w erweitert den Verschlüsselungsschutz auf ausgewählte robuste Management-Frames wie Deauthentifizierung, DisAssoziation und bestimmte Kategorien von Action-Management-Frames. Zusammen wird dies als „Management Frame Protection“ (MFP) bezeichnet." +msgstr "802.11w erweitert starken kryptografischen Schutz auf bestimmte robuste Management-Frames wie Deauthentifizierung, Disassoziation und bestimmte Arten von Action-Management-Frames. Insgesamt wird dies als Management-Frame-Protection (MFP) bezeichnet." msgid "Scan this QR code directly or %s %sprint a sign%s for your users." -msgstr "QR-Code direkt scannen oder %s %sein Zeichen drucken%s für die Benutzer." +msgstr "Scannen Sie diesen QR-Code direkt oder %s %sdrucken Sie ein Schild%s für Ihre Nutzer." msgid "Printable Wi-Fi sign" -msgstr "Printable Wi-Fi sign" +msgstr "Druckbares WLAN-Schild" msgid "To connect with your phone or tablet, scan the QR code above with your camera app." -msgstr "Um sich mit dem Telefon oder Tablet zu verbinden, den obigen QR-Code mit der Kamera-App scannen." +msgstr "Um Ihr Handy oder Tablet zu verbinden, scannen Sie den obigen QR-Code mit Ihrer Kamera-App." msgid "For other devices, use the login credentials below." -msgstr "Für andere Geräte die Anmeldedaten unten verwenden." +msgstr "Für andere Geräte, verwenden Sie die Anmeldedaten unten." msgid "Network" msgstr "Netzwerk" @@ -632,6 +818,24 @@ msgstr "Die ausgewählte Schnittstelle (%s) hat kein drahtloses Interface." msgid "The 802.11ac 5 GHz option is disabled until a compatible wireless regulatory domain is set." msgstr "Die 5 GHz Option ist deaktiviert, bis eine kompatible Domain gesetzt ist." +msgid "WiFi repeater mode: A metric value is already defined for DHCP." +msgstr "" + +msgid "Restart hotspot to enable WiFi repeater mode." +msgstr "Hotspot neu starten, um WLAN-Verstärker Modus zu aktivieren." + +msgid "Unable to obtain metric value for client interface. Repeater mode inactive." +msgstr "" + +msgid "Metric value configured for the %s interface." +msgstr "" + +msgid "Parameter hiddenSSID contains invalid configuration value." +msgstr "" + +msgid "Parameter hiddenSSID is not a number." +msgstr "" + #: includes/networking.php msgid "Summary" msgstr "Zusammenfassung" @@ -675,11 +879,11 @@ msgstr "Einstellungen übernehmen" msgid "Information provided by /sys/class/net" msgstr "Information aus /sys/class/net" -msgid "Network Devices" -msgstr "Netzwerkgeräte" +msgid "Devices" +msgstr "Geräte" -msgid "Mobile Data Settings" -msgstr "Einstellungen für mobile Daten" +msgid "Diagnostics" +msgstr "" msgid "Properties of network devices" msgstr "Eigenschaften der Netzwerkgeräte" @@ -732,6 +936,99 @@ msgstr "Routingtabelle" msgid "raw output" msgstr "Raw Output" +msgid "Setting wireless regulatory domain to %s" +msgstr "" + +msgid "Please provide a valid SSID." +msgstr "" + +msgid "Please provide a valid PSK." +msgstr "" + +msgid "Speedtest" +msgstr "" + +msgid "Selecting a server" +msgstr "" + +msgid "Privacy" +msgstr "Datenschutz" + +msgid "Server" +msgstr "Server" + +msgid "ms" +msgstr "" + +msgid "Mbps" +msgstr "" + +msgid "Ping" +msgstr "" + +msgid "Jitter" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "Start" +msgstr "" + +msgid "Abort" +msgstr "" + +msgid "Wireless LAN routing" +msgstr "" + +msgid "Stop WLAN routing" +msgstr "" + +msgid "Start WLAN routing" +msgstr "" + +msgid "Restart WLAN routing" +msgstr "" + +msgid "This option configures RaspAP to route network traffic from your wireless client (STA) interface to another available interface." +msgstr "" + +msgid "When an output interface is selected, iptables rules are added to route packets using network address translation (NAT). This is often done to share internet connectivity from a WLAN with devices on an eth0, usb0 or predictable enx interface." +msgstr "" + +msgid "Wireless client interface" +msgstr "" + +msgid "Output interface" +msgstr "" + +msgid "Configure a static IP address and DHCP for output interface" +msgstr "" + +msgid "Attempting to enable routing between %s and %s interfaces" +msgstr "" + +msgid "Attempting to disable routing between %s and %s interfaces" +msgstr "" + +msgid "No default DHCP configuration exists for the %s interface" +msgstr "" + +msgid "Configure a static IP and DHCP for this interface in DHCP Server settings" +msgstr "Konfigurieren Sie eine statische IP und DHCP für diese Schnittstelle in den DHCP-Server-Einstellungen" + +msgid "WLAN routing configuration saved" +msgstr "WLAN Routing-Konfiguration gespeichert" + +msgid "Unable to save WLAN routing configuration" +msgstr "Konnte WLAN Routing-Konfiguration nicht speichern" + +msgid "Successfully restarted dnsmasq" +msgstr "dnsmasq erfolgreich neu gestartet" + +msgid "Failed to restart dnsmasq" +msgstr "Neustart von dnsmasq fehlgeschlagen" + #: includes/system.php msgid "System Information" msgstr "Systeminformationen" @@ -760,6 +1057,9 @@ msgstr "Pi-Prüfung" msgid "Uptime" msgstr "Laufzeit" +msgid "System Time" +msgstr "System Zeit" + msgid "Memory Used" msgstr "Genutzter Speicher" @@ -772,6 +1072,18 @@ msgstr "Neustarten" msgid "Shutdown" msgstr "Herunterfahren" +msgid "System reboot" +msgstr "System-Neustart" + +msgid "System shutdown" +msgstr "System-Herunterfahren" + +msgid "Reboot now? The system will be temporarily unavailable." +msgstr "Jetzt neu starten? Das System wird kurzzeitig nicht verfügbar sein." + +msgid "Shutdown now? The system will be unavailable." +msgstr "Jetzt herunterfahren? Das System wird nicht verfügbar sein." + msgid "System Rebooting Now!" msgstr "System wird jetzt neugestartet!" @@ -790,7 +1102,36 @@ msgstr "Betriebssystem" msgid "Kernel" msgstr "Kernel" -#: includes/themes.php +msgid "System reset" +msgstr "System Zurüchsetzten" + +msgid "Reset RaspAP to its initial configuration? This action cannot be undone." +msgstr "RaspAP auf Standardeinstellungen zurücksetzen? Dies ist irreversibel." + +msgid "Reset complete. Restart the hotspot for the changes to take effect." +msgstr "Zurücksetzung abgeschlossen. Hotspot neu starten, um Änderungen anzuwenden." + +msgid "System reset in progress..." +msgstr "Zurücksetzung läuft..." + +msgid "Reset" +msgstr "Zurücksetzen" + +msgid "Restore settings" +msgstr "Einstellungen wiederherstellen" + +msgid "To reset RaspAP to its initial configuration, click or tap the button below." +msgstr "Um RaspAP auf seine ursprüngliche Konfiguration zurückzusetzen, drücken Sie den Knopf unten." + +msgid "Custom files for optional components such as Ad Blocking, WireGuard or OpenVPN will remain on the system." +msgstr "Benutzerdefinierte Dateien für optionale Komponenten wie Ad-Blocking, WireGuard oder OpenVPN verbleiben auf dem System." + +msgid "Perform reset" +msgstr "Zurücksetzen" + +msgid "Restores all access point (AP) service settings to their default values. This applies to hostapd, dhcpcd and dnsmasq." +msgstr "Stellt alle Access-Point (AP) Einstellungen zu ihren Standardwerten wieder her. Das betrifft hostapd, dhcpcd und dnsmasq." + msgid "Theme settings" msgstr "Oberflächendesign Einstellungen" @@ -800,6 +1141,114 @@ msgstr "Oberflächendesign auswählen" msgid "Color" msgstr "Farbe" +msgid "Enable this option for resizable, drag and drop widgets. Best for large displays." +msgstr "Aktivieren Sie diese Einstellung für Größen-verstellbare, Drag-and-Drop-Widgets. Am besten für große Bildschirme." + +msgid "Dynamic widgets" +msgstr "Dynamische WIdgets" + +msgid "Tools" +msgstr "Werkzeuge" + +msgid "System tools" +msgstr "Systemwerkzeuge" + +msgid "To generate a system debug log, click or tap the button below." +msgstr "" + +msgid "Debug log information contains the RaspAP version, current state and configuration of AP related services, installed system packages, Linux kernel version and networking details. No passwords or other sensitive data are included." +msgstr "" + +msgid "Generate debug log" +msgstr "" + +msgid "Debug log generation in progress..." +msgstr "" + +msgid "Diagnostic log size limit (KB)" +msgstr "" + +msgid "Changing log limit size to %s KB" +msgstr "" + +msgid "Information provided by raspap.sysinfo" +msgstr "" + +msgid "The following user plugins are available to extend RaspAP's functionality." +msgstr "" + +msgid "Choose Details for more information and to install a plugin." +msgstr "" + +msgid "Network error" +msgstr "" + +msgid "Unable to load plugins" +msgstr "" + +msgid "Reload" +msgstr "" + +msgid "and try again" +msgstr "" + +msgid "Plugins" +msgstr "" + +msgid "Plugin details" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Plugin source" +msgstr "" + +msgid "Author" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Language locale" +msgstr "" + +msgid "Configuration files" +msgstr "" + +msgid "Dependencies" +msgstr "" + +msgid "Permissions" +msgstr "" + +msgid "Non-privileged users" +msgstr "" + +msgid "Install now" +msgstr "" + +msgid "Installing plugin" +msgstr "" + +msgid "Plugin installation in progress..." +msgstr "" + +msgid "Plugin install completed." +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Installed" +msgstr "" + #: includes/data_usage.php msgid "Data usage" msgstr "Datennutzung" @@ -905,7 +1354,7 @@ msgid "Currently available OpenVPN client configurations are displayed below." msgstr "Derzeit verfügbare OpenVPN-Client-Konfigurationen werden unten angezeigt." msgid "Activating a configuration will restart the openvpn-client service." -msgstr "Die Aktivierung einer Konfiguration startet den Dienst openvpn-client neu." +msgstr "" msgid "Delete OpenVPN client" msgstr "OpenVPN-Client löschen" @@ -1102,14 +1551,14 @@ msgstr "WireGuard Konfiguration hochladen" msgid "This option uploads and installs an existing WireGuard .conf file on this device." msgstr "Diese Option lädt und installiert eine vorhandene WireGuard .conf-Datei auf dieses Gerät." -msgid "Apply iptables rules for AP interface" -msgstr "iptables-Regeln auf AP-Schnittstelle anwenden" +msgid "Apply iptables rules to the selected interface" +msgstr "" -msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on the AP interface." -msgstr "Empfohlen, wenn der Netzwerkverkehr von der wg0-Schnittstelle an Clients weitergeleitet werden soll, die mit der AP-Schnittstelle verbunden sind." +msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on a desired interface. The active AP interface is the default." +msgstr "" -msgid "This option adds iptables Postup and PostDown rules for the configured AP interface (%s)." -msgstr "Diese Option fügt iptables-Regeln Postup und PostDown für die konfigurierte AP-Schnittstelle (%s) hinzu." +msgid "This option adds iptables Postup and PostDown rules for the interface selected below." +msgstr "" msgid "Select WireGuard configuration file (.conf)" msgstr "WireGuard-Konfigurationsdatei auswählen (.conf)" @@ -1198,6 +1647,27 @@ msgstr "WireGuard-Konfiguration erfolgreich aktualisiert" msgid "WireGuard configuration failed to be updated" msgstr "WireGuard-Konfiguration konnte nicht aktualisiert werden" +msgid "Enable kill switch" +msgstr "" + +msgid "This option adds iptables PostUp and PreDown rules for the configured interface." +msgstr "" + +msgid "Recommended if you wish to prevent the flow of unencrypted packets through non-WireGuard interfaces." +msgstr "" + +msgid "iptables rules added to WireGuard configuration" +msgstr "" + +msgid "Existing iptables rules found in WireGuard configuration - not added" +msgstr "" + +msgid "Currently available WireGuard file configurations are displayed below." +msgstr "" + +msgid "Activating a configuration will restart the wg-quick service." +msgstr "" + msgid "Client Firewall" msgstr "Client-Firewall" @@ -1264,6 +1734,516 @@ msgstr "Firewall deaktivieren" msgid "Enable Firewall" msgstr "Firewall aktivieren" +msgid "Changing the firewall status may disrupt or allow incoming traffic. Choose Proceed to continue." +msgstr "" + +msgid "Proceed" +msgstr "" + msgid "Apply changes" msgstr "Änderungen übernehmen" +msgid "Dynamic DNS" +msgstr "" + +msgid "Service provider" +msgstr "" + +msgid "Select a Dynamic DNS service supported by ddclient from the list below. Selecting a known service provider will populate the protocol and server fields. You may also configure the service manually." +msgstr "" + +msgid "Method to obtain IP" +msgstr "" + +msgid "Select the method used by ddclient to obtain an IP address. This value is specified in the -use option." +msgstr "" + +msgid "Discovery page on the web" +msgstr "" + +msgid "Network interface" +msgstr "" + +msgid "Network address" +msgstr "" + +msgid "Firewall status page" +msgstr "" + +msgid "External command" +msgstr "" + +msgid "Web address" +msgstr "" + +msgid "Firewall" +msgstr "" + +msgid "Command" +msgstr "" + +msgid "Example: 192.168.1.254/status.htm." +msgstr "" + +msgid "Example: /usr/local/bin/get-ip." +msgstr "" + +msgid "Domain" +msgstr "" + +msgid "Enable SSL" +msgstr "" + +msgid "Use an encrypted SSL connection for updates. Not supported by all providers." +msgstr "" + +msgid "Value specified in milliseconds (ms). Default is 300." +msgstr "" + +msgid "Use the Generate log button to output detailed ddclient daemon debug info" +msgstr "" + +msgid "Generate log" +msgstr "" + +msgid "Information provided by ddclient" +msgstr "" + +msgid "Start Dynamic DNS" +msgstr "" + +msgid "Stop Dynamic DNS" +msgstr "" + +msgid "Restart Dynamic DNS" +msgstr "" + +msgid "Account details" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Server location" +msgstr "" + +msgid "Choosing Save settings will connect to the selected country." +msgstr "" + +msgid "Choosing Connect %s will connect to a recommended server." +msgstr "" + +msgid "Select a country from the server location list" +msgstr "" + +msgid "Select a country..." +msgstr "" + +msgid "Account information not available from %s's Linux CLI." +msgstr "" + +msgid "Attempting to connect to %s" +msgstr "Versuche mit %s zu verbinden" + +msgid "Attempting to connect VPN provider" +msgstr "Versuche VPN-Anbieter zu verbinden" + +msgid "Attempting to disconnect VPN provider" +msgstr "Versuche VPN-Anbieter zu trennen" + +msgid "Expected %s binary not found at: %s" +msgstr "" + +msgid "Visit the installation instructions for %s's Linux CLI." +msgstr "" + +msgid "Unable to execute %s binary found at: %s" +msgstr "" + +msgid "Check that binary is executable and permissions exist in raspap.sudoers" +msgstr "" + +msgid "Installed Linux CLI: %s" +msgstr "" + +msgid "Current %s connection status is displayed below." +msgstr "" + +msgid "Information provided by %s" +msgstr "" + +msgid "Connect %s" +msgstr "" + +msgid "Disconnect %s" +msgstr "" + +msgid "About" +msgstr "" + +msgid "Insiders" +msgstr "" + +msgid "Contributing" +msgstr "" + +msgid "Check for update" +msgstr "" + +msgid "New release check in progress..." +msgstr "" + +msgid "A new release is available: Version" +msgstr "" + +msgid "Installed version is the latest release." +msgstr "" + +msgid "GitHub authentication" +msgstr "" + +msgid "Updating Insiders requires GitHub authentication." +msgstr "" + +msgid "Your credentials will be sent to GitHub securely with SSL. However, use caution if your RaspAP install is on a WLAN shared by untrusted users." +msgstr "" + +msgid "Personal Access Token" +msgstr "" + +msgid "Please provide a valid token." +msgstr "" + +msgid "Perform update" +msgstr "" + +msgid "Update in progress" +msgstr "" + +msgid "Application is being updated..." +msgstr "" + +msgid "Configuring update" +msgstr "" + +msgid "Updating sources" +msgstr "" + +msgid "Installing package updates" +msgstr "" + +msgid "Downloading latest files" +msgstr "" + +msgid "Installing application" +msgstr "" + +msgid "Update complete" +msgstr "" + +msgid "An error occurred. Check the log at /tmp/raspap_install.log" +msgstr "" + +msgid "RaspAP Exception" +msgstr "" + +msgid "An exception occurred" +msgstr "" + +msgid "RestAPI" +msgstr "" + +msgid "RestAPI settings" +msgstr "" + +msgid "Start RestAPI service" +msgstr "" + +msgid "Stop RestAPI service" +msgstr "" + +msgid "API Key" +msgstr "" + +msgid "Saving API key" +msgstr "" + +msgid "RestAPI status" +msgstr "" + +msgid "Current restapi.service status is displayed below." +msgstr "" + +msgid "RestAPI docs are accessible here%s" +msgstr "" + +msgid "Restarting restapi.service" +msgstr "" + +msgid "Information provided by restapi.service" +msgstr "" + +msgid "Session Expired" +msgstr "" + +msgid "Your session has expired. Please login to continue." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "Administrator login" +msgstr "" + +msgid "Forgot password" +msgstr "" + +msgid "Login failed" +msgstr "" + +msgid "NTP Server" +msgstr "" + +msgid "NTP Server settings" +msgstr "" + +msgid "NTP daemon" +msgstr "" + +msgid "Synchronized time" +msgstr "" + +msgid "NTP servers" +msgstr "" + +msgid "Add an NTP server" +msgstr "" + +msgid "Start NTP service" +msgstr "" + +msgid "Stop NTP service" +msgstr "" + +msgid "Edit mode" +msgstr "" + +msgid "Use the Edit mode toggle to manually edit the current ntp.config configuration." +msgstr "" + +msgid "Specify a public NTP server or a private one on your local network. IPv4 and IPv6 address, or a fully qualified domain name (FQDN) are acceptable values." +msgstr "" + +msgid "Public NTP servers supporting Network Time Security (NTS) may be specified with the nts suffix." +msgstr "" + +msgid "Examples of valid server entries include %s, %s and %s." +msgstr "" + +msgid "Current ntpq peer status is displayed below. An asterisk (*) indicates the preferred server." +msgstr "" + +msgid "NTP configuration cannot be empty" +msgstr "" + +msgid "Restarting ntpd.service" +msgstr "" + +msgid "Please enter a valid NTP server" +msgstr "" + +msgid "Attempting to start ntp.service" +msgstr "" + +msgid "Attempting to stop ntp.service" +msgstr "" + +msgid "NTP configuration not found at %s" +msgstr "" + +msgid "NTP configuration updated" +msgstr "" + +msgid "Advertising device as a Tailscale exit node" +msgstr "" + +msgid "Attempting to optimize UDP throughput" +msgstr "" + +msgid "Kernel transport layer offloads enabled for UDP" +msgstr "" + +msgid "Failed to enable kernel transport layer offloads for UDP" +msgstr "" + +msgid "Attempting to set tailscale up" +msgstr "" + +msgid "Attempting to set tailscale down" +msgstr "" + +msgid "Attempting to disconnect from tailscale" +msgstr "" + +msgid "Disconnected from tailscale and expired node key" +msgstr "" + +msgid "Unable to disconnect from tailscale" +msgstr "" + +msgid "Expected tailscale binary not found at: %" +msgstr "" + +msgid "A Tailscale VPN exit node extension for RaspAP" +msgstr "" + +msgid "Unable to retrieve Tailscale login. Choose %s to continue." +msgstr "" + +msgid "Start Tailscale" +msgstr "" + +msgid "Stop Tailscale" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Device approved and activated as a Tailscale exit node" +msgstr "" + +msgid "Not connected: Login required." +msgstr "" + +msgid "Tailscale VPN" +msgstr "" + +msgid "Exit node activated" +msgstr "" + +msgid "The device %s is connected with the address %s and offers an exit node." +msgstr "" + +msgid "See the %s on how to use this exit node with your devices." +msgstr "" + +msgid "Allow exit node" +msgstr "" + +msgid "The device %s is pending approval as an exit node." +msgstr "" + +msgid "Locate the %s Exit Node badge in the machines list." +msgstr "" + +msgid "Open Tailscale Machines" +msgstr "" + +msgid "To allow this device as an exit node, choose Open Tailscale Machines." +msgstr "" + +msgid "From the %s icon menu of the exit node, open the %s panel." +msgstr "" + +msgid "Edit route settings" +msgstr "" + +msgid "Login to Tailscale" +msgstr "" + +msgid "To connect device %s to your tailnet, choose %s." +msgstr "" + +msgid "After logging in, choose Next to continue." +msgstr "" + +msgid "Configure exit node" +msgstr "" + +msgid "The device %s is connected to your tailnet with the address %s." +msgstr "" + +msgid "By default, Tailscale only routes traffic between the devices on which it's been installed. You can also route all your public internet traffic by configuring a device on your network as a exit node" +msgstr "" + +msgid "When you route all traffic through an exit node, you're effectively using default routes (0.0.0.0/0, ::/0), similar to how you would if you were using a typical VPN." +msgstr "" + +msgid "You have the option of configuring this device as an exit node, or using another exit node in your tailnet." +msgstr "" + +msgid "Select an existing exit node on your tailnet" +msgstr "" + +msgid "This is a typical configuration if you're using this device as a VPN travel router, for example." +msgstr "" + +msgid "Configure this device as a new exit node" +msgstr "" + +msgid "By configuring this device as an exit node, public internet traffic from devices connected in your tailnet will be routed through it." +msgstr "" + +msgid "For security reasons, you must opt in to enable exit node functionality. The first step is to advertise %s as an exit node in your tailnet. In the next step, you'll allow this device to be an exit node." +msgstr "" + +msgid "Advertise %s as an exit node" +msgstr "" + +msgid "This effectively configures Tailscale as a VPN to mask your real location, access region-restricted content, or enhance privacy when connecting from untrusted networks." +msgstr "" + +msgid "This option lets Tailscale know your device is ready to route traffic." +msgstr "" + +msgid "Recommended for Tailscale exit nodes with Linux 6.2 or later kernels, this uses UDP generic receive offload (GRO) forwarding to reduce CPU overhead." +msgstr "" + +msgid "This option enables transport layer offloads for better performance." +msgstr "" + +msgid "Select an exit node" +msgstr "" + +msgid "To use %s as a VPN gateway, configure Tailscale to use an exit node. Tailscale's suggested node is indicated with a star." +msgstr "" + +msgid "Advertise a subnet route for the active %s AP interface" +msgstr "" + +msgid "Subnet routes let you extend your Tailscale network (known as a tailnet) to include devices that don't or can't run the Tailscale client." +msgstr "" + +msgid "A subnet route acts as a gateway between your tailnet and a physical subnet. The subnet of the active AP interface is preconfigured below; edit if necessary." +msgstr "" + +msgid "Route LAN traffic through the exit node." +msgstr "" + +msgid "This will direct all LAN traffic to go through your exit node only." +msgstr "" + +msgid "Choose Next to configure %s to use the selected exit node." +msgstr "" + +msgid "No exit nodes found on your tailnet. Choose Back to continue." +msgstr "" + +msgid "Using exit node" +msgstr "" + +msgid "The device %s is configured to use exit node %s. It has the Tailscale MagicDNS address %s." +msgstr "" + +msgid "Choose Save settings to continue." +msgstr "" + +msgid "Choose Next to continue." +msgstr "" + +msgid "Tailnet status" +msgstr "" + +msgid "Current tailnet status is displayed below." +msgstr "" + diff --git a/locale/es_MX/LC_MESSAGES/messages.mo b/locale/es_MX/LC_MESSAGES/messages.mo index c2fca8c3db39dec452f8d0637a20a9a597967c88..6509e0ede1becf31a6089153f373504270dd1755 100644 GIT binary patch literal 32298 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 G3jQB4UtmT6 delta 7726 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 DFgjCz diff --git a/locale/es_MX/LC_MESSAGES/messages.po b/locale/es_MX/LC_MESSAGES/messages.po index 502296f0..8dbf2e01 100644 --- a/locale/es_MX/LC_MESSAGES/messages.po +++ b/locale/es_MX/LC_MESSAGES/messages.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: raspap\n" "Report-Msgid-Bugs-To: Bill Zimmerman \n" "POT-Creation-Date: 2017-10-19 08:56+0000\n" -"PO-Revision-Date: 2022-01-05 11:43\n" +"PO-Revision-Date: 2025-07-03 05:44\n" "Last-Translator: Bill Zimmerman \n" "Language-Team: Spanish\n" "Language: es_ES\n" @@ -24,8 +24,8 @@ msgstr "Portal de cofiguracion de RaspAP wifi" msgid "Toggle navigation" msgstr "Interrutor de navegacion" -msgid "RaspAP Wifi Portal" -msgstr "Portal de RaspAP wifi" +msgid "RaspAP Admin Panel" +msgstr "" msgid "Dashboard" msgstr "Tablero" @@ -36,11 +36,11 @@ msgstr "Cliente wiFi" msgid "Hotspot" msgstr "Punto de acceso" -msgid "Memory Use" -msgstr "Uso de memoria" +msgid "Mem Use" +msgstr "Uso de mem" -msgid "CPU Temp" -msgstr "Temp de CPU" +msgid "CPU" +msgstr "CPU" msgid "Networking" msgstr "Red de trabajo" @@ -97,6 +97,69 @@ msgstr "Nueva contraseña" msgid "Repeat new password" msgstr "Repetir nueva contraseña" +msgid "Please provide a valid username." +msgstr "Proporcione un nombre de usuario válido." + +msgid "Please provide a valid password." +msgstr "" + +msgid "Please enter your old password." +msgstr "Por favor, introduzca tu contraseña antigua." + +msgid "Please enter a new password." +msgstr "Por favor, introduzca una contraseña nueva." + +msgid "Please re-enter your new password." +msgstr "Por favor vuelva a introducir tu nueva contraseña." + +msgid "Avatar" +msgstr "" + +msgid "Click or tap to upload a new user avatar." +msgstr "" + +msgid "Image files of type JPG, GIF or PNG are accepted. Max file size: 2 MB." +msgstr "" + +msgid "Reset avatar" +msgstr "" + +msgid "Enable limited privilege user" +msgstr "" + +msgid "This option enables a non-admin user who can access RaspAP's management interface, but has limited ability to modify the existing configuration. This user becomes active when the current admin user is logged-out." +msgstr "" + +msgid "Admin login failed. Please try again." +msgstr "" + +msgid "Limited privilege user mode enabled" +msgstr "" + +msgid "Failed to enable limited privilege user mode" +msgstr "" + +msgid "Logout and enable limited user mode" +msgstr "" + +msgid "Limited user login" +msgstr "" + +msgid "Limited user password" +msgstr "" + +msgid "This action will save the limited user's credentials and logout the current admin user. Save and enable limited privilege mode?" +msgstr "" + +msgid "Save and logout" +msgstr "" + +msgid "Admin credentials updated successfully" +msgstr "" + +msgid "Limited user credentials updated successfully" +msgstr "" + #: includes/configure_client.php msgid "Client settings" msgstr "Configuracion de Cliente" @@ -147,31 +210,31 @@ msgid "Not configured" msgstr "No configurado" msgid "Connected" -msgstr "" +msgstr "Conectado" msgid "Known" -msgstr "" +msgstr "Conocidas" msgid "Nearby" -msgstr "" +msgstr "Cercanas" msgid "Note: WEP access points appear as 'Open'. RaspAP does not currently support connecting to WEP" msgstr "Nota: Punto de acceso WEP aparece como 'Abierto'. RaspAP actualmente no soporta conecciones WEP" msgid "No Wifi stations found" -msgstr "" +msgstr "No se encontró ninguna WiFi" -msgid "Reinitializing wpa_supplicant" +msgid "Reinitialized wpa_supplicant. Choose Rescan." msgstr "" msgid "Click 'Rescan' to search for nearby Wifi stations." -msgstr "" +msgstr "Pulse 'Reescanear' para buscar WiFi cercanas." msgid "Click 'Reinitialize' to force reinitialize wpa_supplicant." -msgstr "" +msgstr "Haga clic en 'Reinicializar' para forzar la reinicialización de wpa_supplicant." msgid "Reinitialize" -msgstr "" +msgstr "¿Reiniciar?" #: includes/dashboard.php msgid "Interface Information" @@ -231,8 +294,8 @@ msgstr "Frecuencia" msgid "Link Quality" msgstr "Calidad del Enlace" -msgid "Information provided by ip and iw and from system" -msgstr "Informacion obtenida de ip y iw y del system" +msgid "Information provided by raspap.system" +msgstr "" msgid "No MAC Address Found" msgstr "Dirección MAC no encontrada" @@ -268,57 +331,114 @@ msgid "Connected Devices" msgstr "Dispositivos conectados" msgid "Client: Ethernet cable" +msgstr "Cliente: Cable Ethernet" + +msgid "Current status" msgstr "" msgid "Ethernet" -msgstr "" +msgstr "Ethernet" msgid "Client: Smartphone (USB tethering)" -msgstr "" +msgstr "Cliente: Smartphone (USB tethering)" msgid "Smartphone" -msgstr "" +msgstr "Smartphone" msgid "WiFi" +msgstr "Wi-Fi" + +msgid "Repeater" msgstr "" +msgid "Tethering" +msgstr "" + +msgid "Cellular" +msgstr "" + +msgid "AP" +msgstr "" + +msgid "Bridged" +msgstr "" + +msgid "Adblock" +msgstr "" + +msgid "VPN" +msgstr "" + +msgid "Netmask" +msgstr "" + +msgid "5G" +msgstr "" + +msgid "2.4G" +msgstr "" + +msgid "%d WLAN %s" +msgstr "" + +msgid "client" +msgid_plural "clients" +msgstr[0] "" +msgstr[1] "" + msgid "Mobile Data Client" -msgstr "" +msgstr "Límite de datos móviles" msgid "Mobile Data" -msgstr "" +msgstr "Datos móviles" msgid "No information available" -msgstr "" +msgstr "No hay informaci—n disponible" msgid "Interface name invalid" -msgstr "" +msgstr "Nombre de interfaz inválido" msgid "Required exec function is disabled. Check if exec is not added to php disable_functions." -msgstr "" +msgstr "La función de exec necesaria está deshabilitada. Compruebe si exec no se agrega a php disable_functions." msgid "Waiting for the interface to start ..." -msgstr "" +msgstr "Esperando a que la interfaz se inicie ..." msgid "Stop the Interface" -msgstr "" +msgstr "Detener la interfaz" msgid "Connection mode" -msgstr "" +msgstr "Modo de conexión" msgid "Signal quality" -msgstr "" +msgstr "Calidad de señal" msgid "WAN IP" -msgstr "" +msgstr "WAN IP" msgid "Web-GUI" -msgstr "" +msgstr "Web-GUI" msgid "Signal strength" -msgstr "" +msgstr "Intensidad de la señal" msgid "No Client device or not yet configured" +msgstr "El dispositivo no se ha configurado aún." + +msgid "No Client device found" +msgstr "" + +#: includes/footer.php +msgid "Created by the %s" +msgstr "" + +msgid "RaspAP Team" +msgstr "" + +msgid "Get Insiders" +msgstr "" + +msgid "Thanks for being an Insider" msgstr "" #: includes/dhcp.php @@ -368,7 +488,7 @@ msgid "MAC Address" msgstr "Dirección MAC" msgid "Optional comment" -msgstr "" +msgstr "Comentario opcional" msgid "Host name" msgstr "Nombre de Host" @@ -433,8 +553,8 @@ msgstr "Formato" msgid "Choose a hosted server" msgstr "Elija un servidor alojado" -msgid "Enable these options to log DHCP server activity." -msgstr "Habilitar estas opciones para registrar la actividad del servidor DHCP." +msgid "Enable these options to log dhcpcd and dnsmasq activity." +msgstr "Habilita estas opciones para registrar dhcpcd y dnsmasq." msgid "Log DHCP requests" msgstr "Registrar peticiones DHCP" @@ -463,6 +583,54 @@ msgstr "Esta opción añade entradas dhcp-host a la configuración msgid "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." msgstr "Esto activa la opción gateway/nogateway para esta interfaz en la configuración DHCPCD." +msgid "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." +msgstr "Esto activa la opción nohook wpa_supplicant para esta interfaz en la configuración DHCPCD." + +msgid "Disable wpa_supplicant dhcp hook for this interface" +msgstr "Desactivar wpa_supplicant dhcp hook para esta interfaz" + +msgid "If you manage wireless connections with wpa_supplicant itself, the hook may create unwanted connection events. This option disables the hook." +msgstr "Si administras conexiones inalámbricas con wpa_supplicant en sí, el gancho puede crear eventos de conexión no deseados. Esta opción desactiva el gancho." + +msgid "Please provide a valid IP Address." +msgstr "Por favor proporcione una dirección IP válida." + +msgid "Please provide a valid Default gateway." +msgstr "Por favor, proporcione una pasarela por defecto válida." + +msgid "Please provide a valid Starting IP Address." +msgstr "Por favor, proporcione una dirección IP de inicio válida." + +msgid "Please provide a valid Ending IP Address." +msgstr "Por favor, proporcione una dirección IP de finalización válida." + +msgid "Please provide a valid Lease Time." +msgstr "Por favor, proporcione un tiempo de concesión válida." + +msgid "Invalid interface name." +msgstr "Nombre de interfaz inválido." + +msgid "Invalid static IP address." +msgstr "Dirección IP estática inválida." + +msgid "Invalid default gateway." +msgstr "Pasarela predeterminada inválida." + +msgid "Invalid DHCP range start." +msgstr "" + +msgid "Invalid DHCP range end." +msgstr "Fin de rango DHCP inválido." + +msgid "Invalid DHCP lease time, not a number." +msgstr "" + +msgid "Unknown DHCP lease time unit." +msgstr "" + +msgid "Invalid metric value, not a number." +msgstr "" + #: includes/hostapd.php msgid "Basic" msgstr "Basico" @@ -485,8 +653,8 @@ msgstr "Tipos de Seguridad" msgid "Encryption Type" msgstr "Tipo de Encriptacion" -msgid "PSK" -msgstr "PSK" +msgid "Pre-shared key (PSK)" +msgstr "" msgid "Advanced settings" msgstr "Configuracion Avanzada" @@ -521,11 +689,26 @@ msgstr "Interfaz desconocida" msgid "Country code must be blank or two characters" msgstr "Codido de pais debe quedar en blanco o tener 2 caracteres" -msgid "Wifi Hotspot settings saved" -msgstr "Configuracion del hotspot wifi guardada" +msgid "DHCP configuration for %s enabled." +msgstr "" -msgid "Unable to save wifi hotspot settings" -msgstr "No fue posible guardar la configuracion del hotspot wifi" +msgid "DHCP configuration for %s added." +msgstr "" + +msgid "DHCP configuration for %s updated." +msgstr "" + +msgid "Interface %s has no default settings." +msgstr "" + +msgid "Configure settings in DHCP Server before starting AP." +msgstr "" + +msgid "Wifi hotspot settings saved." +msgstr "" + +msgid "Unable to save WiFi hotspot settings." +msgstr "" msgid "Start hotspot" msgstr "Iniciar hotspot" @@ -548,6 +731,9 @@ msgstr "Modo AP de WiFi de cliente" msgid "Bridged AP mode" msgstr "Modo AP Bridged" +msgid "WiFi repeater mode" +msgstr "" + msgid "Hide SSID in broadcast" msgstr "Ocultar SSID en la transmisión" @@ -555,7 +741,7 @@ msgid "Maximum number of clients" msgstr "Máximo número de clientes" msgid "Configures the max_num_sta option of hostapd. The default and maximum is 2007. If empty or 0, the default applies." -msgstr "" +msgstr "Configura la opción max_num_sta de hostapd. El valor predeterminado y máximo es 2007. Si está vacío o 0, el valor por defecto se aplica." msgid "Beacon interval" msgstr "Intervalo de beacons" @@ -576,12 +762,78 @@ msgid "Enable this option to log hostapd activity." msgstr "Habilitar esta opción para registrar la actividad de hostapd." msgid "Transmit power (dBm)" -msgstr "" +msgstr "Potencia de transmisión" msgid "Sets the txpower option for the AP interface and the configured country." -msgstr "" +msgstr "Establece la opción txpower para la interfaz AP y el país configurado." msgid "dBm is a unit of level used to indicate that a power ratio is expressed in decibels (dB) with reference to one milliwatt (mW). 30 dBm is equal to 1000 mW, while 0 dBm equals 1.25 mW." +msgstr "dBm es una unidad de nivel utilizada para indicar que una proporción de potencia se expresa en decibelios (dB) con referencia a un millwatio (mW). 30 dBm es igual a 1000 mW, mientras que 0 dBm es igual a 1,25 mW." + +msgid "WPA and WPA2" +msgstr "" + +msgid "WPA2 and WPA3-Personal (transitional mode)" +msgstr "" + +msgid "WPA3-Personal (required)" +msgstr "" + +msgid "Enabled (for supported clients)" +msgstr "" + +msgid "Required (for supported clients)" +msgstr "" + +msgid "802.11w extends strong cryptographic protection to a select set of robust management frames, including Deauthentication, Disassociation and certain categories of Action Management frames. Collectively, this is known as Management Frame Protection (MFP)." +msgstr "" + +msgid "Scan this QR code directly or %s %sprint a sign%s for your users." +msgstr "" + +msgid "Printable Wi-Fi sign" +msgstr "" + +msgid "To connect with your phone or tablet, scan the QR code above with your camera app." +msgstr "" + +msgid "For other devices, use the login credentials below." +msgstr "" + +msgid "Network" +msgstr "" + +msgid "The selected interface (%s) has support for the 2.4 GHz wireless band only." +msgstr "" + +msgid "The selected interface (%s) has support for the 2.5 GHz wireless band only." +msgstr "" + +msgid "The selected interface (%s) has support for both the 2.4 and 5 GHz wireless bands." +msgstr "" + +msgid "The selected interface (%s) does not support wireless mode operation." +msgstr "" + +msgid "The 802.11ac 5 GHz option is disabled until a compatible wireless regulatory domain is set." +msgstr "" + +msgid "WiFi repeater mode: A metric value is already defined for DHCP." +msgstr "" + +msgid "Restart hotspot to enable WiFi repeater mode." +msgstr "" + +msgid "Unable to obtain metric value for client interface. Repeater mode inactive." +msgstr "" + +msgid "Metric value configured for the %s interface." +msgstr "" + +msgid "Parameter hiddenSSID contains invalid configuration value." +msgstr "" + +msgid "Parameter hiddenSSID is not a number." msgstr "" #: includes/networking.php @@ -627,55 +879,154 @@ msgstr "Aplicar configuraciones" msgid "Information provided by /sys/class/net" msgstr "Información ofrecida por /sys/class/net" -msgid "Network Devices" +msgid "Devices" msgstr "" -msgid "Mobile Data Settings" +msgid "Diagnostics" msgstr "" msgid "Properties of network devices" -msgstr "" +msgstr "Propiedades de los dispositivos de red" msgid "Device" -msgstr "" +msgstr "Dispositivo" msgid "MAC" -msgstr "" +msgstr "MAC" msgid "USB vid/pid" -msgstr "" +msgstr "USB vid/pid" msgid "Device type" -msgstr "" +msgstr "Tipo de dispositivo" msgid "Fixed name" -msgstr "" +msgstr "Nombre Fijo" msgid "Change" -msgstr "" +msgstr "Cambiar" msgid "Settings for Mobile Data Devices" -msgstr "" +msgstr "Configuración de dispositivos de datos móviles" msgid "PIN of SIM card" -msgstr "" +msgstr "PIN de tarjeta SIM" msgid "APN Settings (Modem device ppp0)" -msgstr "" +msgstr "Ajustes de APN (dispositivo móvil ppp0)" msgid "Access Point Name (APN)" -msgstr "" +msgstr "Nombre del punto de acceso (APN)" msgid "Password" -msgstr "" +msgstr "Password" msgid "Successfully Updated Network Configuration" -msgstr "" +msgstr "Configuración de red actualizada correctamente" msgid "Error saving network configuration to file" -msgstr "" +msgstr "Error al guardar la configuración de red en el archivo" msgid "Unable to detect interface" +msgstr "No se puede detectar la interfaz" + +msgid "Routing table" +msgstr "" + +msgid "raw output" +msgstr "" + +msgid "Setting wireless regulatory domain to %s" +msgstr "" + +msgid "Please provide a valid SSID." +msgstr "" + +msgid "Please provide a valid PSK." +msgstr "" + +msgid "Speedtest" +msgstr "" + +msgid "Selecting a server" +msgstr "" + +msgid "Privacy" +msgstr "" + +msgid "Server" +msgstr "" + +msgid "ms" +msgstr "" + +msgid "Mbps" +msgstr "" + +msgid "Ping" +msgstr "" + +msgid "Jitter" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "Start" +msgstr "" + +msgid "Abort" +msgstr "" + +msgid "Wireless LAN routing" +msgstr "" + +msgid "Stop WLAN routing" +msgstr "" + +msgid "Start WLAN routing" +msgstr "" + +msgid "Restart WLAN routing" +msgstr "" + +msgid "This option configures RaspAP to route network traffic from your wireless client (STA) interface to another available interface." +msgstr "" + +msgid "When an output interface is selected, iptables rules are added to route packets using network address translation (NAT). This is often done to share internet connectivity from a WLAN with devices on an eth0, usb0 or predictable enx interface." +msgstr "" + +msgid "Wireless client interface" +msgstr "" + +msgid "Output interface" +msgstr "" + +msgid "Configure a static IP address and DHCP for output interface" +msgstr "" + +msgid "Attempting to enable routing between %s and %s interfaces" +msgstr "" + +msgid "Attempting to disable routing between %s and %s interfaces" +msgstr "" + +msgid "No default DHCP configuration exists for the %s interface" +msgstr "" + +msgid "Configure a static IP and DHCP for this interface in DHCP Server settings" +msgstr "" + +msgid "WLAN routing configuration saved" +msgstr "" + +msgid "Unable to save WLAN routing configuration" +msgstr "" + +msgid "Successfully restarted dnsmasq" +msgstr "" + +msgid "Failed to restart dnsmasq" msgstr "" #: includes/system.php @@ -706,6 +1057,9 @@ msgstr "Revisión PI" msgid "Uptime" msgstr "Tiempo conectado" +msgid "System Time" +msgstr "" + msgid "Memory Used" msgstr "Memoria Usada" @@ -718,6 +1072,18 @@ msgstr "Reiniciar" msgid "Shutdown" msgstr "Apagar" +msgid "System reboot" +msgstr "" + +msgid "System shutdown" +msgstr "" + +msgid "Reboot now? The system will be temporarily unavailable." +msgstr "" + +msgid "Shutdown now? The system will be unavailable." +msgstr "" + msgid "System Rebooting Now!" msgstr "¡El sistema se está reiniciando ahora!" @@ -730,7 +1096,42 @@ msgstr "Puerto del servidor Web" msgid "Web server bind address" msgstr "Dirección de enlace del servidor web" -#: includes/themes.php +msgid "OS" +msgstr "SO" + +msgid "Kernel" +msgstr "Kernel" + +msgid "System reset" +msgstr "Reinicio del sistema" + +msgid "Reset RaspAP to its initial configuration? This action cannot be undone." +msgstr "¿Reiniciar RaspAP a su configuración inicial? Esta acción no se puede deshacer." + +msgid "Reset complete. Restart the hotspot for the changes to take effect." +msgstr "Reinicio completo. Reinicie el punto de acceso para que los cambios tendran efecto." + +msgid "System reset in progress..." +msgstr "" + +msgid "Reset" +msgstr "" + +msgid "Restore settings" +msgstr "" + +msgid "To reset RaspAP to its initial configuration, click or tap the button below." +msgstr "" + +msgid "Custom files for optional components such as Ad Blocking, WireGuard or OpenVPN will remain on the system." +msgstr "" + +msgid "Perform reset" +msgstr "" + +msgid "Restores all access point (AP) service settings to their default values. This applies to hostapd, dhcpcd and dnsmasq." +msgstr "" + msgid "Theme settings" msgstr "Configuracion de tema" @@ -740,6 +1141,114 @@ msgstr "Seleccionar un tema" msgid "Color" msgstr "Color" +msgid "Enable this option for resizable, drag and drop widgets. Best for large displays." +msgstr "" + +msgid "Dynamic widgets" +msgstr "" + +msgid "Tools" +msgstr "" + +msgid "System tools" +msgstr "" + +msgid "To generate a system debug log, click or tap the button below." +msgstr "" + +msgid "Debug log information contains the RaspAP version, current state and configuration of AP related services, installed system packages, Linux kernel version and networking details. No passwords or other sensitive data are included." +msgstr "" + +msgid "Generate debug log" +msgstr "" + +msgid "Debug log generation in progress..." +msgstr "" + +msgid "Diagnostic log size limit (KB)" +msgstr "" + +msgid "Changing log limit size to %s KB" +msgstr "" + +msgid "Information provided by raspap.sysinfo" +msgstr "" + +msgid "The following user plugins are available to extend RaspAP's functionality." +msgstr "" + +msgid "Choose Details for more information and to install a plugin." +msgstr "" + +msgid "Network error" +msgstr "" + +msgid "Unable to load plugins" +msgstr "" + +msgid "Reload" +msgstr "" + +msgid "and try again" +msgstr "" + +msgid "Plugins" +msgstr "" + +msgid "Plugin details" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Plugin source" +msgstr "" + +msgid "Author" +msgstr "" + +msgid "License" +msgstr "" + +msgid "Language locale" +msgstr "" + +msgid "Configuration files" +msgstr "" + +msgid "Dependencies" +msgstr "" + +msgid "Permissions" +msgstr "" + +msgid "Non-privileged users" +msgstr "" + +msgid "Install now" +msgstr "" + +msgid "Installing plugin" +msgstr "" + +msgid "Plugin installation in progress..." +msgstr "" + +msgid "Plugin install completed." +msgstr "" + +msgid "Details" +msgstr "" + +msgid "Installed" +msgstr "" + #: includes/data_usage.php msgid "Data usage" msgstr "Uso de datos" @@ -845,7 +1354,7 @@ msgid "Currently available OpenVPN client configurations are displayed below." msgstr "Las configuraciones de cliente OpenVPN actualmente disponibles se muestran a continuación." msgid "Activating a configuration will restart the openvpn-client service." -msgstr "Al activar una configuración, se reiniciará el servicio openvpn-client." +msgstr "" msgid "Delete OpenVPN client" msgstr "Eliminar cliente OpenVPN" @@ -958,6 +1467,9 @@ msgstr "arriba" msgid "down" msgstr "abajo" +msgid "Clear log" +msgstr "" + msgid "adblock" msgstr "Bloqueador de anuncios" @@ -1025,43 +1537,43 @@ msgid "Tunnel settings" msgstr "Ajustes del túnel" msgid "Configuration Method" -msgstr "" +msgstr "Método de configuración" msgid "Upload file" -msgstr "" +msgstr "Subir archivo" msgid "Create manually" -msgstr "" +msgstr "Creado manualmente" msgid "Upload a WireGuard config" -msgstr "" +msgstr "Subir una configuración de WireGuard" msgid "This option uploads and installs an existing WireGuard .conf file on this device." +msgstr "Esta opción sube e instala un archivo de WireGuard .conf existente en este dispositivo." + +msgid "Apply iptables rules to the selected interface" msgstr "" -msgid "Apply iptables rules for AP interface" +msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on a desired interface. The active AP interface is the default." msgstr "" -msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on the AP interface." -msgstr "" - -msgid "This option adds iptables Postup and PostDown rules for the configured AP interface (%s)." +msgid "This option adds iptables Postup and PostDown rules for the interface selected below." msgstr "" msgid "Select WireGuard configuration file (.conf)" -msgstr "" +msgstr "Seleccionar archivo de configuración de WireGuard (.conf)" msgid "Create a local WireGuard config" -msgstr "" +msgstr "Crear una configuración local de WireGuard" msgid "Enable server" msgstr "Habilitar servidor" msgid "Enable this option to secure network traffic by creating an encrypted tunnel between RaspAP and configured peers." -msgstr "" +msgstr "Activar esta opción para encriptar el tráfico creando un túnel entre RaspAP y los contactos (peers) configurados." msgid "This setting generates a new WireGuard .conf file on this device." -msgstr "" +msgstr "Este ajuste genera un nuevo archivo .conf de WireGuard en este dispositivo." msgid "Local public key" msgstr "Clave pública local" @@ -1100,10 +1612,10 @@ msgid "Persistent keepalive" msgstr "Mantener activo de forma persistente" msgid "Enable this option to display an updated wg-quick debug log." -msgstr "" +msgstr "Habilitar esta opción para mostrar un registro de depuración wg-quick actualizado." msgid "WireGuard debug log updated" -msgstr "" +msgstr "Registro de depuración de WireGuard actualizado" msgid "Scan this QR code with your client to connect to this tunnel" msgstr "Escanee este código QR con tu dispositivo para conectarte a este túnel" @@ -1135,17 +1647,38 @@ msgstr "Configuración de WireGuard actualizada correctamente" msgid "WireGuard configuration failed to be updated" msgstr "No se pudo actualizar la configuración de WireGuard" -msgid "Client Firewall" +msgid "Enable kill switch" msgstr "" +msgid "This option adds iptables PostUp and PreDown rules for the configured interface." +msgstr "" + +msgid "Recommended if you wish to prevent the flow of unencrypted packets through non-WireGuard interfaces." +msgstr "" + +msgid "iptables rules added to WireGuard configuration" +msgstr "" + +msgid "Existing iptables rules found in WireGuard configuration - not added" +msgstr "" + +msgid "Currently available WireGuard file configurations are displayed below." +msgstr "" + +msgid "Activating a configuration will restart the wg-quick service." +msgstr "" + +msgid "Client Firewall" +msgstr "Cortafuegos del cliente" + msgid "Firewall is ENABLED" -msgstr "" +msgstr "Cortafuegos está ACTIVO" msgid "Firewall is OFF" -msgstr "" +msgstr "Cortafuegos APAGADO" msgid "The default firewall will only allow outgoing and already established traffic." -msgstr "" +msgstr "El cortafuegos por defecto solo permitirá tráfico saliente y ya establecido." msgid "No incoming UDP traffic is allowed." msgstr "" @@ -1201,6 +1734,516 @@ msgstr "" msgid "Enable Firewall" msgstr "" +msgid "Changing the firewall status may disrupt or allow incoming traffic. Choose Proceed to continue." +msgstr "" + +msgid "Proceed" +msgstr "" + msgid "Apply changes" msgstr "" +msgid "Dynamic DNS" +msgstr "" + +msgid "Service provider" +msgstr "" + +msgid "Select a Dynamic DNS service supported by ddclient from the list below. Selecting a known service provider will populate the protocol and server fields. You may also configure the service manually." +msgstr "" + +msgid "Method to obtain IP" +msgstr "" + +msgid "Select the method used by ddclient to obtain an IP address. This value is specified in the -use option." +msgstr "" + +msgid "Discovery page on the web" +msgstr "" + +msgid "Network interface" +msgstr "" + +msgid "Network address" +msgstr "" + +msgid "Firewall status page" +msgstr "" + +msgid "External command" +msgstr "" + +msgid "Web address" +msgstr "" + +msgid "Firewall" +msgstr "" + +msgid "Command" +msgstr "" + +msgid "Example: 192.168.1.254/status.htm." +msgstr "" + +msgid "Example: /usr/local/bin/get-ip." +msgstr "" + +msgid "Domain" +msgstr "" + +msgid "Enable SSL" +msgstr "" + +msgid "Use an encrypted SSL connection for updates. Not supported by all providers." +msgstr "" + +msgid "Value specified in milliseconds (ms). Default is 300." +msgstr "" + +msgid "Use the Generate log button to output detailed ddclient daemon debug info" +msgstr "" + +msgid "Generate log" +msgstr "" + +msgid "Information provided by ddclient" +msgstr "" + +msgid "Start Dynamic DNS" +msgstr "" + +msgid "Stop Dynamic DNS" +msgstr "" + +msgid "Restart Dynamic DNS" +msgstr "" + +msgid "Account details" +msgstr "" + +msgid "My account" +msgstr "" + +msgid "Server location" +msgstr "" + +msgid "Choosing Save settings will connect to the selected country." +msgstr "" + +msgid "Choosing Connect %s will connect to a recommended server." +msgstr "" + +msgid "Select a country from the server location list" +msgstr "" + +msgid "Select a country..." +msgstr "" + +msgid "Account information not available from %s's Linux CLI." +msgstr "" + +msgid "Attempting to connect to %s" +msgstr "" + +msgid "Attempting to connect VPN provider" +msgstr "" + +msgid "Attempting to disconnect VPN provider" +msgstr "" + +msgid "Expected %s binary not found at: %s" +msgstr "" + +msgid "Visit the installation instructions for %s's Linux CLI." +msgstr "" + +msgid "Unable to execute %s binary found at: %s" +msgstr "" + +msgid "Check that binary is executable and permissions exist in raspap.sudoers" +msgstr "" + +msgid "Installed Linux CLI: %s" +msgstr "" + +msgid "Current %s connection status is displayed below." +msgstr "" + +msgid "Information provided by %s" +msgstr "" + +msgid "Connect %s" +msgstr "Conectar %s" + +msgid "Disconnect %s" +msgstr "Desconectar %s" + +msgid "About" +msgstr "Acerca de" + +msgid "Insiders" +msgstr "Insiders" + +msgid "Contributing" +msgstr "Contribuir" + +msgid "Check for update" +msgstr "" + +msgid "New release check in progress..." +msgstr "" + +msgid "A new release is available: Version" +msgstr "" + +msgid "Installed version is the latest release." +msgstr "" + +msgid "GitHub authentication" +msgstr "" + +msgid "Updating Insiders requires GitHub authentication." +msgstr "" + +msgid "Your credentials will be sent to GitHub securely with SSL. However, use caution if your RaspAP install is on a WLAN shared by untrusted users." +msgstr "" + +msgid "Personal Access Token" +msgstr "" + +msgid "Please provide a valid token." +msgstr "" + +msgid "Perform update" +msgstr "" + +msgid "Update in progress" +msgstr "" + +msgid "Application is being updated..." +msgstr "" + +msgid "Configuring update" +msgstr "" + +msgid "Updating sources" +msgstr "" + +msgid "Installing package updates" +msgstr "" + +msgid "Downloading latest files" +msgstr "" + +msgid "Installing application" +msgstr "" + +msgid "Update complete" +msgstr "" + +msgid "An error occurred. Check the log at /tmp/raspap_install.log" +msgstr "" + +msgid "RaspAP Exception" +msgstr "" + +msgid "An exception occurred" +msgstr "" + +msgid "RestAPI" +msgstr "" + +msgid "RestAPI settings" +msgstr "" + +msgid "Start RestAPI service" +msgstr "" + +msgid "Stop RestAPI service" +msgstr "" + +msgid "API Key" +msgstr "" + +msgid "Saving API key" +msgstr "" + +msgid "RestAPI status" +msgstr "" + +msgid "Current restapi.service status is displayed below." +msgstr "" + +msgid "RestAPI docs are accessible here%s" +msgstr "" + +msgid "Restarting restapi.service" +msgstr "" + +msgid "Information provided by restapi.service" +msgstr "" + +msgid "Session Expired" +msgstr "" + +msgid "Your session has expired. Please login to continue." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "Administrator login" +msgstr "" + +msgid "Forgot password" +msgstr "" + +msgid "Login failed" +msgstr "" + +msgid "NTP Server" +msgstr "" + +msgid "NTP Server settings" +msgstr "" + +msgid "NTP daemon" +msgstr "" + +msgid "Synchronized time" +msgstr "" + +msgid "NTP servers" +msgstr "" + +msgid "Add an NTP server" +msgstr "" + +msgid "Start NTP service" +msgstr "" + +msgid "Stop NTP service" +msgstr "" + +msgid "Edit mode" +msgstr "" + +msgid "Use the Edit mode toggle to manually edit the current ntp.config configuration." +msgstr "" + +msgid "Specify a public NTP server or a private one on your local network. IPv4 and IPv6 address, or a fully qualified domain name (FQDN) are acceptable values." +msgstr "" + +msgid "Public NTP servers supporting Network Time Security (NTS) may be specified with the nts suffix." +msgstr "" + +msgid "Examples of valid server entries include %s, %s and %s." +msgstr "" + +msgid "Current ntpq peer status is displayed below. An asterisk (*) indicates the preferred server." +msgstr "" + +msgid "NTP configuration cannot be empty" +msgstr "" + +msgid "Restarting ntpd.service" +msgstr "" + +msgid "Please enter a valid NTP server" +msgstr "" + +msgid "Attempting to start ntp.service" +msgstr "" + +msgid "Attempting to stop ntp.service" +msgstr "" + +msgid "NTP configuration not found at %s" +msgstr "" + +msgid "NTP configuration updated" +msgstr "" + +msgid "Advertising device as a Tailscale exit node" +msgstr "" + +msgid "Attempting to optimize UDP throughput" +msgstr "" + +msgid "Kernel transport layer offloads enabled for UDP" +msgstr "" + +msgid "Failed to enable kernel transport layer offloads for UDP" +msgstr "" + +msgid "Attempting to set tailscale up" +msgstr "" + +msgid "Attempting to set tailscale down" +msgstr "" + +msgid "Attempting to disconnect from tailscale" +msgstr "" + +msgid "Disconnected from tailscale and expired node key" +msgstr "" + +msgid "Unable to disconnect from tailscale" +msgstr "" + +msgid "Expected tailscale binary not found at: %" +msgstr "" + +msgid "A Tailscale VPN exit node extension for RaspAP" +msgstr "" + +msgid "Unable to retrieve Tailscale login. Choose %s to continue." +msgstr "" + +msgid "Start Tailscale" +msgstr "" + +msgid "Stop Tailscale" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Device approved and activated as a Tailscale exit node" +msgstr "" + +msgid "Not connected: Login required." +msgstr "" + +msgid "Tailscale VPN" +msgstr "" + +msgid "Exit node activated" +msgstr "" + +msgid "The device %s is connected with the address %s and offers an exit node." +msgstr "" + +msgid "See the %s on how to use this exit node with your devices." +msgstr "" + +msgid "Allow exit node" +msgstr "" + +msgid "The device %s is pending approval as an exit node." +msgstr "" + +msgid "Locate the %s Exit Node badge in the machines list." +msgstr "" + +msgid "Open Tailscale Machines" +msgstr "" + +msgid "To allow this device as an exit node, choose Open Tailscale Machines." +msgstr "" + +msgid "From the %s icon menu of the exit node, open the %s panel." +msgstr "" + +msgid "Edit route settings" +msgstr "" + +msgid "Login to Tailscale" +msgstr "" + +msgid "To connect device %s to your tailnet, choose %s." +msgstr "" + +msgid "After logging in, choose Next to continue." +msgstr "" + +msgid "Configure exit node" +msgstr "" + +msgid "The device %s is connected to your tailnet with the address %s." +msgstr "" + +msgid "By default, Tailscale only routes traffic between the devices on which it's been installed. You can also route all your public internet traffic by configuring a device on your network as a exit node" +msgstr "" + +msgid "When you route all traffic through an exit node, you're effectively using default routes (0.0.0.0/0, ::/0), similar to how you would if you were using a typical VPN." +msgstr "" + +msgid "You have the option of configuring this device as an exit node, or using another exit node in your tailnet." +msgstr "" + +msgid "Select an existing exit node on your tailnet" +msgstr "" + +msgid "This is a typical configuration if you're using this device as a VPN travel router, for example." +msgstr "" + +msgid "Configure this device as a new exit node" +msgstr "" + +msgid "By configuring this device as an exit node, public internet traffic from devices connected in your tailnet will be routed through it." +msgstr "" + +msgid "For security reasons, you must opt in to enable exit node functionality. The first step is to advertise %s as an exit node in your tailnet. In the next step, you'll allow this device to be an exit node." +msgstr "" + +msgid "Advertise %s as an exit node" +msgstr "" + +msgid "This effectively configures Tailscale as a VPN to mask your real location, access region-restricted content, or enhance privacy when connecting from untrusted networks." +msgstr "" + +msgid "This option lets Tailscale know your device is ready to route traffic." +msgstr "" + +msgid "Recommended for Tailscale exit nodes with Linux 6.2 or later kernels, this uses UDP generic receive offload (GRO) forwarding to reduce CPU overhead." +msgstr "" + +msgid "This option enables transport layer offloads for better performance." +msgstr "" + +msgid "Select an exit node" +msgstr "" + +msgid "To use %s as a VPN gateway, configure Tailscale to use an exit node. Tailscale's suggested node is indicated with a star." +msgstr "" + +msgid "Advertise a subnet route for the active %s AP interface" +msgstr "" + +msgid "Subnet routes let you extend your Tailscale network (known as a tailnet) to include devices that don't or can't run the Tailscale client." +msgstr "" + +msgid "A subnet route acts as a gateway between your tailnet and a physical subnet. The subnet of the active AP interface is preconfigured below; edit if necessary." +msgstr "" + +msgid "Route LAN traffic through the exit node." +msgstr "" + +msgid "This will direct all LAN traffic to go through your exit node only." +msgstr "" + +msgid "Choose Next to configure %s to use the selected exit node." +msgstr "" + +msgid "No exit nodes found on your tailnet. Choose Back to continue." +msgstr "" + +msgid "Using exit node" +msgstr "" + +msgid "The device %s is configured to use exit node %s. It has the Tailscale MagicDNS address %s." +msgstr "" + +msgid "Choose Save settings to continue." +msgstr "" + +msgid "Choose Next to continue." +msgstr "" + +msgid "Tailnet status" +msgstr "" + +msgid "Current tailnet status is displayed below." +msgstr "" + diff --git a/locale/ko_KR/LC_MESSAGES/messages.mo b/locale/ko_KR/LC_MESSAGES/messages.mo index 853967d87a1ceacbfccf36fb394224903fd62d9d..ad39177c15978506bbbcbfa0a6f26148a63a8ac5 100644 GIT binary patch delta 27555 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 delta 14696 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\n" "POT-Creation-Date: 2017-10-19 08:56+0000\n" -"PO-Revision-Date: 2024-11-14 10:23\n" +"PO-Revision-Date: 2025-07-03 05:44\n" "Last-Translator: Bill Zimmerman \n" "Language-Team: Korean\n" "Language: ko_KR\n" @@ -24,8 +24,8 @@ msgstr "RaspAP WiFi 환경설정 포탈" msgid "Toggle navigation" msgstr "토글 내비게이션" -msgid "RaspAP Wifi Portal" -msgstr "RaspAP WiFi 포탈" +msgid "RaspAP Admin Panel" +msgstr "RaspAP 관리자 패널" msgid "Dashboard" msgstr "대시보드 " @@ -294,8 +294,8 @@ msgstr "주파수 " msgid "Link Quality" msgstr "링크 품질 " -msgid "Information provided by ip and iw and from system" -msgstr "ip 및 iw, 시스템에서 제공한 정보" +msgid "Information provided by raspap.system" +msgstr "raspap.system에서 제공한 정보" msgid "No MAC Address Found" msgstr "MAC 주소를 찾을 수 없습니다" @@ -333,6 +333,9 @@ msgstr "연결된 디바이스 " msgid "Client: Ethernet cable" msgstr "클라이언트: 이더넷 케이블" +msgid "Current status" +msgstr "현재 상태" + msgid "Ethernet" msgstr "이더넷" @@ -345,6 +348,43 @@ msgstr "스마트폰" msgid "WiFi" msgstr "WiFi" +msgid "Repeater" +msgstr "리피터" + +msgid "Tethering" +msgstr "테더링" + +msgid "Cellular" +msgstr "셀룰러" + +msgid "AP" +msgstr "AP(Access Point)" + +msgid "Bridged" +msgstr "브릿지" + +msgid "Adblock" +msgstr "광고차단(adblock)" + +msgid "VPN" +msgstr "VPN" + +msgid "Netmask" +msgstr "넷마스크" + +msgid "5G" +msgstr "5G" + +msgid "2.4G" +msgstr "2.4G" + +msgid "%d WLAN %s" +msgstr "%d WLAN %s" + +msgid "client" +msgid_plural "clients" +msgstr[0] "클라이언트" + msgid "Mobile Data Client" msgstr "모바일 데이터 클라이언트" @@ -387,6 +427,19 @@ msgstr "클라이언트 장치가 없거나 아직 구성되지 않았습니다. msgid "No Client device found" msgstr "클라이언트 장치가 발견되지 않았습니다." +#: includes/footer.php +msgid "Created by the %s" +msgstr "%s에 의해 생성됨" + +msgid "RaspAP Team" +msgstr "RaspAP 팀" + +msgid "Get Insiders" +msgstr "인사이더 받기" + +msgid "Thanks for being an Insider" +msgstr "인사이더가 되어 주셔서 감사합니다" + #: includes/dhcp.php msgid "DHCP server settings" msgstr "DHCP 서버 설정 " @@ -1120,6 +1173,81 @@ msgstr "로그 크기 제한을 %s KB로 변경 중입니다." msgid "Information provided by raspap.sysinfo" msgstr "raspap.sysinfo에서 제공된 정보" +msgid "The following user plugins are available to extend RaspAP's functionality." +msgstr "다음 사용자 플러그인을 통해 RaspAP의 기능을 확장할 수 있습니다." + +msgid "Choose Details for more information and to install a plugin." +msgstr "자세한 정보 확인 및 플러그인 설치를 위해 자세히를 선택하세요." + +msgid "Network error" +msgstr "네트워크 오류" + +msgid "Unable to load plugins" +msgstr "플러그인을 불러올 수 없습니다" + +msgid "Reload" +msgstr "새로고침" + +msgid "and try again" +msgstr "다시 시도하세요" + +msgid "Plugins" +msgstr "플러그인" + +msgid "Plugin details" +msgstr "플러그인 세부정보" + +msgid "Name" +msgstr "이름" + +msgid "Version" +msgstr "버전" + +msgid "Description" +msgstr "설명" + +msgid "Plugin source" +msgstr "플러그인 소스" + +msgid "Author" +msgstr "작성자" + +msgid "License" +msgstr "라이선스" + +msgid "Language locale" +msgstr "언어 로케일" + +msgid "Configuration files" +msgstr "구성 파일" + +msgid "Dependencies" +msgstr "종속성" + +msgid "Permissions" +msgstr "권한" + +msgid "Non-privileged users" +msgstr "비권한 사용자" + +msgid "Install now" +msgstr "지금 설치하기" + +msgid "Installing plugin" +msgstr "플러그인 설치 중" + +msgid "Plugin installation in progress..." +msgstr "플러그인 설치 진행 중..." + +msgid "Plugin install completed." +msgstr "플러그인 설치가 완료되었습니다." + +msgid "Details" +msgstr "자세히" + +msgid "Installed" +msgstr "설치됨" + #: includes/data_usage.php msgid "Data usage" msgstr "데이터 사용량 " @@ -1422,14 +1550,14 @@ msgstr "WireGuard 구성(config) 업로드" msgid "This option uploads and installs an existing WireGuard .conf file on this device." msgstr "이 옵션은 이 장치에 기존 WireGuard .conf 파일을 업로드하고 설치합니다" -msgid "Apply iptables rules for AP interface" -msgstr "AP 인터페이스에 iptables 규칙 적용" +msgid "Apply iptables rules to the selected interface" +msgstr "선택한 인터페이스에 iptables 규칙 적용" -msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on the AP interface." -msgstr "wg0 인터페이스에서 AP 인터페이스에 연결된 클라이언트로 네트워크 트래픽을 전달하려는 경우에 권장됩니다." +msgid "Recommended if you wish to forward network traffic from the wg0 interface to clients connected on a desired interface. The active AP interface is the default." +msgstr "wg0 인터페이스에서 원하는 인터페이스에 연결된 클라이언트로 네트워크 트래픽을 포워딩하려는 경우에 권장됩니다. 기본값은 활성화된 AP 인터페이스입니다." -msgid "This option adds iptables Postup and PostDown rules for the configured AP interface (%s)." -msgstr "이 옵션은 구성된 AP 인터페이스(%s) 에 대한 iptables PostupPostDown 규칙을 추가합니다." +msgid "This option adds iptables Postup and PostDown rules for the interface selected below." +msgstr "이 옵션은 아래에서 선택한 인터페이스에 대해 iptables PostupPostDown 규칙을 추가합니다." msgid "Select WireGuard configuration file (.conf)" msgstr "WireGuard 구성 파일(.conf) 선택" @@ -1521,8 +1649,8 @@ msgstr "WireGuard 구성을 업데이트하지 못했습니다." msgid "Enable kill switch" msgstr "킬 스위치 활성화" -msgid "This option adds iptables PostUp and PreDown rules for the configured AP interface (%s)." -msgstr "이 옵션은 구성된 AP 인터페이스(%s) 에 대해 iptables PostUpPreDown 규칙을 추가합니다." +msgid "This option adds iptables PostUp and PreDown rules for the configured interface." +msgstr "이 옵션은 구성된 인터페이스에 대해 iptables PostUpPreDown 규칙을 추가합니다." msgid "Recommended if you wish to prevent the flow of unencrypted packets through non-WireGuard interfaces." msgstr "WireGuard 인터페이스가 아닌 다른 인터페이스를 통한 암호화되지 않은 패킷 흐름을 방지하려는 경우 권장됩니다." @@ -1605,6 +1733,12 @@ msgstr "방화벽 비활성화" msgid "Enable Firewall" msgstr "방화벽 활성화" +msgid "Changing the firewall status may disrupt or allow incoming traffic. Choose Proceed to continue." +msgstr "방화벽 상태를 변경하면 수신 트래픽이 차단되거나 허용될 수 있습니다. 계속하려면 계속을 선택하세요." + +msgid "Proceed" +msgstr "진행" + msgid "Apply changes" msgstr "변경 사항을 적용합니다" @@ -1848,6 +1982,24 @@ msgstr "restapi.service를 재시작하는 중입니다." msgid "Information provided by restapi.service" msgstr "restapi.service에서 제공한 정보" +msgid "Session Expired" +msgstr "세션 만료됨" + +msgid "Your session has expired. Please login to continue." +msgstr "세션이 만료되었습니다. 계속하려면 로그인해 주세요." + +msgid "Login" +msgstr "로그인" + +msgid "Administrator login" +msgstr "관리자 로그인" + +msgid "Forgot password" +msgstr "비밀번호 찾기" + +msgid "Login failed" +msgstr "로그인 실패" + msgid "NTP Server" msgstr "NTP 서버" @@ -1911,3 +2063,186 @@ msgstr "NTP 구성을 %s에서 찾을 수 없습니다." msgid "NTP configuration updated" msgstr "NTP 구성이 업데이트되었습니다." +msgid "Advertising device as a Tailscale exit node" +msgstr "Tailscale 종료 노드로 디바이스 광고 중" + +msgid "Attempting to optimize UDP throughput" +msgstr "UDP 처리량 최적화를 시도하는 중" + +msgid "Kernel transport layer offloads enabled for UDP" +msgstr "UDP에 대해 커널 전송 계층 오프로딩이 활성화됨" + +msgid "Failed to enable kernel transport layer offloads for UDP" +msgstr "UDP에 대한 커널 전송 계층 오프로딩을 활성화하지 못했습니다" + +msgid "Attempting to set tailscale up" +msgstr "Tailscale 설정을 시도하는 중" + +msgid "Attempting to set tailscale down" +msgstr "Tailscale 비활성화를 시도하는 중" + +msgid "Attempting to disconnect from tailscale" +msgstr "Tailscale 연결 해제를 시도하는 중" + +msgid "Disconnected from tailscale and expired node key" +msgstr "Tailscale에서 연결이 해제되었으며 노드 키가 만료되었습니다" + +msgid "Unable to disconnect from tailscale" +msgstr "Tailscale에서 연결을 해제할 수 없습니다" + +msgid "Expected tailscale binary not found at: %" +msgstr "예상된 Tailscale 바이너리가 다음 위치에 없습니다: %" + +msgid "A Tailscale VPN exit node extension for RaspAP" +msgstr "RaspAP용 Tailscale VPN 종료 노드 확장 프로그램" + +msgid "Unable to retrieve Tailscale login. Choose %s to continue." +msgstr "Tailscale 로그인 정보를 가져올 수 없습니다. 계속하려면 %s을 선택하세요." + +msgid "Start Tailscale" +msgstr "Tailscale 시작" + +msgid "Stop Tailscale" +msgstr "Tailscale 중지" + +msgid "Next" +msgstr "다음" + +msgid "Device approved and activated as a Tailscale exit node" +msgstr "디바이스가 승인되어 Tailscale 종료 노드로 활성화되었습니다" + +msgid "Not connected: Login required." +msgstr "연결되지 않음: 로그인 필요." + +msgid "Tailscale VPN" +msgstr "Tailscale VPN" + +msgid "Exit node activated" +msgstr "출구 노드가 활성화되었습니다" + +msgid "The device %s is connected with the address %s and offers an exit node." +msgstr "디바이스 %s는 주소 %s로 연결되어 있으며 출구 노드를 제공합니다." + +msgid "See the %s on how to use this exit node with your devices." +msgstr "이 출구 노드를 디바이스와 함께 사용하는 방법은 %s를 참조하세요." + +msgid "Allow exit node" +msgstr "출구 노드 허용" + +msgid "The device %s is pending approval as an exit node." +msgstr "디바이스 %s는 출구 노드로 승인 대기 중입니다." + +msgid "Locate the %s Exit Node badge in the machines list." +msgstr "머신 목록에서 %s 출구 노드 배지(badge)를 찾으세요." + +msgid "Open Tailscale Machines" +msgstr "Tailscale 머신 열기" + +msgid "To allow this device as an exit node, choose Open Tailscale Machines." +msgstr "이 디바이스를 출구 노드로 허용하려면 Tailscale 머신 열기를 선택하세요." + +msgid "From the %s icon menu of the exit node, open the %s panel." +msgstr "출구 노드의 %s 아이콘 메뉴에서 %s 패널을 여세요." + +msgid "Edit route settings" +msgstr "라우팅 설정 편집" + +msgid "Login to Tailscale" +msgstr "Tailscale에 로그인" + +msgid "To connect device %s to your tailnet, choose %s." +msgstr "디바이스 %s를 tailnet에 연결하려면 %s를 선택하세요." + +msgid "After logging in, choose Next to continue." +msgstr "로그인 후, 계속하려면 다음을 선택하세요." + +msgid "Configure exit node" +msgstr "출구 노드 구성" + +msgid "The device %s is connected to your tailnet with the address %s." +msgstr "디바이스 %s는 주소 %s로 tailnet에 연결되어 있습니다." + +msgid "By default, Tailscale only routes traffic between the devices on which it's been installed. You can also route all your public internet traffic by configuring a device on your network as a exit node" +msgstr "기본적으로 Tailscale은 설치된 디바이스 간의 트래픽만 라우팅합니다. 네트워크의 디바이스를 출구 노드로 구성하면 모든 공용 인터넷 트래픽도 라우팅할 수 있습니다" + +msgid "When you route all traffic through an exit node, you're effectively using default routes (0.0.0.0/0, ::/0), similar to how you would if you were using a typical VPN." +msgstr "모든 트래픽을 출구 노드를 통해 라우팅하면, 일반적인 VPN을 사용할 때처럼 기본 경로(0.0.0.0/0, ::/0)를 사용하는 것과 동일한 방식으로 동작하게 됩니다." + +msgid "You have the option of configuring this device as an exit node, or using another exit node in your tailnet." +msgstr "이 디바이스를 출구 노드로 구성하거나, tailnet 내의 다른 출구 노드를 사용할 수 있습니다." + +msgid "Select an existing exit node on your tailnet" +msgstr "tailnet에서 기존 출구 노드를 선택하세요" + +msgid "This is a typical configuration if you're using this device as a VPN travel router, for example." +msgstr "예를 들어 이 디바이스를 VPN 여행 라우터로 사용할 경우, 이는 일반적인 구성입니다." + +msgid "Configure this device as a new exit node" +msgstr "이 디바이스를 새로운 출구 노드로 구성하세요" + +msgid "By configuring this device as an exit node, public internet traffic from devices connected in your tailnet will be routed through it." +msgstr "이 디바이스를 출구 노드로 구성하면, tailnet에 연결된 디바이스의 공용 인터넷 트래픽이 이 디바이스를 통해 라우팅됩니다." + +msgid "For security reasons, you must opt in to enable exit node functionality. The first step is to advertise %s as an exit node in your tailnet. In the next step, you'll allow this device to be an exit node." +msgstr "보안상의 이유로 출구 노드 기능을 활성화하려면 사용자가 직접 동의해야 합니다. 첫 번째 단계는 %s를 tailnet에서 출구 노드로 광고하는 것입니다. 다음 단계에서는 이 디바이스를 출구 노드로 허용하게 됩니다." + +msgid "Advertise %s as an exit node" +msgstr "%s를 출구 노드로 광고" + +msgid "This effectively configures Tailscale as a VPN to mask your real location, access region-restricted content, or enhance privacy when connecting from untrusted networks." +msgstr "이는 Tailscale을 VPN으로 구성하여 실제 위치를 숨기거나, 지역 제한 콘텐츠에 접근하거나, 신뢰되지 않은 네트워크에서 연결할 때 프라이버시를 강화하는 데 효과적입니다." + +msgid "This option lets Tailscale know your device is ready to route traffic." +msgstr "이 옵션은 Tailscale에 디바이스가 트래픽을 라우팅할 준비가 되었음을 알립니다." + +msgid "Recommended for Tailscale exit nodes with Linux 6.2 or later kernels, this uses UDP generic receive offload (GRO) forwarding to reduce CPU overhead." +msgstr "Linux 6.2 이상 커널을 사용하는 Tailscale 출구 노드에 권장되며, UDP 일반 수신 오프로딩(GRO) 포워딩을 사용하여 CPU 부하를 줄입니다." + +msgid "This option enables transport layer offloads for better performance." +msgstr "이 옵션은 더 나은 성능을 위해 전송 계층 오프로딩을 활성화합니다." + +msgid "Select an exit node" +msgstr "출구 노드를 선택하세요" + +msgid "To use %s as a VPN gateway, configure Tailscale to use an exit node. Tailscale's suggested node is indicated with a star." +msgstr "%s를 VPN 게이트웨이로 사용하려면, Tailscale에서 출구 노드를 사용하도록 구성하세요. Tailscale이 추천하는 노드는 별표로 표시됩니다." + +msgid "Advertise a subnet route for the active %s AP interface" +msgstr "활성화된 %s AP 인터페이스에 대해 서브넷 라우트를 광고하세요" + +msgid "Subnet routes let you extend your Tailscale network (known as a tailnet) to include devices that don't or can't run the Tailscale client." +msgstr "서브넷 라우트를 사용하면 Tailscale 네트워크(즉, tailnet)를 Tailscale 클라이언트를 실행할 수 없거나 실행하지 않는 디바이스까지 확장할 수 있습니다." + +msgid "A subnet route acts as a gateway between your tailnet and a physical subnet. The subnet of the active AP interface is preconfigured below; edit if necessary." +msgstr "서브넷 라우트는 tailnet과 물리적 서브넷 간의 게이트웨이 역할을 합니다. 활성화된 AP 인터페이스의 서브넷이 아래에 미리 구성되어 있으며, 필요에 따라 수정할 수 있습니다." + +msgid "Route LAN traffic through the exit node." +msgstr "LAN 트래픽을 출구 노드를 통해 라우팅하세요." + +msgid "This will direct all LAN traffic to go through your exit node only." +msgstr "이 설정은 모든 LAN 트래픽을 출구 노드를 통해서만 전달하도록 합니다." + +msgid "Choose Next to configure %s to use the selected exit node." +msgstr "선택한 출구 노드를 사용하도록 %s를 구성하려면 다음을 선택하세요." + +msgid "No exit nodes found on your tailnet. Choose Back to continue." +msgstr "tailnet에서 출구 노드를 찾을 수 없습니다. 계속하려면 뒤로를 선택하세요." + +msgid "Using exit node" +msgstr "출구 노드 사용 중" + +msgid "The device %s is configured to use exit node %s. It has the Tailscale MagicDNS address %s." +msgstr "디바이스 %s는 출구 노드 %s를 사용하도록 구성되어 있습니다. Tailscale MagicDNS 주소는 %s입니다." + +msgid "Choose Save settings to continue." +msgstr "계속하려면 설정 저장을 선택하세요." + +msgid "Choose Next to continue." +msgstr "계속하려면 다음을 선택하세요." + +msgid "Tailnet status" +msgstr "Tailnet 상태" + +msgid "Current tailnet status is displayed below." +msgstr "현재 tailnet 상태가 아래에 표시됩니다." + From ac9a39b5be31172120df725e579c248f16fa02db Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 2 Jul 2025 23:55:39 -0700 Subject: [PATCH 016/122] =?UTF-8?q?Update=20w/=20adjective=20eingeschr?= =?UTF-8?q?=C3=A4nkten=20-=20thanks=20@Noschvie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locale/de_DE/LC_MESSAGES/messages.mo | Bin 40392 -> 40395 bytes locale/de_DE/LC_MESSAGES/messages.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/de_DE/LC_MESSAGES/messages.mo b/locale/de_DE/LC_MESSAGES/messages.mo index f1b886d51a8d7f888239db3129bdb48c3e712f7b..23c5f0fb84d22b88e5b321bd8bbb93bc24bf1154 100644 GIT binary patch delta 602 zcmXZZJ4k|26u|L=^fHq`D^wf|dSMVfpc;EjJrF{difHOdluHCbIW$P4WoBj28b|~N zQqmA%1VOY(*c7+~5p6;c{ZD?=@0@e*ch5cdd(4=gGN#LfE0h$G`-sR02KPjKc!Tq3 z-WLgI9mg??qiBnYOyUeKV%o?(2O?YKn>dX{3}PTAQjb|QV;*f-h)GE71Vsu~tT~kb z+-2B-7V1OThCb}V4eZ8qY{oa#2BvsvzXSE$CTzrhti>s#zKm7m+wl-h3BnYd7)AZy z0xR$ZTQG;Z?-%>fa#Xs|FuKWC(2Kk1#4FSbJYyxkV;O#+4)lc%{0R{>5Y!!uv|>B< zpdWWIjC!!;gekBWJ8=y?c!oOp9qJ3_P#YAC^=}*?wIgD?VcrmZD;alj4g;Jz7uY zKew3nqMiN-`fw8aaSI3V5<9Voy1_5%d{?ZzP6u|72hoAEW`6}OE+ z2Ul2wPuPV8)OkNRgqE}Nfdc3uuVD`!pc`*cFP6tze8nn!Lp|U-w&J%z(M-{JF4B!Y z^kWG3F^syfnf}@igkw-J{+pmnwRk+Z9Wx&&Z4Y4}9Kc9{>OV diff --git a/locale/de_DE/LC_MESSAGES/messages.po b/locale/de_DE/LC_MESSAGES/messages.po index 91ad6efa..c287cd9e 100644 --- a/locale/de_DE/LC_MESSAGES/messages.po +++ b/locale/de_DE/LC_MESSAGES/messages.po @@ -149,7 +149,7 @@ msgid "Limited user password" msgstr "" msgid "This action will save the limited user's credentials and logout the current admin user. Save and enable limited privilege mode?" -msgstr "Dieser Vorgang wird die Anmeldedaten des beschränkten Nutzers speichern und den aktuellen Admin-Nutzer abmelden. Speichern und Begrenzten-Berechtigungsmodus aktivieren?" +msgstr "Dieser Vorgang wird die Anmeldedaten des eingeschränkten Nutzers speichern und den aktuellen Admin-Nutzer abmelden. Speichern und Begrenzten-Berechtigungsmodus aktivieren?" msgid "Save and logout" msgstr "Speichern und abmelden" From 345c331b10314109347f6c463065ee238fa444e4 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 3 Jul 2025 00:58:14 -0700 Subject: [PATCH 017/122] fix(i18n): improve German translation for limited user privileges - thx @Noschvie --- locale/de_DE/LC_MESSAGES/messages.mo | Bin 40395 -> 40407 bytes locale/de_DE/LC_MESSAGES/messages.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/de_DE/LC_MESSAGES/messages.mo b/locale/de_DE/LC_MESSAGES/messages.mo index 23c5f0fb84d22b88e5b321bd8bbb93bc24bf1154..40aaa4270bfb3dd05ca7fcb55f2b049593b3f9be 100644 GIT binary patch delta 639 zcmXZZNhm~d9Ki8k)7ZBn%xD@<*>kWIazZX-xhNq~BgmXk$$|vDJ+bO z3~C*F@d10#y&*D;e)M6|%Ih{omdRIe6zy9gGdO{T_<$Msj`{etB~d9Luy2dxpaV6p zv~0sH@_www5p2L1HsTpp;0NjkxjX;s%TedlV;Oc~9!^>Hc}yo?-HD216T~Sv@euWc zOH9RAti(6eb(y;&Em(|tpaFE@0@mUtI`JCy056z^pJ>N#)C>Kf1Jj~=B1HrSR$&V^ z;W)-Hjykw_U!)Z~u^t!Ez+=>#-=n_Z8|nsMR{s~<$=wN&3mnH3bRPWg+fhnjP-sSN z451DhL;dFg%*1o6pG1ASThtppo<6&Rk($2g$;g}$^oI@8AM%=W9^Y*8C=>{rA*0ir SH9fwt-y5M3I`ypkvi<;h_;n`$ delta 627 zcmXZZODKd<6u|M*rrzF`J_pbqqjrT7yiC?P1?6RE*^ zY(+1wU<~zO>poLp8#ZAWU3i2#`3>p|rcfKao9kcLNp3$7xkN8!qU|u Date: Tue, 8 Jul 2025 20:26:05 +0200 Subject: [PATCH 018/122] fix(color): add validation for CSS color formats in getColorOpt function --- app/img/devices/compute.php | 140 ++++++++++++++++++------------------ includes/functions.php | 10 +++ 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/app/img/devices/compute.php b/app/img/devices/compute.php index bf744a3a..63ddeb44 100644 --- a/app/img/devices/compute.php +++ b/app/img/devices/compute.php @@ -8,78 +8,78 @@ $color = getColorOpt(); viewBox="0 0 291.5 203.2" style="enable-background:new 0 0 291.5 203.2;" xml:space="preserve"> diff --git a/includes/functions.php b/includes/functions.php index f1b90b9e..a3e53ef6 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -664,6 +664,16 @@ function getColorOpt() } else { $color = $_COOKIE['color']; } + + // Define the regex pattern for valid CSS color formats + $colorPattern = "/^(#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})|rgb\((\s*\d+\s*,){2}\s*\d+\s*\)|rgba\((\s*\d+\s*,){3}\s*(0|0\.\d+|1)\)|[a-zA-Z]+)$/i"; + + // Validate the color + if (!preg_match($colorPattern, $color)) { + // Return a default color if validation fails + $color = "#2b8080"; + } + return $color; } From ad22fb693b8f275e512096b25e08afe283645b2f Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 10 Jul 2025 12:07:51 -0700 Subject: [PATCH 019/122] Initial commit --- installers/hostapd@.service | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 installers/hostapd@.service diff --git a/installers/hostapd@.service b/installers/hostapd@.service new file mode 100644 index 00000000..89ed5803 --- /dev/null +++ b/installers/hostapd@.service @@ -0,0 +1,12 @@ +[Unit] +Description=Hostapd access point for %i +After=network.target + +[Service] +ExecStart=/usr/sbin/hostapd -P /run/hostapd_%i.pid /etc/hostapd/hostapd-%i.conf +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure + +[Install] +WantedBy=multi-user.target + From 780803b0ec1dc6cb84a686f305e0d5ae52852abb Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 10 Jul 2025 12:18:22 -0700 Subject: [PATCH 020/122] Create _install_raspap_hostapd() --- installers/common.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/installers/common.sh b/installers/common.sh index 47db2624..4bb34618 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -51,6 +51,7 @@ function _install_raspap() { _download_latest_files _change_file_ownership _create_hostapd_scripts + _install_raspap_hostapd _create_plugin_scripts _create_lighttpd_scripts _install_lighttpd_configs @@ -777,6 +778,13 @@ function _enable_raspap_daemon() { sudo systemctl enable raspapd.service || _install_status 1 "Failed to enable raspap.service" } +# Install hostapd@.service +function _install_raspap_hostapd() { + _install_log "Installing RaspAP hostapd@.service" + sudo cp $webroot_dir/installers/hostapd@.service /etc/systemd/system/ || _install_status 1 "Unable to copy hostapd@.service file" + sudo systemctl daemon-reload +} + # Configure IP forwarding, set IP tables rules, prompt to install RaspAP daemon function _configure_networking() { _install_log "Configuring networking" From 697c622f76c1f2cca75d267343b1e45a406af2cc Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 10 Jul 2025 22:26:53 -0700 Subject: [PATCH 021/122] Return _install_status 0 --- installers/common.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/common.sh b/installers/common.sh index 4bb34618..28014b61 100755 --- a/installers/common.sh +++ b/installers/common.sh @@ -783,6 +783,7 @@ function _install_raspap_hostapd() { _install_log "Installing RaspAP hostapd@.service" sudo cp $webroot_dir/installers/hostapd@.service /etc/systemd/system/ || _install_status 1 "Unable to copy hostapd@.service file" sudo systemctl daemon-reload + _install_status 0 } # Configure IP forwarding, set IP tables rules, prompt to install RaspAP daemon From 7fc7b25479c3b1eae4a23225e926489f69d12bc1 Mon Sep 17 00:00:00 2001 From: billz Date: Thu, 10 Jul 2025 23:57:36 -0700 Subject: [PATCH 022/122] Cleanup obsolete/deprecated files --- crowdin.yml | 3 - includes/CSRF.php | 0 includes/get_clients.php | 307 --------------------------------------- 3 files changed, 310 deletions(-) delete mode 100644 crowdin.yml mode change 100644 => 100755 includes/CSRF.php delete mode 100755 includes/get_clients.php diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index 63ff1718..00000000 --- a/crowdin.yml +++ /dev/null @@ -1,3 +0,0 @@ -files: - - source: /locale/en_US/LC_MESSAGES/messages.po - translation: /locale/%locale_with_underscore%/LC_MESSAGES/%original_file_name% diff --git a/includes/CSRF.php b/includes/CSRF.php old mode 100644 new mode 100755 diff --git a/includes/get_clients.php b/includes/get_clients.php deleted file mode 100755 index c167a538..00000000 --- a/includes/get_clients.php +++ /dev/null @@ -1,307 +0,0 @@ - /dev/null"); - } - } - foreach ($rawdevs as $i => $dev) { - $cl["device"][$i]["name"]=$dev; - $nam = (preg_match("/^(\w+)[0-9]$/",$dev,$nam) === 1) ? $nam=$nam[1] : ""; - $cl["device"][$i]["type"]=$ty=getClientType($dev); - unset($udevinfo); - exec("udevadm info /sys/class/net/$dev 2> /dev/null", $udevinfo); - if ($nam == "ppp" && isset($devtty)) { - exec("udevadm info --name='$devtty' 2> /dev/null", $udevinfo); - } - if (!empty($udevinfo) && is_array($udevinfo)) { - $model = preg_only_match("/ID_MODEL_ENC=(.*)$/", $udevinfo); - if (empty($model) || preg_match("/^[0-9a-f]{4}$/", $model) === 1) { - $model = preg_only_match("/ID_MODEL_FROM_DATABASE=(.*)$/", $udevinfo); - } - if (empty($model)) { - $model = preg_only_match("/ID_OUI_FROM_DATABASE=(.*)$/", $udevinfo); - } - $vendor = preg_only_match("/ID_VENDOR_ENC=(.*)$/", $udevinfo); - if (empty($vendor) || preg_match("/^[0-9a-f]{4}$/", $vendor) === 1) { - $vendor = preg_only_match("/ID_VENDOR_FROM_DATABASE=(.*)$/", $udevinfo); - } - $driver = preg_only_match("/ID_NET_DRIVER=(.*)$/", $udevinfo); - $vendorid = preg_only_match("/ID_VENDOR_ID=(.*)$/", $udevinfo); - $productid = preg_only_match("/ID_MODEL_ID=(.*)$/", $udevinfo); - } - $cl["device"][$i]["model"] = preg_replace("/\\\\x20/", " ", $model); - $cl["device"][$i]["vendor"] = preg_replace("/\\\\x20/", " ", $vendor); - $cl["device"][$i]["vid"] = $vendorid; - $cl["device"][$i]["pid"] = $productid; - unset($mac); - exec("cat /sys/class/net/$dev/address 2> /dev/null", $mac); - $cl["device"][$i]["mac"] = empty($mac) ? "":$mac[0]; - unset($ip); - exec("ifconfig $dev 2> /dev/null", $ip); - $cl["device"][$i]["ipaddress"] = preg_only_match("/.*inet ([0-9\.]+) .*/", $ip); - - switch($ty) { - case "eth": - unset($res); - exec("ip link show $dev 2> /dev/null | grep -oP ' UP '", $res); - if (empty($res) && empty($ipadd)) { - $cl["device"][$i]["connected"] = "n"; - } else { - $cl["device"][$i]["connected"] = "y"; - } - break; - case "wlan": - unset($retiw); - exec("iwconfig $dev 2> /dev/null | sed -rn 's/.*(mode:master).*/1/ip'", $retiw); - $cl["device"][$i]["isAP"] = !empty($retiw); - unset($retiw); - exec("iw dev $dev link 2> /dev/null", $retiw); - if (!$simple && !empty($ssid=preg_only_match("/.*SSID:\s*([^\"]*).*/", $retiw)) ) { - $cl["device"][$i]["connected"] = "y"; - $cl["device"][$i]["ssid"] = $ssid; - $cl["device"][$i]["ssidutf8"] = ssid2utf8($ssid); - $cl["device"][$i]["ap-mac"] = preg_only_match("/^Connected to ([0-9a-f\:]*).*$/", $retiw); - $sig = preg_only_match("/.*signal: (.*)$/", $retiw); - $val = preg_only_match("/^([0-9\.-]*).*$/", $sig); - if (!is_numeric($val)) { - $val = -100; - } - if ($val >= -50 ) { - $qual=100; - } else if ($val < -100) { - $qual=0; - } else { - $qual=round($val*2+200); - } - $cl["device"][$i]["signal"] = "$sig (".$qual."%)"; - $cl["device"][$i]["bitrate"] = preg_only_match("/.*bitrate: ([0-9\.]* \w*\/s).*$/", $retiw); - $cl["device"][$i]["freq"] = preg_only_match("/.*freq: (.*)$/", $retiw); - $cl["device"][$i]["ap-mac"] = preg_only_match("/^Connected to ([0-9a-f\:]*).*$/", $retiw); - } else { - $cl["device"][$i]["connected"] = "n"; - } - break; - case "ppp": - unset($res); - exec("ip link show $dev 2> /dev/null | grep -oP '( UP | UNKNOWN)'", $res); - if ($simple) { - if (empty($res)) { - $cl["device"][$i]["connected"] = "n"; - $cl["device"][$i]["signal"] = "-100 dB (0%)"; - } else { - $cl["device"][$i]["connected"] = "y"; - $cl["device"][$i]["signal"] = "-0 dB (0%)"; - } - break; - } - if (empty($res) && empty($ipadd)) { - $cl["device"][$i]["connected"] = "n"; - } else { - $cl["device"][$i]["connected"] = "y"; - } - unset($res); - exec("$path/info_huawei.sh mode modem", $res); - $cl["device"][$i]["mode"] = $res[0]; - unset($res); - exec("$path/info_huawei.sh device modem", $res); - if ($res[0] != "none" ) { - $cl["device"][$i]["model"] = $res[0]; - } - unset($res); - exec("$path/info_huawei.sh signal modem", $res); - $cl["device"][$i]["signal"] = $res[0]; - unset($res); - exec("$path/info_huawei.sh operator modem", $res); - $cl["device"][$i]["operator"] = $res[0]; - break; - case "hilink": - $pin=$user=$pw=""; - getMobileLogin($pin,$pw,$user); - $opts=$pin.' '.$user.' '.$pw; - unset($res); - // exec("ip link show $dev 2> /dev/null | grep -oP ' UP '",$res); - exec("ifconfig -a | grep -i $dev -A 1 | grep -oP '(?<=inet )([0-9]{1,3}\.){3}'", $apiadd); - $apiadd = !empty($apiadd) ? $apiadd[0]."1" : ""; - unset($res); - exec("$path/info_huawei.sh mode hilink $apiadd \"$opts\" ", $res); - $cl["device"][$i]["mode"] = $res[0]; - unset($res); - exec("$path/info_huawei.sh device hilink $apiadd \"$opts\" ", $res); - if ($res[0] != "none" ) { - $cl["device"][$i]["model"] = $res[0]; - } - unset($res); - exec("$path/info_huawei.sh signal hilink $apiadd \"$opts\" ", $res); - $cl["device"][$i]["signal"] = $res[0]; - unset($ipadd); - exec("$path/info_huawei.sh ipaddress hilink $apiadd \"$opts\" ", $ipadd); - if (!empty($ipadd) && $ipadd[0] !== "none" ) { - $cl["device"][$i]["connected"] = "y"; - $cl["device"][$i]["wan_ip"] = $ipadd[0]; - } else { - $cl["device"][$i]["connected"] = "n"; - $cl["device"][$i]["wan_ip"] = "-"; - } - unset($res); - exec("$path/info_huawei.sh operator hilink $apiadd \"$opts\" ", $res); - $cl["device"][$i]["operator"] = $res[0]; - break; - case "phone": - case "usb": - $cl["device"][$i]["connected"] = "y"; - break; - default: - } - if (!isset($cl["device"][$i]["signal"])) { - $cl["device"][$i]["signal"]= $cl["device"][$i]["connected"] == "n" ? "-100 dB (0%)": "0 dB (100%)";; - } - if (!isset($cl["device"][$i]["isAP"])) { - $cl["device"][$i]["isAP"]=false; - } - } - } - return $cl; -} - -function getClientType($dev) { - loadClientConfig(); - // check if device type stored in DEVTYPE or raspapType (from UDEV rule) protperty of the device - exec("udevadm info /sys/class/net/$dev 2> /dev/null", $udevadm); - $type="none"; - if (!empty($udevadm)) { - $type=preg_only_match("/raspapType=(\w*)/i",$udevadm); - if (empty($type)) { - $type=preg_only_match("/DEVTYPE=(\w*)/i",$udevadm); - } - } - if (empty($type) || $type == "none" || array_search($type, $_SESSION["net-device-name-prefix"]) === false) { - // no device type yet -> get device type from device name - if (preg_match("/^(\w+)[0-9]$/",$dev,$nam) === 1) $nam=$nam[1]; - else $nam="none"; - if (($n = array_search($nam, $_SESSION["net-device-name-prefix"])) === false) $n = count($_SESSION["net-device-types"])-1; - $type = $_SESSION["net-device-types"][$n]; - } - return $type; -} - -function getMobileLogin(&$pin,&$pw,&$user) { - if (file_exists(($f = RASPI_MOBILEDATA_CONFIG))) { - $dat = parse_ini_file($f); - $pin = (isset($dat["pin"]) && preg_match("/^[0-9]*$/", $dat["pin"])) ? "-p ".$dat["pin"] : ""; - $user = (isset($dat["router_user"]) && !empty($dat["router_user"]) ) ? "-u ".$dat["router_user"] : ""; - $pw = (isset($dat["router_pw"]) && !empty($dat["router_pw"]) ) ? "-P ".$dat["router_pw"] : ""; - } -} - -function loadClientConfig() -{ - // load network device config file for UDEV rules into $_SESSION - if (!isset($_SESSION["udevrules"])) { - $_SESSION["net-device-types"]=array(); - $_SESSION["net-device-name-prefix"]=array(); - try { - $udevrules = file_get_contents(RASPI_CLIENT_CONFIG_PATH); - $_SESSION["udevrules"] = json_decode($udevrules, true); - // get device types - foreach ($_SESSION["udevrules"]["network_devices"] as $dev) { - $_SESSION["net-device-name-prefix"][]=$dev["name_prefix"]; - $_SESSION["net-device-types"][]=$dev["type"]; - $_SESSION["net-device-types-info"][]=$dev["type_info"]; - } - } catch (Exception $e) { - $_SESSION["udevrules"]= null; - } - $_SESSION["net-device-types"][]="none"; - $_SESSION["net-device-types-info"][]="unknown"; - $_SESSION["net-device-name-prefix"][]="none"; - } -} - -function findCurrentClientIndex($clients) -{ - $devid = -1; - if (!empty($clients)) { - $ncl=$clients["clients"]; - if ($ncl > 0) { - $ty=-1; - foreach ($clients["device"] as $i => $dev) { - $id=array_search($dev["type"], $_SESSION["net-device-types"]); - if ($id >=0 && $_SESSION["udevrules"]["network_devices"][$id]["clientid"] > $ty && !$dev["isAP"]) { - $ty=$id; - $devid=$i; - } - } - } - } - return $devid; -} - -function waitClientConnected($dev, $timeout=10) -{ - do { - exec('ifconfig -a | grep -i '.$dev.' -A 1 | grep -oP "(?<=inet )([0-9]{1,3}\.){3}[0-9]{1,3}"', $res); - $connected= !empty($res); - if (!$connected) { - sleep(1); - } - } while (!$connected && --$timeout > 0); - return $connected; -} - -function setClientState($state) -{ - $clients=getClients(); - if (($idx = findCurrentClientIndex($clients)) >= 0) { - $dev = $clients["device"][$idx]; - exec('ifconfig -a | grep -i '.$dev["name"].' -A 1 | grep -oP "(?<=inet )([0-9]{1,3}\.){3}[0-9]{1,3}"', $res); - if (!empty($res)) { - $connected=$res[0]; - } - switch($dev["type"]) { - case "wlan": - if ($state =="up") { - exec('sudo ip link set '.$dev["name"].' up'); - } - if (!empty($connected) && $state =="down") { - exec('sudo ip link set '.$dev["name"].' down'); - } - break; - case "hilink": - preg_match("/^([0-9]{1,3}\.){3}/", $connected, $ipadd); - $ipadd = $ipadd[0].'1'; // ip address of the Hilink api - $mode = ($state == "up") ? 1 : 0; - $pin=$user=$pw=""; - getMobileLogin($pin,$pw,$user); - exec('sudo '.RASPI_CLIENT_SCRIPT_PATH.'/onoff_huawei_hilink.sh -c '.$mode.' -h '.$ipadd.' '.$pin.' '.$user.' '.$pw); - break; - case "ppp": - if ($state == "up") { - exec('sudo ifup '.$dev["name"]); - } - if (!empty($connected) && $state == "down") { - exec('sudo ifdown '.$dev["name"]); - } - break; - default: - break; - } - if ($state=="up") { - waitClientConnected($dev["name"], 15); - } - } -} From eee8575d3f45dfe2d3920088d5350dee65334c30 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 11 Jul 2025 02:29:54 -0700 Subject: [PATCH 023/122] Initial commit --- includes/bootstrap.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 includes/bootstrap.php diff --git a/includes/bootstrap.php b/includes/bootstrap.php new file mode 100644 index 00000000..962a2539 --- /dev/null +++ b/includes/bootstrap.php @@ -0,0 +1,8 @@ + Date: Fri, 11 Jul 2025 02:30:44 -0700 Subject: [PATCH 024/122] Add cache-busting versioning to CSS and JS assets --- index.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/index.php b/index.php index 2b244391..14aa8e8b 100755 --- a/index.php +++ b/index.php @@ -23,6 +23,7 @@ * as you leave these references intact in the header comments of your source files. */ +require_once 'includes/bootstrap.php'; require_once 'includes/config.php'; require_once 'includes/autoload.php'; $handler = new RaspAP\Exceptions\ExceptionHandler; @@ -67,19 +68,19 @@ initializeApp(); <?php echo RASPI_BRAND_TITLE; ?> - + - + - + - + - + " title="main" rel="stylesheet"> @@ -123,25 +124,25 @@ initializeApp();
- + - + - + - + - + - + - + From 96aa83347701cfa8b97fcad6b729c50ffb97554a Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 11 Jul 2025 10:53:16 -0700 Subject: [PATCH 025/122] Fix: Replace match in detectBrowserLocale() for PHP 7.4 --- includes/locale.php | 75 ++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/includes/locale.php b/includes/locale.php index 17ab03e5..1b9d97bd 100755 --- a/includes/locale.php +++ b/includes/locale.php @@ -70,29 +70,56 @@ function detectBrowserLocale(): string return 'en_GB.UTF-8'; } - $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); - return match ($lang) { - 'de' => 'de_DE.UTF-8', - 'fr' => 'fr_FR.UTF-8', - 'it' => 'it_IT.UTF-8', - 'pt' => 'pt_BR.UTF-8', - 'sv' => 'sv_SE.UTF-8', - 'nl' => 'nl_NL.UTF-8', - 'zh' => ($_SERVER['HTTP_ACCEPT_LANGUAGE'] === 'zh_TW') ? 'zh_TW.UTF-8' : 'zh_CN.UTF-8', - 'cs' => 'cs_CZ.UTF-8', - 'ru' => 'ru_RU.UTF-8', - 'es' => 'es_MX.UTF-8', - 'fi' => 'fi_FI.UTF-8', - 'da' => 'da_DK.UTF-8', - 'tr' => 'tr_TR.UTF-8', - 'id' => 'id_ID.UTF-8', - 'ko' => 'ko_KR.UTF-8', - 'ja' => 'ja_JP.UTF-8', - 'vi' => 'vi_VN.UTF-8', - 'el' => 'el_GR.UTF-8', - 'pl' => 'pl_PL.UTF-8', - 'sk' => 'sk_SK.UTF-8', - default => 'en_GB.UTF-8', - }; + $acceptLang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; + $lang = strtolower(substr($acceptLang, 0, 2)); + + if ($lang === 'zh' && strpos($acceptLang, 'zh-TW') === 0) { + return 'zh_TW.UTF-8'; + } + + switch ($lang) { + case 'de': + return 'de_DE.UTF-8'; + case 'fr': + return 'fr_FR.UTF-8'; + case 'it': + return 'it_IT.UTF-8'; + case 'pt': + return 'pt_BR.UTF-8'; + case 'sv': + return 'sv_SE.UTF-8'; + case 'nl': + return 'nl_NL.UTF-8'; + case 'zh': + return 'zh_CN.UTF-8'; + case 'cs': + return 'cs_CZ.UTF-8'; + case 'ru': + return 'ru_RU.UTF-8'; + case 'es': + return 'es_MX.UTF-8'; + case 'fi': + return 'fi_FI.UTF-8'; + case 'da': + return 'da_DK.UTF-8'; + case 'tr': + return 'tr_TR.UTF-8'; + case 'id': + return 'id_ID.UTF-8'; + case 'ko': + return 'ko_KR.UTF-8'; + case 'ja': + return 'ja_JP.UTF-8'; + case 'vi': + return 'vi_VN.UTF-8'; + case 'el': + return 'el_GR.UTF-8'; + case 'pl': + return 'pl_PL.UTF-8'; + case 'sk': + return 'sk_SK.UTF-8'; + default: + return 'en_GB.UTF-8'; + } } From 80a5d97eee857af0bf04443c9e27109d6c767fca Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 12 Jul 2025 00:37:17 -0700 Subject: [PATCH 026/122] Update release version --- README.md | 2 +- includes/defaults.php | 2 +- index.php | 2 +- plugins | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fab84586..2fd0abba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![RaspAP Custom OS images](https://github.com/user-attachments/assets/e871adf1-123c-450b-94eb-80a185c242cc) -[![Release 3.3.6](https://img.shields.io/badge/release-v3.3.6-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) +[![Release 3.3.7](https://img.shields.io/badge/release-v3.3.7-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. diff --git a/includes/defaults.php b/includes/defaults.php index 33bbc68e..f658a51e 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) { $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel', - 'RASPI_VERSION' => '3.3.6', + 'RASPI_VERSION' => '3.3.7', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', diff --git a/index.php b/index.php index 14aa8e8b..5958cf8e 100755 --- a/index.php +++ b/index.php @@ -14,7 +14,7 @@ * @author Lawrence Yau * @author Bill Zimmerman * @license GNU General Public License, version 3 (GPL-3.0) - * @version 3.3.6 + * @version 3.3.7 * @link https://github.com/RaspAP/raspap-webgui/ * @link https://raspap.com/ * @see http://sirlagz.net/2013/02/08/raspap-webgui/ diff --git a/plugins b/plugins index 38331709..a867897e 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 38331709b6c8198c0dbf1c7c85cb52b8ae3ea79a +Subproject commit a867897ee51ef982a663c3f76b99b9950b8d4128 From 810218b67ea078e939cf284bd3f0212ef1767bf8 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:17:01 -0700 Subject: [PATCH 027/122] Add speedtest dependencies --- app/js/speedtestUI.js | 189 +++++ dist/speedtest/backend/empty.php | 14 + dist/speedtest/backend/garbage.php | 66 ++ dist/speedtest/backend/getIP.php | 325 ++++++++ .../speedtest/backend/getIP_ipInfo_apikey.php | 4 + .../backend/getIP_serverLocation.php | 3 + dist/speedtest/backend/getIP_util.php | 21 + dist/speedtest/speedtest.css | 231 ++++++ dist/speedtest/speedtest.js | 327 ++++++++ dist/speedtest/speedtest_worker.js | 725 ++++++++++++++++++ 10 files changed, 1905 insertions(+) create mode 100644 app/js/speedtestUI.js create mode 100755 dist/speedtest/backend/empty.php create mode 100755 dist/speedtest/backend/garbage.php create mode 100755 dist/speedtest/backend/getIP.php create mode 100755 dist/speedtest/backend/getIP_ipInfo_apikey.php create mode 100644 dist/speedtest/backend/getIP_serverLocation.php create mode 100755 dist/speedtest/backend/getIP_util.php create mode 100644 dist/speedtest/speedtest.css create mode 100755 dist/speedtest/speedtest.js create mode 100755 dist/speedtest/speedtest_worker.js diff --git a/app/js/speedtestUI.js b/app/js/speedtestUI.js new file mode 100644 index 00000000..e97b7c09 --- /dev/null +++ b/app/js/speedtestUI.js @@ -0,0 +1,189 @@ +function I(i){return document.getElementById(i);} + +const origin=window.location.origin; +const host=window.location.host; +var SPEEDTEST_SERVERS=[ + { + "name":"RaspAP Speedtest server (US)", + "server":"https://speedtest.raspap.com/", + "dlURL":"backend/garbage.php", + "ulURL":"backend/empty.php", + "pingURL":"backend/empty.php", + "getIpURL":"backend/getIP.php" + }, + { + "name":"RaspAP ("+host+")", + "server":origin, + "dlURL":"dist/speedtest/backend/garbage.php", + "ulURL":"dist/speedtest/backend/empty.php", + "pingURL":"dist/speedtest/backend/empty.php", + "getIpURL":"dist/speedtest/backend/getIP.php" + } +]; + +//INITIALIZE SPEEDTEST +var s=new Speedtest(); //create speedtest object +s.setParameter("telemetry_level","basic"); //enable telemetry + +//SERVER AUTO SELECTION +function initServers(){ + var noServersAvailable=function(){ + I("message").innerHTML="No servers available"; + } + var runServerSelect=function(){ + s.selectServer(function(server){ + if(server!=null){ //at least 1 server is available + I("loading").className="hidden"; //hide loading message + //populate server list for manual selection + for(var i=0;i 1024) { + return 1024; + } + + return (int) $_GET['ckSize']; +} + +/** + * @return void + */ +function sendHeaders() +{ + header('HTTP/1.1 200 OK'); + + if (isset($_GET['cors'])) { + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST'); + } + + // Indicate a file download + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename=random.dat'); + header('Content-Transfer-Encoding: binary'); + + // Cache settings: never cache this request + header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, s-maxage=0'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); +} + +// Determine how much data we should send +$chunks = getChunkCount(); + +// Generate data +if (function_exists('random_bytes')) { + $data = random_bytes(1048576); +} else { + $data = openssl_random_pseudo_bytes(1048576); +} + +// Deliver chunks of 1048576 bytes +sendHeaders(); +for ($i = 0; $i < $chunks; $i++) { + echo $data; + flush(); +} diff --git a/dist/speedtest/backend/getIP.php b/dist/speedtest/backend/getIP.php new file mode 100755 index 00000000..c5147400 --- /dev/null +++ b/dist/speedtest/backend/getIP.php @@ -0,0 +1,325 @@ + $processedString, + 'rawIspInfo' => $rawIspInfo ?: '', + ] + ); +} + +$ip = getClientIp(); + +$localIpInfo = getLocalOrPrivateIpInfo($ip); +// local ip, no need to fetch further information +if (is_string($localIpInfo)) { + sendResponse($ip, $localIpInfo); + exit; +} + +if (!isset($_GET['isp'])) { + sendResponse($ip); + exit; +} + +$rawIspInfo = getIspInfo($ip); +$isp = getIsp($rawIspInfo); +$distance = getDistance($rawIspInfo); + +sendResponse($ip, $isp, $distance, $rawIspInfo); diff --git a/dist/speedtest/backend/getIP_ipInfo_apikey.php b/dist/speedtest/backend/getIP_ipInfo_apikey.php new file mode 100755 index 00000000..e200c3f1 --- /dev/null +++ b/dist/speedtest/backend/getIP_ipInfo_apikey.php @@ -0,0 +1,4 @@ +* { + display: block; + width: 100%; + height: auto; + margin: 0.25em 0; +} + +#privacyPolicy { + position: fixed; + top: 2em; + bottom: 2em; + left: 2em; + right: 2em; + overflow-y: auto; + margin: 0 auto; + width: 50%; + height: auto; + box-shadow: 0 0 3em 1em #333; + z-index: 999999; + text-align: left; + background-color: #FFFFFF; + padding: 1em; + border-radius: 0.3em; + color: #858796; +} + +#privacyPolicy h4, h5 { + color: #212529; +} + +a.privacy { + text-align: center; + font-size: 0.8em; + color: #808080; + display: block; +} + +@media all and (max-width:40em) { + body { + font-size: 0.8em; + } +} + +div.visible { + animation: fadeIn 0.4s; + display: block; +} + +div.hidden { + animation: fadeOut 0.4s; + display: none; +} + +div.centered { + margin: 0 auto; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + display: block; + opacity: 1; + } + + 100% { + display: block; + opacity: 0; + } +} diff --git a/dist/speedtest/speedtest.js b/dist/speedtest/speedtest.js new file mode 100755 index 00000000..1197cb4f --- /dev/null +++ b/dist/speedtest/speedtest.js @@ -0,0 +1,327 @@ +/* + LibreSpeed - Main + by Federico Dossena + https://github.com/librespeed/speedtest/ + GNU LGPLv3 License +*/ + +function Speedtest() { + this._serverList = []; //when using multiple points of test, this is a list of test points + this._selectedServer = null; //when using multiple points of test, this is the selected server + this._settings = {}; //settings for the speedtest worker + this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done +} + +Speedtest.prototype = { + constructor: Speedtest, + /** + * Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done + */ + getState: function() { + return this._state; + }, + /** + * Change one of the test settings from their defaults. + * - parameter: string with the name of the parameter that you want to set + * - value: new value for the parameter + * + * Invalid values or nonexistant parameters will be ignored by the speedtest worker. + */ + setParameter: function(parameter, value) { + if (this._state == 3) + throw "You cannot change the test settings while running the test"; + this._settings[parameter] = value; + if(parameter === "telemetry_extra"){ + this._originalExtra=this._settings.telemetry_extra; + } + }, + /** + * Used internally to check if a server object contains all the required elements. + * Also fixes the server URL if needed. + */ + _checkServerDefinition: function(server) { + try { + if (typeof server.name !== "string") + throw "Name string missing from server definition (name)"; + if (typeof server.server !== "string") + throw "Server address string missing from server definition (server)"; + if (server.server.charAt(server.server.length - 1) != "/") + server.server += "/"; + if (server.server.indexOf("//") == 0) + server.server = location.protocol + server.server; + if (typeof server.dlURL !== "string") + throw "Download URL string missing from server definition (dlURL)"; + if (typeof server.ulURL !== "string") + throw "Upload URL string missing from server definition (ulURL)"; + if (typeof server.pingURL !== "string") + throw "Ping URL string missing from server definition (pingURL)"; + if (typeof server.getIpURL !== "string") + throw "GetIP URL string missing from server definition (getIpURL)"; + } catch (e) { + throw "Invalid server definition"; + } + }, + /** + * Add a test point (multiple points of test) + * server: the server to be added as an object. Defined in app/js/speedtestUI.js + */ + addTestPoint: function(server) { + this._checkServerDefinition(server); + if (this._state == 0) this._state = 1; + if (this._state != 1) throw "You can't add a server after server selection"; + this._settings.mpot = true; + this._serverList.push(server); + }, + /** + * Same as addTestPoint, but you can pass an array of servers + */ + addTestPoints: function(list) { + for (var i = 0; i < list.length; i++) this.addTestPoint(list[i]); + }, + /** + * Load a JSON server list from URL (multiple points of test) + * url: the url where the server list can be fetched. Must be an array with objects containing the following elements: + * result: callback to be called when the list is loaded correctly. An array with the loaded servers will be passed to this function, or null if it failed + */ + loadServerList: function(url,result) { + if (this._state == 0) this._state = 1; + if (this._state != 1) throw "You can't add a server after server selection"; + this._settings.mpot = true; + var xhr = new XMLHttpRequest(); + xhr.onload = function(){ + try{ + var servers=JSON.parse(xhr.responseText); + for(var i=0;i= 3) + throw "You can't select a server while the test is running"; + } + if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true; + /*this function goes through a list of servers. For each server, the ping is measured, then the server with the function selected is called with the best server, or null if all the servers were down. + */ + var select = function(serverList, selected) { + //pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong. + var PING_TIMEOUT = 2000; + var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers + if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) { + //IE11 doesn't support XHR timeout + USE_PING_TIMEOUT = false; + } + var ping = function(url, rtt) { + url += (url.match(/\?/) ? "&" : "?") + "cors=true"; + var xhr = new XMLHttpRequest(); + var t = new Date().getTime(); + xhr.onload = function() { + if (xhr.responseText.length == 0) { + //we expect an empty response + var instspd = new Date().getTime() - t; //rough timing estimate + try { + //try to get more accurate timing using performance API + var p = performance.getEntriesByName(url); + p = p[p.length - 1]; + var d = p.responseStart - p.requestStart; + if (d <= 0) d = p.duration; + if (d > 0 && d < instspd) instspd = d; + } catch (e) {} + rtt(instspd); + } else rtt(-1); + }.bind(this); + xhr.onerror = function() { + rtt(-1); + }.bind(this); + xhr.open("GET", url); + if (USE_PING_TIMEOUT) { + try { + xhr.timeout = PING_TIMEOUT; + xhr.ontimeout = xhr.onerror; + } catch (e) {} + } + xhr.send(); + }.bind(this); + + /** + * This function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. + * At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong. + */ + var PINGS = 3, //up to 3 pings are performed, unless the server is down... + SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold + var checkServer = function(server, done) { + var i = 0; + server.pingT = -1; + if (server.server.indexOf(location.protocol) == -1) done(); + else { + var nextPing = function() { + if (i++ == PINGS) { + done(); + return; + } + ping( + server.server + server.pingURL, + function(t) { + if (t >= 0) { + if (t < server.pingT || server.pingT == -1) server.pingT = t; + if (t < SLOW_THRESHOLD) nextPing(); + else done(); + } else done(); + }.bind(this) + ); + }.bind(this); + nextPing(); + } + }.bind(this); + //check servers in list, one by one + var i = 0; + var done = function() { + var bestServer = null; + for (var i = 0; i < serverList.length; i++) { + if ( + serverList[i].pingT != -1 && + (bestServer == null || serverList[i].pingT < bestServer.pingT) + ) + bestServer = serverList[i]; + } + selected(bestServer); + }.bind(this); + var nextServer = function() { + if (i == serverList.length) { + done(); + return; + } + checkServer(serverList[i++], nextServer); + }.bind(this); + nextServer(); + }.bind(this); + + //parallel server selection + var CONCURRENCY = 6; + var serverLists = []; + for (var i = 0; i < CONCURRENCY; i++) { + serverLists[i] = []; + } + for (var i = 0; i < this._serverList.length; i++) { + serverLists[i % CONCURRENCY].push(this._serverList[i]); + } + var completed = 0; + var bestServer = null; + for (var i = 0; i < CONCURRENCY; i++) { + select( + serverLists[i], + function(server) { + if (server != null) { + if (bestServer == null || server.pingT < bestServer.pingT) + bestServer = server; + } + completed++; + if (completed == CONCURRENCY) { + this._selectedServer = bestServer; + this._state = 2; + if (result) result(bestServer); + } + }.bind(this) + ); + } + }, + /** + * Starts the test. + * During the test, the onupdate(data) callback function will be called periodically with data from the worker. + * At the end of the test, the onend(aborted) function will be called with a boolean telling you if the test was aborted or if it ended normally. + */ + start: function() { + if (this._state == 3) throw "Test already running"; + this.worker = new Worker("dist/speedtest/speedtest_worker.js?r=" + Math.random()); + this.worker.onmessage = function(e) { + if (e.data === this._prevData) return; + else this._prevData = e.data; + var data = JSON.parse(e.data); + try { + if (this.onupdate) this.onupdate(data); + } catch (e) { + console.error("Speedtest onupdate event threw exception: " + e); + } + if (data.testState >= 4) { + clearInterval(this.updater); + this._state = 4; + try { + if (this.onend) this.onend(data.testState == 5); + } catch (e) { + console.error("Speedtest onend event threw exception: " + e); + } + } + }.bind(this); + this.updater = setInterval( + function() { + this.worker.postMessage("status"); + }.bind(this), + 200 + ); + + if (this._state == 1) + throw "When using multiple points of test, you must call selectServer before starting the test"; + if (this._state == 2) { + this._settings.url_dl = + this._selectedServer.server + this._selectedServer.dlURL; + this._settings.url_ul = + this._selectedServer.server + this._selectedServer.ulURL; + this._settings.url_ping = + this._selectedServer.server + this._selectedServer.pingURL; + this._settings.url_getIp = + this._selectedServer.server + this._selectedServer.getIpURL; + if (typeof this._originalExtra !== "undefined") { + this._settings.telemetry_extra = JSON.stringify({ + server: this._selectedServer.name, + extra: this._originalExtra + }); + } else + this._settings.telemetry_extra = JSON.stringify({ + server: this._selectedServer.name + }); + } + this._state = 3; + this.worker.postMessage("start " + JSON.stringify(this._settings)); + }, + /** + * Aborts the test while it's running. + */ + abort: function() { + if (this._state < 3) throw "You cannot abort a test that's not started yet"; + if (this._state < 4) this.worker.postMessage("abort"); + } +}; + diff --git a/dist/speedtest/speedtest_worker.js b/dist/speedtest/speedtest_worker.js new file mode 100755 index 00000000..a575ba96 --- /dev/null +++ b/dist/speedtest/speedtest_worker.js @@ -0,0 +1,725 @@ +/* + LibreSpeed - Worker + by Federico Dossena + https://github.com/librespeed/speedtest/ + GNU LGPLv3 License + */ + +// data reported to main thread +var testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort +var dlStatus = ""; // download speed in megabit/s with 2 decimal digits +var ulStatus = ""; // upload speed in megabit/s with 2 decimal digits +var pingStatus = ""; // ping in milliseconds with 2 decimal digits +var jitterStatus = ""; // jitter in milliseconds with 2 decimal digits +var clientIp = ""; // client's IP address as reported by getIP.php +var dlProgress = 0; //progress of download test 0-1 +var ulProgress = 0; //progress of upload test 0-1 +var pingProgress = 0; //progress of ping+jitter test 0-1 +var testId = null; //test ID (sent back by telemetry if used, null otherwise) + +var log = ""; //telemetry log +function tlog(s) { + if (settings.telemetry_level >= 2) { + log += Date.now() + ": " + s + "\n"; + } +} +function tverb(s) { + if (settings.telemetry_level >= 3) { + log += Date.now() + ": " + s + "\n"; + } +} +function twarn(s) { + if (settings.telemetry_level >= 2) { + log += Date.now() + " WARN: " + s + "\n"; + } + console.warn(s); +} + +// test settings. can be overridden by sending specific values with the start command +var settings = { + mpot: true, //set to true when in MPOT mode + test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay + time_ul_max: 15, // max duration of upload test in seconds + time_dl_max: 15, // max duration of download test in seconds + time_auto: true, // if set to true, tests will take less time on faster connections + time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill) + time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase) + count_ping: 10, // number of pings to perform in ping test + url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file + url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file + url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file + url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip + getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address + getIp_ispInfo_distance: "km", //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work + xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active) + xhr_ulMultistream: 3, // number of upload streams to use (can be different if enable_quirks is active) + xhr_multistreamDelay: 300, //how much concurrent requests should be delayed + xhr_ignoreErrors: 1, // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors + xhr_dlUseBlob: false, // if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream) + xhr_ul_blob_megabytes: 20, //size in megabytes of the upload blobs sent in the upload test (forced to 4 on chrome mobile) + garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active) + enable_quirks: true, // enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command + ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided. + overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values) + useMebibits: false, //if set to true, speed will be reported in mebibits/s instead of megabits/s + telemetry_level: 1, // 0=disabled, 1=basic (results only), 2=full (results and timing) 3=debug (results+log) + url_telemetry: "https://speedtest.raspap.com/results/telemetry.php", // path to the script that adds telemetry data to the database + telemetry_extra: "", //extra data that can be passed to the telemetry through the settings + forceIE11Workaround: false //when set to true, it will foce the IE11 upload test on all browsers. Debug only +}; + +var xhr = null; // array of currently active xhr requests +var interval = null; // timer used in tests +var test_pointer = 0; //pointer to the next test to run inside settings.test_order + +/* + this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator + */ +function url_sep(url) { + return url.match(/\?/) ? "&" : "?"; +} + +/* + listener for commands from main thread to this worker. + commands: + -status: returns the current status as a JSON string containing testState, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress + -abort: aborts the current test + -start: starts the test. optionally, settings can be passed as JSON. + example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"} + */ +this.addEventListener("message", function(e) { + var params = e.data.split(" "); + if (params[0] === "status") { + // return status + postMessage( + JSON.stringify({ + testState: testState, + dlStatus: dlStatus, + ulStatus: ulStatus, + pingStatus: pingStatus, + clientIp: clientIp, + jitterStatus: jitterStatus, + dlProgress: dlProgress, + ulProgress: ulProgress, + pingProgress: pingProgress, + testId: testId + }) + ); + } + if (params[0] === "start" && testState === -1) { + // start new test + testState = 0; + try { + // parse settings, if present + var s = {}; + try { + var ss = e.data.substring(5); + if (ss) s = JSON.parse(ss); + } catch (e) { + twarn("Error parsing custom settings JSON. Please check your syntax"); + } + //copy custom settings + for (var key in s) { + if (typeof settings[key] !== "undefined") settings[key] = s[key]; + else twarn("Unknown setting ignored: " + key); + } + var ua = navigator.userAgent; + // quirks for specific browsers. apply only if not overridden. more may be added in future releases + if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) { + if (/Firefox.(\d+\.\d+)/i.test(ua)) { + if (typeof s.ping_allowPerformanceApi === "undefined") { + // ff performance API sucks + settings.ping_allowPerformanceApi = false; + } + } + if (/Edge.(\d+\.\d+)/i.test(ua)) { + if (typeof s.xhr_dlMultistream === "undefined") { + // edge more precise with 3 download streams + settings.xhr_dlMultistream = 3; + } + } + if (/Chrome.(\d+)/i.test(ua) && !!self.fetch) { + if (typeof s.xhr_dlMultistream === "undefined") { + // chrome more precise with 5 streams + settings.xhr_dlMultistream = 5; + } + } + } + if (/Edge.(\d+\.\d+)/i.test(ua)) { + //Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy + settings.forceIE11Workaround = true; + } + if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) { + //PS4 browser has the same bug as IE11/Edge + settings.forceIE11Workaround = true; + } + if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) { + //cheap af + //Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes + settings.xhr_ul_blob_megabytes = 4; + } + if (/^((?!chrome|android|crios|fxios).)*safari/i.test(ua)) { + //Safari also needs the IE11 workaround but only for the MPOT version + settings.forceIE11Workaround = true; + } + //telemetry_level has to be parsed and not just copied + if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level + //transform test_order to uppercase, just in case + settings.test_order = settings.test_order.toUpperCase(); + } catch (e) { + twarn("Possible error in custom test settings. Some settings might not have been applied. Exception: " + e); + } + // run the tests + tverb(JSON.stringify(settings)); + test_pointer = 0; + var iRun = false, + dRun = false, + uRun = false, + pRun = false; + var runNextTest = function() { + if (testState == 5) return; + if (test_pointer >= settings.test_order.length) { + //test is finished + if (settings.telemetry_level > 0) + sendTelemetry(function(id) { + testState = 4; + if (id != null) testId = id; + }); + else testState = 4; + return; + } + switch (settings.test_order.charAt(test_pointer)) { + case "I": + { + test_pointer++; + if (iRun) { + runNextTest(); + return; + } else iRun = true; + getIp(runNextTest); + } + break; + case "D": + { + test_pointer++; + if (dRun) { + runNextTest(); + return; + } else dRun = true; + testState = 1; + dlTest(runNextTest); + } + break; + case "U": + { + test_pointer++; + if (uRun) { + runNextTest(); + return; + } else uRun = true; + testState = 3; + ulTest(runNextTest); + } + break; + case "P": + { + test_pointer++; + if (pRun) { + runNextTest(); + return; + } else pRun = true; + testState = 2; + pingTest(runNextTest); + } + break; + case "_": + { + test_pointer++; + setTimeout(runNextTest, 1000); + } + break; + default: + test_pointer++; + } + }; + runNextTest(); + } + if (params[0] === "abort") { + // abort command + if (testState >= 4) return; + tlog("manually aborted"); + clearRequests(); // stop all xhr activity + runNextTest = null; + if (interval) clearInterval(interval); // clear timer if present + if (settings.telemetry_level > 1) sendTelemetry(function() {}); + testState = 5; //set test as aborted + dlStatus = ""; + ulStatus = ""; + pingStatus = ""; + jitterStatus = ""; + clientIp = ""; + dlProgress = 0; + ulProgress = 0; + pingProgress = 0; + } +}); +// stops all XHR activity, aggressively +function clearRequests() { + tverb("stopping pending XHRs"); + if (xhr) { + for (var i = 0; i < xhr.length; i++) { + try { + xhr[i].onprogress = null; + xhr[i].onload = null; + xhr[i].onerror = null; + } catch (e) {} + try { + xhr[i].upload.onprogress = null; + xhr[i].upload.onload = null; + xhr[i].upload.onerror = null; + } catch (e) {} + try { + xhr[i].abort(); + } catch (e) {} + try { + delete xhr[i]; + } catch (e) {} + } + xhr = null; + } +} +// gets client's IP using url_getIp, then calls the done function +var ipCalled = false; // used to prevent multiple accidental calls to getIp +var ispInfo = ""; //used for telemetry +function getIp(done) { + tverb("getIp"); + if (ipCalled) return; + else ipCalled = true; // getIp already called? + var startT = new Date().getTime(); + xhr = new XMLHttpRequest(); + xhr.onload = function() { + tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms"); + try { + var data = JSON.parse(xhr.responseText); + clientIp = data.processedString; + ispInfo = data.rawIspInfo; + } catch (e) { + clientIp = xhr.responseText; + ispInfo = ""; + } + done(); + }; + xhr.onerror = function() { + tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms"); + done(); + }; + xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.mpot ? "cors=true&" : "") + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true); + xhr.send(); +} +// download test, calls done function when it's over +var dlCalled = false; // used to prevent multiple accidental calls to dlTest +function dlTest(done) { + tverb("dlTest"); + if (dlCalled) return; + else dlCalled = true; // dlTest already called? + var totLoaded = 0.0, // total number of loaded bytes + startT = new Date().getTime(), // timestamp when test was started + bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) + graceTimeDone = false, //set to true after the grace time is past + failed = false; // set to true if a stream fails + xhr = []; + // function to create a download stream. streams are slightly delayed so that they will not end at the same time + var testStream = function(i, delay) { + setTimeout( + function() { + if (testState !== 1) return; // delayed stream ended up starting after the end of the download test + tverb("dl test stream started " + i + " " + delay); + var prevLoaded = 0; // number of bytes loaded last time onprogress was called + var x = new XMLHttpRequest(); + xhr[i] = x; + xhr[i].onprogress = function(event) { + tverb("dl stream progress event " + i + " " + event.loaded); + if (testState !== 1) { + try { + x.abort(); + } catch (e) {} + } // just in case this XHR is still running after the download test + // progress event, add number of new loaded bytes to totLoaded + var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; + if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case + totLoaded += loadDiff; + prevLoaded = event.loaded; + }.bind(this); + xhr[i].onload = function() { + // the large file has been loaded entirely, start again + tverb("dl stream finished " + i); + try { + xhr[i].abort(); + } catch (e) {} // reset the stream data to empty ram + testStream(i, 0); + }.bind(this); + xhr[i].onerror = function() { + // error + tverb("dl stream failed " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; //abort + try { + xhr[i].abort(); + } catch (e) {} + delete xhr[i]; + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream + }.bind(this); + // send xhr + try { + if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob"; + else xhr[i].responseType = "arraybuffer"; + } catch (e) {} + xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching + xhr[i].send(); + }.bind(this), + 1 + delay + ); + }.bind(this); + // open streams + for (var i = 0; i < settings.xhr_dlMultistream; i++) { + testStream(i, settings.xhr_multistreamDelay * i); + } + // every 200ms, update dlStatus + interval = setInterval( + function() { + tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)")); + var t = new Date().getTime() - startT; + if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000); + if (t < 200) return; + if (!graceTimeDone) { + if (t > 1000 * settings.time_dlGraceTime) { + if (totLoaded > 0) { + // if the connection is so slow that we didn't get a single chunk yet, do not reset + startT = new Date().getTime(); + bonusT = 0; + totLoaded = 0.0; + } + graceTimeDone = true; + } + } else { + var speed = totLoaded / (t / 1000.0); + if (settings.time_auto) { + //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here + var bonus = (5.0 * speed) / 100000; + bonusT += bonus > 400 ? 400 : bonus; + } + //update status + dlStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits + if ((t + bonusT) / 1000.0 > settings.time_dl_max || failed) { + // test is over, stop streams and timer + if (failed || isNaN(dlStatus)) dlStatus = "Fail"; + clearRequests(); + clearInterval(interval); + dlProgress = 1; + tlog("dlTest: " + dlStatus + ", took " + (new Date().getTime() - startT) + "ms"); + done(); + } + } + }.bind(this), + 200 + ); +} +// upload test, calls done function whent it's over +var ulCalled = false; // used to prevent multiple accidental calls to ulTest +function ulTest(done) { + tverb("ulTest"); + if (ulCalled) return; + else ulCalled = true; // ulTest already called? + // garbage data for upload test + var r = new ArrayBuffer(1048576); + var maxInt = Math.pow(2, 32) - 1; + try { + r = new Uint32Array(r); + for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; + } catch (e) {} + var req = []; + var reqsmall = []; + for (var i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r); + req = new Blob(req); + r = new ArrayBuffer(262144); + try { + r = new Uint32Array(r); + for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; + } catch (e) {} + reqsmall.push(r); + reqsmall = new Blob(reqsmall); + var testFunction = function() { + var totLoaded = 0.0, // total number of transmitted bytes + startT = new Date().getTime(), // timestamp when test was started + bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) + graceTimeDone = false, //set to true after the grace time is past + failed = false; // set to true if a stream fails + xhr = []; + // function to create an upload stream. streams are slightly delayed so that they will not end at the same time + var testStream = function(i, delay) { + setTimeout( + function() { + if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test + tverb("ul test stream started " + i + " " + delay); + var prevLoaded = 0; // number of bytes transmitted last time onprogress was called + var x = new XMLHttpRequest(); + xhr[i] = x; + var ie11workaround; + if (settings.forceIE11Workaround) ie11workaround = true; + else { + try { + xhr[i].upload.onprogress; + ie11workaround = false; + } catch (e) { + ie11workaround = true; + } + } + if (ie11workaround) { + // IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections + xhr[i].onload = xhr[i].onerror = function() { + tverb("ul stream progress event (ie11wa)"); + totLoaded += reqsmall.size; + testStream(i, 0); + }; + xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching + try { + xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) + } catch (e) {} + //No Content-Type header in MPOT branch because it triggers bugs in some browsers + xhr[i].send(reqsmall); + } else { + // REGULAR version, no workaround + xhr[i].upload.onprogress = function(event) { + tverb("ul stream progress event " + i + " " + event.loaded); + if (testState !== 3) { + try { + x.abort(); + } catch (e) {} + } // just in case this XHR is still running after the upload test + // progress event, add number of new loaded bytes to totLoaded + var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; + if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case + totLoaded += loadDiff; + prevLoaded = event.loaded; + }.bind(this); + xhr[i].upload.onload = function() { + // this stream sent all the garbage data, start again + tverb("ul stream finished " + i); + testStream(i, 0); + }.bind(this); + xhr[i].upload.onerror = function() { + tverb("ul stream failed " + i); + if (settings.xhr_ignoreErrors === 0) failed = true; //abort + try { + xhr[i].abort(); + } catch (e) {} + delete xhr[i]; + if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream + }.bind(this); + // send xhr + xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching + try { + xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) + } catch (e) {} + //No Content-Type header in MPOT branch because it triggers bugs in some browsers + xhr[i].send(req); + } + }.bind(this), + delay + ); + }.bind(this); + // open streams + for (var i = 0; i < settings.xhr_ulMultistream; i++) { + testStream(i, settings.xhr_multistreamDelay * i); + } + // every 200ms, update ulStatus + interval = setInterval( + function() { + tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)")); + var t = new Date().getTime() - startT; + if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000); + if (t < 200) return; + if (!graceTimeDone) { + if (t > 1000 * settings.time_ulGraceTime) { + if (totLoaded > 0) { + // if the connection is so slow that we didn't get a single chunk yet, do not reset + startT = new Date().getTime(); + bonusT = 0; + totLoaded = 0.0; + } + graceTimeDone = true; + } + } else { + var speed = totLoaded / (t / 1000.0); + if (settings.time_auto) { + //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here + var bonus = (5.0 * speed) / 100000; + bonusT += bonus > 400 ? 400 : bonus; + } + //update status + ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits + if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) { + // test is over, stop streams and timer + if (failed || isNaN(ulStatus)) ulStatus = "Fail"; + clearRequests(); + clearInterval(interval); + ulProgress = 1; + tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms"); + done(); + } + } + }.bind(this), + 200 + ); + }.bind(this); + if (settings.mpot) { + tverb("Sending POST request before performing upload test"); + xhr = []; + xhr[0] = new XMLHttpRequest(); + xhr[0].onload = xhr[0].onerror = function() { + tverb("POST request sent, starting upload test"); + testFunction(); + }.bind(this); + xhr[0].open("POST", settings.url_ul) + (settings.mpot ? "cors=true&" : ""); + xhr[0].send(); + } else testFunction(); +} +// ping+jitter test, function done is called when it's over +var ptCalled = false; // used to prevent multiple accidental calls to pingTest +function pingTest(done) { + tverb("pingTest"); + if (ptCalled) return; + else ptCalled = true; // pingTest already called? + var startT = new Date().getTime(); //when the test was started + var prevT = null; // last time a pong was received + var ping = 0.0; // current ping value + var jitter = 0.0; // current jitter value + var i = 0; // counter of pongs received + var prevInstspd = 0; // last ping time, used for jitter calculation + xhr = []; + // ping function + var doPing = function() { + tverb("ping"); + pingProgress = i / settings.count_ping; + prevT = new Date().getTime(); + xhr[0] = new XMLHttpRequest(); + xhr[0].onload = function() { + // pong + tverb("pong"); + if (i === 0) { + prevT = new Date().getTime(); // first pong + } else { + var instspd = new Date().getTime() - prevT; + if (settings.ping_allowPerformanceApi) { + try { + //try to get accurate performance timing using performance api + var p = performance.getEntries(); + p = p[p.length - 1]; + var d = p.responseStart - p.requestStart; + if (d <= 0) d = p.duration; + if (d > 0 && d < instspd) instspd = d; + } catch (e) { + //if not possible, keep the estimate + tverb("Performance API not supported, using estimate"); + } + } + //noticed that some browsers randomly have 0ms ping + if (instspd < 1) instspd = prevInstspd; + if (instspd < 1) instspd = 1; + var instjitter = Math.abs(instspd - prevInstspd); + if (i === 1) ping = instspd; + /* first ping, can't tell jitter yet*/ else { + if (instspd < ping) ping = instspd; // update ping, if the instant ping is lower + if (i === 2) jitter = instjitter; + //discard the first jitter measurement because it might be much higher than it should be + else jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2; // update jitter, weighted average. spikes in ping values are given more weight. + } + prevInstspd = instspd; + } + pingStatus = ping.toFixed(2); + jitterStatus = jitter.toFixed(2); + i++; + tverb("ping: " + pingStatus + " jitter: " + jitterStatus); + if (i < settings.count_ping) doPing(); + else { + // more pings to do? + pingProgress = 1; + tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms"); + done(); + } + }.bind(this); + xhr[0].onerror = function() { + // a ping failed, cancel test + tverb("ping failed"); + if (settings.xhr_ignoreErrors === 0) { + //abort + pingStatus = "Fail"; + jitterStatus = "Fail"; + clearRequests(); + tlog("ping test failed, took " + (new Date().getTime() - startT) + "ms"); + pingProgress = 1; + done(); + } + if (settings.xhr_ignoreErrors === 1) doPing(); //retry ping + if (settings.xhr_ignoreErrors === 2) { + //ignore failed ping + i++; + if (i < settings.count_ping) doPing(); + else { + // more pings to do? + pingProgress = 1; + tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms"); + done(); + } + } + }.bind(this); + // send xhr + xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching + xhr[0].send(); + }.bind(this); + doPing(); // start first ping +} +// telemetry +function sendTelemetry(done) { + if (settings.telemetry_level < 1) return; + xhr = new XMLHttpRequest(); + xhr.onload = function() { + try { + var parts = xhr.responseText.split(" "); + if (parts[0] == "id") { + try { + var id = parts[1]; + done(id); + } catch (e) { + done(null); + } + } else done(null); + } catch (e) { + done(null); + } + }; + xhr.onerror = function() { + console.log("TELEMETRY ERROR " + xhr.status); + done(null); + }; + xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); + var telemetryIspInfo = { + processedString: clientIp, + rawIspInfo: typeof ispInfo === "object" ? ispInfo : "" + }; + try { + var fd = new FormData(); + fd.append("ispinfo", JSON.stringify(telemetryIspInfo)); + fd.append("dl", dlStatus); + fd.append("ul", ulStatus); + fd.append("ping", pingStatus); + fd.append("jitter", jitterStatus); + fd.append("log", settings.telemetry_level > 1 ? log : ""); + fd.append("extra", settings.telemetry_extra); + xhr.send(fd); + } catch (ex) { + var postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&ispinfo=" + encodeURIComponent(JSON.stringify(telemetryIspInfo)) + "&dl=" + encodeURIComponent(dlStatus) + "&ul=" + encodeURIComponent(ulStatus) + "&ping=" + encodeURIComponent(pingStatus) + "&jitter=" + encodeURIComponent(jitterStatus) + "&log=" + encodeURIComponent(settings.telemetry_level > 1 ? log : ""); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.send(postData); + } +} + From b5e79b9148d15dfc632d80f2d4bde365ae73f7c1 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:18:56 -0700 Subject: [PATCH 028/122] Enable wg kill switch --- includes/wireguard.php | 99 ++++++++++++++++++++++++++++++---------- templates/wg/general.php | 11 ++++- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/includes/wireguard.php b/includes/wireguard.php index 233dd33b..ea2b1fe9 100755 --- a/includes/wireguard.php +++ b/includes/wireguard.php @@ -1,28 +1,33 @@ addMessage('Attempting to start WireGuard', 'info'); + exec('sudo /bin/systemctl enable wg-quick@wg0', $return); exec('sudo /bin/systemctl start wg-quick@wg0', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); @@ -30,6 +35,7 @@ function DisplayWireGuardConfig() } elseif (isset($_POST['stopwg'])) { $status->addMessage('Attempting to stop WireGuard', 'info'); exec('sudo /bin/systemctl stop wg-quick@wg0', $return); + exec('sudo /bin/systemctl disable wg-quick@wg0', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); } @@ -70,11 +76,18 @@ function DisplayWireGuardConfig() $wg_state = ($wgstatus[0] == 'active' ? true : false ); $public_ip = get_public_ip(); - // retrieve wg log - $wg_log = ""; + // fetch uploaded file configs + exec("sudo ls ".RASPI_WIREGUARD_PATH, $clist); + $configs = preg_grep('/^((?!wg0).)*\.conf/', $clist); + exec("sudo readlink ".RASPI_WIREGUARD_CONFIG." | xargs basename", $ret); + $conf_default = empty($ret) ? "none" : $ret[0]; + + // fetch wg log + exec('sudo chmod o+r /tmp/wireguard.log'); if (file_exists('/tmp/wireguard.log')) { - exec('sudo chmod o+r /tmp/wireguard.log'); - $wg_log = file_get_contents('/tmp/wireguard.log'); + $log = file_get_contents('/tmp/wireguard.log'); + } else { + $log = ''; } $peer_id = $peer_id ?? "1"; @@ -90,6 +103,7 @@ function DisplayWireGuardConfig() "public_ip", "interfaces", "optRules", + "optKSwitch", "optLogEnable", "peer_id", "wg_srvpubkey", @@ -104,7 +118,9 @@ function DisplayWireGuardConfig() "wg_pendpoint", "wg_pallowedips", "wg_pkeepalive", - "wg_log" + "configs", + "conf_default", + "log" ) ); } @@ -116,10 +132,11 @@ function DisplayWireGuardConfig() * @param object $status * @param object $file * @param boolean $optRules + * @param boolean $optKSwitch * @param string $optInterface * @return object $status */ -function SaveWireGuardUpload($status, $file, $optRules, $optInterface) +function SaveWireGuardUpload($status, $file, $optRules, $optKSwitch, $optInterface) { define('KB', 1024); $tmp_destdir = '/tmp/'; @@ -148,19 +165,56 @@ function SaveWireGuardUpload($status, $file, $optRules, $optInterface) $tmp_wgconfig = $results['full_path']; $tmp_contents = file_get_contents($tmp_wgconfig); - // Set iptables rules - if (isset($optRules) && !preg_match('/PostUp|PostDown/m',$tmp_contents)) { - $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUp'); - $rules[] = 'PostDown = '.getDefaultNetValue('wireguard','server','PostDown'); - $rules[] = ''; - $rules = join(PHP_EOL, $rules); - $rules = preg_replace('/wlan0/m', $optInterface, $rules); - $tmp_contents = preg_replace('/^\s*$/ms', $rules, $tmp_contents, 1); - file_put_contents($tmp_wgconfig, $tmp_contents); + // Check for existing iptables rules + if ((isset($optRules) || isset($optKSwitch)) && preg_match('/PostUp|PostDown|PreDown/m',$tmp_contents)) { + $status->addMessage('Existing iptables rules found in WireGuard configuration - not added', 'info'); + } else { + // Set rules from default config + if (isset($optRules)) { + $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUp'); + $rules[] = 'PostDown = '.getDefaultNetValue('wireguard','server','PostDown'); + $rules = preg_replace('/wlan0/m', $optInterface, $rules); + } + if (isset($optKSwitch)) { + // Get ap static ip_addr from system config, fallback to default if undefined + $jsonData = json_decode(getNetConfig($optInterface), true); + $ip_addr = ($jsonData['StaticIP'] == '') ? getDefaultNetValue('dhcp', $optInterface, 'static ip_address') : $jsonData['StaticIP']; + $mask = ($jsonData['SubnetMask'] == '') ? getDefaultNetValue('dhcp', $optInterface, 'subnetmask') : $jsonData['SubnetMask']; + + // if empty, try to detect IP/mask from system + if (empty($ip_addr) || empty($mask)) { + $ipDetails = shell_exec("ip -4 -o addr show dev " . escapeshellarg($optInterface)); + if (preg_match('/inet (\d+\.\d+\.\d+\.\d+)\/(\d+)/', $ipDetails, $matches)) { + $ip_addr = $matches[1]; + $cidr = $matches[2]; + } else { + $ip_addr = '0.0.0.0'; + $cidr = '24'; + } + } else { + $cidr = mask2cidr($mask); + } + $cidr_ip = strpos($ip_addr, '/') === false ? "$ip_addr/$cidr" : $ip_addr; + + $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUpEx'); + $rules[] = 'PreDown = '.getDefaultNetValue('wireguard','server','PreDown'); + $rules = preg_replace('/%s/m', $cidr_ip, $rules); + } + if ((isset($rules) && count($rules) > 0)) { + $rules[] = ''; + $rules = join(PHP_EOL, $rules); + $tmp_contents = preg_replace('/^\s*$/ms', $rules, $tmp_contents, 1); + file_put_contents($tmp_wgconfig, $tmp_contents); + $status->addMessage('iptables rules added to WireGuard configuration', 'info'); + } } - // Move processed file from tmp to destination - system("sudo mv $tmp_wgconfig ". RASPI_WIREGUARD_CONFIG, $return); + // Move processed file from /tmp and create symlink + $client_wg = RASPI_WIREGUARD_PATH.pathinfo($file['name'], PATHINFO_FILENAME).'.conf'; + chmod($tmp_wgconfig, 0644); + system("sudo mv $tmp_wgconfig $client_wg", $return); + system("sudo rm ".RASPI_WIREGUARD_CONFIG, $return); + system("sudo ln -s $client_wg ".RASPI_WIREGUARD_CONFIG, $return); if ($return ==0) { $status->addMessage('WireGuard configuration uploaded successfully', 'info'); @@ -225,7 +279,7 @@ function SaveWireGuardConfig($status) $wg_pendpoint_seg = substr($_POST['wg_pendpoint'],0,strpos($_POST['wg_pendpoint'],':')); $host_port = explode(':', $wg_pendpoint_seg); $hostname = $host_port[0]; - if (!filter_var($hostname, FILTER_VALIDATE_IP) && + if (!filter_var($hostname, FILTER_VALIDATE_IP) && !filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { $status->addMessage('Invalid value for endpoint address', 'danger'); $good_input = false; @@ -295,11 +349,10 @@ function SaveWireGuardConfig($status) } $config[] = ''; $config = join(PHP_EOL, $config); - file_put_contents("/tmp/wgdata", $config); system('sudo cp /tmp/wgdata '.RASPI_WIREGUARD_PATH.'client.conf', $return); } else { - # remove selected conf + keys + # remove selected conf + keys system('sudo rm '. RASPI_WIREGUARD_PATH .'wg-peer-private.key', $return); system('sudo rm '. RASPI_WIREGUARD_PATH .'wg-peer-public.key', $return); system('sudo rm '. RASPI_WIREGUARD_PATH.'client.conf', $return); diff --git a/templates/wg/general.php b/templates/wg/general.php index 9b8b8246..e7e0399a 100644 --- a/templates/wg/general.php +++ b/templates/wg/general.php @@ -39,9 +39,18 @@ ">

iptables Postup and PostDown rules for the interface selected below."); ?> -

+

+
+
+
+ + /> + + "> +

+ iptables PostUp and PreDown rules for the configured interface."); ?>

From 698b8bf80976409cd32bf1d3b9da124df04e64f6 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:19:50 -0700 Subject: [PATCH 029/122] Update w/ versioned Librespeed CSS + JS --- index.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.php b/index.php index 5958cf8e..3ca02507 100755 --- a/index.php +++ b/index.php @@ -79,6 +79,9 @@ initializeApp(); + + + @@ -141,6 +144,9 @@ initializeApp(); + + + From 51d528fd42b6218e75b7daf2808e713ab588daa2 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:20:22 -0700 Subject: [PATCH 030/122] Add $extraFooterScripts param to DisplayNetworkingConfig() --- includes/page_actions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/page_actions.php b/includes/page_actions.php index c9649792..4d218850 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -32,7 +32,7 @@ function handleCorePageAction(string $page, array &$extraFooterScripts): void DisplayWPAConfig(); break; case "/network_conf": - DisplayNetworkingConfig(); + DisplayNetworkingConfig($extraFooterScripts); break; case "/hostapd_conf": DisplayHostAPDConfig(); From 5c979424f372c4aa4d9d8b6f6f6e9bb7e78b1698 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:22:42 -0700 Subject: [PATCH 031/122] Initial commit (source: raspap-insiders) --- app/lib/signprint.php | 78 +++++++++++++++++ src/RaspAP/Networking/DeviceScanner.php | 109 ++++++++++++++++++++++++ templates/networking/diagnostics.php | 95 +++++++++++++++++++++ templates/networking/general.php | 92 ++++++++++++++++++++ 4 files changed, 374 insertions(+) create mode 100644 app/lib/signprint.php create mode 100644 src/RaspAP/Networking/DeviceScanner.php create mode 100755 templates/networking/diagnostics.php create mode 100755 templates/networking/general.php diff --git a/app/lib/signprint.php b/app/lib/signprint.php new file mode 100644 index 00000000..d5f326b9 --- /dev/null +++ b/app/lib/signprint.php @@ -0,0 +1,78 @@ + + + + + <?php echo _("Printable Wi-Fi sign"); ?> + + + + + + + + + + +
+
+
+
+
+
+

+
+
+
+ +
+
+
+ RaspAP Wifi QR code +
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +

+ +

+
+
+
+
+ +
+
+
+ + diff --git a/src/RaspAP/Networking/DeviceScanner.php b/src/RaspAP/Networking/DeviceScanner.php new file mode 100644 index 00000000..d3a812f3 --- /dev/null +++ b/src/RaspAP/Networking/DeviceScanner.php @@ -0,0 +1,109 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +namespace RaspAP\Networking; + +class DeviceScanner +{ + public function listDevices(): array + { + $devices = []; + + foreach (glob('/sys/class/net/*') as $ifacePath) { + $iface = basename($ifacePath); + if ($iface === 'lo') { + continue; // skip loopback + } + + $device = [ + 'name' => $iface, + 'mac' => $this->readFile("$ifacePath/address"), + 'ipaddress' => $this->getIPAddress($iface), + 'vendor' => '', + 'model' => '', + 'vid' => '', + 'pid' => '', + 'driver' => '', + 'type' => $this->getInterfaceType($iface), + 'isAP' => false, + 'connected' => 'y', // placeholder + 'signal' => '0 dB (100%)' // placeholder + ]; + + $udev = $this->getUdevAttributes($iface); + $device['vendor'] = $this->getVendorName($udev); + $device['model'] = $udev['ID_MODEL_FROM_DATABASE'] ?? $udev['ID_MODEL'] ?? ''; + $device['vid'] = $udev['ID_VENDOR_ID'] ?? ''; + $device['pid'] = $udev['ID_MODEL_ID'] ?? ''; + $device['driver'] = $udev['ID_NET_DRIVER'] ?? ''; + + $devices[] = $device; + } + + return $devices; + } + + private function readFile(string $path): string + { + return is_readable($path) ? trim(file_get_contents($path)) : ''; + } + + private function getIPAddress(string $iface): string + { + $cmd = "ip -4 -o addr show dev " . escapeshellarg($iface) . " | awk '{print $4}' | cut -d/ -f1"; + $result = []; + exec($cmd, $result); + return $result[0] ?? ''; + } + + private function getInterfaceType(string $iface): string + { + $wirelessPath = "/sys/class/net/{$iface}/wireless"; + if (is_dir($wirelessPath)) { + return 'wlan'; + } + + $typeFile = "/sys/class/net/{$iface}/type"; + $type = $this->readFile($typeFile); + + return match ($type) { + '1' => 'eth', // ARPHRD_ETHER + '772' => 'loopback', + '512' => 'ppp', + default => 'unknown' + }; + } + + private function getUdevAttributes(string $iface): array + { + $attributes = []; + $output = []; + $path = escapeshellarg("/sys/class/net/{$iface}"); + + exec("udevadm info {$path}", $output); + + foreach ($output as $line) { + if (preg_match('/E: (\w+)=([^\n]+)/', $line, $matches)) { + $attributes[$matches[1]] = $matches[2]; + } + } + + return $attributes; + } + + private function getVendorName(array $udev): ?string + { + return $udev['ID_VENDOR_FROM_DATABASE'] + ?? $udev['ID_VENDOR'] + ?? $udev['ID_OUI_FROM_DATABASE'] + ?? null; + } +} + diff --git a/templates/networking/diagnostics.php b/templates/networking/diagnostics.php new file mode 100755 index 00000000..011ae9c2 --- /dev/null +++ b/templates/networking/diagnostics.php @@ -0,0 +1,95 @@ +
+ +

+
+
+ +
+

...

+
+ + + + +
+
+
+ diff --git a/templates/networking/general.php b/templates/networking/general.php new file mode 100755 index 00000000..41b1d823 --- /dev/null +++ b/templates/networking/general.php @@ -0,0 +1,92 @@ +
+

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
No route to the internet found

+

Access point

+
+

"> + " > +

+

"> + " > +

+

+ " > +

+
+
+
+
+ +

+
+
+
+
+
+
+ + + + +
+ + + +
+
+
+
+
+
+ +

+
+ + + +
+
+
+
+

+              
+
+
+ + +
+ +
+ From 4f57e259dd6ac641292c772157c67fe8c7f4c1e6 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:23:48 -0700 Subject: [PATCH 032/122] Define RASPI_IPTABLES_CONF, ACCESS_CHECK_* --- config/config.php | 5 +++++ includes/defaults.php | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/config/config.php b/config/config.php index 178081b1..2dd0223c 100755 --- a/config/config.php +++ b/config/config.php @@ -32,6 +32,7 @@ define('RASPI_OPENVPN_CLIENT_CONFIG', '/etc/openvpn/client/client.conf'); define('RASPI_OPENVPN_CLIENT_LOGIN', '/etc/openvpn/client/login.conf'); define('RASPI_WIREGUARD_PATH', '/etc/wireguard/'); define('RASPI_WIREGUARD_CONFIG', RASPI_WIREGUARD_PATH.'wg0.conf'); +define('RASPI_IPTABLES_CONF', RASPI_CONFIG.'/networking/iptables_rules.json'); define('RASPI_TORPROXY_CONFIG', '/etc/tor/torrc'); define('RASPI_LIGHTTPD_CONFIG', '/etc/lighttpd/lighttpd.conf'); define('RASPI_ACCESS_CHECK_IP', '1.1.1.1'); @@ -40,6 +41,10 @@ define('RASPI_ACCESS_CHECK_DNS', 'one.one.one.one'); // Constant for the GitHub API latest release endpoint define('RASPI_API_ENDPOINT', 'https://api.github.com/repos/RaspAP/raspap-webgui/releases/latest'); +// Captive portal detection - returns 204 or 200 is successful +define('RASPI_ACCESS_CHECK_URL', 'http://detectportal.firefox.com'); +define('RASPI_ACCESS_CHECK_URL_CODE', 200); + // Constant for the 5GHz wireless regulatory domain define("RASPI_5GHZ_CHANNEL_MIN", 100); define("RASPI_5GHZ_CHANNEL_MAX", 192); diff --git a/includes/defaults.php b/includes/defaults.php index f658a51e..dc7683ec 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -37,11 +37,17 @@ $defaults = [ 'RASPI_OPENVPN_CLIENT_LOGIN' => '/etc/openvpn/client/login.conf', 'RASPI_WIREGUARD_PATH' => '/etc/wireguard/', 'RASPI_WIREGUARD_CONFIG' => RASPI_WIREGUARD_PATH.'wg0.conf', + 'RASPI_IPTABLES_CONF' => RASPI_CONFIG.'/networking/iptables_rules.json', + 'RASPI_TORPROXY_ENABLED' => false, 'RASPI_TORPROXY_CONFIG' => '/etc/tor/torrc', 'RASPI_LIGHTTPD_CONFIG' => '/etc/lighttpd/lighttpd.conf', 'RASPI_ACCESS_CHECK_IP' => '1.1.1.1', 'RASPI_ACCESS_CHECK_DNS' => 'one.one.one.one', + // Captive portal detection - returns 204 or 200 is successful + 'RASPI_ACCESS_CHECK_URL' => 'http://detectportal.firefox.com', + 'RASPI_ACCESS_CHECK_URL_CODE' => 200, + // Constants for the 5GHz wireless regulatory domain 'RASPI_5GHZ_CHANNEL_MIN' => 100, 'RASPI_5GHZ_CHANNEL_MAX' => 192, @@ -58,7 +64,6 @@ $defaults = [ 'RASPI_OPENVPN_ENABLED' => false, 'RASPI_VPN_PROVIDER_ENABLED' => false, 'RASPI_WIREGUARD_ENABLED' => false, - 'RASPI_TORPROXY_ENABLED' => false, 'RASPI_CONFAUTH_ENABLED' => true, 'RASPI_CHANGETHEME_ENABLED' => true, 'RASPI_VNSTAT_ENABLED' => true, From c21d5a1790ec42afbb50df02151cbdc19b01b7c0 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:24:49 -0700 Subject: [PATCH 033/122] Update w/ checkHTTPAccess() --- includes/internetRoute.php | 76 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/includes/internetRoute.php b/includes/internetRoute.php index 3c39ae22..e9aaf7ac 100755 --- a/includes/internetRoute.php +++ b/includes/internetRoute.php @@ -1,5 +1,4 @@ $route) { $prop = explode(' ', $route); - $rInfo[$i]["interface"] = $prop[0]; + $rInfo[$i]["interface"] = $dev = $prop[0]; $rInfo[$i]["ip-address"] = $prop[1]; $rInfo[$i]["gateway"] = $prop[2]; // resolve the name of the gateway (if possible) unset($host); exec('host ' . $prop[2] . ' | sed -rn "s/.*domain name pointer (.*)\./\1/p" | head -n 1', $host); $rInfo[$i]["gw-name"] = empty($host) ? "*" : $host[0]; - if (isset($checkAccess) && $checkAccess) { + // check if AP + unset($isAP); + exec("iwconfig $dev 2> /dev/null | sed -rn 's/.*(mode:master).*/1/ip'", $isAP); + $isAP = !empty($isAP); + $rInfo[$i]["isAP"] = $isAP; + if (isset($checkAccess) && $checkAccess && !$isAP) { // check internet connectivity w/ and w/o DNS resolution unset($okip); exec('ping -W1 -c 1 -I ' . $prop[0] . ' ' . RASPI_ACCESS_CHECK_IP . ' | sed -rn "s/.*icmp_seq=1.*time=.*/OK/p"', $okip); @@ -47,6 +51,7 @@ function getRouteInfo($checkAccess) unset($okdns); exec('ping -W1 -c 1 -I ' . $prop[0] . ' ' . RASPI_ACCESS_CHECK_DNS . ' | sed -rn "s/.*icmp_seq=1.*time=.*/OK/p"', $okdns); $rInfo[$i]["access-dns"] = empty($okdns) ? false : true; + $rInfo[$i]["access-url"] = preg_match('/OK.*/',checkHTTPAccess($prop[0])); } } } else { @@ -55,6 +60,70 @@ function getRouteInfo($checkAccess) return $rInfo; } +function detectCaptivePortal($iface) { + $result=checkHTTPAccess($iface, true); + $checkConnect=array( "state"=>"FAILED", "URL"=>"", "interface"=> $iface, "url" => "" ); + if ( !empty($result) && !preg_match('/FAILED/i',$result) ) { + $checkConnect["state"]=preg_match('/(PORTAL|OK)/i',$result); + if ( preg_match('/PORTAL (.*)/i',$result ,$url) && !empty($url) ) { + $checkConnect["URL"]=$url[1]; + } + } + return $checkConnect; +} + +function checkHTTPAccess($iface, $detectPortal=false) { + + $ret="FAILED no HTTP access"; + exec('timeout 5 curl -is ' . RASPI_ACCESS_CHECK_URL . ' --interface ' . $iface, $rcurl); + if ( !empty($rcurl) && preg_match("/^HTTP\/[0-9\.]+ ([0-9]+)/m",$rcurl=implode("\n",$rcurl),$code) ) { + $code = $code[1]; + if ( $code == 200 ) { + if ( preg_match("//", $rcurl, $url) ) { + $code = 302; + $rcurl = "Location: " . $url[1]; + unset($url); + } + } + switch($code) { + case 302: + case 307: + if ( $detectPortal ) { + if ( preg_match("/^Location:\s*(https?:\/\/[^?[:space:]]+)/m", $rcurl, $url) ) { + $url=$url[1]; + if ( preg_match('/^https?:\/\/([^:\/]*).*/i', $url, $srv) && isset($srv[1]) ) { + $srv=$srv[1]; + if ( preg_match('/^(([0-9]{1,3}\.){3}[0-9]{1,3}).*/', $srv, $ip) && isset($ip[1]) ) { + $ret="PORTAL " . $url; + } + else { + exec('timeout 7 sudo nmap --script=broadcast-dhcp-discover -e ' . $iface . ' 2> /dev/null | sed -rn "s/.*Domain Name Server:\s*(([0-9]{1,3}\.){3}[0-9]{1,3}).*/\1/pi"', $nameserver); + if ( !empty($nameserver) ) { + $nameserver=$nameserver[0]; + exec('host ' . $srv . ' ' . $nameserver . ' | sed -rn "s/.*has address ((([0-9]{1,3}\.){3}[0-9]{1,3})).*/\1/p"', $ip2); + if ( !empty($ip2) ) { + $ip2=$ip2[0]; + $url=preg_replace("/" . $srv . "/",$ip2,$url); + $ret="PORTAL " . $url; + } + else $ret="FAILED name " . $srv . " could not be resolved"; + } + else $ret="FAILED no name server"; + } + } + } + } + break; + case RASPI_ACCESS_CHECK_URL_CODE: + $ret="OK internet access"; + break; + default: + $ret="FAILED unexpected response " . $code[0]; + break; + } + } + return $ret; +} /* * Fetches raw output of ip route * @@ -65,4 +134,3 @@ function getRouteInfoRaw() exec('ip route list', $routes); return $routes; } - From 7a0b93a0e8488293a9303aade9cdb81e8d6fd16c Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:26:30 -0700 Subject: [PATCH 034/122] Update networking w/ speedtestUI.js, diagnostics tab --- includes/networking.php | 7 +-- templates/networking.php | 100 ++++++--------------------------------- 2 files changed, 18 insertions(+), 89 deletions(-) diff --git a/includes/networking.php b/includes/networking.php index 9669fca4..83c587ed 100755 --- a/includes/networking.php +++ b/includes/networking.php @@ -1,12 +1,12 @@ 'app/js/speedtestUI.js', 'defer'=>false); } diff --git a/templates/networking.php b/templates/networking.php index f1cd45c3..1767878d 100755 --- a/templates/networking.php +++ b/templates/networking.php @@ -1,7 +1,7 @@ +
-
@@ -9,96 +9,24 @@
-
+ showMessages(); ?> +
+
+ + +
-
-

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
No route to the internet found

-

- "> -

-

- "> -

-
-
-
-
- -

-
-
-
-
-
-
- - - - -
- - - -
-
-
-
-
-
- -

-
- - - -
-
-
-
-

-                      
-
-
- - -
- -
-
+ + +
From 7fceaf536ccf798fd3d6e14f00648109b1a69473 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:27:01 -0700 Subject: [PATCH 035/122] Suppress call to loadCurrentSettings (deprecated) --- app/js/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/custom.js b/app/js/custom.js index 3e8a7c58..85babd07 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -43,7 +43,7 @@ function setupTabs() { var target = $(e.target).attr('href'); if(!target.match('summary')) { var int = target.replace("#",""); - loadCurrentSettings(int); + // loadCurrentSettings(int); } }); } From 7994fa3c33f04f971498bd48a89ca8edaa84cf1e Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:28:36 -0700 Subject: [PATCH 036/122] Update security options w/ 802.11w, printable sign link --- templates/hostapd/security.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/templates/hostapd/security.php b/templates/hostapd/security.php index 1decef3d..cc7eb141 100644 --- a/templates/hostapd/security.php +++ b/templates/hostapd/security.php @@ -4,15 +4,21 @@
- +
- +
+ + + +
+ +
- +
@@ -22,7 +28,12 @@
RaspAP Wifi QR code -
+
+ ', + '', + ''); ?> +
From c0df273c3640e1a6d4467360ebe9c47f2a8d88ee Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 02:29:44 -0700 Subject: [PATCH 037/122] Refactor hostapd cfg parse/write (source: raspap-insiders) --- includes/hostapd.php | 324 ++++++++++++++++++++++++++++++------------- 1 file changed, 229 insertions(+), 95 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index e4c7e69c..bf44a020 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -25,15 +25,18 @@ function DisplayHostAPDConfig() $languageCode = strtok($_SESSION['locale'], '_'); $countryCodes = getCountryCodes($languageCode); - $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => 'WPA+WPA2', 'none' => _("None")); + $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => _("WPA and WPA2")); + $arrSecurity += [4 => _("WPA2 and WPA3-Personal (transitional mode)")]; + $arrSecurity += [5 => 'WPA3-Personal (required)']; + $arrSecurity += ['none' => _("None")]; $arrEncType = array('TKIP' => 'TKIP', 'CCMP' => 'CCMP', 'TKIP CCMP' => 'TKIP+CCMP'); + $arr80211w = array(3 => _("Disabled"), 1 => _("Enabled (for supported clients)"), 2 => _("Required (for supported clients)")); $arrTxPower = getDefaultNetOpts('txpower','dbm'); $managedModeEnabled = false; exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); sort($interfaces); $reg_domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); - $cmd = "iw dev ".escapeshellarg($_SESSION['ap_interface'])." info | awk '$1==\"txpower\" {print $2}'"; exec($cmd, $txpower); $txpower = intval($txpower[0]); @@ -41,6 +44,7 @@ function DisplayHostAPDConfig() if (isset($_POST['interface'])) { $interface = escapeshellarg($_POST['interface']); } + if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveHostAPDSettings'])) { SaveHostAPDConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); @@ -61,7 +65,7 @@ function DisplayHostAPDConfig() } elseif ($arrHostapdConf['WifiAPEnable'] == 1) { exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 1', $return); } else { - // systemctl expects a unit name like raspap-network-activity@wlan0.service, no extra quotes + // systemctl expects a unit name like raspap-network-activity@wlan0.service $iface_nonescaped = $_POST['interface']; if (preg_match('/^[a-zA-Z0-9_-]+$/', $iface_nonescaped)) { // validate interface name exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface ' .$iface_nonescaped. ' --seconds 1', $return); @@ -110,12 +114,10 @@ function DisplayHostAPDConfig() } else { $arrConfig['disassoc_low_ack_bool'] = 0; } - // assign country_code from iw reg if not set in config if (empty($arrConfig['country_code']) && isset($country_code[0])) { $arrConfig['country_code'] = $country_code[0]; } - // set txpower with iw if value is non-default ('auto') if (isset($_POST['txpower'])) { if ($_POST['txpower'] != 'auto') { @@ -129,6 +131,12 @@ function DisplayHostAPDConfig() $status->addMessage('Setting transmit power to '.$_POST['txpower'].'.', 'success'); $txpower = $_POST['txpower']; } + } + // map wpa_key_mgmt to security types + if ($arrConfig['wpa_key_mgmt'] == 'WPA-PSK WPA-PSK-SHA256 SAE') { + $arrConfig['wpa'] = 4; + } elseif ($arrConfig['wpa_key_mgmt'] == 'SAE') { + $arrConfig['wpa'] = 5; } $selectedHwMode = $arrConfig['hw_mode']; @@ -166,6 +174,7 @@ function DisplayHostAPDConfig() "selectedHwMode", "arrSecurity", "arrEncType", + "arr80211w", "arrTxPower", "txpower", "arrHostapdConf", @@ -226,6 +235,19 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $bridgedEnable = 1; } } + // Check for WiFi repeater mode checkbox + $repeaterEnable = 0; + if ($bridgedEnable == 0) { // enable client mode actions when not bridged + if ($arrHostapdConf['RepeaterEnable'] == 0) { + if (isset($_POST['repeaterEnable'])) { + $repeaterEnable = 1; + } + } else { + if (isset($_POST['repeaterEnable'])) { + $repeaterEnable = 1; + } + } + } // Check for WiFi client AP mode checkbox $wifiAPEnable = 0; if ($bridgedEnable == 0) { // enable client mode actions when not bridged @@ -277,6 +299,7 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom // Save previous Client mode status when Bridged $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $arrHostapdConf['WifiAPEnable'] : $wifiAPEnable); $cfg['BridgedEnable'] = $bridgedEnable; + $cfg['RepeaterEnable'] = $repeaterEnable; $cfg['WifiManaged'] = $cli_iface; write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini'); $_SESSION['ap_interface'] = $session_iface; @@ -305,7 +328,6 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $status->addMessage('Parameter hiddenSSID contains an invalid configuration value.', 'danger'); $good_input = false; } - if (! in_array($_POST['interface'], $interfaces)) { $status->addMessage('Unknown interface '.htmlspecialchars($_POST['interface'], ENT_QUOTES), 'danger'); $good_input = false; @@ -330,7 +352,29 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $_POST['max_num_sta'] = $_POST['max_num_sta'] < 1 ? null : $_POST['max_num_sta']; if ($good_input) { - $return = updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable); + $config = buildHostapdConfig([ + 'interface' => $_POST['interface'], + 'ssid' => $_POST['ssid'], + 'channel' => $_POST['channel'], + 'wpa' => $_POST['wpa'], + '80211w' => $_POST['80211w'] ?? 0, + 'wpa_passphrase' => $_POST['wpa_passphrase'], + 'wpa_pairwise' => $_POST['wpa_pairwise'], + 'hw_mode' => $_POST['hw_mode'], + 'country_code' => $_POST['country_code'], + 'hiddenSSID' => $_POST['hiddenSSID'], + 'max_num_sta' => $_POST['max_num_sta'] ?? null, + 'beacon_interval' => $_POST['beacon_interval'] ?? null, + 'disassoc_low_ack' => $_POST['disassoc_low_ackEnable'] ?? null, + 'bridge' => $bridgedEnable ? 'br0' : null + ]); + + file_put_contents('/tmp/hostapddata', $config); + if ($dualAPEnable) { + system("sudo cp /tmp/hostapddata " . '/etc/hostapd/hostapd-'.$_POST['interface'].'.conf', $result); + } else { + system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result); + } if (trim($country_code) != trim($reg_domain)) { $return = iwRegSet($country_code, $status); @@ -377,21 +421,46 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $jsonData = json_decode(getNetConfig($ap_iface), true); $ip_address = empty($jsonData['StaticIP']) ? getDefaultNetValue('dhcp', $ap_iface, 'static ip_address') : $jsonData['StaticIP']; - $domain_name_server = empty($jsonData['StaticDNS']) + $domain_name_server = empty($jsonData['StaticDNS']) ? getDefaultNetValue('dhcp', $ap_iface, 'static domain_name_server') : $jsonData['StaticDNS']; - $routers = empty($jsonData['StaticRouters']) + $routers = empty($jsonData['StaticRouters']) ? getDefaultNetValue('dhcp', $ap_iface, 'static routers') : $jsonData['StaticRouters']; - $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') + $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') ? getDefaultNetValue('dhcp', $ap_iface, 'subnetmask') : $jsonData['SubnetMask']; if (isset($ip_address) && !preg_match('/.*\/\d+/', $ip_address)) { $ip_address.='/'.mask2cidr($netmask); } + $hasDefaults = !( + empty($ip_address) || + empty($domain_name_server) || + empty($routers) || + empty($netmask) || + $netmask === '0.0.0.0' + ); + if (!$hasDefaults) { + $status->addMessage(sprintf(_('Interface %s has no default settings.'), $ap_iface), 'warning'); + $status->addMessage(('Configure settings in DHCP Server before starting AP.'), 'warning'); + } if ($bridgedEnable == 1) { $config = array_keys(getDefaultNetOpts('dhcp','options')); $config[] = PHP_EOL.'# RaspAP br0 configuration'; $config[] = 'denyinterfaces eth0 wlan0'; $config[] = 'interface br0'; $config[] = PHP_EOL; + } elseif ($repeaterEnable == 1) { + $config = [ '# RaspAP '.$ap_iface.' configuration' ]; + $config[] = 'interface '.$ap_iface; + $config[] = 'static ip_address='.$ip_address; + $config[] = 'static routers='.$routers; + $config[] = 'static domain_name_server='.$domain_name_server; + $client_metric = getIfaceMetric($_SESSION['wifi_client_interface']); + if (is_int($client_metric)) { + $ap_metric = (int)$client_metric + 1; + $config[] = 'metric '.$ap_metric; + } else { + $status->addMessage('Unable to obtain metric value for client interface. Repeater mode inactive.', 'warning'); + $repeaterEnable = false; + } } elseif ($wifiAPEnable == 1) { $config = array_keys(getDefaultNetOpts('dhcp','options')); $config[] = PHP_EOL.'# RaspAP uap0 configuration'; @@ -399,15 +468,12 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $config[] = 'static ip_address='.$ip_address; $config[] = 'nohook wpa_supplicant'; $config[] = PHP_EOL; - } else { $config = updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain_name_server); } - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - $skip_dhcp = false; - if (preg_match('/wlan[2-9]\d*|wlan[1-9]\d+/', $ap_iface)) { + if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) { $skip_dhcp = true; } elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) { $dhcp_cfg = join(PHP_EOL, $config); @@ -418,114 +484,183 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); $dhcp_cfg .= $config; - $status->addMessage(sprintf(_('DHCP configuration for %s added.'), $ap_iface), 'success'); } else { $config = join(PHP_EOL, $config); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=\s*^\s*$)/ms', $config, $dhcp_cfg, 1); - $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success'); - } - if (!$skip_dhcp) { - file_put_contents("/tmp/dhcpddata", $dhcp_cfg); - system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return); - if ($return == 0) { - $status->addMessage('Wifi Hotspot settings saved', 'success'); + if (!strpos($dhcp_cfg, 'metric')) { + $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1); } else { - $status->addMessage('Unable to save wifi hotspot settings', 'danger'); + $metrics = true; } + } + if ($repeaterEnable && $metrics) { + $status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning'); + } else if ($repeaterEnable && !$metrics) { + $status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success'); + $status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success'); + persistDHCPConfig($dhcp_cfg, $ap_iface, $status); + } elseif (!$skip_dhcp) { + persistDHCPConfig($dhcp_cfg, $ap_iface, $status); } else { - $status->addMessage(sprintf(_('Interface %s has no default settings.'), $ap_iface), 'warning'); - $status->addMessage(('Configure settings in DHCP Server before starting AP.'), 'warning'); - $status->addMessage('Wifi Hotspot settings saved', 'success'); + $status->addMessage('WiFi hotspot settings saved.', 'success'); } } else { - $status->addMessage('Unable to save wifi hotspot settings', 'danger'); + $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); return false; } return true; } /** - * Updates a hostapd configuration + * Persists a DHCP configuration * - * @return boolean $result + * @param string $dhcp_cfg + * @param string $ap_iface + * @param object $status + * @return $status */ -function updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable) +function persistDHCPConfig($dhcp_cfg, $ap_iface, $status) { - // Fixed values - $country_code = $_POST['country_code']; - $config = 'driver=nl80211'.PHP_EOL; - $config.= 'ctrl_interface='.RASPI_HOSTAPD_CTRL_INTERFACE.PHP_EOL; - $config.= 'ctrl_interface_group=0'.PHP_EOL; - $config.= 'auth_algs=1'.PHP_EOL; - $config.= 'wpa_key_mgmt=WPA-PSK'.PHP_EOL; - if (isset($_POST['beaconintervalEnable'])) { - $config.= 'beacon_int='.$_POST['beacon_interval'].PHP_EOL; - } - if (isset($_POST['disassoc_low_ackEnable'])) { - $config.= 'disassoc_low_ack=0'.PHP_EOL; - } - $config.= 'ssid='.$_POST['ssid'].PHP_EOL; - $config.= 'channel='.$_POST['channel'].PHP_EOL; - - // Set VHT center frequency segment value - if ((int)$_POST['channel'] < RASPI_5GHZ_CHANNEL_MIN) { - $vht_freq_idx = 42; + file_put_contents("/tmp/dhcpddata", $dhcp_cfg); + system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return); + if ($return == 0) { + $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success'); + $status->addMessage('WiFi hotspot settings saved.', 'success'); } else { - $vht_freq_idx = 155; + $status->addMessage('Unable to save WiFi hotspot settings.', 'danger'); } + return $status; +} - if ($_POST['hw_mode'] === 'n') { - $config.= 'hw_mode=g'.PHP_EOL; - $config.= 'ieee80211n=1'.PHP_EOL; - // Enable basic Quality of service - $config.= 'wmm_enabled=1'.PHP_EOL; - } elseif ($_POST['hw_mode'] === 'ac') { - $config.= 'hw_mode=a'.PHP_EOL.PHP_EOL; - $config.= '# N'.PHP_EOL; - $config.= 'ieee80211n=1'.PHP_EOL; - $config.= 'require_ht=1'.PHP_EOL; - $config.= 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'.PHP_EOL.PHP_EOL; - $config.= '# AC'.PHP_EOL; - $config.= 'ieee80211ac=1'.PHP_EOL; - $config.= 'require_vht=1'.PHP_EOL; - $config.= 'ieee80211d=0'.PHP_EOL; - $config.= 'ieee80211h=0'.PHP_EOL; - $config.= 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'.PHP_EOL; - $config.= 'vht_oper_chwidth=1'.PHP_EOL; - $config.= 'vht_oper_centr_freq_seg0_idx='.$vht_freq_idx.PHP_EOL.PHP_EOL; - } elseif ($_POST['hw_mode'] === 'w') { - $config.= 'ieee80211w=2'.PHP_EOL; - $config.= 'wpa_key_mgmt=WPA-EAP-SHA256'.PHP_EOL; +/** + * Returns a count of hostapd-.conf files + * + * @return int + */ +function countHostapdConfigs(): int +{ + $configs = glob('/etc/hostapd/hostapd-*.conf'); + return is_array($configs) ? count($configs) : 0; +} + +/** + * Retrieves the metric value for a given interface + * + * @param string $iface + * @return int $metric + */ +function getIfaceMetric($iface) +{ + $metric = shell_exec("ip -o -4 route show dev ".$iface." | awk '/metric/ {print \$NF; exit}'"); + if (isset($metric)) { + $metric = (int)$metric; + return $metric; } else { - $config.= 'hw_mode='.$_POST['hw_mode'].PHP_EOL; - $config.= 'ieee80211n=0'.PHP_EOL; + return false; } - if ($_POST['wpa'] !== 'none') { - $config.= 'wpa_passphrase='.$_POST['wpa_passphrase'].PHP_EOL; +} + +/** + * Builds a hostapd configuration string for a given interface + * + * @param array $params Associative array of config values + * @return string + */ +function buildHostapdConfig(array $params): string +{ + $config = []; + $config[] = 'driver=nl80211'; + $config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE; + $config[] = 'ctrl_interface_group=0'; + $config[] = 'auth_algs=1'; + + $wpa = $params['wpa']; + $wpa_key_mgmt = 'WPA-PSK'; + + if ($wpa == 4) { + $config[] = 'ieee80211w=1'; + $wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE'; + $wpa = 2; + } elseif ($wpa == 5) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'SAE'; + $wpa = 2; } - if ($wifiAPEnable == 1) { - $config.= 'interface=uap0'.PHP_EOL; - } elseif ($bridgedEnable == 1) { - $config.='interface='.$_POST['interface'].PHP_EOL; - $config.= 'bridge=br0'.PHP_EOL; + + if ($params['80211w'] == 1) { + $config[] = 'ieee80211w=1'; + $wpa_key_mgmt = 'WPA-PSK'; + } elseif ($params['80211w'] == 2) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'WPA-PSK-SHA256'; + } + + $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; + + if (!empty($params['beacon_interval'])) { + $config[] = 'beacon_int=' . $params['beacon_interval']; + } + + if (!empty($params['disassoc_low_ack'])) { + $config[] = 'disassoc_low_ack=0'; + } + + $config[] = 'ssid=' . $params['ssid']; + $config[] = 'channel=' . $params['channel']; + + // Choose VHT segment index (fallback only if required) + $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; + + switch ($params['hw_mode']) { + case 'n': + $config[] = 'hw_mode=g'; + $config[] = 'ieee80211n=1'; + $config[] = 'wmm_enabled=1'; + break; + case 'ac': + $config[] = 'hw_mode=a'; + $config[] = '# N'; + $config[] = 'ieee80211n=1'; + $config[] = 'require_ht=1'; + $config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'; + $config[] = '# AC'; + $config[] = 'ieee80211ac=1'; + $config[] = 'require_vht=1'; + $config[] = 'ieee80211d=0'; + $config[] = 'ieee80211h=0'; + $config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'; + $config[] = 'vht_oper_chwidth=1'; + $config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx; + break; + default: + $config[] = 'hw_mode=' . $params['hw_mode']; + $config[] = 'ieee80211n=0'; + } + + if ($params['wpa'] !== 'none') { + $config[] = 'wpa_passphrase=' . $params['wpa_passphrase']; + } + + if (!empty($params['bridge'])) { + $config[] = 'interface=' . $params['interface']; + $config[] = 'bridge=' . $params['bridge']; } else { - $config.= 'interface='.$_SESSION['ap_interface'].PHP_EOL; - } - $config.= 'wpa='.$_POST['wpa'].PHP_EOL; - $config.= 'wpa_pairwise='.$_POST['wpa_pairwise'].PHP_EOL; - $config.= 'country_code='.$_POST['country_code'].PHP_EOL; - $config.= 'ignore_broadcast_ssid='.$ignore_broadcast_ssid.PHP_EOL; - if (isset($_POST['max_num_sta'])) { - $config.= 'max_num_sta='.$_POST['max_num_sta'].PHP_EOL; + $config[] = 'interface=' . $params['interface']; } - $config.= parseUserHostapdCfg(); + $config[] = 'wpa=' . $wpa; + $config[] = 'wpa_pairwise=' . $params['wpa_pairwise']; + $config[] = 'country_code=' . $params['country_code']; + $config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID']; + if (!empty($params['max_num_sta'])) { + $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; + } + + // Optional additional user config + $config[] = parseUserHostapdCfg(); - file_put_contents("/tmp/hostapddata", $config); - system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result); - return $result; + return implode(PHP_EOL, $config) . PHP_EOL; } /** @@ -628,4 +763,3 @@ function parseUserHostapdCfg() return $tmp; } } - From e126f3f664d5e2db55a0797bb9962d5c60b630fb Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 05:50:43 -0700 Subject: [PATCH 038/122] Update social link --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a74c1a0..627d24dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ RaspAP is made possible by a strong [community of developers](https://github.com * [GitHub discussions](https://github.com/RaspAP/raspap-webgui/discussions) * [Discord chat](https://discord.gg/KVAsaAR) -* [Twitter](https://twitter.com/rasp_ap) +* [X](https://x.com/rasp_ap) * [Reddit](https://www.reddit.com/r/RaspAP/) If you enjoy using RaspAP and would like to support our work financially, consider becoming an [Insider](https://github.com/sponsors/RaspAP). From c33522b0151a171eedcd614cfc6a0a05fca833b9 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 06:31:17 -0700 Subject: [PATCH 039/122] Clarify gateway/nogateway option w/ tooltip, update en_US messages --- locale/en_US/LC_MESSAGES/messages.mo | Bin 65161 -> 65371 bytes locale/en_US/LC_MESSAGES/messages.po | 7 +++++-- templates/dhcp/general.php | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index 039abaa18cf40e8b04f2d23c530aef1fe778fb23..e2720b14d0f3c1a1664497ab06c8a0808894ac39 100644 GIT binary patch delta 15518 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 delta 15279 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 ZG^v!)?Q!s?e2<&t+0^h$?}-0j^?z6vp4R{X diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index 70a5c706..5986aa27 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -581,12 +581,15 @@ msgstr "Clients with a particular hardware MAC address can always be allocated t msgid "This option adds dhcp-host entries to the dnsmasq configuration." msgstr "This option adds dhcp-host entries to the dnsmasq configuration." -msgid "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." -msgstr "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." +msgid "This toggles the gateway/nogateway option for this interface in the dhcpcd.conf file." +msgstr "This toggles the gateway/nogateway option for this interface in the dhcpcd.conf file." msgid "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." msgstr "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." +msgid "Enable this only if you want your device to use this interface as its primary route to the internet." +msgstr "Enable this only if you want your device to use this interface as its primary route to the internet." + msgid "Disable wpa_supplicant dhcp hook for this interface" msgstr "Disable wpa_supplicant dhcp hook for this interface" diff --git a/templates/dhcp/general.php b/templates/dhcp/general.php index b788c87a..13bc7b52 100644 --- a/templates/dhcp/general.php +++ b/templates/dhcp/general.php @@ -69,9 +69,10 @@
+ ">

- gateway/nogateway option for this interface in the DHCPCD configuration.") ?> + gateway/nogateway option for this interface in the dhcpcd.conf file.") ?>

@@ -84,7 +85,7 @@ ">

- nohook wpa_supplicant option for this interface in the DHCPCD configuration.") ?> + nohook wpa_supplicant option for this interface in the dhcpcd.conf file.") ?>

From 6f380299dbcf21c4376ec34772986fd8c8489ed4 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 07:08:35 -0700 Subject: [PATCH 040/122] Update sudoers w/ wireguard permissions --- installers/raspap.sudoers | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 9933cf18..3b7fd055 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -59,10 +59,13 @@ www-data ALL=(ALL) NOPASSWD:/bin/chmod o+r /var/log/dnsmasq.log www-data ALL=(ALL) NOPASSWD:/bin/chmod o+r /tmp/wireguard.log www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/dnsmasqdata /etc/dnsmasq.d/090_adblock.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/dnsmasq_custom /etc/raspap/adblock/custom.txt +www-data ALL=(ALL) NOPASSWD:/etc/raspap/adblock/update_blocklist.sh www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wgdata /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/wg-*.key /etc/wireguard/wg-*.key www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/wg/* /etc/wireguard/*.conf -www-data ALL=(ALL) NOPASSWD:/etc/raspap/adblock/update_blocklist.sh +www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/wg0.conf +www-data ALL=(ALL) NOPASSWD:/usr/bin/ls /etc/wireguard/ +www-data ALL=(ALL) NOPASSWD:/usr/bin/ln -s /etc/wireguard/*.conf /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/usr/bin/socat - /dev/ttyUSB[0-9] www-data ALL=(ALL) NOPASSWD:/usr/local/sbin/onoff_huawei_hilink.sh * www-data ALL=(ALL) NOPASSWD:/bin/sed -i * /etc/wvdial.conf @@ -75,8 +78,10 @@ www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wireguard/wg-*.key www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/wg-*.key +www-data ALL=(ALL) NOPASSWD:/usr/bin/readlink /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/usr/sbin/netplan www-data ALL=(ALL) NOPASSWD:/bin/truncate -s 0 /tmp/*.log,/bin/truncate -s 0 /var/log/dnsmasq.log +www-data ALL=(ALL) NOPASSWD:/usr/bin/nmap --script=broadcast-dhcp-discover -e [a-zA-Z0-9]* www-data ALL=(ALL) NOPASSWD:/usr/bin/vnstat * www-data ALL=(ALL) NOPASSWD:/usr/sbin/visudo -cf * www-data ALL=(ALL) NOPASSWD:/etc/raspap/plugins/plugin_helper.sh From 918f7daa74472d6ab7262e7649872e7cc3e86eea Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 09:30:10 -0700 Subject: [PATCH 041/122] Refactor nearbyWifiStations() to use iw instead of wpa_cli --- includes/wifi_functions.php | 142 +++++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 52 deletions(-) diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php index ad0982a6..c26d8b57 100755 --- a/includes/wifi_functions.php +++ b/includes/wifi_functions.php @@ -51,6 +51,12 @@ function knownWifiStations(&$networks) } } +/** + * Scans for nearby WiFi networks using `iw` and updates the reference array + * + * @param array $networks Reference to the array of known and discovered networks. + * @param bool $cached If false, bypasses the cache and performs a fresh scan. + */ function nearbyWifiStations(&$networks, $cached = true) { $cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG); @@ -60,67 +66,99 @@ function nearbyWifiStations(&$networks, $cached = true) deleteCache($cacheKey); } + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + $scan_results = cache( - $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'); + $cacheKey, + function () use ($iface) { + $stdout = shell_exec("sudo iw dev $iface scan"); return preg_split("/\n/", $stdout); } ); - // get the name of the AP. Should be excluded from nearby networks - exec('cat '.RASPI_HOSTAPD_CONFIG.' | sed -rn "s/ssid=(.*)\s*$/\1/p" ', $ap_ssid); - $ap_ssid = $ap_ssid[0]; + + // exclude the AP from nearby networks + exec('sed -rn "s/ssid=(.*)\s*$/\1/p" ' . escapeshellarg(RASPI_HOSTAPD_CONFIG), $ap_ssid); + $ap_ssid = $ap_ssid[0] ?? ''; $index = 0; - if ( !empty($networks) ) { + if (!empty($networks)) { $lastnet = end($networks); - if ( isset($lastnet['index']) ) $index = $lastnet['index'] + 1; - } - - if (is_array($scan_results)) { - array_shift($scan_results); - foreach ($scan_results as $network) { - $arrNetwork = preg_split("/[\t]+/", $network); // split result into array - $ssid = $arrNetwork[4]; - - // exclude raspap ssid - if (empty($ssid) || $ssid == $ap_ssid) { - continue; - } - - // filter SSID string: unprintable 7bit ASCII control codes, delete or quotes -> ignore network - if (preg_match('[\x00-\x1f\x7f\'\`\´\"]', $ssid)) { - continue; - } - - // If network is saved - if (array_key_exists($ssid, $networks)) { - $networks[$ssid]['visible'] = true; - $networks[$ssid]['channel'] = ConvertToChannel($arrNetwork[1]); - // TODO What if the security has changed? - } else { - $networks[$ssid] = array( - 'ssid' => $ssid, - 'configured' => false, - 'protocol' => ConvertToSecurity($arrNetwork[3]), - 'channel' => ConvertToChannel($arrNetwork[1]), - 'passphrase' => '', - 'visible' => true, - 'connected' => false, - 'index' => $index - ); - ++$index; - } - - // Save RSSI, if the current value is larger than the already stored - if (array_key_exists(4, $arrNetwork) && array_key_exists($arrNetwork[4], $networks)) { - if (! array_key_exists('RSSI', $networks[$arrNetwork[4]]) || $networks[$ssid]['RSSI'] < $arrNetwork[2]) { - $networks[$ssid]['RSSI'] = $arrNetwork[2]; - } - } + if (isset($lastnet['index'])) { + $index = $lastnet['index'] + 1; } } + + $current = []; + $commitCurrent = function () use (&$current, &$networks, &$index, $ap_ssid) { + if (empty($current['ssid'])) { + return; + } + + $ssid = $current['ssid']; + + // unprintable 7bit ASCII control codes, delete or quotes -> ignore network + if ($ssid === $ap_ssid || preg_match('/[\x00-\x1f\x7f\'`\´"]/', $ssid)) { + return; + } + + $channel = ConvertToChannel($current['freq'] ?? 0); + $rssi = $current['signal'] ?? -100; + + // if network is saved + if (array_key_exists($ssid, $networks)) { + $networks[$ssid]['visible'] = true; + $networks[$ssid]['channel'] = $channel; + if (!isset($networks[$ssid]['RSSI']) || $networks[$ssid]['RSSI'] < $rssi) { + $networks[$ssid]['RSSI'] = $rssi; + } + } else { + $networks[$ssid] = [ + 'ssid' => $ssid, + 'configured' => false, + 'protocol' => $current['security'] ?? 'OPEN', + 'channel' => $channel, + 'passphrase' => '', + 'visible' => true, + 'connected' => false, + 'RSSI' => $rssi, + 'index' => $index + ]; + ++$index; + } + }; + + foreach ($scan_results as $line) { + $line = trim($line); + + if (preg_match('/^BSS\s+([0-9a-f:]{17})/', $line, $match)) { + $commitCurrent(); // commit previous + $current = [ + 'bssid' => $match[1], + 'ssid' => '', + 'signal' => null, + 'freq' => null, + 'security' => 'OPEN' + ]; + continue; + } + if (preg_match('/^SSID:\s*(.*)$/', $line, $match)) { + $current['ssid'] = $match[1]; + continue; + } + if (preg_match('/^signal:\s*(-?\d+\.\d+)/', $line, $match)) { + $current['signal'] = (float)$match[1]; + continue; + } + if (preg_match('/^freq:\s*(\d+)/', $line, $match)) { + $current['freq'] = (int)$match[1]; + continue; + } + if (preg_match('/^RSN:/', $line) || preg_match('/^WPA:/', $line)) { + $current['security'] = 'WPA/WPA2'; + continue; + } + } + $commitCurrent(); } function connectedWifiStations(&$networks) From 349c5af574e365f5938520895c249feb582d6b4e Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 09:30:41 -0700 Subject: [PATCH 042/122] Adjust top position of loading-spinner --- app/css/all.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/css/all.css b/app/css/all.css index 6c054e18..f6aec2cd 100644 --- a/app/css/all.css +++ b/app/css/all.css @@ -130,7 +130,7 @@ th { .loading-spinner::before { position: absolute; - top: 0; + top: 120px; left: 0; width: 100%; height: calc(100vh / 4); @@ -138,10 +138,10 @@ th { justify-content: center; align-items: center; color: var(--raspap-text-muted); - content: "\f1ce"; /* Unicode for the circle-notch icon */ + content: "\f1ce"; font-family: "Font Awesome 5 Free"; - font-weight: 900; /* Adjust as needed */ - font-size: 54px; /* Adjust icon size as needed */ + font-weight: 900; + font-size: 54px; animation: spin 1.2s linear infinite; width: 100%; } From b374befa8ed25eaae8ee085bc413fda0e839452c Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 14 Jul 2025 22:57:59 -0700 Subject: [PATCH 043/122] Apply input mask to MAC address field using [0-9a-fA-F] pattern --- app/js/custom.js | 6 +++--- templates/dhcp/static_leases.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/js/custom.js b/app/js/custom.js index 3e8a7c58..626ce796 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -606,10 +606,10 @@ $(document).ready(function(){ }, placeholder: "___.___.___.___" }); - $('.date').mask('FF:FF:FF:FF:FF:FF', { + $('.mac_address').mask('FF:FF:FF:FF:FF:FF', { translation: { - "F": { - pattern: /[0-9a-z]/, optional: true + 'F': { + pattern: /[0-9a-fA-F]/, optional: false } }, placeholder: "__:__:__:__:__:__" diff --git a/templates/dhcp/static_leases.php b/templates/dhcp/static_leases.php index e92772ae..fb108529 100644 --- a/templates/dhcp/static_leases.php +++ b/templates/dhcp/static_leases.php @@ -30,7 +30,7 @@
- " class="form-control date" autofocus="autofocus"> + " class="form-control mac_address" autofocus="autofocus">
" class="form-control ip_address" maxlength="15"> From f29f0f2b53a1ee71a1fdfb956b74836cdaf0c6b6 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 15 Jul 2025 00:11:37 -0700 Subject: [PATCH 044/122] Update release version --- README.md | 2 +- includes/defaults.php | 2 +- index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2fd0abba..f3c38fe6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![RaspAP Custom OS images](https://github.com/user-attachments/assets/e871adf1-123c-450b-94eb-80a185c242cc) -[![Release 3.3.7](https://img.shields.io/badge/release-v3.3.7-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) +[![Release 3.3.8](https://img.shields.io/badge/release-v3.3.8-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. diff --git a/includes/defaults.php b/includes/defaults.php index dc7683ec..39d26491 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) { $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel', - 'RASPI_VERSION' => '3.3.7', + 'RASPI_VERSION' => '3.3.8', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', diff --git a/index.php b/index.php index 3ca02507..c5b8d4fa 100755 --- a/index.php +++ b/index.php @@ -14,7 +14,7 @@ * @author Lawrence Yau * @author Bill Zimmerman * @license GNU General Public License, version 3 (GPL-3.0) - * @version 3.3.7 + * @version 3.3.8 * @link https://github.com/RaspAP/raspap-webgui/ * @link https://raspap.com/ * @see http://sirlagz.net/2013/02/08/raspap-webgui/ From 83a3057e7fd40cf3c1e40d53367ec119b83dd364 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 15 Jul 2025 02:07:42 -0700 Subject: [PATCH 045/122] Update BACKERS.md --- BACKERS.md | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/BACKERS.md b/BACKERS.md index 964785a7..40d6c71a 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -13,38 +13,46 @@ You can become a sponsor using your individual or organization's GitHub account. **Important**: If you're sponsoring [RaspAP](https://github.com/RaspAP/sponsors) through a GitHub organization, please send a short email to [sponsors@raspap.com](mailto:sponsors@raspap.com) with the name of your organization and the account that should be added as a collaborator. ## Exclusive features -The following features are currently available exclusively to sponsors. A tangible side benefit of sponsorship is that Insiders are able to help steer future development of RaspAP. This is done through Insiders' access to discussions, feature requests, issues and pull requests in the private GitHub repository. +The following features are currently available exclusively to sponsors. A tangible side benefit of sponsorship is that Insiders are able to help steer future development of RaspAP. This is done through your Insiders access to discussions, feature requests, issues and pull requests in the private GitHub repository. ✅ [Network device management](https://docs.raspap.com/net-devices/) - ✅ [Firewall settings](https://docs.raspap.com/firewall/) - ✅ [WPA3-Personal AP security](https://docs.raspap.com/ap-basics/#wpa3-personal) - ✅ [802.11w Protected Management Frames](https://docs.raspap.com/ap-basics/#80211w) - ✅ [Printable Wi-Fi signs](https://docs.raspap.com/ap-basics/#printable-signs) ✅ [MAC address cloning](https://docs.raspap.com/net-devices/#changing-the-mac-address) - ✅ [Network diagnostics](https://docs.raspap.com/net-devices/#diagnostics) - ✅ [WireGuard VPN kill switch](https://docs.raspap.com/wireguard/#kill-switch) - ✅ [Dynamic DNS support](https://docs.raspap.com/dynamicdns/) ✅ [Multiple WireGuard configs](https://docs.raspap.com/wireguard/#multiple-configs) ✅ [Wireless LAN routing](https://docs.raspap.com/wlanrouting/) ✅ [Custom user avatars](https://docs.raspap.com/authentication/#custom-user-avatars) ✅ [WiFi repeater mode](https://docs.raspap.com/ap-basics/#wifi-repeater-mode) - ✅ [NTP Service](https://docs.raspap.com/ntp/) ✅ [Limited privilege user role](https://docs.raspap.com/authentication/#limited-privilege-user-role) ✅ [Tailscale VPN](https://docs.raspap.com/tailscale/) -Look for the list above to grow as we add more exclusive features. Be sure to visit this page from time to time to learn about what's new, check the [Insiders docs page](https://docs.raspap.com/insiders/) and follow [@RaspAP on Twitter](https://twitter.com/rasp_ap) to stay updated. +Look for the list above to grow as we add more exlcusive features. Have an idea or suggestion for a future enhancement? Start or join an [Insiders discussion](https://github.com/RaspAP/raspap-insiders/discussions) and let us know! ## Funding targets Below is a list of funding targets. When a funding target is reached, the features that are tied to it are merged back into RaspAP and released to the public for general availability. -### $1000 -The second **Insiders Edition** includes the features listed above. +### $1,500 - 3rd Insiders Edition +The **3rd Insiders Edition** includes the exclusive features listed above. -### $500 -The [first Insiders Edition goal](https://docs.raspap.com/insiders/#500-1st-insiders-edition) was reached in December 2021. Thank you sponsors! +### $500 - 1st Insiders Edition (completed) +✅ Multiple OpenVPN client configs +✅ OpenVPN certificate authentication +✅ OpenVPN service logging +✅ Night mode toggle +✅ Restrict network to static clients +✅ WireGuard support +✅ Set AP transmit power + +### $1,000 - 2nd Insiders Edition (completed) +✅ Firewall settings +✅ WPA3-Personal AP security +✅ 802.11w Protected Management Frames +✅ Printable Wi-Fi signs +✅ Network diagnostics +✅ Dynamic DNS +✅ WireGuard kill switch +✅ NTP Service ## Quarterly giving -Beginning in 2022, each quarter 15% of all proceeds from Insiders will be donated directly to the [Raspberry Pi Foundation](https://www.raspberrypi.org/). The Raspberry Pi Foundation is a UK-based charity that works to put the power of computing and digital making into the hands of people all over the world. +Each quarter, 15% of all proceeds from Insiders are [donated directly to the Raspberry Pi Foundation](https://docs.raspap.com/insiders/#quarterly-giving). The Raspberry Pi Foundation is a UK-based charity that works to put the power of computing and digital making into the hands of people all over the world. [![Get involved with the Raspberry Pi Foundation](https://img.youtube.com/vi/dEzg92g1LHw/0.jpg)](https://www.youtube.com/watch?v=dEzg92g1LHw) From 478ba9973f88f391bba2fb5154c50261ecc66311 Mon Sep 17 00:00:00 2001 From: Lukasz Tulikowski Date: Wed, 16 Jul 2025 10:01:13 +0200 Subject: [PATCH 046/122] fix(color): enhance regex pattern for CSS color validation in getColorOpt function --- includes/functions.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index a3e53ef6..9541e307 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -666,7 +666,12 @@ function getColorOpt() } // Define the regex pattern for valid CSS color formats - $colorPattern = "/^(#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})|rgb\((\s*\d+\s*,){2}\s*\d+\s*\)|rgba\((\s*\d+\s*,){3}\s*(0|0\.\d+|1)\)|[a-zA-Z]+)$/i"; + $colorPattern = "/^(" . + "#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})" . "|" . // Hex colors (#RGB or #RRGGBB) + "rgb\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*\)" . "|" . // RGB format + "rgba\(\s*(?:\d{1,3}\s*,\s*){3}\s*(0|0\.\d+|1)\s*\)" . "|" . // RGBA format + "[a-zA-Z]+" . // Named colors + ")$/i"; // Validate the color if (!preg_match($colorPattern, $color)) { @@ -1020,4 +1025,3 @@ function callbackTimeout(callable $callback, int $interval) return $result; } - From 807d903c8aefba4c7d9666317240bd4c17f89240 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 01:12:04 -0700 Subject: [PATCH 047/122] Add app/js/plugins/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 71641545..25983d20 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ vendor .env locale/**/*.mo app/net_activity +app/js/plugins/ From 02b31a025475545d1e33645beb674e6e3e226d88 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 01:12:56 -0700 Subject: [PATCH 048/122] Split custom.js into ajax/ + ui/ for sanity --- app/js/{custom.js => ajax/main.js} | 584 +---------------------------- app/js/ui/main.js | 583 ++++++++++++++++++++++++++++ 2 files changed, 584 insertions(+), 583 deletions(-) rename app/js/{custom.js => ajax/main.js} (53%) create mode 100644 app/js/ui/main.js diff --git a/app/js/custom.js b/app/js/ajax/main.js similarity index 53% rename from app/js/custom.js rename to app/js/ajax/main.js index 090cc826..c9dae9ea 100644 --- a/app/js/custom.js +++ b/app/js/ajax/main.js @@ -1,21 +1,3 @@ -function msgShow(retcode,msg) { - if(retcode == 0) { var alertType = 'success'; - } else if(retcode == 2 || retcode == 1) { - var alertType = 'danger'; - } - var htmlMsg = ''; - return htmlMsg; -} - -function createNetmaskAddr(bitCount) { - var mask=[]; - for(i=0;i<4;i++) { - var n = Math.min(bitCount, 8); - mask.push(256 - Math.pow(2, 8-n)); - bitCount -= n; - } - return mask.join('.'); -} function loadSummary(strInterface) { var csrfToken = $('meta[name=csrf_token]').attr('content'); @@ -38,94 +20,6 @@ function getAllInterfaces() { }); } -function setupTabs() { - $('a[data-bs-toggle="tab"]').on('shown.bs.tab',function(e){ - var target = $(e.target).attr('href'); - if(!target.match('summary')) { - var int = target.replace("#",""); - // loadCurrentSettings(int); - } - }); -} - -$(document).on("click", ".js-add-dhcp-static-lease", function(e) { - e.preventDefault(); - var container = $(".js-new-dhcp-static-lease"); - var mac = $("input[name=mac]", container).val().trim(); - var ip = $("input[name=ip]", container).val().trim(); - var comment = $("input[name=comment]", container).val().trim(); - if (mac == "" || ip == "") { - return; - } - var row = $("#js-dhcp-static-lease-row").html() - .replace("{{ mac }}", mac) - .replace("{{ ip }}", ip) - .replace("{{ comment }}", comment); - $(".js-dhcp-static-lease-container").append(row); - - $("input[name=mac]", container).val(""); - $("input[name=ip]", container).val(""); - $("input[name=comment]", container).val(""); -}); - -$(document).on("click", ".js-remove-dhcp-static-lease", function(e) { - e.preventDefault(); - $(this).parents(".js-dhcp-static-lease-row").remove(); -}); - -$(document).on("submit", ".js-dhcp-settings-form", function(e) { - $(".js-add-dhcp-static-lease").trigger("click"); -}); - -$(document).on("click", ".js-add-dhcp-upstream-server", function(e) { - e.preventDefault(); - - var field = $("#add-dhcp-upstream-server-field") - var row = $("#dhcp-upstream-server").html().replace("{{ server }}", field.val()) - - if (field.val().trim() == "") { return } - - $(".js-dhcp-upstream-servers").append(row) - - field.val("") -}); - -$(document).on("click", ".js-remove-dhcp-upstream-server", function(e) { - e.preventDefault(); - $(this).parents(".js-dhcp-upstream-server").remove(); -}); - -$(document).on("submit", ".js-dhcp-settings-form", function(e) { - $(".js-add-dhcp-upstream-server").trigger("click"); -}); - -/** - * mark a form field, e.g. a select box, with the class `.js-field-preset` - * and give it an attribute `data-field-preset-target` with a text field's - * css selector. - * - * now, if the element marked `.js-field-preset` receives a `change` event, - * its value will be copied to all elements matching the selector in - * data-field-preset-target. - */ -$(document).on("change", ".js-field-preset", function(e) { - var selector = this.getAttribute("data-field-preset-target") - var value = "" + this.value - var syncValue = function(el) { el.value = value } - - if (value.trim() === "") { return } - - document.querySelectorAll(selector).forEach(syncValue) -}); - -$(document).on("click", "#gen_wpa_passphrase", function(e) { - $('#txtwpapassphrase').val(genPassword(63)); -}); - -$(document).on("click", "#gen_apikey", function(e) { - $('#txtapikey').val(genPassword(32).toLowerCase()); -}); - $(document).on("click", "#js-clearhostapd-log", function(e) { var csrfToken = $('meta[name=csrf_token]').attr('content'); $.post('ajax/logging/clearlog.php?',{'logfile':'/tmp/hostapd.log', 'csrf_token': csrfToken},function(data){ @@ -150,54 +44,6 @@ $(document).on("click", "#js-clearopenvpn-log", function(e) { }); }); - -// Enable Bootstrap tooltips -$(function () { - $('[data-bs-toggle="tooltip"]').tooltip() -}) - -function genPassword(pwdLen) { - var pwdChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var rndPass = Array(pwdLen).fill(pwdChars).map(function(x) { return x[Math.floor(Math.random() * x.length)] }).join(''); - return rndPass; -} - -function setupBtns() { - $('#btnSummaryRefresh').click(function(){getAllInterfaces();}); - $('.intsave').click(function(){ - var int = $(this).data('int'); - saveNetworkSettings(int); - }); - $('.intapply').click(function(){ - applyNetworkSettings(); - }); -} - -function setCSRFTokenHeader(event, xhr, settings) { - var csrfToken = $('meta[name=csrf_token]').attr('content'); - if (/^(POST|PATCH|PUT|DELETE)$/i.test(settings.type)) { - xhr.setRequestHeader("X-CSRF-Token", csrfToken); - } -} - -function contentLoaded() { - pageCurrent = window.location.href.split("/").pop(); - switch(pageCurrent) { - case "network_conf": - getAllInterfaces(); - setupTabs(); - setupBtns(); - break; - case "hostapd_conf": - getChannel(); - setHardwareModeTooltip(); - break; - case "dhcpd_conf": - loadInterfaceDHCPSelect(); - break; - } -} - function loadWifiStations(refresh) { return function() { var complete = function() { $(this).removeClass('loading-spinner'); } @@ -257,26 +103,6 @@ function loadInterfaceDHCPSelect() { }); } -function setDHCPToggles(state) { - if ($('#chkfallback').is(':checked') && state) { - $('#chkfallback').prop('checked', state); - } - if ($('#dhcp-iface').is(':checked') && !state) { - $('#dhcp-iface').prop('checked', state); - setDhcpFieldsDisabled(); - } - $('#chkfallback').prop('disabled', state); - $('#dhcp-iface').prop('disabled', !state); -} - -$('#chkfallback').change(function() { - if ($('#chkfallback').is(':checked')) { - setStaticFieldsEnabled(); - } else { - setStaticFieldsDisabled(); - } -}); - $('#debugModal').on('shown.bs.modal', function (e) { var csrfToken = $('meta[name=csrf_token]').attr('content'); $.post('ajax/system/sys_debug.php',{'csrf_token': csrfToken},function(data){ @@ -327,10 +153,6 @@ $('#performUpdate').on('submit', function(event) { $('#performupdateModal').modal('show'); }); -$('#performupdateModal').on('shown.bs.modal', function (e) { - fetchUpdateResponse(); -}); - function fetchUpdateResponse() { const complete = 6; const error = 7; @@ -372,22 +194,6 @@ function fetchUpdateResponse() { }); } -$('#hostapdModal').on('shown.bs.modal', function (e) { - var seconds = 3; - var pct = 0; - var countDown = setInterval(function(){ - if(seconds <= 0){ - clearInterval(countDown); - } - document.getElementsByClassName('progress-bar').item(0).setAttribute('style','width:'+Number(pct)+'%'); - seconds --; - pct = Math.floor(100-(seconds*100/4)); - }, 500); -}); - -$('#configureClientModal').on('shown.bs.modal', function (e) { -}); - $('#ovpn-confirm-delete').on('click', '.btn-delete', function (e) { var cfg_id = $(this).data('recordId'); var csrfToken = $('meta[name=csrf_token]').attr('content'); @@ -418,21 +224,6 @@ $('#ovpn-confirm-activate').on('click', '.btn-activate', function (e) { }); }); -$('#ovpn-confirm-activate').on('shown.bs.modal', function (e) { - var data = $(e.relatedTarget).data(); - $('.btn-activate', this).data('recordId', data.recordId); -}); - -$('#ovpn-userpw,#ovpn-certs').on('click', function (e) { - if (this.id == 'ovpn-userpw') { - $('#PanelCerts').hide(); - $('#PanelUserPW').show(); - } else if (this.id == 'ovpn-certs') { - $('#PanelUserPW').hide(); - $('#PanelCerts').show(); - } -}); - $('#js-system-reset-confirm').on('click', function (e) { var progressText = $('#js-system-reset-confirm').attr('data-message'); var successHtml = $('#system-reset-message').attr('data-message'); @@ -463,49 +254,6 @@ $('#js-sys-reboot, #js-sys-shutdown').on('click', function (e) { }); }); -$('#install-user-plugin').on('shown.bs.modal', function (e) { - var button = $(e.relatedTarget); - $(this).data('button', button); - var manifestData = button.data('plugin-manifest'); - var installed = button.data('plugin-installed') || false; - var repoPublic = button.data('repo-public') || false; - var installPath = manifestData.install_path; - - if (!installed && repoPublic && installPath === 'plugins-available') { - insidersHTML = 'Available with Insiders'; - $('#plugin-additional').html(insidersHTML); - } else { - $('#plugin-additional').empty(); - } - if (manifestData) { - $('#plugin-docs').html(manifestData.plugin_docs - ? `${manifestData.plugin_docs}` - : 'Unknown'); - $('#plugin-icon').attr('class', `${manifestData.icon || 'fas fa-plug'} link-secondary h5 me-2`); - $('#plugin-name').text(manifestData.name || 'Unknown'); - $('#plugin-version').text(manifestData.version || 'Unknown'); - $('#plugin-description').text(manifestData.description || 'No description provided'); - $('#plugin-author').html(manifestData.author - ? manifestData.author + (manifestData.author_uri - ? ` (profile)` : '') : 'Unknown'); - $('#plugin-license').text(manifestData.license || 'Unknown'); - $('#plugin-locale').text(manifestData.default_locale || 'Unknown'); - $('#plugin-configuration').html(formatProperty(manifestData.configuration || 'None')); - $('#plugin-packages').html(formatProperty(manifestData.keys || 'None')); - $('#plugin-dependencies').html(formatProperty(manifestData.dependencies || 'None')); - $('#plugin-javascript').html(formatProperty(manifestData.javascript || 'None')); - $('#plugin-sudoers').html(formatProperty(manifestData.sudoers || 'None')); - $('#plugin-user-name').html((manifestData.user_nonprivileged && manifestData.user_nonprivileged.name) || 'None'); - } - if (installed) { - $('#js-install-plugin-confirm').html('OK'); - } else if (!installed && repoPublic && installPath == 'plugins-available') { - $('#js-install-plugin-confirm').html('Get Insiders'); - } else { - $('#js-install-plugin-confirm').html('Install now'); - } -}); - $('#js-install-plugin-confirm').on('click', function (e) { var button = $('#install-user-plugin').data('button'); var manifestData = button.data('plugin-manifest'); @@ -572,75 +320,7 @@ $('#js-install-plugin-confirm').on('click', function (e) { } }); -$('#js-install-plugin-ok').on('click', function (e) { - $("#install-plugin-progress").modal('hide'); - window.location.reload(); -}); - -function formatProperty(prop) { - if (Array.isArray(prop)) { - if (typeof prop[0] === 'object') { - return prop.map(item => { - return Object.entries(item) - .map(([key, value]) => `${key}: ${value}`) - .join('
'); - }).join('
'); - } - return prop.map(line => `${line}
`).join(''); - } - if (typeof prop === 'object') { - return Object.entries(prop) - .map(([key, value]) => `${key}: ${value}`) - .join('
'); - } - return prop || 'None'; -} - -$(document).ready(function(){ - $("#PanelManual").hide(); - $('.ip_address').mask('0ZZ.0ZZ.0ZZ.0ZZ', { - translation: { - 'Z': { - pattern: /[0-9]/, optional: true - } - }, - placeholder: "___.___.___.___" - }); - $('.mac_address').mask('FF:FF:FF:FF:FF:FF', { - translation: { - 'F': { - pattern: /[0-9a-fA-F]/, optional: false - } - }, - placeholder: "__:__:__:__:__:__" - }); -}); - -$(document).ready(function() { - $('.cidr').mask('099.099.099.099/099', { - translation: { - '0': { pattern: /[0-9]/ } - }, - placeholder: "___.___.___.___/___" - }); -}); - -$('#wg-upload,#wg-manual').on('click', function (e) { - if (this.id == 'wg-upload') { - $('#PanelManual').hide(); - $('#PanelUpload').show(); - } else if (this.id == 'wg-manual') { - $('#PanelUpload').hide(); - $('#PanelManual').show(); - } -}); - -$(".custom-file-input").on("change", function() { - var fileName = $(this).val().split("\\").pop(); - $(this).siblings(".custom-file-label").addClass("selected").html(fileName); -}); - - // Retrieves the 'channel' value specified in hostapd.conf +// Retrieves the 'channel' value specified in hostapd.conf function getChannel() { $.get('ajax/networking/get_channel.php',function(data){ jsonData = JSON.parse(data); @@ -827,22 +507,6 @@ $('.wg-client-dl').click(function(){ req.send(); }) -// Event listener for Bootstrap's form validation -window.addEventListener('load', function() { - // Fetch all the forms we want to apply custom Bootstrap validation styles to - var forms = document.getElementsByClassName('needs-validation'); - // Loop over them and prevent submission - var validation = Array.prototype.filter.call(forms, function(form) { - form.addEventListener('submit', function(event) { - if (form.checkValidity() === false) { - event.preventDefault(); - event.stopPropagation(); - } - form.classList.add('was-validated'); - }, false); - }); -}, false); - let sessionCheckInterval = setInterval(checkSession, 5000); function checkSession() { @@ -861,249 +525,3 @@ function checkSession() { }); } -function showSessionExpiredModal() { - $('#sessionTimeoutModal').modal('show'); -} - -$(document).on("click", "#js-session-expired-login", function(e) { - const loginModal = $('#modal-admin-login'); - const redirectUrl = window.location.pathname; - window.location.href = `/login?action=${encodeURIComponent(redirectUrl)}`; -}); - -// show modal login on page load -$(document).ready(function () { - const params = new URLSearchParams(window.location.search); - const redirectUrl = $('#redirect-url').val() || params.get('action') || '/'; - $('#modal-admin-login').modal('show'); - $('#redirect-url').val(redirectUrl); - $('#username').focus(); - $('#username').addClass("focusedInput"); -}); - -// DHCP or Static IP option group -$('#chkstatic').on('change', function() { - if (this.checked) { - setStaticFieldsEnabled(); - } -}); - -$('#chkdhcp').on('change', function() { - this.checked ? setStaticFieldsDisabled() : null; -}); - - -$('input[name="dhcp-iface"]').change(function() { - if ($('input[name="dhcp-iface"]:checked').val() == '1') { - setDhcpFieldsEnabled(); - } else { - setDhcpFieldsDisabled(); - } -}); - - -function setStaticFieldsEnabled() { - $('#txtipaddress').prop('required', true); - $('#txtsubnetmask').prop('required', true); - $('#txtgateway').prop('required', true); - - $('#txtipaddress').removeAttr('disabled'); - $('#txtsubnetmask').removeAttr('disabled'); - $('#txtgateway').removeAttr('disabled'); -} - -function setStaticFieldsDisabled() { - $('#txtipaddress').prop('disabled', true); - $('#txtsubnetmask').prop('disabled', true); - $('#txtgateway').prop('disabled', true); - - $('#txtipaddress').removeAttr('required'); - $('#txtsubnetmask').removeAttr('required'); - $('#txtgateway').removeAttr('required'); -} - -function setDhcpFieldsEnabled() { - $('#txtrangestart').prop('required', true); - $('#txtrangeend').prop('required', true); - $('#txtrangeleasetime').prop('required', true); - $('#cbxrangeleasetimeunits').prop('required', true); - - $('#txtrangestart').removeAttr('disabled'); - $('#txtrangeend').removeAttr('disabled'); - $('#txtrangeleasetime').removeAttr('disabled'); - $('#cbxrangeleasetimeunits').removeAttr('disabled'); - $('#txtdns1').removeAttr('disabled'); - $('#txtdns2').removeAttr('disabled'); - $('#txtmetric').removeAttr('disabled'); -} - -function setDhcpFieldsDisabled() { - $('#txtrangestart').removeAttr('required'); - $('#txtrangeend').removeAttr('required'); - $('#txtrangeleasetime').removeAttr('required'); - $('#cbxrangeleasetimeunits').removeAttr('required'); - - $('#txtrangestart').prop('disabled', true); - $('#txtrangeend').prop('disabled', true); - $('#txtrangeleasetime').prop('disabled', true); - $('#cbxrangeleasetimeunits').prop('disabled', true); - $('#txtdns1').prop('disabled', true); - $('#txtdns2').prop('disabled', true); - $('#txtmetric').prop('disabled', true); -} - -// Static Array method -Array.range = (start, end) => Array.from({length: (end - start)}, (v, k) => k + start); - -$(document).on("click", ".js-toggle-password", function(e) { - var button = $(e.currentTarget); - var field = $(button.data("bsTarget")); - if (field.is(":input")) { - e.preventDefault(); - - if (!button.data("__toggle-with-initial")) { - $("i", button).removeClass("fas fa-eye").addClass(button.attr("data-toggle-with")); - } - - if (field.attr("type") === "password") { - field.attr("type", "text"); - } else { - $("i", button).removeClass("fas fa-eye-slash").addClass("fas fa-eye"); - field.attr("type", "password"); - } - } -}); - -$(function() { - $('#theme-select').change(function() { - var theme = themes[$( "#theme-select" ).val() ]; - - var hasDarkTheme = theme === 'custom.php'; - var nightModeChecked = $("#night-mode").prop("checked"); - - if (nightModeChecked && hasDarkTheme) { - if (theme === "custom.php") { - set_theme("dark.css"); - } - } else { - set_theme(theme); - } - }); -}); - -function set_theme(theme) { - $('link[title="main"]').attr('href', 'app/css/' + theme); - // persist selected theme in cookie - setCookie('theme',theme,90); -} - -$(function() { - var currentTheme = getCookie('theme'); - // Check if the current theme is a dark theme - var isDarkTheme = currentTheme === 'dark.css'; - - $('#night-mode').prop('checked', isDarkTheme); - $('#night-mode').change(function() { - var state = $(this).is(':checked'); - var currentTheme = getCookie('theme'); - - if (state == true) { - if (currentTheme == 'custom.php') { - set_theme('dark.css'); - } - } else { - if (currentTheme == 'dark.css') { - set_theme('custom.php'); - } - } - }); -}); - -function setCookie(cname, cvalue, exdays) { - var d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = "expires="+ d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; -} - -function getCookie(cname) { - var regx = new RegExp(cname + "=([^;]+)"); - var value = regx.exec(document.cookie); - return (value != null) ? unescape(value[1]) : null; -} - -// Define themes -var themes = { - "default": "custom.php", - "hackernews" : "hackernews.css" -} - -// Adds active class to current nav-item -$(window).bind("load", function() { - var url = window.location; - $('.sb-nav-link-icon a').filter(function() { - return this.href == url; - }).parent().addClass('active'); -}); - -// Sets focus on a specified tab -document.addEventListener("DOMContentLoaded", function () { - const params = new URLSearchParams(window.location.search); - const targetTab = params.get("tab"); - if (targetTab) { - let tabElement = document.querySelector(`[data-bs-toggle="tab"][href="#${targetTab}"]`); - if (tabElement) { - let tab = new bootstrap.Tab(tabElement); - tab.show(); - } - } -}); - -function disableValidation(form) { - form.removeAttribute("novalidate"); - form.classList.remove("needs-validation"); - form.querySelectorAll("[required]").forEach(function (field) { - field.removeAttribute("required"); - }); -} - -function updateActivityLED() { - const threshold_bytes = 300; - fetch('/app/net_activity') - .then(res => res.text()) - .then(data => { - const activity = parseInt(data.trim()); - const leds = document.querySelectorAll('.hostapd-led'); - - if (!isNaN(activity)) { - leds.forEach(led => { - if (activity > threshold_bytes) { - led.classList.add('led-pulse'); - setTimeout(() => { - led.classList.remove('led-pulse'); - }, 50); - } else { - led.classList.remove('led-pulse'); - } - }); - } - }) - .catch(() => { /* ignore fetch errors */ }); -} -setInterval(updateActivityLED, 100); - -$(document).ready(function() { - const $htmlElement = $('html'); - const $modeswitch = $('#night-mode'); - $modeswitch.on('change', function() { - const isChecked = $(this).is(':checked'); - const newTheme = isChecked ? 'dark' : 'light'; - $htmlElement.attr('data-bs-theme', newTheme); - localStorage.setItem('bsTheme', newTheme); - }); -}); - -$(document) - .ajaxSend(setCSRFTokenHeader) - .ready(contentLoaded) - .ready(loadWifiStations()); diff --git a/app/js/ui/main.js b/app/js/ui/main.js new file mode 100644 index 00000000..97824fcc --- /dev/null +++ b/app/js/ui/main.js @@ -0,0 +1,583 @@ + +function msgShow(retcode,msg) { + if(retcode == 0) { var alertType = 'success'; + } else if(retcode == 2 || retcode == 1) { + var alertType = 'danger'; + } + var htmlMsg = ''; + return htmlMsg; +} + +function createNetmaskAddr(bitCount) { + var mask=[]; + for(i=0;i<4;i++) { + var n = Math.min(bitCount, 8); + mask.push(256 - Math.pow(2, 8-n)); + bitCount -= n; + } + return mask.join('.'); +} + +function setupTabs() { + $('a[data-bs-toggle="tab"]').on('shown.bs.tab',function(e){ + var target = $(e.target).attr('href'); + if(!target.match('summary')) { + var int = target.replace("#",""); + } + }); +} + +$(document).on("click", ".js-add-dhcp-static-lease", function(e) { + e.preventDefault(); + var container = $(".js-new-dhcp-static-lease"); + var mac = $("input[name=mac]", container).val().trim(); + var ip = $("input[name=ip]", container).val().trim(); + var comment = $("input[name=comment]", container).val().trim(); + if (mac == "" || ip == "") { + return; + } + var row = $("#js-dhcp-static-lease-row").html() + .replace("{{ mac }}", mac) + .replace("{{ ip }}", ip) + .replace("{{ comment }}", comment); + $(".js-dhcp-static-lease-container").append(row); + + $("input[name=mac]", container).val(""); + $("input[name=ip]", container).val(""); + $("input[name=comment]", container).val(""); +}); + +$(document).on("click", ".js-remove-dhcp-static-lease", function(e) { + e.preventDefault(); + $(this).parents(".js-dhcp-static-lease-row").remove(); +}); + +$(document).on("submit", ".js-dhcp-settings-form", function(e) { + $(".js-add-dhcp-static-lease").trigger("click"); +}); + +$(document).on("click", ".js-add-dhcp-upstream-server", function(e) { + e.preventDefault(); + + var field = $("#add-dhcp-upstream-server-field") + var row = $("#dhcp-upstream-server").html().replace("{{ server }}", field.val()) + + if (field.val().trim() == "") { return } + + $(".js-dhcp-upstream-servers").append(row) + + field.val("") +}); + +$(document).on("click", ".js-remove-dhcp-upstream-server", function(e) { + e.preventDefault(); + $(this).parents(".js-dhcp-upstream-server").remove(); +}); + +$(document).on("submit", ".js-dhcp-settings-form", function(e) { + $(".js-add-dhcp-upstream-server").trigger("click"); +}); + +/** + * mark a form field, e.g. a select box, with the class `.js-field-preset` + * and give it an attribute `data-field-preset-target` with a text field's + * css selector. + * + * now, if the element marked `.js-field-preset` receives a `change` event, + * its value will be copied to all elements matching the selector in + * data-field-preset-target. + */ +$(document).on("change", ".js-field-preset", function(e) { + var selector = this.getAttribute("data-field-preset-target") + var value = "" + this.value + var syncValue = function(el) { el.value = value } + + if (value.trim() === "") { return } + + document.querySelectorAll(selector).forEach(syncValue) +}); + +$(document).on("click", "#gen_wpa_passphrase", function(e) { + $('#txtwpapassphrase').val(genPassword(63)); +}); + +$(document).on("click", "#gen_apikey", function(e) { + $('#txtapikey').val(genPassword(32).toLowerCase()); +}); + +// Enable Bootstrap tooltips +$(function () { + $('[data-bs-toggle="tooltip"]').tooltip() +}) + +function genPassword(pwdLen) { + var pwdChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + var rndPass = Array(pwdLen).fill(pwdChars).map(function(x) { return x[Math.floor(Math.random() * x.length)] }).join(''); + return rndPass; +} + +function setupBtns() { + $('#btnSummaryRefresh').click(function(){getAllInterfaces();}); + $('.intsave').click(function(){ + var int = $(this).data('int'); + saveNetworkSettings(int); + }); + $('.intapply').click(function(){ + applyNetworkSettings(); + }); +} + +function setCSRFTokenHeader(event, xhr, settings) { + var csrfToken = $('meta[name=csrf_token]').attr('content'); + if (/^(POST|PATCH|PUT|DELETE)$/i.test(settings.type)) { + xhr.setRequestHeader("X-CSRF-Token", csrfToken); + } +} + +function contentLoaded() { + pageCurrent = window.location.href.split("/").pop(); + switch(pageCurrent) { + case "network_conf": + getAllInterfaces(); + setupTabs(); + setupBtns(); + break; + case "hostapd_conf": + getChannel(); + setHardwareModeTooltip(); + break; + case "dhcpd_conf": + loadInterfaceDHCPSelect(); + break; + } +} + +function setDHCPToggles(state) { + if ($('#chkfallback').is(':checked') && state) { + $('#chkfallback').prop('checked', state); + } + if ($('#dhcp-iface').is(':checked') && !state) { + $('#dhcp-iface').prop('checked', state); + setDhcpFieldsDisabled(); + } + $('#chkfallback').prop('disabled', state); + $('#dhcp-iface').prop('disabled', !state); +} + +$('#chkfallback').change(function() { + if ($('#chkfallback').is(':checked')) { + setStaticFieldsEnabled(); + } else { + setStaticFieldsDisabled(); + } +}); + +$('#performupdateModal').on('shown.bs.modal', function (e) { + fetchUpdateResponse(); +}); + +$('#hostapdModal').on('shown.bs.modal', function (e) { + var seconds = 3; + var pct = 0; + var countDown = setInterval(function(){ + if(seconds <= 0){ + clearInterval(countDown); + } + document.getElementsByClassName('progress-bar').item(0).setAttribute('style','width:'+Number(pct)+'%'); + seconds --; + pct = Math.floor(100-(seconds*100/4)); + }, 500); +}); + +$('#configureClientModal').on('shown.bs.modal', function (e) { +}); + +$('#ovpn-confirm-activate').on('shown.bs.modal', function (e) { + var data = $(e.relatedTarget).data(); + $('.btn-activate', this).data('recordId', data.recordId); +}); + +$('#ovpn-userpw,#ovpn-certs').on('click', function (e) { + if (this.id == 'ovpn-userpw') { + $('#PanelCerts').hide(); + $('#PanelUserPW').show(); + } else if (this.id == 'ovpn-certs') { + $('#PanelUserPW').hide(); + $('#PanelCerts').show(); + } +}); + +$('#install-user-plugin').on('shown.bs.modal', function (e) { + var button = $(e.relatedTarget); + $(this).data('button', button); + var manifestData = button.data('plugin-manifest'); + var installed = button.data('plugin-installed') || false; + var repoPublic = button.data('repo-public') || false; + var installPath = manifestData.install_path; + + if (!installed && repoPublic && installPath === 'plugins-available') { + insidersHTML = 'Available with Insiders'; + $('#plugin-additional').html(insidersHTML); + } else { + $('#plugin-additional').empty(); + } + if (manifestData) { + $('#plugin-docs').html(manifestData.plugin_docs + ? `${manifestData.plugin_docs}` + : 'Unknown'); + $('#plugin-icon').attr('class', `${manifestData.icon || 'fas fa-plug'} link-secondary h5 me-2`); + $('#plugin-name').text(manifestData.name || 'Unknown'); + $('#plugin-version').text(manifestData.version || 'Unknown'); + $('#plugin-description').text(manifestData.description || 'No description provided'); + $('#plugin-author').html(manifestData.author + ? manifestData.author + (manifestData.author_uri + ? ` (profile)` : '') : 'Unknown'); + $('#plugin-license').text(manifestData.license || 'Unknown'); + $('#plugin-locale').text(manifestData.default_locale || 'Unknown'); + $('#plugin-configuration').html(formatProperty(manifestData.configuration || 'None')); + $('#plugin-packages').html(formatProperty(manifestData.keys || 'None')); + $('#plugin-dependencies').html(formatProperty(manifestData.dependencies || 'None')); + $('#plugin-javascript').html(formatProperty(manifestData.javascript || 'None')); + $('#plugin-sudoers').html(formatProperty(manifestData.sudoers || 'None')); + $('#plugin-user-name').html((manifestData.user_nonprivileged && manifestData.user_nonprivileged.name) || 'None'); + } + if (installed) { + $('#js-install-plugin-confirm').html('OK'); + } else if (!installed && repoPublic && installPath == 'plugins-available') { + $('#js-install-plugin-confirm').html('Get Insiders'); + } else { + $('#js-install-plugin-confirm').html('Install now'); + } +}); + +$('#js-install-plugin-ok').on('click', function (e) { + $("#install-plugin-progress").modal('hide'); + window.location.reload(); +}); + +function formatProperty(prop) { + if (Array.isArray(prop)) { + if (typeof prop[0] === 'object') { + return prop.map(item => { + return Object.entries(item) + .map(([key, value]) => `${key}: ${value}`) + .join('
'); + }).join('
'); + } + return prop.map(line => `${line}
`).join(''); + } + if (typeof prop === 'object') { + return Object.entries(prop) + .map(([key, value]) => `${key}: ${value}`) + .join('
'); + } + return prop || 'None'; +} + +$(document).ready(function(){ + $("#PanelManual").hide(); + $('.ip_address').mask('0ZZ.0ZZ.0ZZ.0ZZ', { + translation: { + 'Z': { + pattern: /[0-9]/, optional: true + } + }, + placeholder: "___.___.___.___" + }); + $('.mac_address').mask('FF:FF:FF:FF:FF:FF', { + translation: { + 'F': { + pattern: /[0-9a-fA-F]/, optional: false + } + }, + placeholder: "__:__:__:__:__:__" + }); +}); + +$(document).ready(function() { + $('.cidr').mask('099.099.099.099/099', { + translation: { + '0': { pattern: /[0-9]/ } + }, + placeholder: "___.___.___.___/___" + }); +}); + +$('#wg-upload,#wg-manual').on('click', function (e) { + if (this.id == 'wg-upload') { + $('#PanelManual').hide(); + $('#PanelUpload').show(); + } else if (this.id == 'wg-manual') { + $('#PanelUpload').hide(); + $('#PanelManual').show(); + } +}); + +$(".custom-file-input").on("change", function() { + var fileName = $(this).val().split("\\").pop(); + $(this).siblings(".custom-file-label").addClass("selected").html(fileName); +}); + +// Event listener for Bootstrap's form validation +window.addEventListener('load', function() { + // Fetch all the forms we want to apply custom Bootstrap validation styles to + var forms = document.getElementsByClassName('needs-validation'); + // Loop over them and prevent submission + var validation = Array.prototype.filter.call(forms, function(form) { + form.addEventListener('submit', function(event) { + if (form.checkValidity() === false) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); + }); +}, false); + +function showSessionExpiredModal() { + $('#sessionTimeoutModal').modal('show'); +} + +$(document).on("click", "#js-session-expired-login", function(e) { + const loginModal = $('#modal-admin-login'); + const redirectUrl = window.location.pathname; + window.location.href = `/login?action=${encodeURIComponent(redirectUrl)}`; +}); + +// show modal login on page load +$(document).ready(function () { + const params = new URLSearchParams(window.location.search); + const redirectUrl = $('#redirect-url').val() || params.get('action') || '/'; + $('#modal-admin-login').modal('show'); + $('#redirect-url').val(redirectUrl); + $('#username').focus(); + $('#username').addClass("focusedInput"); +}); + +// DHCP or Static IP option group +$('#chkstatic').on('change', function() { + if (this.checked) { + setStaticFieldsEnabled(); + } +}); + +$('#chkdhcp').on('change', function() { + this.checked ? setStaticFieldsDisabled() : null; +}); + + +$('input[name="dhcp-iface"]').change(function() { + if ($('input[name="dhcp-iface"]:checked').val() == '1') { + setDhcpFieldsEnabled(); + } else { + setDhcpFieldsDisabled(); + } +}); + + +function setStaticFieldsEnabled() { + $('#txtipaddress').prop('required', true); + $('#txtsubnetmask').prop('required', true); + $('#txtgateway').prop('required', true); + + $('#txtipaddress').removeAttr('disabled'); + $('#txtsubnetmask').removeAttr('disabled'); + $('#txtgateway').removeAttr('disabled'); +} + +function setStaticFieldsDisabled() { + $('#txtipaddress').prop('disabled', true); + $('#txtsubnetmask').prop('disabled', true); + $('#txtgateway').prop('disabled', true); + + $('#txtipaddress').removeAttr('required'); + $('#txtsubnetmask').removeAttr('required'); + $('#txtgateway').removeAttr('required'); +} + +function setDhcpFieldsEnabled() { + $('#txtrangestart').prop('required', true); + $('#txtrangeend').prop('required', true); + $('#txtrangeleasetime').prop('required', true); + $('#cbxrangeleasetimeunits').prop('required', true); + + $('#txtrangestart').removeAttr('disabled'); + $('#txtrangeend').removeAttr('disabled'); + $('#txtrangeleasetime').removeAttr('disabled'); + $('#cbxrangeleasetimeunits').removeAttr('disabled'); + $('#txtdns1').removeAttr('disabled'); + $('#txtdns2').removeAttr('disabled'); + $('#txtmetric').removeAttr('disabled'); +} + +function setDhcpFieldsDisabled() { + $('#txtrangestart').removeAttr('required'); + $('#txtrangeend').removeAttr('required'); + $('#txtrangeleasetime').removeAttr('required'); + $('#cbxrangeleasetimeunits').removeAttr('required'); + + $('#txtrangestart').prop('disabled', true); + $('#txtrangeend').prop('disabled', true); + $('#txtrangeleasetime').prop('disabled', true); + $('#cbxrangeleasetimeunits').prop('disabled', true); + $('#txtdns1').prop('disabled', true); + $('#txtdns2').prop('disabled', true); + $('#txtmetric').prop('disabled', true); +} + +// Static Array method +Array.range = (start, end) => Array.from({length: (end - start)}, (v, k) => k + start); + +$(document).on("click", ".js-toggle-password", function(e) { + var button = $(e.currentTarget); + var field = $(button.data("bsTarget")); + if (field.is(":input")) { + e.preventDefault(); + + if (!button.data("__toggle-with-initial")) { + $("i", button).removeClass("fas fa-eye").addClass(button.attr("data-toggle-with")); + } + + if (field.attr("type") === "password") { + field.attr("type", "text"); + } else { + $("i", button).removeClass("fas fa-eye-slash").addClass("fas fa-eye"); + field.attr("type", "password"); + } + } +}); + +$(function() { + $('#theme-select').change(function() { + var theme = themes[$( "#theme-select" ).val() ]; + + var hasDarkTheme = theme === 'custom.php'; + var nightModeChecked = $("#night-mode").prop("checked"); + + if (nightModeChecked && hasDarkTheme) { + if (theme === "custom.php") { + set_theme("dark.css"); + } + } else { + set_theme(theme); + } + }); +}); + +function set_theme(theme) { + $('link[title="main"]').attr('href', 'app/css/' + theme); + // persist selected theme in cookie + setCookie('theme',theme,90); +} + +$(function() { + var currentTheme = getCookie('theme'); + // Check if the current theme is a dark theme + var isDarkTheme = currentTheme === 'dark.css'; + + $('#night-mode').prop('checked', isDarkTheme); + $('#night-mode').change(function() { + var state = $(this).is(':checked'); + var currentTheme = getCookie('theme'); + + if (state == true) { + if (currentTheme == 'custom.php') { + set_theme('dark.css'); + } + } else { + if (currentTheme == 'dark.css') { + set_theme('custom.php'); + } + } + }); +}); + +function setCookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays*24*60*60*1000)); + var expires = "expires="+ d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function getCookie(cname) { + var regx = new RegExp(cname + "=([^;]+)"); + var value = regx.exec(document.cookie); + return (value != null) ? unescape(value[1]) : null; +} + +// Define themes +var themes = { + "default": "custom.php", + "hackernews" : "hackernews.css" +} + +// Adds active class to current nav-item +$(window).bind("load", function() { + var url = window.location; + $('.sb-nav-link-icon a').filter(function() { + return this.href == url; + }).parent().addClass('active'); +}); + +// Sets focus on a specified tab +document.addEventListener("DOMContentLoaded", function () { + const params = new URLSearchParams(window.location.search); + const targetTab = params.get("tab"); + if (targetTab) { + let tabElement = document.querySelector(`[data-bs-toggle="tab"][href="#${targetTab}"]`); + if (tabElement) { + let tab = new bootstrap.Tab(tabElement); + tab.show(); + } + } +}); + +function disableValidation(form) { + form.removeAttribute("novalidate"); + form.classList.remove("needs-validation"); + form.querySelectorAll("[required]").forEach(function (field) { + field.removeAttribute("required"); + }); +} + +function updateActivityLED() { + const threshold_bytes = 300; + fetch('/app/net_activity') + .then(res => res.text()) + .then(data => { + const activity = parseInt(data.trim()); + const leds = document.querySelectorAll('.hostapd-led'); + + if (!isNaN(activity)) { + leds.forEach(led => { + if (activity > threshold_bytes) { + led.classList.add('led-pulse'); + setTimeout(() => { + led.classList.remove('led-pulse'); + }, 50); + } else { + led.classList.remove('led-pulse'); + } + }); + } + }) + .catch(() => { /* ignore fetch errors */ }); +} +setInterval(updateActivityLED, 100); + +$(document).ready(function() { + const $htmlElement = $('html'); + const $modeswitch = $('#night-mode'); + $modeswitch.on('change', function() { + const isChecked = $(this).is(':checked'); + const newTheme = isChecked ? 'dark' : 'light'; + $htmlElement.attr('data-bs-theme', newTheme); + localStorage.setItem('bsTheme', newTheme); + }); +}); + +$(document) + .ajaxSend(setCSRFTokenHeader) + .ready(contentLoaded) + .ready(loadWifiStations()); + From 5040507750c623b425e64942dd6609c0a853fcfd Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 01:13:23 -0700 Subject: [PATCH 049/122] Load scripts from js/vendor for sanity --- includes/dashboard.php | 2 +- includes/data_usage.php | 2 +- includes/networking.php | 2 +- includes/system.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/dashboard.php b/includes/dashboard.php index f0f6e3e8..f01917ca 100755 --- a/includes/dashboard.php +++ b/includes/dashboard.php @@ -123,7 +123,7 @@ function DisplayDashboard(&$extraFooterScripts): void "status" ) ); - $extraFooterScripts[] = array('src'=>'app/js/dashboardchart.js', 'defer'=>false); + $extraFooterScripts[] = array('src'=>'app/js/vendor/dashboardchart.js', 'defer'=>false); } /** diff --git a/includes/data_usage.php b/includes/data_usage.php index 3c7753bd..8ea698d6 100755 --- a/includes/data_usage.php +++ b/includes/data_usage.php @@ -9,5 +9,5 @@ function DisplayDataUsage(&$extraFooterScripts) echo renderTemplate("data_usage", [ "interfaces" => $interfacesWlo ]); $extraFooterScripts[] = array('src'=>'dist/datatables/jquery.dataTables.min.js', 'defer'=>false); - $extraFooterScripts[] = array('src'=>'app/js/bandwidthcharts.js', 'defer'=>false); + $extraFooterScripts[] = array('src'=>'app/js/vendor/bandwidthcharts.js', 'defer'=>false); } diff --git a/includes/networking.php b/includes/networking.php index 83c587ed..5ebfffe4 100755 --- a/includes/networking.php +++ b/includes/networking.php @@ -23,5 +23,5 @@ function DisplayNetworkingConfig(&$extraFooterScripts) "routeInfoRaw", "bridgedEnabled") ); - $extraFooterScripts[] = array('src'=>'app/js/speedtestUI.js', 'defer'=>false); + $extraFooterScripts[] = array('src'=>'app/js/vendor/speedtestUI.js', 'defer'=>false); } diff --git a/includes/system.php b/includes/system.php index 343e1aab..6bc73875 100755 --- a/includes/system.php +++ b/includes/system.php @@ -122,7 +122,7 @@ function DisplaySystem(&$extraFooterScripts) ]; $selectedTheme = array_search($_COOKIE['theme'], $themeFiles); $extraFooterScripts[] = array('src'=>'dist/huebee/huebee.pkgd.min.js', 'defer'=>false); - $extraFooterScripts[] = array('src'=>'app/js/huebee.js', 'defer'=>false); + $extraFooterScripts[] = array('src'=>'app/js/vendor/huebee.js', 'defer'=>false); $logLimit = isset($_SESSION['log_limit']) ? $_SESSION['log_limit'] : RASPI_LOG_SIZE_LIMIT; $plugins = $pluginInstaller->getUserPlugins(); From 134f80ada89295c332354b499ec26bfed9b2b307 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 01:14:15 -0700 Subject: [PATCH 050/122] Replace monolithic custom.js with ajax + ui --- index.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.php b/index.php index c5b8d4fa..8250a153 100755 --- a/index.php +++ b/index.php @@ -147,8 +147,9 @@ initializeApp(); - - + + + From e31ccd09e8ae6008f9a355f4bfcf7f88365c4c3b Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 01:14:38 -0700 Subject: [PATCH 051/122] Group vendor related js into js/vendor --- app/js/bandwidthcharts.js | 147 -------------------------- app/js/bandwidthcharts.min.js | 7 -- app/js/custom.min.js | 7 -- app/js/dashboardchart.js | 106 ------------------- app/js/huebee.js | 22 ---- app/js/linkquality.js | 88 ---------------- app/js/speedtestUI.js | 189 ---------------------------------- plugins | 2 +- 8 files changed, 1 insertion(+), 567 deletions(-) delete mode 100644 app/js/bandwidthcharts.js delete mode 100644 app/js/bandwidthcharts.min.js delete mode 100644 app/js/custom.min.js delete mode 100644 app/js/dashboardchart.js delete mode 100644 app/js/huebee.js delete mode 100644 app/js/linkquality.js delete mode 100644 app/js/speedtestUI.js diff --git a/app/js/bandwidthcharts.js b/app/js/bandwidthcharts.js deleted file mode 100644 index 2bc63d72..00000000 --- a/app/js/bandwidthcharts.js +++ /dev/null @@ -1,147 +0,0 @@ -(function($, _t) { - "use strict"; - - /** - * Create a Chart.js barchart. - */ - function CreateChart(ctx, labels) { - var barchart = new Chart(ctx,{ - type: 'line', - options: { - responsive: true, - scales: { - xAxes: [{ - scaleLabel: { - display: true, - labelString: 'date', - }, - ticks: { - maxRotation: 90, - minRotation: 80 - } - }], - yAxes: [{ - id: 'y-axis-1', - type: 'linear', - display: true, - position: 'left', - ticks: { - beginAtZero: true - } - }] - } - }, - data: { - labels: labels, - datasets: [] - } - }); - - return barchart; - } - - /** - * Create a jquery bootstrap datatable. - */ - function CreateDataTable(placeholder, timeunits) { - $("#"+placeholder).append(''+ - '
daterxtx
'); - } - - /** - * Figure out which tab is selected and remove all existing charts and then - * construct the proper barchart. - */ - function ShowBandwidthChartHandler(e) { - // Remove all chartjs charts - $('#divChartBandwidthhourly').empty(); - $('#divChartBandwidthdaily').empty(); - $('#divChartBandwidthmonthly').empty(); - // Remove all datatables - $('#divTableBandwidthhourly').empty(); - $('#divTableBandwidthdaily').empty(); - $('#divTableBandwidthmonthly').empty(); - // Construct ajax uri for getting the proper data. - var timeunit = $('ul#tabbarBandwidth li.nav-item a.nav-link.active').attr('href').substr(1); - var uri = 'ajax/bandwidth/get_bandwidth.php?'; - uri += 'inet='; - uri += encodeURIComponent($('#cbxInterface'+timeunit+' option:selected').text()); - uri += '&tu='; - uri += encodeURIComponent(timeunit.substr(0, 1)); - var datasizeunits = 'mb'; - uri += '&dsu='+encodeURIComponent(datasizeunits); - // Init. datatable html - var datatable = CreateDataTable('divTableBandwidth'+timeunit, timeunit); - // Get data for chart - $.ajax({ - url: uri, - dataType: 'json', - beforeSend: function() { - $('#divLoaderBandwidth'+timeunit).show(); - } - }).done(function(jsondata) { - $('#divLoaderBandwidth'+timeunit).hide(); - // Map json values to label array - var labels = jsondata.map(function(e) { - return e.date; - }); - // Init. chart with label series - var barchart = CreateChart('divChartBandwidth'+timeunit, labels); - var dataRx = jsondata.map(function(e) { - return e.rx; - }); - var dataTx = jsondata.map(function(e) { - return e.tx; - }); - - addData(barchart, dataRx, dataTx, datasizeunits); - $('#tableBandwidth'+timeunit).DataTable({ - 'searching': false, - 'paging': false, - 'data': jsondata, - 'order': [[ 0, 'ASC' ]], - 'columns': [ - { 'data': 'date' }, - { 'data': 'rx', "title": _t['receive']+' '+datasizeunits.toUpperCase() }, - { 'data': 'tx', "title": _t['send']+' '+datasizeunits.toUpperCase() }] - }); - }).fail(function(xhr, textStatus) { - if (window.console) { - console.error('server error'); - } else { - alert("server error"); - } - }); - } - /** - * Add data array to datasets of current chart. - */ - function addData(chart, dataRx, dataTx, datasizeunits) { - chart.data.datasets.push({ - label: 'Receive'+' '+datasizeunits.toUpperCase(), - yAxisID: 'y-axis-1', - borderColor: 'rgba(75, 192, 192, 1)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - data: dataRx - }); - chart.data.datasets.push({ - label: 'Send'+' '+datasizeunits.toUpperCase(), - yAxisID: 'y-axis-1', - borderColor: 'rgba(192, 192, 192, 1)', - backgroundColor: 'rgba(192, 192, 192, 0.2)', - data: dataTx - }); - chart.update(); - } - - $(document).ready(function() { - $('#tabbarBandwidth a[data-toggle="tab"]').on('shown.bs.tab', ShowBandwidthChartHandler); - $('#cbxInterfacehourly').on('change', ShowBandwidthChartHandler); - $('#cbxInterfacedaily').on('change', ShowBandwidthChartHandler); - $('#cbxInterfacemonthly').on('change', ShowBandwidthChartHandler); - ShowBandwidthChartHandler(); - }); - -})(jQuery, t); - diff --git a/app/js/bandwidthcharts.min.js b/app/js/bandwidthcharts.min.js deleted file mode 100644 index 400015dd..00000000 --- a/app/js/bandwidthcharts.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * RaspAP - RaspAP WiFi Configuration Portal v1.6.1 (https://github.com/billz/raspap-webgui) - * Copyright 2013-2019 RaspAP Developers - * Licensed under MIT (https://github.com/raspap-webgui/raspap-webgui/blob/master/LICENSE) - */ - -!function(r,i){"use strict";function t(t){r("#divChartBandwidthhourly").empty(),r("#divChartBandwidthdaily").empty(),r("#divChartBandwidthmonthly").empty(),r("#divTableBandwidthhourly").empty(),r("#divTableBandwidthdaily").empty(),r("#divTableBandwidthmonthly").empty();var e=r("ul#tabbarBandwidth li.active a").attr("href").substr(1),a="ajax/bandwidth/get_bandwidth.php?";a+="inet=",a+=encodeURIComponent(r("#cbxInterface"+e+" option:selected").text()),a+="&tu=",a+=encodeURIComponent(e.substr(0,1));var d="mb";a+="&dsu="+encodeURIComponent(d);var n=function(t,e){return new Morris.Bar({element:t,xkey:"date",ykeys:["rx","tx"],labels:[i.receive+" "+e.toUpperCase(),i.send+" "+e.toUpperCase()]})}("divChartBandwidth"+e,d);!function(t,e){r("#"+t).append('
daterxtx
')}("divTableBandwidth"+e,e);r.ajax({url:a,dataType:"json",beforeSend:function(){r("#divLoaderBandwidth"+e).removeClass("hidden")}}).done(function(t){r("#divLoaderBandwidth"+e).addClass("hidden"),n.setData(t),r("#tableBandwidth"+e).DataTable({searching:!1,paging:!1,data:t,order:[[0,"ASC"]],columns:[{data:"date"},{data:"rx",title:i.receive+" "+d.toUpperCase()},{data:"tx",title:i.send+" "+d.toUpperCase()}]})}).fail(function(t,e){window.console?console.error("server error"):alert("server error")})}r(document).ready(function(){r('#tabbarBandwidth a[data-toggle="tab"]').on("shown.bs.tab",t),r("#cbxInterfacehourly").on("change",t),r("#cbxInterfacedaily").on("change",t),r("#cbxInterfacemonthly").on("change",t),t()})}(jQuery,t); \ No newline at end of file diff --git a/app/js/custom.min.js b/app/js/custom.min.js deleted file mode 100644 index 67444fb6..00000000 --- a/app/js/custom.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * RaspAP - RaspAP WiFi Configuration Portal v1.6.1 (https://github.com/billz/raspap-webgui) - * Copyright 2013-2019 RaspAP Developers - * Licensed under MIT (https://github.com/raspap-webgui/raspap-webgui/blob/master/LICENSE) - */ - -function msgShow(t,a){if(0==t)var e="success";else if(2==t||1==t)e="danger";return'"}function createNetmaskAddr(t){var a=[];for(i=0;i<4;i++){var e=Math.min(t,8);a.push(256-Math.pow(2,8-e)),t-=e}return a.join(".")}function loadSummary(a){$.post("/ajax/networking/get_ip_summary.php",{interface:a},function(t){jsonData=JSON.parse(t),console.log(jsonData),0==jsonData.return?$("#"+a+"-summary").html(jsonData.output.join("
")):2==jsonData.return&&$("#"+a+"-summary").append('")})}function getAllInterfaces(){$.get("/ajax/networking/get_all_interfaces.php",function(t){jsonData=JSON.parse(t),$.each(jsonData,function(t,a){loadSummary(a)})})}function setupTabs(){$('a[data-toggle="tab"]').on("shown.bs.tab",function(t){var a=$(t.target).attr("href");a.match("summary")||loadCurrentSettings(a.replace("#",""))})}function loadCurrentSettings(t){$.post("/ajax/networking/get_int_config.php",{interface:t},function(t){jsonData=JSON.parse(t),$.each(jsonData.output,function(t,a){var n=a.interface;$.each(a,function(t,a){switch(t){case"static":"true"==a?($("#"+n+"-static").click(),$("#"+n+"-nofailover").click()):$("#"+n+"-dhcp").click();break;case"failover":"true"===a?$("#"+n+"-failover").click():$("#"+n+"-nofailover").click();break;case"ip_address":var e=a.split("/");$("#"+n+"-ipaddress").val(e[0]),$("#"+n+"-netmask").val(createNetmaskAddr(e[1]));break;case"routers":$("#"+n+"-gateway").val(a);break;case"domain_name_server":svrsDNS=a.split(" "),$("#"+n+"-dnssvr").val(svrsDNS[0]),$("#"+n+"-dnssvralt").val(svrsDNS[1])}})})})}function saveNetworkSettings(t){var a=$("#frm-"+t).find(":input"),e={};$.each(a,function(t,a){"radio"==$(a).attr("type")?e[$(a).attr("id")]=$(a).prop("checked"):e[$(a).attr("id")]=$(a).val()}),e.interface=t,$.post("/ajax/networking/save_int_config.php",e,function(t){var a=JSON.parse(t);$("#msgNetworking").html(msgShow(a.return,a.output))})}function applyNetworkSettings(){$(this).data("int");arrFormData={generate:""},$.post("/ajax/networking/gen_int_config.php",arrFormData,function(t){console.log(t);var a=JSON.parse(t);$("#msgNetworking").html(msgShow(a.return,a.output))})}function setupBtns(){$("#btnSummaryRefresh").click(function(){getAllInterfaces()}),$(".intsave").click(function(){saveNetworkSettings($(this).data("int"))}),$(".intapply").click(function(){applyNetworkSettings()})}function setCSRFTokenHeader(t,a,e){var n=$("meta[name=csrf_token]").attr("content");/^(POST|PATCH|PUT|DELETE)$/i.test(e.type)&&a.setRequestHeader("X-CSRF-Token",n)}function contentLoaded(){switch(pageCurrent=window.location.href.split("?")[1].split("=")[1],pageCurrent=pageCurrent.replace("#",""),$("#side-menu").metisMenu(),pageCurrent){case"network_conf":getAllInterfaces(),setupTabs(),setupBtns()}}function loadWifiStations(a){return function(){var t=!0===a?"?refresh":"";$(".js-wifi-stations").addClass("loading-spinner").empty().load("/ajax/networking/wifi_stations.php"+t,function(){$(this).removeClass("loading-spinner")})}}$(document).on("click",".js-add-dhcp-static-lease",function(t){t.preventDefault();var a=$(".js-new-dhcp-static-lease"),e=$("input[name=mac]",a).val().trim(),n=$("input[name=ip]",a).val().trim();if(""!=e&&""!=n){var i=$("#js-dhcp-static-lease-row").html().replace("{{ mac }}",e).replace("{{ ip }}",n);$(".js-dhcp-static-lease-container").append(i),$("input[name=mac]",a).val(""),$("input[name=ip]",a).val("")}}),$(document).on("click",".js-remove-dhcp-static-lease",function(t){t.preventDefault(),$(this).parents(".js-dhcp-static-lease-row").remove()}),$(document).on("submit",".js-dhcp-settings-form",function(t){$(".js-add-dhcp-static-lease").trigger("click")}),$(".js-reload-wifi-stations").on("click",loadWifiStations(!0)),$(document).on("click",".js-toggle-password",function(t){var a=$(t.target),e=$(a.data("target"));e.is(":input")&&(t.preventDefault(),a.data("__toggle-with-initial")||a.data("__toggle-with-initial",a.text()),"password"===e.attr("type")?(a.text(a.data("toggle-with")),e.attr("type","text")):(a.text(a.data("__toggle-with-initial")),e.attr("type","password")))}),$(document).on("keyup",".js-validate-psk",function(t){var a=$(t.target),e=a.data("colors").split(","),n=$(a.data("target"));a.val().length<8||63 Date: Wed, 16 Jul 2025 10:04:32 +0200 Subject: [PATCH 052/122] fix(color): sanitize color output in SVG and CSS files to prevent XSS vulnerabilities --- app/css/custom.php | 6 +++--- app/img/devices/default.php | 4 ++-- app/img/devices/zero.php | 2 +- app/img/raspAP-logo.php | 8 ++++---- app/img/solid.php | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/css/custom.php b/app/css/custom.php index 6f4db818..9113ef39 100644 --- a/app/css/custom.php +++ b/app/css/custom.php @@ -14,9 +14,9 @@ License: GNU General Public License v3.0 @import url('all.css'); :root { - --raspap-theme-color: ; - --raspap-theme-lighter: ; - --raspap-theme-darker: ; + --raspap-theme-color: ; + --raspap-theme-lighter: ; + --raspap-theme-darker: ; } body { diff --git a/app/img/devices/default.php b/app/img/devices/default.php index 9311b504..71ea0703 100644 --- a/app/img/devices/default.php +++ b/app/img/devices/default.php @@ -7,9 +7,9 @@ $color = getColorOpt(); viewBox="0 0 431 321" style="enable-background:new 0 0 431 321;" xml:space="preserve"> diff --git a/app/img/devices/zero.php b/app/img/devices/zero.php index 68c17007..4e688ad7 100644 --- a/app/img/devices/zero.php +++ b/app/img/devices/zero.php @@ -7,7 +7,7 @@ $color = getColorOpt(); diff --git a/app/img/raspAP-logo.php b/app/img/raspAP-logo.php index c23399ab..c8115634 100755 --- a/app/img/raspAP-logo.php +++ b/app/img/raspAP-logo.php @@ -32,20 +32,20 @@ $color = getColorOpt(); transform="translate(192.6768,123.4365)" id="g20"> diff --git a/app/img/solid.php b/app/img/solid.php index 9952e7fa..23bd418a 100644 --- a/app/img/solid.php +++ b/app/img/solid.php @@ -42,24 +42,24 @@ if ($showJoint) { for ($i = 1; $i < count($activeYs); $i++) { $y1 = $activeYs[$i-1]; $y2 = $activeYs[$i]; - echo ""; + echo ""; } } ?> - + - + - + - + - + From 99b46ce08641b2af3b0d33edce038f3886f7f0ee Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 16 Jul 2025 23:48:04 -0700 Subject: [PATCH 053/122] Update BACKERS.md --- BACKERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/BACKERS.md b/BACKERS.md index 40d6c71a..f43b880e 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -23,6 +23,7 @@ The following features are currently available exclusively to sponsors. A tangib ✅ [WiFi repeater mode](https://docs.raspap.com/ap-basics/#wifi-repeater-mode) ✅ [Limited privilege user role](https://docs.raspap.com/authentication/#limited-privilege-user-role) ✅ [Tailscale VPN](https://docs.raspap.com/tailscale/) + ✅ [Inspect network adapters](https://docs.raspap.com/troubleshooting/#inspect-network-adapters) Look for the list above to grow as we add more exlcusive features. Have an idea or suggestion for a future enhancement? Start or join an [Insiders discussion](https://github.com/RaspAP/raspap-insiders/discussions) and let us know! From cc4370151f4a885b11700b05d80ad4cfdb43804c Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 18 Jul 2025 01:29:13 -0700 Subject: [PATCH 054/122] Fix: Persist dhcp-host option to dnsmasq cfg --- includes/hostapd.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/hostapd.php b/includes/hostapd.php index bf44a020..0d7c9819 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -408,6 +408,9 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $config[] = 'interface='.$_POST['interface']; $config[] = 'domain-needed'; $config[] = 'dhcp-range='.$dhcp_range; + if (!empty($syscfg['dhcp-host'])) { + $config[] = 'dhcp-host='.$syscfg['dhcp-host']; + } if (!empty($syscfg['dhcp-option'])) { $config[] = 'dhcp-option='.$syscfg['dhcp-option']; } From fcca855c444951c94475951811bb60927b177cb7 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 18 Jul 2025 01:51:17 -0700 Subject: [PATCH 055/122] Use ParseConfig() to read/write multiple dhcp-* values --- includes/hostapd.php | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index 0d7c9819..5def9a71 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -380,8 +380,9 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $return = iwRegSet($country_code, $status); } - // Fetch dhcp-range, lease time from system config - $syscfg = parse_ini_file(RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', false, INI_SCANNER_RAW); + // Parse dnsmasq config for selected interface + exec('cat '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $lines); + $syscfg = ParseConfig($lines); if ($wifiAPEnable == 1) { // Enable uap0 configuration for ap-sta mode @@ -402,24 +403,39 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $config = join(PHP_EOL, $config); file_put_contents("/tmp/dnsmasqdata", $config); system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $return); - } elseif ($bridgedEnable !==1) { - $dhcp_range = ($syscfg['dhcp-range'] =='') ? getDefaultNetValue('dnsmasq',$ap_iface,'dhcp-range') : $syscfg['dhcp-range']; - $config = [ '# RaspAP '.$_POST['interface'].' configuration' ]; - $config[] = 'interface='.$_POST['interface']; + + } elseif ($bridgedEnable !== 1) { + $dhcp_range = ($syscfg['dhcp-range'] == '') + ? getDefaultNetValue('dnsmasq', $ap_iface, 'dhcp-range') + : $syscfg['dhcp-range']; + $config = [ '# RaspAP ' . $_POST['interface'] . ' configuration' ]; + $config[] = 'interface=' . $_POST['interface']; $config[] = 'domain-needed'; - $config[] = 'dhcp-range='.$dhcp_range; + $config[] = 'dhcp-range=' . $dhcp_range; + // handle multiple dhcp-host + option entries if (!empty($syscfg['dhcp-host'])) { - $config[] = 'dhcp-host='.$syscfg['dhcp-host']; + if (is_array($syscfg['dhcp-host'])) { + foreach ($syscfg['dhcp-host'] as $host) { + $config[] = 'dhcp-host=' . $host; + } + } else { + $config[] = 'dhcp-host=' . $syscfg['dhcp-host']; + } } if (!empty($syscfg['dhcp-option'])) { - $config[] = 'dhcp-option='.$syscfg['dhcp-option']; + if (is_array($syscfg['dhcp-option'])) { + foreach ($syscfg['dhcp-option'] as $opt) { + $config[] = 'dhcp-option=' . $opt; + } + } else { + $config[] = 'dhcp-option=' . $syscfg['dhcp-option']; + } } $config[] = PHP_EOL; $config = join(PHP_EOL, $config); file_put_contents("/tmp/dnsmasqdata", $config); - system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $return); + //system('sudo cp /tmp/dnsmasqdata ' . RASPI_DNSMASQ_PREFIX . $ap_iface . '.conf', $return); } - // Set dhcp values from system config, fallback to default if undefined $jsonData = json_decode(getNetConfig($ap_iface), true); $ip_address = empty($jsonData['StaticIP']) @@ -766,3 +782,4 @@ function parseUserHostapdCfg() return $tmp; } } + From b293355eac9a44b1238be8039dcbf33ffc331c82 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 18 Jul 2025 14:43:25 -0700 Subject: [PATCH 056/122] Replace procedural code w/ HostapdManager, DnsmasqManager method calls --- includes/hostapd.php | 213 ++++++++++--------------------------------- 1 file changed, 47 insertions(+), 166 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index bf44a020..99774591 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -3,6 +3,9 @@ require_once 'includes/wifi_functions.php'; require_once 'includes/config.php'; +use RaspAP\Networking\Hotspot\DnsmasqManager; +use RaspAP\Networking\Hotspot\HostapdManager; + getWifiInterface(); /** @@ -85,39 +88,24 @@ function DisplayHostAPDConfig() } } } - exec('cat '. RASPI_HOSTAPD_CONFIG, $hostapdconfig); if (isset($_SESSION['wifi_client_interface'])) { exec('iwgetid '.escapeshellarg($_SESSION['wifi_client_interface']). ' -r', $wifiNetworkID); if (!empty($wifiNetworkID[0])) { $managedModeEnabled = true; } } + + // Parse hostapd configuration + $hostapd = new HostapdManager(); + try { + $arrConfig = $hostapd->getConfig(); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + $hostapdstatus = $system->hostapdStatus(); $serviceStatus = $hostapdstatus[0] == 0 ? "down" : "up"; - - foreach ($hostapdconfig as $hostapdconfigline) { - if (strlen($hostapdconfigline) === 0) { - continue; - } - if ($hostapdconfigline[0] != "#") { - $arrLine = explode("=", $hostapdconfigline); - $arrConfig[$arrLine[0]]=$arrLine[1]; - } - }; - // assign beacon_int boolean if value is set - if (isset($arrConfig['beacon_int'])) { - $arrConfig['beacon_interval_bool'] = 1; - } - // assign disassoc_low_ack boolean if value is set - if (isset($arrConfig['disassoc_low_ack'])) { - $arrConfig['disassoc_low_ack_bool'] = 1; - } else { - $arrConfig['disassoc_low_ack_bool'] = 0; - } - // assign country_code from iw reg if not set in config - if (empty($arrConfig['country_code']) && isset($country_code[0])) { - $arrConfig['country_code'] = $country_code[0]; - } + // set txpower with iw if value is non-default ('auto') if (isset($_POST['txpower'])) { if ($_POST['txpower'] != 'auto') { @@ -132,33 +120,6 @@ function DisplayHostAPDConfig() $txpower = $_POST['txpower']; } } - // map wpa_key_mgmt to security types - if ($arrConfig['wpa_key_mgmt'] == 'WPA-PSK WPA-PSK-SHA256 SAE') { - $arrConfig['wpa'] = 4; - } elseif ($arrConfig['wpa_key_mgmt'] == 'SAE') { - $arrConfig['wpa'] = 5; - } - - $selectedHwMode = $arrConfig['hw_mode']; - if (isset($arrConfig['ieee80211n'])) { - if (strval($arrConfig['ieee80211n']) === '1') { - $selectedHwMode = 'n'; - } - } - if (isset($arrConfig['ieee80211ac'])) { - if (strval($arrConfig['ieee80211ac']) === '1') { - $selectedHwMode = 'ac'; - } - } - if (isset($arrConfig['ieee80211w'])) { - if (strval($arrConfig['ieee80211w']) === '2') { - $selectedHwMode = 'w'; - } - } - - $arrConfig['ignore_broadcast_ssid'] ??= 0; - $arrConfig['max_num_sta'] ??= 0; - $arrConfig['wep_default_key'] ??= 0; exec('sudo /bin/chmod o+r '.RASPI_HOSTAPD_LOG); $logdata = getLogLimited(RASPI_HOSTAPD_LOG); @@ -224,6 +185,8 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom } $arrHostapdConf = parse_ini_file('/etc/raspap/hostapd.ini'); + $dualAPEnable = false; + // Check for Bridged AP mode checkbox $bridgedEnable = 0; if ($arrHostapdConf['BridgedEnable'] == 0) { @@ -351,8 +314,10 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $_POST['max_num_sta'] = $_POST['max_num_sta'] > 2007 ? 2007 : $_POST['max_num_sta']; $_POST['max_num_sta'] = $_POST['max_num_sta'] < 1 ? null : $_POST['max_num_sta']; + $hostapd = new HostapdManager(); + if ($good_input) { - $config = buildHostapdConfig([ + $config = $hostapd->buildConfig([ 'interface' => $_POST['interface'], 'ssid' => $_POST['ssid'], 'channel' => $_POST['channel'], @@ -369,19 +334,23 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom 'bridge' => $bridgedEnable ? 'br0' : null ]); - file_put_contents('/tmp/hostapddata', $config); - if ($dualAPEnable) { - system("sudo cp /tmp/hostapddata " . '/etc/hostapd/hostapd-'.$_POST['interface'].'.conf', $result); - } else { - system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result); + try { + $arrConfig = $hostapd->saveConfig($config, $dualAPEnable, $ap_iface); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); } if (trim($country_code) != trim($reg_domain)) { $return = iwRegSet($country_code, $status); } - // Fetch dhcp-range, lease time from system config - $syscfg = parse_ini_file(RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', false, INI_SCANNER_RAW); + // Parse dnsmasq config for selected interface + $dnsmasq = new DnsmasqManager(); + try { + $syscfg = $dnsmasq->getConfig($ap_iface ?? RASPI_WIFI_AP_INTERFACE); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } if ($wifiAPEnable == 1) { // Enable uap0 configuration for ap-sta mode @@ -408,13 +377,27 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $config[] = 'interface='.$_POST['interface']; $config[] = 'domain-needed'; $config[] = 'dhcp-range='.$dhcp_range; + // handle multiple dhcp-host + option entries + if (!empty($syscfg['dhcp-host'])) { + if (is_array($syscfg['dhcp-host'])) { + foreach ($syscfg['dhcp-host'] as $host) { + $config[] = 'dhcp-host=' . $host; + } + } else { + $config[] = 'dhcp-host=' . $syscfg['dhcp-host']; + } + } if (!empty($syscfg['dhcp-option'])) { - $config[] = 'dhcp-option='.$syscfg['dhcp-option']; + if (is_array($syscfg['dhcp-option'])) { + foreach ($syscfg['dhcp-option'] as $opt) { + $config[] = 'dhcp-option=' . $opt; + } + } else { + $config[] = 'dhcp-option=' . $syscfg['dhcp-option']; + } } $config[] = PHP_EOL; - $config = join(PHP_EOL, $config); - file_put_contents("/tmp/dnsmasqdata", $config); - system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $return); + $dnsmasq->saveConfig($config, $ap_iface); } // Set dhcp values from system config, fallback to default if undefined @@ -561,108 +544,6 @@ function getIfaceMetric($iface) } } -/** - * Builds a hostapd configuration string for a given interface - * - * @param array $params Associative array of config values - * @return string - */ -function buildHostapdConfig(array $params): string -{ - $config = []; - $config[] = 'driver=nl80211'; - $config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE; - $config[] = 'ctrl_interface_group=0'; - $config[] = 'auth_algs=1'; - - $wpa = $params['wpa']; - $wpa_key_mgmt = 'WPA-PSK'; - - if ($wpa == 4) { - $config[] = 'ieee80211w=1'; - $wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE'; - $wpa = 2; - } elseif ($wpa == 5) { - $config[] = 'ieee80211w=2'; - $wpa_key_mgmt = 'SAE'; - $wpa = 2; - } - - if ($params['80211w'] == 1) { - $config[] = 'ieee80211w=1'; - $wpa_key_mgmt = 'WPA-PSK'; - } elseif ($params['80211w'] == 2) { - $config[] = 'ieee80211w=2'; - $wpa_key_mgmt = 'WPA-PSK-SHA256'; - } - - $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; - - if (!empty($params['beacon_interval'])) { - $config[] = 'beacon_int=' . $params['beacon_interval']; - } - - if (!empty($params['disassoc_low_ack'])) { - $config[] = 'disassoc_low_ack=0'; - } - - $config[] = 'ssid=' . $params['ssid']; - $config[] = 'channel=' . $params['channel']; - - // Choose VHT segment index (fallback only if required) - $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; - - switch ($params['hw_mode']) { - case 'n': - $config[] = 'hw_mode=g'; - $config[] = 'ieee80211n=1'; - $config[] = 'wmm_enabled=1'; - break; - case 'ac': - $config[] = 'hw_mode=a'; - $config[] = '# N'; - $config[] = 'ieee80211n=1'; - $config[] = 'require_ht=1'; - $config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'; - $config[] = '# AC'; - $config[] = 'ieee80211ac=1'; - $config[] = 'require_vht=1'; - $config[] = 'ieee80211d=0'; - $config[] = 'ieee80211h=0'; - $config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'; - $config[] = 'vht_oper_chwidth=1'; - $config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx; - break; - default: - $config[] = 'hw_mode=' . $params['hw_mode']; - $config[] = 'ieee80211n=0'; - } - - if ($params['wpa'] !== 'none') { - $config[] = 'wpa_passphrase=' . $params['wpa_passphrase']; - } - - if (!empty($params['bridge'])) { - $config[] = 'interface=' . $params['interface']; - $config[] = 'bridge=' . $params['bridge']; - } else { - $config[] = 'interface=' . $params['interface']; - } - - $config[] = 'wpa=' . $wpa; - $config[] = 'wpa_pairwise=' . $params['wpa_pairwise']; - $config[] = 'country_code=' . $params['country_code']; - $config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID']; - if (!empty($params['max_num_sta'])) { - $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; - } - - // Optional additional user config - $config[] = parseUserHostapdCfg(); - - return implode(PHP_EOL, $config) . PHP_EOL; -} - /** * Updates the dhcpcd configuration for a given interface, preserving existing settings * From 2b2a76c5126138496c7f2e5ddd9f22cab1bf2e67 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 18 Jul 2025 14:43:47 -0700 Subject: [PATCH 057/122] Update w/ reload dnsmasq.service --- installers/raspap.sudoers | 1 + 1 file changed, 1 insertion(+) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 3b7fd055..2d051035 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -22,6 +22,7 @@ www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop hostapd.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl start dnsmasq.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop dnsmasq.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl restart dnsmasq.service +www-data ALL=(ALL) NOPASSWD:/bin/systemctl reload dnsmasq.service www-data ALL=(ALL) NOPASSWD:/bin/systemctl start openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl enable openvpn-client@client www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop openvpn-client@client From 619bfdc04dcf55eaaea2b59a7fa1a01ca8f2982a Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 18 Jul 2025 14:44:52 -0700 Subject: [PATCH 058/122] Create getConfig(), saveConfig() class methods --- .../Networking/Hotspot/DnsmasqManager.php | 89 +++++ .../Networking/Hotspot/HostapdManager.php | 315 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/RaspAP/Networking/Hotspot/DnsmasqManager.php create mode 100644 src/RaspAP/Networking/Hotspot/HostapdManager.php diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php new file mode 100644 index 00000000..46f121a1 --- /dev/null +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -0,0 +1,89 @@ + after save + * @return bool + * @throws \RuntimeException + */ + public function saveConfig(string $config, bool $dualMode, string $iface, bool $restart = false): bool + { + $configFile = $this->resolveConfigPath($iface, $dualMode); + //$configFile = self::CONF_DEFAULT; + $tempFile = self::CONF_TMP; + + + if (file_put_contents($tempFile, $config) === false) { + throw new \RuntimeException("Failed to write temp hostapd config"); + } + + exec(sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)), $o, $status); + if ($status !== 0) { + throw new \RuntimeException("Failed to apply new hostapd config"); + } + + if ($restart) { + $this->restartService($iface); + } + + return true; + } + + /** + * Sets transmit power for an interface + * + * @param string $iface + * @param int|string $dbm + * @return bool + */ + public function setTxPower(string $iface, $dbm): bool + { + return false; + } + + /** + * Sets regulatory domain + * + * @param string $countryCode + * @return bool + */ + public function setRegDomain(string $countryCode): bool + { + return false; + } + + /** + * Parses optional /etc/hostapd/hostapd.conf.users file + * + * @return string $tmp + */ + function parseUserHostapdCfg() + { + if (file_exists(CONF_DEFAULT . '.users')) { + exec('cat '. CONF_DEFAULT . '.users', $hostapdconfigusers); + foreach ($hostapdconfigusers as $hostapdconfigusersline) { + if (strlen($hostapdconfigusersline) === 0) { + continue; + } + if ($hostapdconfigusersline[0] != "#") { + $arrLine = explode("=", $hostapdconfigusersline); + $tmp.= $arrLine[0]."=".$arrLine[1].PHP_EOL;; + } + } + return $tmp; + } + } + + /** + * Determines the hostapd config file for a given interface + * + * @param string $iface + * @param bool $dualMode + * @return string + */ + private function resolveConfigPath(string $iface, bool $dualMode): string + { + if ($dualMode) { + return SELF::CONF_PATH_PREFIX . $iface . '.conf'; + } + // primary interface uses the canonical config path + return self::CONF_DEFAULT; + } + + /** + * Restarts hostapd systemd instance + * + * @param string $iface + * @throws \RuntimeException + */ + private function restartService(string $iface): void + { + // sanitize + if (!preg_match('/^[A-Za-z0-9_-]+$/', $iface)) { + throw new \RuntimeException("Invalid interface name: $iface"); + } + + // use instance unit (preferred) if available + $cmds = [ + sprintf('sudo systemctl restart hostapd@%s', $iface), + // fallback to singleton service + 'sudo systemctl restart hostapd.service' + ]; + + foreach ($cmds as $cmd) { + exec($cmd, $out, $rc); + if ($rc === 0) { + return; + } + } + + throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback)."); + } + +} + + From 4e55f5a97fcfa016e2151071e25e8abc5aadd578 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:18:18 -0700 Subject: [PATCH 059/122] Initial commit --- src/RaspAP/Networking/Hotspot/WiFiManager.php | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 src/RaspAP/Networking/Hotspot/WiFiManager.php diff --git a/src/RaspAP/Networking/Hotspot/WiFiManager.php b/src/RaspAP/Networking/Hotspot/WiFiManager.php new file mode 100644 index 00000000..e7002a7a --- /dev/null +++ b/src/RaspAP/Networking/Hotspot/WiFiManager.php @@ -0,0 +1,392 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +class WiFiManager +{ + + private const MIN_RSSI = -100; + private const MAX_RSSI = -55; + + public function knownWifiStations(&$networks) + { + // find currently configured networks + exec(' sudo cat ' . RASPI_WPA_SUPPLICANT_CONFIG, $known_return); + $index = 0; + foreach ($known_return as $line) { + if (preg_match('/network\s*=/', $line)) { + $network = array('visible' => false, 'configured' => true, 'connected' => false, 'index' => null); + ++$index; + } elseif (isset($network) && $network !== null) { + if (preg_match('/^\s*}\s*$/', $line)) { + $networks[$ssid] = $network; + $network = null; + $ssid = null; + } elseif ($lineArr = preg_split('/\s*=\s*/', trim($line), 2)) { + switch (strtolower($lineArr[0])) { + case 'ssid': + $ssid = trim($lineArr[1], '"'); + $ssid = str_replace('P"','',$ssid); + $network['ssid'] = $ssid; + $index = $this->getNetworkIdBySSID($ssid); + $network['index'] = $index; + break; + case 'psk': + $network['passkey'] = trim($lineArr[1]); + $network['protocol'] = 'WPA'; + break; + case '#psk': + $network['protocol'] = 'WPA'; + case 'wep_key0': // Untested + $network['passphrase'] = trim($lineArr[1], '"'); + break; + case 'key_mgmt': + if (! array_key_exists('passphrase', $network) && $lineArr[1] === 'NONE') { + $network['protocol'] = 'Open'; + } + break; + case 'priority': + $network['priority'] = trim($lineArr[1], '"'); + break; + } + } + } + } + } + + /** + * Scans for nearby WiFi networks using `iw` and updates the reference array + * + * @param array $networks Reference to the array of known and discovered networks + * @param bool $cached If false, bypasses the cache and performs a fresh scan + */ + public function nearbyWifiStations(&$networks, $cached = true) + { + $cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG); + $cacheKey = "nearby_wifi_stations_$cacheTime"; + + if ($cached == false) { + deleteCache($cacheKey); + } + + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + + $scan_results = cache( + $cacheKey, + function () use ($iface) { + $stdout = shell_exec("sudo iw dev $iface scan"); + return preg_split("/\n/", $stdout); + } + ); + + // exclude the AP from nearby networks + exec('sed -rn "s/ssid=(.*)\s*$/\1/p" ' . escapeshellarg(RASPI_HOSTAPD_CONFIG), $ap_ssid); + $ap_ssid = $ap_ssid[0] ?? ''; + + $index = 0; + if (!empty($networks)) { + $lastnet = end($networks); + if (isset($lastnet['index'])) { + $index = $lastnet['index'] + 1; + } + } + + $current = []; + $commitCurrent = function () use (&$current, &$networks, &$index, $ap_ssid) { + if (empty($current['ssid'])) { + return; + } + + $ssid = $current['ssid']; + + // unprintable 7bit ASCII control codes, delete or quotes -> ignore network + if ($ssid === $ap_ssid || preg_match('/[\x00-\x1f\x7f\'`\´"]/', $ssid)) { + return; + } + + $channel = ConvertToChannel($current['freq'] ?? 0); + $rssi = $current['signal'] ?? -100; + + // if network is saved + if (array_key_exists($ssid, $networks)) { + $networks[$ssid]['visible'] = true; + $networks[$ssid]['channel'] = $channel; + if (!isset($networks[$ssid]['RSSI']) || $networks[$ssid]['RSSI'] < $rssi) { + $networks[$ssid]['RSSI'] = $rssi; + } + } else { + $networks[$ssid] = [ + 'ssid' => $ssid, + 'configured' => false, + 'protocol' => $current['security'] ?? 'OPEN', + 'channel' => $channel, + 'passphrase' => '', + 'visible' => true, + 'connected' => false, + 'RSSI' => $rssi, + 'index' => $index + ]; + ++$index; + } + }; + + foreach ($scan_results as $line) { + $line = trim($line); + + if (preg_match('/^BSS\s+([0-9a-f:]{17})/', $line, $match)) { + $commitCurrent(); // commit previous + $current = [ + 'bssid' => $match[1], + 'ssid' => '', + 'signal' => null, + 'freq' => null, + 'security' => 'OPEN' + ]; + continue; + } + if (preg_match('/^SSID:\s*(.*)$/', $line, $match)) { + $current['ssid'] = $match[1]; + continue; + } + if (preg_match('/^signal:\s*(-?\d+\.\d+)/', $line, $match)) { + $current['signal'] = (float)$match[1]; + continue; + } + if (preg_match('/^freq:\s*(\d+)/', $line, $match)) { + $current['freq'] = (int)$match[1]; + continue; + } + if (preg_match('/^RSN:/', $line) || preg_match('/^WPA:/', $line)) { + $current['security'] = 'WPA/WPA2'; + continue; + } + } + $commitCurrent(); + } + + /** + * + */ + public function connectedWifiStations(&$networks) + { + exec('iwconfig ' .$_SESSION['wifi_client_interface'], $iwconfig_return); + foreach ($iwconfig_return as $line) { + if (preg_match('/ESSID:\"([^"]+)\"/i', $line, $iwconfig_ssid)) { + $ssid=hexSequence2lower($iwconfig_ssid[1]); + $networks[$ssid]['connected'] = true; + $check=detectCaptivePortal($_SESSION['wifi_client_interface']); + $networks[$ssid]["portal-url"]=$check["URL"]; + } + } + } + + /** + * + * + */ + public function sortNetworksByRSSI(&$networks) + { + $valRSSI = array(); + foreach ($networks as $SSID => $net) { + if (!array_key_exists('RSSI', $net)) { + $net['RSSI'] = -1000; + } + $valRSSI[$SSID] = $net['RSSI']; + } + $nets = $networks; + arsort($valRSSI); + $networks = array(); + foreach ($valRSSI as $SSID => $RSSI) { + $networks[$SSID] = $nets[$SSID]; + $networks[$SSID]['RSSI'] = $RSSI; + } + } + + /* + * Determines the configured wireless AP interface + * + * If not saved in /etc/raspap/hostapd.ini, check for a second + * wireless interface with iw dev. Fallback to the constant + * value defined in config.php + */ + public function getWifiInterface() + { + $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; + $arrHostapdConf = file_exists($hostapdIni) ? parse_ini_file($hostapdIni) : []; + + $iface = $_SESSION['ap_interface'] = $arrHostapdConf['WifiInterface'] ?? RASPI_WIFI_AP_INTERFACE; + + if (!validateInterface($iface)) { + $iface = RASPI_WIFI_AP_INTERFACE; + } + + // check for 2nd wifi interface -> wifi client on different interface + exec("iw dev | awk '$1==\"Interface\" && $2!=\"$iface\" {print $2}'", $iface2); + $client_iface = $_SESSION['wifi_client_interface'] = empty($iface2) ? $iface : trim($iface2[0]); + + // handle special case for RPi Zero W in AP-STA mode + if ($client_iface === "uap0" && ($arrHostapdConf['WifiAPEnable'] ?? 0)) { + $_SESSION['wifi_client_interface'] = $iface; + $_SESSION['ap_interface'] = $client_iface; + } + } + + /* + * Reinitializes wpa_supplicant for the wireless client interface + * The 'force' parameter deletes the socket in /var/run/wpa_supplicant/ + * + * @param boolean $force + */ + public function reinitializeWPA($force) + { + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + if ($force == true) { + $cmd = "sudo wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i$iface"; + $result = shell_exec($cmd); + } + $cmd = "sudo wpa_cli -i $iface reconfigure"; + $result = shell_exec($cmd); + sleep(1); + return $result; + } + + /* + * Replace escaped bytes (hex) by binary - assume UTF8 encoding + * + * @param string $ssid + */ + public function ssid2utf8($ssid) + { + return evalHexSequence($ssid); + } + + /* + * Returns a signal strength indicator based on RSSI value + * + * @param string $rssi + */ + public function getSignalBars($rssi) + { + // assign css class based on RSSI value + $class = ''; + if ($rssi >= SELF::MAX_RSSI) { + $class = 'strong'; + } elseif ($rssi >= -56) { + $class = 'medium'; + } elseif ($rssi >= -67) { + $class = 'weak'; + } elseif ($rssi >= -89) { + $class = ''; + } + + // calculate percent strength + if ($rssi >= -50) { + $pct = 100; + } elseif ($rssi <= SELF::MIN_RSSI) { + $pct = 0; + } else { + $pct = 2*($rssi + 100); + } + $elem = '
'.PHP_EOL; + for ($n = 0; $n < 3; $n++ ) { + $elem .= '
'.PHP_EOL; + } + $elem .= '
'.PHP_EOL; + 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 + */ + public function setKnownStationsWPA($networks) + { + $iface = escapeshellarg($_SESSION['wifi_client_interface']); + $output = shell_exec("sudo wpa_cli -i $iface list_networks"); + $lines = explode("\n", $output); + 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 (!$this->networkExists($ssid, $wpaCliNetworks)) { + $ssid = escapeshellarg('"'.$network['ssid'].'"'); + $psk = escapeshellarg('"'.$network['passphrase'].'"'); + $protocol = $network['protocol']; + $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" + ]; + if ($protocol === 'Open') { + $commands[1] = "sudo wpa_cli -i $iface set_network $netid key_mgmt NONE"; + } + foreach ($commands as $cmd) { + exec($cmd); + usleep(1000); + } + } + } + } + } + + /* + * Parses wpa_cli list_networks output and returns the id + * of a corresponding network SSID + * + * @param string $ssid + * @return integer id + */ + public 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; + } + + /** + * + */ + public function networkExists($ssid, $collection) + { + foreach ($collection as $network) { + if ($network['ssid'] === $ssid) { + return true; + } + } + return false; + } + +} + From dea3e7c4850aa990b922d99fc573d7b2089fa1a5 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:19:24 -0700 Subject: [PATCH 060/122] Modify select to use selected_hw_mode from config --- templates/hostapd/basic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/hostapd/basic.php b/templates/hostapd/basic.php index 1845c829..e30a2309 100644 --- a/templates/hostapd/basic.php +++ b/templates/hostapd/basic.php @@ -19,7 +19,7 @@
"> - +
From 094ebdb85f7ebf1f2c27c42b823c993fea95d5bc Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:23:14 -0700 Subject: [PATCH 061/122] Replace procedural code w/ dnsmasq->buildConfig, hostapd->persistHostapdIni --- includes/hostapd.php | 81 ++++++++------------------------------------ 1 file changed, 15 insertions(+), 66 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index 99774591..3b2e3e21 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -1,12 +1,12 @@ getWifiInterface(); /** * Initialize hostapd values, display interface @@ -132,7 +132,6 @@ function DisplayHostAPDConfig() "interfaces", "arrConfig", "arr80211Standard", - "selectedHwMode", "arrSecurity", "arrEncType", "arr80211w", @@ -160,8 +159,10 @@ function DisplayHostAPDConfig() */ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) { - // It should not be possible to send bad data for these fields. - // If wpa fields are absent, return false and log securely. + $hostapd = new HostapdManager(); + $dnsmasq = new DnsmasqManager(); + + // If wpa fields are absent, return false and log securely if (!(array_key_exists($_POST['wpa'], $wpa_array) && array_key_exists($_POST['wpa_pairwise'], $enc_types) && array_key_exists($_POST['hw_mode'], $modes)) @@ -255,16 +256,8 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $cli_iface = $session_iface = 'br0'; } - // persist user options to /etc/raspap - $cfg = []; - $cfg['WifiInterface'] = $ap_iface; - $cfg['LogEnable'] = $logEnable; - // Save previous Client mode status when Bridged - $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $arrHostapdConf['WifiAPEnable'] : $wifiAPEnable); - $cfg['BridgedEnable'] = $bridgedEnable; - $cfg['RepeaterEnable'] = $repeaterEnable; - $cfg['WifiManaged'] = $cli_iface; - write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini'); + $hostapd->persistHostapdIni($ap_iface, $logEnable, $bridgedEnable, $arrHostapdConf['WifiAPEnable'], $wifiAPEnable, $repeaterEnable, $cli_iface); + $_SESSION['ap_interface'] = $session_iface; // Verify input @@ -314,8 +307,6 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $_POST['max_num_sta'] = $_POST['max_num_sta'] > 2007 ? 2007 : $_POST['max_num_sta']; $_POST['max_num_sta'] = $_POST['max_num_sta'] < 1 ? null : $_POST['max_num_sta']; - $hostapd = new HostapdManager(); - if ($good_input) { $config = $hostapd->buildConfig([ 'interface' => $_POST['interface'], @@ -345,59 +336,17 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom } // Parse dnsmasq config for selected interface - $dnsmasq = new DnsmasqManager(); try { $syscfg = $dnsmasq->getConfig($ap_iface ?? RASPI_WIFI_AP_INTERFACE); } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); } - - if ($wifiAPEnable == 1) { - // Enable uap0 configuration for ap-sta mode - // Set dhcp-range from system config, fallback to default if undefined - $dhcp_range = ($syscfg['dhcp-range'] == '') ? getDefaultNetValue('dnsmasq','uap0','dhcp-range') : $syscfg['dhcp-range']; - $config = [ '# RaspAP uap0 configuration' ]; - $config[] = 'interface=lo,uap0 # Enable uap0 interface for wireless client AP mode'; - $config[] = 'bind-dynamic # Hybrid between --bind-interfaces and default'; - $config[] = 'server=8.8.8.8 # Forward DNS requests to Google DNS'; - $config[] = 'domain-needed # Don\'t forward short names'; - $config[] = 'bogus-priv # Never forward addresses in the non-routed address spaces'; - $config[] = 'dhcp-range='.$dhcp_range; - if (!empty($syscfg['dhcp-option'])) { - $config[] = 'dhcp-option='.$syscfg['dhcp-option']; - } - $config[] = PHP_EOL; - scanConfigDir('/etc/dnsmasq.d/','uap0',$status); - $config = join(PHP_EOL, $config); - file_put_contents("/tmp/dnsmasqdata", $config); - system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $return); - } elseif ($bridgedEnable !==1) { - $dhcp_range = ($syscfg['dhcp-range'] =='') ? getDefaultNetValue('dnsmasq',$ap_iface,'dhcp-range') : $syscfg['dhcp-range']; - $config = [ '# RaspAP '.$_POST['interface'].' configuration' ]; - $config[] = 'interface='.$_POST['interface']; - $config[] = 'domain-needed'; - $config[] = 'dhcp-range='.$dhcp_range; - // handle multiple dhcp-host + option entries - if (!empty($syscfg['dhcp-host'])) { - if (is_array($syscfg['dhcp-host'])) { - foreach ($syscfg['dhcp-host'] as $host) { - $config[] = 'dhcp-host=' . $host; - } - } else { - $config[] = 'dhcp-host=' . $syscfg['dhcp-host']; - } - } - if (!empty($syscfg['dhcp-option'])) { - if (is_array($syscfg['dhcp-option'])) { - foreach ($syscfg['dhcp-option'] as $opt) { - $config[] = 'dhcp-option=' . $opt; - } - } else { - $config[] = 'dhcp-option=' . $syscfg['dhcp-option']; - } - } - $config[] = PHP_EOL; + // Build and save dsnmasq config + try { + $config = $dnsmasq->buildConfig($syscfg, $ap_iface, $wifiAPEnable, $bridgedEnable); $dnsmasq->saveConfig($config, $ap_iface); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); } // Set dhcp values from system config, fallback to default if undefined From 3ad5a987983e91da80c71127720130bfd292182a Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:24:01 -0700 Subject: [PATCH 062/122] Replace global wifi_functions with WiFiManager class --- includes/configure_client.php | 13 +- includes/dashboard.php | 18 +- includes/dhcp.php | 9 +- includes/openvpn.php | 5 +- includes/wifi_functions.php | 366 ---------------------------------- includes/wireguard.php | 6 +- 6 files changed, 33 insertions(+), 384 deletions(-) delete mode 100755 includes/wifi_functions.php diff --git a/includes/configure_client.php b/includes/configure_client.php index d390b8c5..097b9ac6 100755 --- a/includes/configure_client.php +++ b/includes/configure_client.php @@ -1,6 +1,6 @@ getWifiInterface(); + $wifi->knownWifiStations($networks); + $wifi->setKnownStationsWPA($networks); $iface = escapeshellarg($_SESSION['wifi_client_interface']); @@ -30,7 +31,7 @@ function DisplayWPAConfig() } elseif (isset($_POST['wpa_reinit'])) { $status->addMessage('Attempting to reinitialize wpa_supplicant', 'warning'); $force_remove = true; - $result = reinitializeWPA($force_remove); + $result = $wifi->reinitializeWPA($force_remove); } elseif (isset($_POST['client_settings'])) { $tmp_networks = $networks; if ($wpa_file = fopen('/tmp/wifidata', 'w')) { @@ -90,7 +91,7 @@ function DisplayWPAConfig() if (strlen($network['passphrase']) >=8 && strlen($network['passphrase']) <= 63) { unset($wpa_passphrase); unset($line); - exec('wpa_passphrase '. ssid2utf8( escapeshellarg($ssid) ) . ' ' . escapeshellarg($network['passphrase']), $wpa_passphrase); + exec('wpa_passphrase '. $wifi->ssid2utf8( escapeshellarg($ssid) ) . ' ' . escapeshellarg($network['passphrase']), $wpa_passphrase); foreach ($wpa_passphrase as $line) { if (preg_match('/^\s*}\s*$/', $line)) { if (array_key_exists('priority', $network)) { diff --git a/includes/dashboard.php b/includes/dashboard.php index f01917ca..303060f1 100755 --- a/includes/dashboard.php +++ b/includes/dashboard.php @@ -1,22 +1,28 @@ getWifiInterface(); $interface = $_SESSION['ap_interface'] ?? 'wlan0'; $clientInterface = $_SESSION['wifi_client_interface']; diff --git a/includes/dhcp.php b/includes/dhcp.php index fd6a47ac..4c35fac1 100755 --- a/includes/dhcp.php +++ b/includes/dhcp.php @@ -2,12 +2,18 @@ require_once 'config.php'; +use RaspAP\Networking\Hotspot\WiFiManager; +use RaspAP\Messages\StatusMessage; + /** * Manage DHCP configuration */ function DisplayDHCPConfig() { - $status = new \RaspAP\Messages\StatusMessage; + $status = new StatusMessage(); + $wifi = new WiFiManager(); + $wifi->getWifiInterface(); + if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['savedhcpdsettings'])) { saveDHCPConfig($status); @@ -43,7 +49,6 @@ function DisplayDHCPConfig() } } } - getWifiInterface(); $ap_iface = $_SESSION['ap_interface']; $serviceStatus = $dnsmasq_state ? "up" : "down"; exec('cat '. RASPI_DNSMASQ_PREFIX.'raspap.conf', $return); diff --git a/includes/openvpn.php b/includes/openvpn.php index 2b59666b..d207f069 100755 --- a/includes/openvpn.php +++ b/includes/openvpn.php @@ -1,9 +1,10 @@ getWifiInterface(); /** * Manage OpenVPN configuration diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php deleted file mode 100755 index c26d8b57..00000000 --- a/includes/wifi_functions.php +++ /dev/null @@ -1,366 +0,0 @@ - false, 'configured' => true, 'connected' => false, 'index' => null); - ++$index; - } elseif (isset($network) && $network !== null) { - if (preg_match('/^\s*}\s*$/', $line)) { - $networks[$ssid] = $network; - $network = null; - $ssid = null; - } elseif ($lineArr = preg_split('/\s*=\s*/', trim($line), 2)) { - switch (strtolower($lineArr[0])) { - case 'ssid': - $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]); - $network['protocol'] = 'WPA'; - break; - case '#psk': - $network['protocol'] = 'WPA'; - case 'wep_key0': // Untested - $network['passphrase'] = trim($lineArr[1], '"'); - break; - case 'key_mgmt': - if (! array_key_exists('passphrase', $network) && $lineArr[1] === 'NONE') { - $network['protocol'] = 'Open'; - } - break; - case 'priority': - $network['priority'] = trim($lineArr[1], '"'); - break; - } - } - } - } -} - -/** - * Scans for nearby WiFi networks using `iw` and updates the reference array - * - * @param array $networks Reference to the array of known and discovered networks. - * @param bool $cached If false, bypasses the cache and performs a fresh scan. - */ -function nearbyWifiStations(&$networks, $cached = true) -{ - $cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG); - $cacheKey = "nearby_wifi_stations_$cacheTime"; - - if ($cached == false) { - deleteCache($cacheKey); - } - - $iface = escapeshellarg($_SESSION['wifi_client_interface']); - - $scan_results = cache( - $cacheKey, - function () use ($iface) { - $stdout = shell_exec("sudo iw dev $iface scan"); - return preg_split("/\n/", $stdout); - } - ); - - // exclude the AP from nearby networks - exec('sed -rn "s/ssid=(.*)\s*$/\1/p" ' . escapeshellarg(RASPI_HOSTAPD_CONFIG), $ap_ssid); - $ap_ssid = $ap_ssid[0] ?? ''; - - $index = 0; - if (!empty($networks)) { - $lastnet = end($networks); - if (isset($lastnet['index'])) { - $index = $lastnet['index'] + 1; - } - } - - $current = []; - $commitCurrent = function () use (&$current, &$networks, &$index, $ap_ssid) { - if (empty($current['ssid'])) { - return; - } - - $ssid = $current['ssid']; - - // unprintable 7bit ASCII control codes, delete or quotes -> ignore network - if ($ssid === $ap_ssid || preg_match('/[\x00-\x1f\x7f\'`\´"]/', $ssid)) { - return; - } - - $channel = ConvertToChannel($current['freq'] ?? 0); - $rssi = $current['signal'] ?? -100; - - // if network is saved - if (array_key_exists($ssid, $networks)) { - $networks[$ssid]['visible'] = true; - $networks[$ssid]['channel'] = $channel; - if (!isset($networks[$ssid]['RSSI']) || $networks[$ssid]['RSSI'] < $rssi) { - $networks[$ssid]['RSSI'] = $rssi; - } - } else { - $networks[$ssid] = [ - 'ssid' => $ssid, - 'configured' => false, - 'protocol' => $current['security'] ?? 'OPEN', - 'channel' => $channel, - 'passphrase' => '', - 'visible' => true, - 'connected' => false, - 'RSSI' => $rssi, - 'index' => $index - ]; - ++$index; - } - }; - - foreach ($scan_results as $line) { - $line = trim($line); - - if (preg_match('/^BSS\s+([0-9a-f:]{17})/', $line, $match)) { - $commitCurrent(); // commit previous - $current = [ - 'bssid' => $match[1], - 'ssid' => '', - 'signal' => null, - 'freq' => null, - 'security' => 'OPEN' - ]; - continue; - } - if (preg_match('/^SSID:\s*(.*)$/', $line, $match)) { - $current['ssid'] = $match[1]; - continue; - } - if (preg_match('/^signal:\s*(-?\d+\.\d+)/', $line, $match)) { - $current['signal'] = (float)$match[1]; - continue; - } - if (preg_match('/^freq:\s*(\d+)/', $line, $match)) { - $current['freq'] = (int)$match[1]; - continue; - } - if (preg_match('/^RSN:/', $line) || preg_match('/^WPA:/', $line)) { - $current['security'] = 'WPA/WPA2'; - continue; - } - } - $commitCurrent(); -} - -function connectedWifiStations(&$networks) -{ - exec('iwconfig ' .$_SESSION['wifi_client_interface'], $iwconfig_return); - foreach ($iwconfig_return as $line) { - if (preg_match('/ESSID:\"([^"]+)\"/i', $line, $iwconfig_ssid)) { - $networks[hexSequence2lower($iwconfig_ssid[1])]['connected'] = true; - } - } -} - -function sortNetworksByRSSI(&$networks) -{ - $valRSSI = array(); - foreach ($networks as $SSID => $net) { - if (!array_key_exists('RSSI', $net)) { - $net['RSSI'] = -1000; - } - $valRSSI[$SSID] = $net['RSSI']; - } - $nets = $networks; - arsort($valRSSI); - $networks = array(); - foreach ($valRSSI as $SSID => $RSSI) { - $networks[$SSID] = $nets[$SSID]; - $networks[$SSID]['RSSI'] = $RSSI; - } -} - -/* - * Determines the configured wireless AP interface - * - * If not saved in /etc/raspap/hostapd.ini, check for a second - * wireless interface with iw dev. Fallback to the constant - * value defined in config.php - */ -function getWifiInterface() -{ - $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; - $arrHostapdConf = file_exists($hostapdIni) ? parse_ini_file($hostapdIni) : []; - - $iface = $_SESSION['ap_interface'] = $arrHostapdConf['WifiInterface'] ?? RASPI_WIFI_AP_INTERFACE; - - if (!validateInterface($iface)) { - $iface = RASPI_WIFI_AP_INTERFACE; - } - - // check for 2nd wifi interface -> wifi client on different interface - exec("iw dev | awk '$1==\"Interface\" && $2!=\"$iface\" {print $2}'", $iface2); - $client_iface = $_SESSION['wifi_client_interface'] = empty($iface2) ? $iface : trim($iface2[0]); - - // handle special case for RPi Zero W in AP-STA mode - if ($client_iface === "uap0" && ($arrHostapdConf['WifiAPEnable'] ?? 0)) { - $_SESSION['wifi_client_interface'] = $iface; - $_SESSION['ap_interface'] = $client_iface; - } -} - -/* - * Reinitializes wpa_supplicant for the wireless client interface - * The 'force' parameter deletes the socket in /var/run/wpa_supplicant/ - * - * @param boolean $force - */ -function reinitializeWPA($force) -{ - $iface = escapeshellarg($_SESSION['wifi_client_interface']); - if ($force == true) { - $cmd = "sudo /bin/rm /var/run/wpa_supplicant/$iface"; - $result = shell_exec($cmd); - } - $cmd = "sudo wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i$iface"; - $result = shell_exec($cmd); - sleep(1); - return $result; -} - -/* - * Replace escaped bytes (hex) by binary - assume UTF8 encoding - * - * @param string $ssid - */ -function ssid2utf8($ssid) -{ - return evalHexSequence($ssid); -} - -/* - * Returns a signal strength indicator based on RSSI value - * - * @param string $rssi - */ -function getSignalBars($rssi) -{ - // assign css class based on RSSI value - if ($rssi >= MAX_RSSI) { - $class = 'strong'; - } elseif ($rssi >= -56) { - $class = 'medium'; - } elseif ($rssi >= -67) { - $class = 'weak'; - } elseif ($rssi >= -89) { - $class = ''; - } - - // calculate percent strength - if ($rssi >= -50) { - $pct = 100; - } elseif ($rssi <= MIN_RSSI) { - $pct = 0; - } else { - $pct = 2*($rssi + 100); - } - $elem = '
'.PHP_EOL; - for ($n = 0; $n < 3; $n++ ) { - $elem .= '
'.PHP_EOL; - } - $elem .= '
'.PHP_EOL; - 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); - 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'].'"'); - $protocol = $network['protocol']; - $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" - ]; - if ($protocol === 'Open') { - $commands[1] = "sudo wpa_cli -i $iface set_network $netid key_mgmt NONE"; - } - foreach ($commands as $cmd) { - exec($cmd); - usleep(1000); - } - } - } - } -} - -/* - * 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) { - if ($network['ssid'] === $ssid) { - return true; - } - } - return false; -} - diff --git a/includes/wireguard.php b/includes/wireguard.php index ea2b1fe9..af6dbe28 100755 --- a/includes/wireguard.php +++ b/includes/wireguard.php @@ -1,9 +1,11 @@ getWifiInterface(); /** * Displays wireguard server & peer configuration From f1ced918115e1125ab0a491e146358218cfb4615 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:24:41 -0700 Subject: [PATCH 063/122] Update w/ WiFiManager class method calls --- ajax/networking/wifi_stations.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ajax/networking/wifi_stations.php b/ajax/networking/wifi_stations.php index b6298046..d61974ab 100644 --- a/ajax/networking/wifi_stations.php +++ b/ajax/networking/wifi_stations.php @@ -6,17 +6,21 @@ require_once '../../includes/config.php'; require_once '../../includes/authenticate.php'; require_once '../../includes/defaults.php'; require_once '../../includes/functions.php'; -require_once '../../includes/wifi_functions.php'; + +use RaspAP\Networking\Hotspot\WiFiManager; + +$wifi = new WiFiManager(); $networks = []; $network = null; $ssid = null; -knownWifiStations($networks); -nearbyWifiStations($networks, !isset($_REQUEST["refresh"])); -connectedWifiStations($networks); -sortNetworksByRSSI($networks); -foreach ($networks as $ssid => $network) $networks[$ssid]["ssidutf8"] = ssid2utf8( $ssid ); +$wifi->knownWifiStations($networks); +$wifi->nearbyWifiStations($networks, !isset($_REQUEST["refresh"])); +$wifi->connectedWifiStations($networks); +$wifi->sortNetworksByRSSI($networks); + +foreach ($networks as $ssid => $network) $networks[$ssid]["ssidutf8"] = $wifi->ssid2utf8( $ssid ); $connected = array_filter($networks, function($n) { return $n['connected']; } ); $known = array_filter($networks, function($n) { return !$n['connected'] && $n['configured']; } ); From a91e44107331cd54975f6d8b4e0dcbb6cac3bb63 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:25:18 -0700 Subject: [PATCH 064/122] Implement persistHostapdIni() --- .../Networking/Hotspot/HostapdManager.php | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index 1819d5df..0e114108 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -76,6 +76,7 @@ class HostapdManager $selectedHwMode = 'w'; } } + $config['selected_hw_mode'] = $selectedHwMode; $config['ignore_broadcast_ssid'] ??= 0; $config['max_num_sta'] ??= 0; $config['wep_default_key'] ??= 0; @@ -199,8 +200,7 @@ class HostapdManager public function saveConfig(string $config, bool $dualMode, string $iface, bool $restart = false): bool { $configFile = $this->resolveConfigPath($iface, $dualMode); - //$configFile = self::CONF_DEFAULT; - $tempFile = self::CONF_TMP; + $tempFile = SELF::CONF_TMP; if (file_put_contents($tempFile, $config) === false) { @@ -310,6 +310,34 @@ class HostapdManager throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback)."); } + /** + * Persists options to /etc/raspap/ + * + * @param string $apIface + * @param bool $logEnable + * @param bool $bridgedEnable + * @param bool $cfgWifiAPEnable + * @param bool $wifiAPEnable + * @param bool $repeaterEnable + * @param string $cliIface + * @return bool + */ + public function persistHostapdIni($apIface, $logEnable, $bridgedEnable, $cfgWifiAPEnable, $wifiAPEnable, $repeaterEnable, $cliIface): bool + { + $cfg = []; + $cfg['WifiInterface'] = $apIface; + $cfg['LogEnable'] = $logEnable; + $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $cfgWifiAPEnable : $wifiAPEnable); + $cfg['BridgedEnable'] = $bridgedEnable; + $cfg['RepeaterEnable'] = $repeaterEnable; + $cfg['WifiManaged'] = $cliIface; + $success = write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini'); + if (!$success) { + throw new \RuntimeException("Unable to write to hostapd.ini"); + } + return true; + } + } From da6f469982ac58f271d21b670ee3ee551a88b897 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:25:46 -0700 Subject: [PATCH 065/122] Implement buildConfig() --- .../Networking/Hotspot/DnsmasqManager.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 46f121a1..16a6ce41 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -41,6 +41,63 @@ class DnsmasqManager return $config; } + /** + * Builds a dnsmasq configuration + * @param array $syscfg + * @param string $iface + * @param bool $wifiAPEnable + * @param bool $bridgedEnable + * @return array $config + * @throws \RuntimeException + */ + public function buildConfig(array $syscfg, string $iface, bool $wifiAPEnable, bool $bridgedEnable): array + { + if ($wifiAPEnable == 1) { + // Enable uap0 configuration for ap-sta mode + // Set dhcp-range from system config, fallback to default if undefined + $dhcp_range = ($syscfg['dhcp-range'] == '') ? getDefaultNetValue('dnsmasq','uap0','dhcp-range') : $syscfg['dhcp-range']; + $config = [ '# RaspAP uap0 configuration' ]; + $config[] = 'interface=lo,uap0 # Enable uap0 interface for wireless client AP mode'; + $config[] = 'bind-dynamic # Hybrid between --bind-interfaces and default'; + $config[] = 'server=8.8.8.8 # Forward DNS requests to Google DNS'; + $config[] = 'domain-needed # Don\'t forward short names'; + $config[] = 'bogus-priv # Never forward addresses in the non-routed address spaces'; + $config[] = 'dhcp-range='.$dhcp_range; + if (!empty($syscfg['dhcp-option'])) { + $config[] = 'dhcp-option='.$syscfg['dhcp-option']; + } + $config[] = PHP_EOL; + scanConfigDir('/etc/dnsmasq.d/','uap0',$status); + } elseif ($bridgedEnable !==1) { + $dhcp_range = ($syscfg['dhcp-range'] =='') ? getDefaultNetValue('dnsmasq',$iface,'dhcp-range') : $syscfg['dhcp-range']; + $config = [ '# RaspAP '.$_POST['interface'].' configuration' ]; + $config[] = 'interface='.$_POST['interface']; + $config[] = 'domain-needed'; + $config[] = 'dhcp-range='.$dhcp_range; + // handle multiple dhcp-host + option entries + if (!empty($syscfg['dhcp-host'])) { + if (is_array($syscfg['dhcp-host'])) { + foreach ($syscfg['dhcp-host'] as $host) { + $config[] = 'dhcp-host=' . $host; + } + } else { + $config[] = 'dhcp-host=' . $syscfg['dhcp-host']; + } + } + if (!empty($syscfg['dhcp-option'])) { + if (is_array($syscfg['dhcp-option'])) { + foreach ($syscfg['dhcp-option'] as $opt) { + $config[] = 'dhcp-option=' . $opt; + } + } else { + $config[] = 'dhcp-option=' . $syscfg['dhcp-option']; + } + } + $config[] = PHP_EOL; + } + return $config; + } + /** * Saves dnsmasq configuration for an interface * From dd3b300931d28329216dfd19a4b3ed11feeb9a12 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 00:26:53 -0700 Subject: [PATCH 066/122] Instantiate WiFiManager, update w/ $wifi->getSignalBars() --- templates/wifi_stations/network.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/templates/wifi_stations/network.php b/templates/wifi_stations/network.php index 98df7c8c..26685647 100644 --- a/templates/wifi_stations/network.php +++ b/templates/wifi_stations/network.php @@ -1,3 +1,9 @@ +
@@ -31,7 +37,7 @@
= -200) { echo '
'; - echo getSignalBars($network['RSSI']); + echo $wifi->getSignalBars($network['RSSI']); echo '
' .htmlspecialchars($network['RSSI'], ENT_QUOTES) . "dB" . "
"; echo '
'; } else { From fa38ac615344608a725a3b9fae0460e4bac8dbd7 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 03:16:47 -0700 Subject: [PATCH 067/122] Initial commit --- .../Hotspot/Validators/HostapdValidator.php | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php diff --git a/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php new file mode 100644 index 00000000..e391fc49 --- /dev/null +++ b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php @@ -0,0 +1,141 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ +class HostapdValidator +{ + + /** + * Validates full hostapd parameter set + * + * @param array $post raw $_POST object + * @param array $wpaArray allowed WPA values + * @param array $encTypes allowed encryption types + * @param array $modes allowed hardware modes + * @param array $interfaces valid interface list + * @param string $regDomain regulatory domain + * @param StatusMessage $status Status message collector + * @return array|false validated configuration array or false on failure + */ + public function validate( + array $post, + array $wpaArray, + array $encTypes, + array $modes, + array $interfaces, + string $regDomain, + ?StatusMessage $status = null + ) { + $goodInput = true; + + // check WPA and encryption + if ( + !array_key_exists($post['wpa'], $wpaArray) || + !array_key_exists($post['wpa_pairwise'], $encTypes) || + !array_key_exists($post['hw_mode'], $modes) + ) { + $err = "Invalid WPA or encryption settings: " + . "wpa='{$post['wpa']}', " + . "wpa_pairwise='{$post['wpa_pairwise']}', " + . "hw_mode='{$post['hw_mode']}'"; + error_log($err); + return false; + } + + // validate channel + if (!filter_var($post['channel'], FILTER_VALIDATE_INT)) { + $status->addMessage('Attempting to set channel to invalid number.', 'danger'); + $goodInput = false; + } + if ((int)$post['channel'] < 1 || (int)$post['channel'] > RASPI_5GHZ_CHANNEL_MAX) { + $status->addMessage('Attempting to set channel outside of permitted range', 'danger'); + $goodInput = false; + } + + // validate SSID + if (empty($post['ssid']) || strlen($post['ssid']) > 32) { + $status->addMessage('SSID must be between 1 and 32 characters', 'danger'); + $goodInput = false; + } + + // validate WPA passphrase + if ($post['wpa'] !== 'none') { + if (strlen($post['wpa_passphrase']) < 8 || strlen($post['wpa_passphrase']) > 63) { + $status->addMessage('WPA passphrase must be between 8 and 63 characters', 'danger'); + $goodInput = false; + } elseif (!ctype_print($post['wpa_passphrase'])) { + $status->addMessage('WPA passphrase must be comprised of printable ASCII characters', 'danger'); + $goodInput = false; + } + } + + // hidden SSID + $ignoreBroadcastSSID = $post['hiddenSSID'] ?? '0'; + if (!ctype_digit($ignoreBroadcastSSID) || (int)$ignoreBroadcastSSID < 0 || (int)$ignoreBroadcastSSID >= 3) { + $status->addMessage('Invalid hiddenSSID parameter.', 'danger'); + $goodInput = false; + } + + // validate interface + if (!in_array($post['interface'], $interfaces, true)) { + $status->addMessage('Unknown interface '.htmlspecialchars($post['interface'], ENT_QUOTES), 'danger'); + $goodInput = false; + } + + // country code + $countryCode = $post['country_code']; + if (strlen($countryCode) !== 0 && strlen($countryCode) !== 2) { + $status->addMessage('Country code must be blank or two characters', 'danger'); + $goodInput = false; + } + + // beacon Interval + if (!empty($post['beaconintervalEnable'])) { + if (!is_numeric($post['beacon_interval'])) { + $status->addMessage('Beacon interval must be numeric', 'danger'); + $goodInput = false; + } elseif ($post['beacon_interval'] < 15 || $post['beacon_interval'] > 65535) { + $status->addMessage('Beacon interval must be between 15 and 65535', 'danger'); + $goodInput = false; + } + } + + // max number of clients + $post['max_num_sta'] = (int) ($post['max_num_sta'] ?? 0); + $post['max_num_sta'] = $post['max_num_sta'] > 2007 ? 2007 : $post['max_num_sta']; + $post['max_num_sta'] = $post['max_num_sta'] < 1 ? null : $post['max_num_sta']; + + if (!$goodInput) { + return false; + } + + // return normalized config array + return [ + 'interface' => $post['interface'], + 'ssid' => $post['ssid'], + 'channel' => (int)$post['channel'], + 'wpa' => $post['wpa'], + '80211w' => $post['80211w'] ?? 0, + 'wpa_passphrase' => $post['wpa_passphrase'], + 'wpa_pairwise' => $post['wpa_pairwise'], + 'hw_mode' => $post['hw_mode'], + 'country_code' => $countryCode, + 'hiddenSSID' => (int)$ignoreBroadcastSSID, + 'max_num_sta' => $post['max_num_sta'], + 'beacon_interval' => $post['beacon_interval'] ?? null, + 'disassoc_low_ack' => $post['disassoc_low_ackEnable'] ?? null, + 'bridge' => $post['bridgedEnable'] ? 'br0' : null, + ]; + } + +} + From 9d03517896e9f247827dfa89f58dd111eeb59745 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 03:17:17 -0700 Subject: [PATCH 068/122] Add getIfaceMetric public method --- .../Networking/Hotspot/DhcpcdManager.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/RaspAP/Networking/Hotspot/DhcpcdManager.php diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php new file mode 100644 index 00000000..6cff77d2 --- /dev/null +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -0,0 +1,66 @@ + Date: Sat, 19 Jul 2025 03:17:49 -0700 Subject: [PATCH 069/122] Minor: comment block Minor: comment block --- src/RaspAP/Networking/Hotspot/DnsmasqManager.php | 7 ++++++- src/RaspAP/Networking/Hotspot/WiFiManager.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 16a6ce41..94a45803 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -3,8 +3,13 @@ namespace RaspAP\Networking\Hotspot; /** - * Manages dnsmasq configuration for DHCP/DNS services + * A dnsmasq configuration manager for RaspAP + * + * @description Class methods to get, build and save dnsmasq configs + * @author Bill Zimmerman + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE */ + class DnsmasqManager { private const CONF_SUFFIX = '.conf'; diff --git a/src/RaspAP/Networking/Hotspot/WiFiManager.php b/src/RaspAP/Networking/Hotspot/WiFiManager.php index e7002a7a..4e2e4a9f 100644 --- a/src/RaspAP/Networking/Hotspot/WiFiManager.php +++ b/src/RaspAP/Networking/Hotspot/WiFiManager.php @@ -4,7 +4,7 @@ namespace RaspAP\Networking\Hotspot; /** * Wireless utility class - * @description A collection of wireless utlity methods for RaspAP + * @description A collection of wireless utility methods for RaspAP * @author Bill Zimmerman * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE */ From b9642371e022f204f13a8000f0a13f499ffc7d2c Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 03:20:58 -0700 Subject: [PATCH 070/122] Use HostapdValidator, add deriveInterfaces, deriveModeStates, iwRegSet methods --- .../Networking/Hotspot/HostapdManager.php | 194 +++++++++++++++--- .../Networking/Hotspot/HotspotService.php | 86 ++++++++ 2 files changed, 255 insertions(+), 25 deletions(-) create mode 100644 src/RaspAP/Networking/Hotspot/HotspotService.php diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index 0e114108..d1e03058 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -2,8 +2,15 @@ namespace RaspAP\Networking\Hotspot; +use RaspAP\Networking\Hotspot\Validators\HostapdValidator; +use RaspAP\Messages\StatusMessage; + /** - * Manages hostapd configurations and runtime settings + * Hostapd manager class for RaspAP + * + * @description Manages hostapd configurations and runtime settings + * @author Bill Zimmerman + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE */ class HostapdManager { @@ -11,10 +18,19 @@ class HostapdManager private const CONF_PATH_PREFIX = '/etc/hostapd/hostapd-'; private const CONF_TMP = '/tmp/hostapddata'; + /** @var HostapdValidator */ + private $validator; + + public function __construct(?HostapdValidator $validator = null) + { + $this->validator = $validator ?: new HostapdValidator(); + } + /** * Retrieves current hostapd config * * @return array + * @throws \RuntimeException */ public function getConfig(): array { @@ -85,6 +101,30 @@ class HostapdManager } + /** + * Validates a hostapd configuration + * + * @param array $post raw $_POST object + * @param array $wpaArray allowed WPA values + * @param array $encTypes allowed encryption types + * @param array $modes allowed hardware modes + * @param array $interfaces valid interface list + * @param string $regDomain regulatory domain + * @param StatusMessage $status Status message collector + * @return array|false validated configuration array or false on failure + */ + public function validate( + array $post, + array $wpaArray, + array $encTypes, + array $modes, + array $interfaces, + string $regDomain, + StatusMessage $status + ) { + return $this->validator->validate($post, $wpaArray, $encTypes, $modes, $interfaces, $regDomain, $status); + } + /** * Builds hostapd configuration text from array * @@ -182,7 +222,7 @@ class HostapdManager } // Optional additional user config - $config[] = parseUserHostapdCfg(); + $config[] = $this->parseUserHostapdCfg(); return implode(PHP_EOL, $config) . PHP_EOL; } @@ -219,6 +259,81 @@ class HostapdManager return true; } + /** + * Derives mode checkbox states from POST + existing ini + * + * @param array $post raw $_POST + * @param array $currentIni parsed hostapd.ini + * @return array normalized states + */ + public function deriveModeStates(array $post, array $currentIni): array + { + $prevWifiAPEnable = (int)($currentIni['WifiAPEnable'] ?? 0); + $bridgedEnable = isset($post['bridgedEnable']) ? 1 : 0; + $repeaterEnable = 0; + $wifiAPEnable = 0; + + if ($bridgedEnable === 0) { + // Only meaningful when not bridged + $repeaterEnable = isset($post['repeaterEnable']) ? 1 : 0; + $wifiAPEnable = isset($post['wifiAPEnable']) ? 1 : 0; + } + + $logEnable = isset($post['logEnable']) ? 1 : 0; + + $effectiveWifiAPEnable = $bridgedEnable === 1 ? $prevWifiAPEnable : $wifiAPEnable; + + return [ + 'BridgedEnable' => $bridgedEnable, + 'RepeaterEnable' => $repeaterEnable, + 'WifiAPEnable' => $effectiveWifiAPEnable, + 'LogEnable' => $logEnable, + '_rawPostedWifiAPEnable' => $wifiAPEnable + ]; + } + + /** + * Determine AP interface, client (managed) interface and session/monitor interface + * Uses these semantics: + * - Base interface = user selection (validated) or RASPI_WIFI_AP_INTERFACE + * - AP-STA mode (WifiAPEnable=1): AP is 'uap0', client is base iface + * - Bridged mode: client/session use 'br0', AP remains base iface + * + * @param string $baseIface Selected interface from form + * @param array $states Output from deriveModeStates() + * @return array [ap_iface, cli_iface, session_iface] + */ + public function deriveInterfaces(string $baseIface, array $states): array + { + $apIface = $baseIface; + $cliIface = $baseIface; + $sessionIface = $baseIface; + + if ($states['WifiAPEnable'] === 1 && $states['BridgedEnable'] === 0) { + // client AP (AP-STA) – uap0 is AP, base iface remains client + $apIface = 'uap0'; + $sessionIface = 'uap0'; + $cliIface = $baseIface; + } elseif ($states['BridgedEnable'] === 1) { + // bridged mode – monitor br0, AP stays as base wireless iface + $cliIface = 'br0'; + $sessionIface = 'br0'; + } + + return [$apIface, $cliIface, $sessionIface]; + } + + /** + * Enables or disables hostapd logging + * + * @param int $logEnable + */ + private function handleLogState(int $logEnable): void + { + $script = $logEnable === 1 ? 'enablelog.sh' : 'disablelog.sh'; + exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script); + } + /** * Sets transmit power for an interface * @@ -247,10 +362,10 @@ class HostapdManager * * @return string $tmp */ - function parseUserHostapdCfg() + private function parseUserHostapdCfg() { - if (file_exists(CONF_DEFAULT . '.users')) { - exec('cat '. CONF_DEFAULT . '.users', $hostapdconfigusers); + if (file_exists(SELF::CONF_DEFAULT . '.users')) { + exec('cat '. SELF::CONF_DEFAULT . '.users', $hostapdconfigusers); foreach ($hostapdconfigusers as $hostapdconfigusersline) { if (strlen($hostapdconfigusersline) === 0) { continue; @@ -311,33 +426,62 @@ class HostapdManager } /** - * Persists options to /etc/raspap/ + * Persist hostapd.ini with mode / interface user settings * - * @param string $apIface - * @param bool $logEnable - * @param bool $bridgedEnable - * @param bool $cfgWifiAPEnable - * @param bool $wifiAPEnable - * @param bool $repeaterEnable - * @param string $cliIface + * @param array $states states from deriveModeStates() + * @param string $apIface the AP interface + * @param string $cliIface the managed interface + * @param array $previousIni existing ini * @return bool */ - public function persistHostapdIni($apIface, $logEnable, $bridgedEnable, $cfgWifiAPEnable, $wifiAPEnable, $repeaterEnable, $cliIface): bool + public function persistHostapdIni(array $states, string $apIface, string $cliIface, array $previousIni = []): bool { - $cfg = []; - $cfg['WifiInterface'] = $apIface; - $cfg['LogEnable'] = $logEnable; - $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $cfgWifiAPEnable : $wifiAPEnable); - $cfg['BridgedEnable'] = $bridgedEnable; - $cfg['RepeaterEnable'] = $repeaterEnable; - $cfg['WifiManaged'] = $cliIface; - $success = write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini'); - if (!$success) { - throw new \RuntimeException("Unable to write to hostapd.ini"); + $this->applyLogState($states['LogEnable']); + + // compose new ini payload + $cfg = [ + 'WifiInterface' => $apIface, + 'LogEnable' => $states['LogEnable'], + 'WifiAPEnable' => $states['WifiAPEnable'], + 'BridgedEnable' => $states['BridgedEnable'], + 'RepeaterEnable' => $states['RepeaterEnable'], + 'WifiManaged' => $cliIface + ]; + foreach ($previousIni as $k => $v) { + if (!array_key_exists($k, $cfg)) { + $cfg[$k] = $v; + } } - return true; + return write_php_ini($cfg, RASPI_CONFIG . '/hostapd.ini'); } + /** + * Enables or disables hostapd logging + * + * @param int $logEnable 1 = enable, 0 = disable + */ + private function applyLogState(int $logEnable): void + { + $script = $logEnable === 1 ? 'enablelog.sh' : 'disablelog.sh'; + exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script); + } + + /** + * Executes iw to set the specified ISO 2-letter country code + * + * @param string $country_code + * @param object $status + * @return boolean $result + */ + public function iwRegSet(string $country_code, $status): bool + { + $country_code = escapeshellarg($country_code); + $result = shell_exec("sudo iw reg set $country_code"); + $status->addMessage(sprintf(_('Setting wireless regulatory domain to %s'), $country_code, 'success')); + return $result; + } + + } diff --git a/src/RaspAP/Networking/Hotspot/HotspotService.php b/src/RaspAP/Networking/Hotspot/HotspotService.php new file mode 100644 index 00000000..cc331f2e --- /dev/null +++ b/src/RaspAP/Networking/Hotspot/HotspotService.php @@ -0,0 +1,86 @@ +hostapdManager = new HostapdManager(); + $this->dnsmasqManager = new DnsmasqManager(); + $this->dhcpcdManager = new DhcpcdManager(); + } + + /** + * Apply configuration changes for hotspot. + * + * @param array $params + * @return bool + */ + public function configureHotspot(array $params): bool + { + // TODO: validate params, orchestrate managers + return false; + } + + /** + * Start hotspot services for given interface. + * + * @param string $iface + * @return bool + */ + public function start(string $iface): bool + { + // TODO: implement systemctl or service logic + return false; + } + + /** + * Stop hotspot services. + * + * @return bool + */ + public function stop(): bool + { + // TODO: implement + return false; + } + + /** + * Restart hotspot services for given interface. + * + * @param string $iface + * @return bool + */ + public function restart(string $iface): bool + { + // TODO: implement + return false; + } + + /** + * Get current hotspot status. + * + * @return array + */ + public function getStatus(): array + { + // TODO: query service state + configs + return []; + } +} + From 3b352b12d8b2f9046aa97462ba3011fb7b55327d Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 03:21:50 -0700 Subject: [PATCH 071/122] WIP: migrate functions to class methods, refactor saveHostAPDConfig() --- includes/hostapd.php | 259 +++++++------------------------------------ 1 file changed, 40 insertions(+), 219 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index 3b2e3e21..0d03f0ba 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -4,6 +4,8 @@ use RaspAP\Networking\Hotspot\DnsmasqManager; use RaspAP\Networking\Hotspot\HostapdManager; use RaspAP\Networking\Hotspot\DhcpcdManager; use RaspAP\Networking\Hotspot\WiFiManager; +use RaspAP\Messages\StatusMessage; +use RaspAP\System\Sysinfo; $wifi = new WiFiManager(); $wifi->getWifiInterface(); @@ -14,9 +16,10 @@ $wifi->getWifiInterface(); */ function DisplayHostAPDConfig() { - $status = new \RaspAP\Messages\StatusMessage; - $system = new \RaspAP\System\Sysinfo; + $status = new StatusMessage(); + $system = new Sysinfo(); $operatingSystem = $system->operatingSystem(); + $arrConfig = array(); $arr80211Standard = [ 'a' => '802.11a - 5 GHz', @@ -50,7 +53,7 @@ function DisplayHostAPDConfig() if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveHostAPDSettings'])) { - SaveHostAPDConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); + saveHostAPDConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); } } @@ -157,182 +160,56 @@ function DisplayHostAPDConfig() * @param object $status * @return boolean */ -function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) +function saveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) { $hostapd = new HostapdManager(); $dnsmasq = new DnsmasqManager(); - - // If wpa fields are absent, return false and log securely - if (!(array_key_exists($_POST['wpa'], $wpa_array) - && array_key_exists($_POST['wpa_pairwise'], $enc_types) - && array_key_exists($_POST['hw_mode'], $modes)) - ) { - $err = "Attempting to set hostapd config with wpa='".escapeshellarg($_POST['wpa']); - $err .= "', wpa_pairwise='".$escapeshellarg(_POST['wpa_pairwise']); - $err .= "and hw_mode='".$escapeshellarg(_POST['hw_mode'])."'"; - error_log($err); - return false; - } - // Validate input - $good_input = true; - - if (!filter_var($_POST['channel'], FILTER_VALIDATE_INT)) { - $status->addMessage('Attempting to set channel to invalid number.', 'danger'); - $good_input = false; - } - if (intval($_POST['channel']) < 1 || intval($_POST['channel']) > RASPI_5GHZ_CHANNEL_MAX) { - $status->addMessage('Attempting to set channel outside of permitted range', 'danger'); - $good_input = false; - } - $arrHostapdConf = parse_ini_file('/etc/raspap/hostapd.ini'); - + $dhcpcd = new DhcpcdManager(); $dualAPEnable = false; - // Check for Bridged AP mode checkbox - $bridgedEnable = 0; - if ($arrHostapdConf['BridgedEnable'] == 0) { - if (isset($_POST['bridgedEnable'])) { - $bridgedEnable = 1; - } - } else { - if (isset($_POST['bridgedEnable'])) { - $bridgedEnable = 1; - } - } - // Check for WiFi repeater mode checkbox - $repeaterEnable = 0; - if ($bridgedEnable == 0) { // enable client mode actions when not bridged - if ($arrHostapdConf['RepeaterEnable'] == 0) { - if (isset($_POST['repeaterEnable'])) { - $repeaterEnable = 1; - } - } else { - if (isset($_POST['repeaterEnable'])) { - $repeaterEnable = 1; - } - } - } - // Check for WiFi client AP mode checkbox - $wifiAPEnable = 0; - if ($bridgedEnable == 0) { // enable client mode actions when not bridged - if ($arrHostapdConf['WifiAPEnable'] == 0) { - if (isset($_POST['wifiAPEnable'])) { - $wifiAPEnable = 1; - } - } else { - if (isset($_POST['wifiAPEnable'])) { - $wifiAPEnable = 1; - } - } - } - // Check for Logfile output checkbox - $logEnable = 0; - if ($arrHostapdConf['LogEnable'] == 0) { - if (isset($_POST['logEnable'])) { - $logEnable = 1; - exec('sudo '.RASPI_CONFIG.'/hostapd/enablelog.sh'); - } else { - exec('sudo '.RASPI_CONFIG.'/hostapd/disablelog.sh'); - } - } else { - if (isset($_POST['logEnable'])) { - $logEnable = 1; - exec('sudo '.RASPI_CONFIG.'/hostapd/enablelog.sh'); - } else { - exec('sudo '.RASPI_CONFIG.'/hostapd/disablelog.sh'); - } - } + $hostapdIniPath = RASPI_CONFIG . '/hostapd.ini'; + $arrHostapdConf = file_exists($hostapdIniPath) ? parse_ini_file($hostapdIniPath) : []; - // set AP interface default, override for ap-sta & bridged options - $iface = validateInterface($_POST['interface']) ? $_POST['interface'] : RASPI_WIFI_AP_INTERFACE; + // derive mode states + $states = $hostapd->deriveModeStates($_POST, $arrHostapdConf); - $ap_iface = $iface; // the hostap AP interface - $cli_iface = $iface; // the wifi client interface - $session_iface = $iface; // the interface that the UI needs to monitor for data usage etc. - if ($wifiAPEnable) { // for AP-STA we monitor the uap0 interface, which is always the ap interface. - $ap_iface = $session_iface = 'uap0'; - } - if ($bridgedEnable) { // for bridged mode we monitor the bridge, but keep the selected interface as AP. - $cli_iface = $session_iface = 'br0'; - } + // determine base interface (validated or fallback) + $baseIface = validateInterface($_POST['interface']) ? $_POST['interface'] : RASPI_WIFI_AP_INTERFACE; - $hostapd->persistHostapdIni($ap_iface, $logEnable, $bridgedEnable, $arrHostapdConf['WifiAPEnable'], $wifiAPEnable, $repeaterEnable, $cli_iface); + // derive interface roles + [$apIface, $cliIface, $sessionIface] = $hostapd->deriveInterfaces($baseIface, $states); - $_SESSION['ap_interface'] = $session_iface; + // persist hostapd.ini + $hostapd->persistHostapdIni($states, $apIface, $cliIface, $arrHostapdConf); - // Verify input - if (empty($_POST['ssid']) || strlen($_POST['ssid']) > 32) { - $status->addMessage('SSID must be between 1 and 32 characters', 'danger'); - $good_input = false; - } + // store session (compatibility) + $_SESSION['ap_interface'] = $sessionIface; - # NB: A pass-phrase is a sequence of between 8 and 63 ASCII-encoded characters (IEEE Std. 802.11i-2004) - # Each character in the pass-phrase must have an encoding in the range of 32 to 126 (decimal). (IEEE Std. 802.11i-2004, Annex H.4.1) - if ($_POST['wpa'] !== 'none' && (strlen($_POST['wpa_passphrase']) < 8 || strlen($_POST['wpa_passphrase']) > 63)) { - $status->addMessage('WPA passphrase must be between 8 and 63 characters', 'danger'); - $good_input = false; - } elseif (!ctype_print($_POST['wpa_passphrase'])) { - $status->addMessage('WPA passphrase must be comprised of printable ASCII characters', 'danger'); - $good_input = false; - } - - $ignore_broadcast_ssid = $_POST['hiddenSSID'] ?? '0'; - if (!ctype_digit($ignore_broadcast_ssid)) { - $status->addMessage('Parameter hiddenSSID not a number.', 'danger'); - $good_input = false; - } elseif ((int)$ignore_broadcast_ssid < 0 || (int)$ignore_broadcast_ssid >= 3) { - $status->addMessage('Parameter hiddenSSID contains an invalid configuration value.', 'danger'); - $good_input = false; - } - if (! in_array($_POST['interface'], $interfaces)) { - $status->addMessage('Unknown interface '.htmlspecialchars($_POST['interface'], ENT_QUOTES), 'danger'); - $good_input = false; - } - if (strlen($_POST['country_code']) !== 0 && strlen($_POST['country_code']) != 2) { - $status->addMessage('Country code must be blank or two characters', 'danger'); - $good_input = false; - } else { - $country_code = $_POST['country_code']; - } - if (isset($_POST['beaconintervalEnable'])) { - if (!is_numeric($_POST['beacon_interval'])) { - $status->addMessage('Beacon interval must be a numeric value', 'danger'); - $good_input = false; - } elseif ($_POST['beacon_interval'] < 15 || $_POST['beacon_interval'] > 65535) { - $status->addMessage('Beacon interval must be between 15 and 65535', 'danger'); - $good_input = false; - } - } - $_POST['max_num_sta'] = (int) $_POST['max_num_sta']; - $_POST['max_num_sta'] = $_POST['max_num_sta'] > 2007 ? 2007 : $_POST['max_num_sta']; - $_POST['max_num_sta'] = $_POST['max_num_sta'] < 1 ? null : $_POST['max_num_sta']; - - if ($good_input) { - $config = $hostapd->buildConfig([ - 'interface' => $_POST['interface'], - 'ssid' => $_POST['ssid'], - 'channel' => $_POST['channel'], - 'wpa' => $_POST['wpa'], - '80211w' => $_POST['80211w'] ?? 0, - 'wpa_passphrase' => $_POST['wpa_passphrase'], - 'wpa_pairwise' => $_POST['wpa_pairwise'], - 'hw_mode' => $_POST['hw_mode'], - 'country_code' => $_POST['country_code'], - 'hiddenSSID' => $_POST['hiddenSSID'], - 'max_num_sta' => $_POST['max_num_sta'] ?? null, - 'beacon_interval' => $_POST['beacon_interval'] ?? null, - 'disassoc_low_ack' => $_POST['disassoc_low_ackEnable'] ?? null, - 'bridge' => $bridgedEnable ? 'br0' : null - ]); + // validate config from $_POST + $validated = $hostapd->validate($_POST, $wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status); + if ($validated !== false) { try { - $arrConfig = $hostapd->saveConfig($config, $dualAPEnable, $ap_iface); + $validated['interface'] = $apIface; + $validated['bridge'] = $states['BridgedEnable'] ? 'br0' : null; + + $config = $hostapd->buildConfig($validated); + $hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); + $status->addMessage('WiFi hotspot settings saved.', 'success'); + } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); } - + } else { + $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); + return false; + } + + /// TODO: build out DHCP class + /// finish processing save + /* if (trim($country_code) != trim($reg_domain)) { - $return = iwRegSet($country_code, $status); + $return = $hostapd->iwRegSet($country_code, $status); } // Parse dnsmasq config for selected interface @@ -437,10 +314,7 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom } else { $status->addMessage('WiFi hotspot settings saved.', 'success'); } - } else { - $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); - return false; - } + */ return true; } @@ -476,23 +350,6 @@ function countHostapdConfigs(): int return is_array($configs) ? count($configs) : 0; } -/** - * Retrieves the metric value for a given interface - * - * @param string $iface - * @return int $metric - */ -function getIfaceMetric($iface) -{ - $metric = shell_exec("ip -o -4 route show dev ".$iface." | awk '/metric/ {print \$NF; exit}'"); - if (isset($metric)) { - $metric = (int)$metric; - return $metric; - } else { - return false; - } -} - /** * Updates the dhcpcd configuration for a given interface, preserving existing settings * @@ -557,39 +414,3 @@ function updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain return $config; } -/** - * Executes iw to set the specified ISO 2-letter country code - * - * @param string $country_code - * @param object $status - * @return boolean $result - */ -function iwRegSet(string $country_code, $status) -{ - $country_code = escapeshellarg($country_code); - $result = shell_exec("sudo iw reg set $country_code"); - $status->addMessage(sprintf(_('Setting wireless regulatory domain to %s'), $country_code, 'success')); - return $result; -} - -/** - * Parses optional /etc/hostapd/hostapd.conf.users file - * - * @return string $tmp - */ -function parseUserHostapdCfg() -{ - if (file_exists(RASPI_HOSTAPD_CONFIG . '.users')) { - exec('cat '. RASPI_HOSTAPD_CONFIG . '.users', $hostapdconfigusers); - foreach ($hostapdconfigusers as $hostapdconfigusersline) { - if (strlen($hostapdconfigusersline) === 0) { - continue; - } - if ($hostapdconfigusersline[0] != "#") { - $arrLine = explode("=", $hostapdconfigusersline); - $tmp.= $arrLine[0]."=".$arrLine[1].PHP_EOL;; - } - } - return $tmp; - } -} From 126f64a793e63e2187b5c325b96d2e77534885ab Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 05:56:32 -0700 Subject: [PATCH 072/122] Fix PHP warning Fix PHP warning --- src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php | 2 +- templates/hostapd/security.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php index e391fc49..864ff371 100644 --- a/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php +++ b/src/RaspAP/Networking/Hotspot/Validators/HostapdValidator.php @@ -133,7 +133,7 @@ class HostapdValidator 'max_num_sta' => $post['max_num_sta'], 'beacon_interval' => $post['beacon_interval'] ?? null, 'disassoc_low_ack' => $post['disassoc_low_ackEnable'] ?? null, - 'bridge' => $post['bridgedEnable'] ? 'br0' : null, + 'bridge' => ($post['bridgedEnable'] ?? false) ? 'br0' : null ]; } diff --git a/templates/hostapd/security.php b/templates/hostapd/security.php index cc7eb141..a0aa09a1 100644 --- a/templates/hostapd/security.php +++ b/templates/hostapd/security.php @@ -13,7 +13,7 @@
- +
From 5cd07a83a9dda70a3efb517dc48ea78d7744cf35 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 05:59:32 -0700 Subject: [PATCH 073/122] Revise /sbin/iw permissions scope --- installers/raspap.sudoers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 2d051035..e3985826 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -45,7 +45,7 @@ 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 +www-data ALL=(ALL) NOPASSWD:/sbin/iw dev* www-data ALL=(ALL) NOPASSWD:/bin/cp /etc/raspap/networking/dhcpcd.conf /etc/dhcpcd.conf www-data ALL=(ALL) NOPASSWD:/etc/raspap/hostapd/enablelog.sh www-data ALL=(ALL) NOPASSWD:/etc/raspap/hostapd/disablelog.sh From e12be86c8caf8daaa8a88efc4a043a817b121630 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 06:01:23 -0700 Subject: [PATCH 074/122] Add static methods for 802.11 standards, resolveHwMode() --- .../Networking/Hotspot/HostapdManager.php | 252 ++++++++++++++---- 1 file changed, 195 insertions(+), 57 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index d1e03058..bacbd232 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -18,6 +18,22 @@ class HostapdManager private const CONF_PATH_PREFIX = '/etc/hostapd/hostapd-'; private const CONF_TMP = '/tmp/hostapddata'; + // IEEE 802.11 standards + private const IEEE_80211_STANDARD = [ + 'a' => '802.11a - 5 GHz', + 'b' => '802.11b - 2.4 GHz', + 'g' => '802.11g - 2.4 GHz', + 'n' => '802.11n - 2.4/5 GHz', + 'ac' => '802.11ac - 5 GHz' + ]; + + // encryption types + private const ENC_TYPES = [ + 'TKIP' => 'TKIP', + 'CCMP' => 'CCMP', + 'TKIP CCMP' => 'TKIP+CCMP' + ]; + /** @var HostapdValidator */ private $validator; @@ -26,6 +42,50 @@ class HostapdManager $this->validator = $validator ?: new HostapdValidator(); } + /** + * Returns IEEE 802.11 standards + */ + public static function get80211Standards(): array + { + return self::IEEE_80211_STANDARD; + } + + /** + * Returns encryption types + */ + public static function getEncTypes(): array + { + return self::ENC_TYPES; + } + + /** + * Returns translated security modes. + */ + public static function getSecurityModes(): array + { + // Build each call to ensure translation occurs under current locale. + return [ + 1 => 'WPA', + 2 => 'WPA2', + 3 => _('WPA and WPA2'), + 4 => _('WPA2 and WPA3-Personal (transitional mode)'), + 5 => 'WPA3-Personal (required)', + 'none' => _('None'), + ]; + } + + /** + * Returns translated 802.11w options + */ + public static function get80211wOptions(): array + { + return [ + 3 => _('Disabled'), + 1 => _('Enabled (for supported clients)'), + 2 => _('Required (for supported clients)'), + ]; + } + /** * Retrieves current hostapd config * @@ -46,7 +106,6 @@ class HostapdManager if ($status !== 0 || empty($hostapdconfig)) { throw new \RuntimeException("Failed to read hostapd config: $configFile"); } - //error_log("HostapdManager::getConfig() hostapdconfig =" . print_r($hostapdconfig, true)); foreach ($hostapdconfig as $hostapdconfigline) { if (strlen($hostapdconfigline) === 0) { @@ -76,23 +135,7 @@ class HostapdManager } elseif ($config['wpa_key_mgmt'] == 'SAE') { $config['wpa'] = 5; } - $selectedHwMode = $config['hw_mode']; - if (isset($config['ieee80211n'])) { - if (strval($config['ieee80211n']) === '1') { - $selectedHwMode = 'n'; - } - } - if (isset($config['ieee80211ac'])) { - if (strval($config['ieee80211ac']) === '1') { - $selectedHwMode = 'ac'; - } - } - if (isset($config['ieee80211w'])) { - if (strval($config['ieee80211w']) === '2') { - $selectedHwMode = 'w'; - } - } - $config['selected_hw_mode'] = $selectedHwMode; + $config['selected_hw_mode'] = $this->resolveHwMode($config); $config['ignore_broadcast_ssid'] ??= 0; $config['max_num_sta'] ??= 0; $config['wep_default_key'] ??= 0; @@ -101,6 +144,29 @@ class HostapdManager } + /** + * Determines the selected hardware mode based on config + * + * @param array $config + * @return string + */ + private function resolveHwMode(array $config): string + { + $selected = $config['hw_mode'] ?? 'g'; // default fallback + + if (!empty($config['ieee80211n']) && strval($config['ieee80211n']) === '1') { + $selected = 'n'; + } + if (!empty($config['ieee80211ac']) && strval($config['ieee80211ac']) === '1') { + $selected = 'ac'; + } + if (!empty($config['ieee80211w']) && strval($config['ieee80211w']) === '2') { + $selected = 'w'; + } + + return $selected; + } + /** * Validates a hostapd configuration * @@ -128,10 +194,11 @@ class HostapdManager /** * Builds hostapd configuration text from array * - * @param array $params + * @param array $params + * @param StatusMessage $status * @return string */ - public function buildConfig(array $params): string + public function buildConfig(array $params, StatusMessage $status): string { $config = []; $config[] = 'driver=nl80211'; @@ -220,8 +287,10 @@ class HostapdManager if (!empty($params['max_num_sta'])) { $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; } - - // Optional additional user config + + $result = $this->maybeSetRegDomain($params['country_code'], $status); + + // optional additional user config $config[] = $this->parseUserHostapdCfg(); return implode(PHP_EOL, $config) . PHP_EOL; @@ -232,7 +301,7 @@ class HostapdManager * * @param string $config, rendered hostapd.conf * @param string $interface, named interface - * @param bool $dualMode, dual-band AP mode enabled + * @param bool $dualMode, dual-band AP mode enabled * @param bool $restart, option to restart hostapd@ after save * @return bool * @throws \RuntimeException @@ -262,8 +331,8 @@ class HostapdManager /** * Derives mode checkbox states from POST + existing ini * - * @param array $post raw $_POST - * @param array $currentIni parsed hostapd.ini + * @param array $post raw $_POST + * @param array $currentIni parsed hostapd.ini * @return array normalized states */ public function deriveModeStates(array $post, array $currentIni): array @@ -334,29 +403,6 @@ class HostapdManager exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script); } - /** - * Sets transmit power for an interface - * - * @param string $iface - * @param int|string $dbm - * @return bool - */ - public function setTxPower(string $iface, $dbm): bool - { - return false; - } - - /** - * Sets regulatory domain - * - * @param string $countryCode - * @return bool - */ - public function setRegDomain(string $countryCode): bool - { - return false; - } - /** * Parses optional /etc/hostapd/hostapd.conf.users file * @@ -467,20 +513,112 @@ class HostapdManager } /** - * Executes iw to set the specified ISO 2-letter country code + * Sets transmit power for an interface * - * @param string $country_code - * @param object $status - * @return boolean $result + * @param string $iface + * @param int|string $dbm + * @param StatusMessage $status + * @return bool */ - public function iwRegSet(string $country_code, $status): bool + public function maybeSetTxPower(string $iface, $dbm, StatusMessage $status): bool { - $country_code = escapeshellarg($country_code); - $result = shell_exec("sudo iw reg set $country_code"); - $status->addMessage(sprintf(_('Setting wireless regulatory domain to %s'), $country_code, 'success')); - return $result; + $currentTxPower = $this->getTxPower($iface); + + if ($currentTxPower === $dbm) { + return true; + } + + if ($dbm === 'auto') { + exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower auto', $return); + $status->addMessage('Setting transmit power to auto.', 'success'); + } else { + $sdBm = (int)$dbm * 100; + exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower fixed ' . $sdBm, $return); + $status->addMessage('Setting transmit power to ' . $dbm . ' dBm.', 'success'); + } + return true; } + /** + * Gets transmit power for an interface + * + * @param string $iface + * @return string + */ + public function getTxPower(string $iface): string + { + $cmd = "iw dev ".escapeshellarg($iface)." info | awk '$1==\"txpower\" {print $2}'"; + exec($cmd, $txpower); + return intval($txpower[0]); + } + + /** + * Sets a new regulatory domain if value has changed + * + * @param string $countryCode + * @return bool + */ + public function maybeSetRegDomain($countryCode, StatusMessage $status): bool + { + $currentDomain = $this->getRegDomain(); + if (trim($countryCode) !== trim($currentDomain)) { + $result = $this->setRegDomain($countryCode, $status); + if ($result !== true) { + return false; + } + } + return true; + } + + /** + * Gets the current regulatory domain + * + * @return string + */ + public function getRegDomain(): string + { + $domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); + return $domain; + } + + /** + * Sets the specified wireless regulatory domain + * + * @param string $country_code ISO 2-letter country code + * @param object $status StatusMessage object + * @return boolean $result + */ + public function setRegDomain(string $country_code, StatusMessage $status): bool + { + $country_code = escapeshellarg($country_code); + exec("sudo iw reg set $country_code", $output, $result); + if ($result !== 0) { + $status->addMessage(sprintf(_('Unable to set wireless regulatory domain to %s'), $country_code, 'warning')); + return false; + } else { + $status->addMessage(sprintf(_('Setting wireless regulatory domain to %s'), $country_code, 'success')); + return true; + } + } + + /** + * Enumerates available network interfaces + * + * @return array $interfaces + */ + public function getInterfaces(): array + { + exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); + + // filter out loopback, docker, bridges + other virtual interfaces + // that are incapable of hosting an AP + $interfaces = array_filter($interfaces, function ($iface) { + return !preg_match('/^(lo|docker|br-|veth|tun|tap|tailscale)/', $iface); + }); + sort($interfaces); + + return array_values($interfaces); + } } From c5ff6912ead4e9a3bc3fbeaeb593068bb21174f0 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 06:03:46 -0700 Subject: [PATCH 075/122] Set properties from class methods, process txpower input --- includes/hostapd.php | 70 ++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index 0d03f0ba..c2e0d01b 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -16,40 +16,31 @@ $wifi->getWifiInterface(); */ function DisplayHostAPDConfig() { + $hostapd = new HostapdManager(); $status = new StatusMessage(); $system = new Sysinfo(); $operatingSystem = $system->operatingSystem(); $arrConfig = array(); - $arr80211Standard = [ - 'a' => '802.11a - 5 GHz', - 'b' => '802.11b - 2.4 GHz', - 'g' => '802.11g - 2.4 GHz', - 'n' => '802.11n - 2.4/5 GHz', - 'ac' => '802.11ac - 5 GHz' - ]; + $arr80211Standard = $hostapd->get80211Standards(); + $arrSecurity = $hostapd->getSecurityModes(); + $arrEncType = $hostapd->getEncTypes(); + $arr80211w = $hostapd->get80211wOptions(); $languageCode = strtok($_SESSION['locale'], '_'); $countryCodes = getCountryCodes($languageCode); + $reg_domain = $hostapd->getRegDomain(); + $interfaces = $hostapd->getInterfaces(); - $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => _("WPA and WPA2")); - $arrSecurity += [4 => _("WPA2 and WPA3-Personal (transitional mode)")]; - $arrSecurity += [5 => 'WPA3-Personal (required)']; - $arrSecurity += ['none' => _("None")]; - $arrEncType = array('TKIP' => 'TKIP', 'CCMP' => 'CCMP', 'TKIP CCMP' => 'TKIP+CCMP'); - $arr80211w = array(3 => _("Disabled"), 1 => _("Enabled (for supported clients)"), 2 => _("Required (for supported clients)")); + // set defaults $arrTxPower = getDefaultNetOpts('txpower','dbm'); $managedModeEnabled = false; - exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); - sort($interfaces); - - $reg_domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); - $cmd = "iw dev ".escapeshellarg($_SESSION['ap_interface'])." info | awk '$1==\"txpower\" {print $2}'"; - exec($cmd, $txpower); - $txpower = intval($txpower[0]); if (isset($_POST['interface'])) { - $interface = escapeshellarg($_POST['interface']); + $interface = $_POST['interface']; + } else { + $interface = $_SESSION['ap_interface']; } + $txpower = $hostapd->getTxPower($interface); if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveHostAPDSettings'])) { @@ -98,31 +89,29 @@ function DisplayHostAPDConfig() } } - // Parse hostapd configuration - $hostapd = new HostapdManager(); + // process txpower user input + if (isset($_POST['txpower'])) { + if ($_POST['txpower'] != 'auto') { + $txpower = intval($_POST['txpower']); + $hostapd->maybeSetTxPower($interface, $txpower, $status); + } elseif ($_POST['txpower'] == 'auto') { + $hostapd->maybeSetTxPower($interface, 'auto', $status); + } + $txpower = $_POST['txpower']; + } + + // parse hostapd configuration try { $arrConfig = $hostapd->getConfig(); } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); } + // assign disassoc_low_ack boolean if value is set + $arrConfig['disassoc_low_ack_bool'] = isset($arrConfig['disassoc_low_ack']) ? 1 : 0; $hostapdstatus = $system->hostapdStatus(); $serviceStatus = $hostapdstatus[0] == 0 ? "down" : "up"; - // set txpower with iw if value is non-default ('auto') - if (isset($_POST['txpower'])) { - if ($_POST['txpower'] != 'auto') { - $txpower = intval($_POST['txpower']); - $sdBm = $txpower * 100; - exec('sudo /sbin/iw dev '.$interface.' set txpower fixed '.$sdBm, $return); - $status->addMessage('Setting transmit power to '.$_POST['txpower'].' dBm.', 'success'); - $txpower = $_POST['txpower']; - } elseif ($_POST['txpower'] == 'auto') { - exec('sudo /sbin/iw dev '.$interface.' set txpower auto', $return); - $status->addMessage('Setting transmit power to '.$_POST['txpower'].'.', 'success'); - $txpower = $_POST['txpower']; - } - } exec('sudo /bin/chmod o+r '.RASPI_HOSTAPD_LOG); $logdata = getLogLimited(RASPI_HOSTAPD_LOG); @@ -142,7 +131,6 @@ function DisplayHostAPDConfig() "txpower", "arrHostapdConf", "operatingSystem", - "selectedHwMode", "countryCodes", "logdata" ) @@ -192,11 +180,11 @@ function saveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom try { $validated['interface'] = $apIface; $validated['bridge'] = $states['BridgedEnable'] ? 'br0' : null; - - $config = $hostapd->buildConfig($validated); + $validated['txpower'] = $txpower; + // build and save configuration + $config = $hostapd->buildConfig($validated, $status); $hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); $status->addMessage('WiFi hotspot settings saved.', 'success'); - } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); } From 92f9cf745e1724046695a034ed5b785110a1ad76 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 15:04:16 -0700 Subject: [PATCH 076/122] Add getHostapdIni, countHostapdConfigs methods --- .../Networking/Hotspot/HostapdManager.php | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index bacbd232..cbd9491b 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -343,21 +343,19 @@ class HostapdManager $wifiAPEnable = 0; if ($bridgedEnable === 0) { - // Only meaningful when not bridged + // only meaningful when not bridged $repeaterEnable = isset($post['repeaterEnable']) ? 1 : 0; $wifiAPEnable = isset($post['wifiAPEnable']) ? 1 : 0; } $logEnable = isset($post['logEnable']) ? 1 : 0; - $effectiveWifiAPEnable = $bridgedEnable === 1 ? $prevWifiAPEnable : $wifiAPEnable; return [ 'BridgedEnable' => $bridgedEnable, 'RepeaterEnable' => $repeaterEnable, 'WifiAPEnable' => $effectiveWifiAPEnable, - 'LogEnable' => $logEnable, - '_rawPostedWifiAPEnable' => $wifiAPEnable + 'LogEnable' => $logEnable ]; } @@ -471,6 +469,20 @@ class HostapdManager throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback)."); } + /** + * Gets system hostapd.ini + * + * @return array $config + */ + public function getHostapdIni() + { + $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; + if (file_exists($hostapdIni)) { + $config = parse_ini_file($hostapdIni); + return $config; + } + } + /** * Persist hostapd.ini with mode / interface user settings * @@ -491,6 +503,7 @@ class HostapdManager 'WifiAPEnable' => $states['WifiAPEnable'], 'BridgedEnable' => $states['BridgedEnable'], 'RepeaterEnable' => $states['RepeaterEnable'], + 'DualAPEnable' => $states['DualAPEnable'], 'WifiManaged' => $cliIface ]; foreach ($previousIni as $k => $v) { @@ -620,6 +633,16 @@ class HostapdManager return array_values($interfaces); } + /** + * Returns a count of hostapd-.conf files + * + * @return int + */ + private function countHostapdConfigs(): int + { + $configs = glob('/etc/hostapd/hostapd-*.conf'); + return is_array($configs) ? count($configs) : 0; + } + } - From 3fe4990cfd3a40409bdee0b267415b782aa52b6d Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 15:08:13 -0700 Subject: [PATCH 077/122] Implement buildConfig, saveConfig, getInterfaceConfig methods --- .../Networking/Hotspot/DhcpcdManager.php | 359 +++++++++++++++++- 1 file changed, 339 insertions(+), 20 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php index 6cff77d2..9e397112 100644 --- a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -2,46 +2,229 @@ namespace RaspAP\Networking\Hotspot; +use RaspAP\Messages\StatusMessage; + /** * Handles dhcpcd.conf interface configuration. */ class DhcpcdManager { + private const CONF_DEFAULT = RASPI_DHCPCD_CONFIG; + private const CONF_TMP = '/tmp/dhcpddata'; + /** - * Get dhcpcd settings for an interface. + * Builds a dhcpcd config for an interface * - * @param string $iface - * @return array + * @param string $ap_iface + * @param bool $bridgedEnable + * @param bool $repeaterEnable + * @param bool $wifiAPEnable + * @param bool $dualAPEnable + * @param StatusMessage $status + * @return string */ - public function getInterfaceSection(string $iface): array + public function buildConfig( + string $ap_iface, + bool $bridgedEnable, + bool $repeaterEnable, + bool $wifiAPEnable, + bool $dualAPEnable, + StatusMessage $status + ): bool { - // TODO: parse dhcpcd.conf - return []; + // determine static IP, routers, DNS + $jsonData = $this->getInterfaceConfig($ap_iface); + //error_log("DhcpcdManager::buildConfig() jsonData =" . print_r($jsonData, true)); + $ip_address = empty($jsonData['StaticIP']) + ? getDefaultNetValue('dhcp', $ap_iface, 'static ip_address') + : $jsonData['StaticIP']; + $domain_name_server = empty($jsonData['StaticDNS']) + ? getDefaultNetValue('dhcp', $ap_iface, 'static domain_name_server') + : $jsonData['StaticDNS']; + $routers = empty($jsonData['StaticRouters']) + ? getDefaultNetValue('dhcp', $ap_iface, 'static routers') + : $jsonData['StaticRouters']; + $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') + ? getDefaultNetValue('dhcp', $ap_iface, 'subnetmask') + : $jsonData['SubnetMask']; + if (!preg_match('/.*\/\d+/', $ip_address)) { + $ip_address .= '/' . mask2cidr($netmask); + } + $config = []; + + if ($bridgedEnable) { + $config = array_keys(getDefaultNetOpts('dhcp', 'options')); + $config[] = '# RaspAP br0 configuration'; + $config[] = 'denyinterfaces eth0 wlan0'; + $config[] = 'interface br0'; + } elseif ($repeaterEnable) { + $config = [ + '# RaspAP ' . $ap_iface . ' configuration', + 'interface ' . $ap_iface, + 'static ip_address=' . $ip_address, + 'static routers=' . $routers, + 'static domain_name_server=' . $domain_name_server + ]; + $client_metric = getIfaceMetric($_SESSION['wifi_client_interface']); + if (is_int($client_metric)) { + $config[] = 'metric ' . ((int)$client_metric + 1); + } else { + $status->addMessage( + 'Unable to obtain metric value for client interface. Repeater mode inactive.', + 'warning' + ); + } + } elseif ($wifiAPEnable) { + $config = array_keys(getDefaultNetOpts('dhcp', 'options')); + $config[] = '# RaspAP uap0 configuration'; + $config[] = 'interface uap0'; + $config[] = 'static ip_address=' . $ip_address; + $config[] = 'nohook wpa_supplicant'; + } elseif ($dualAPEnable) { + $config = [ + '# RaspAP ' . $ap_iface . ' configuration', + 'interface ' . $ap_iface, + 'static ip_address=' . $ip_address, + 'static routers=' . $routers, + 'static domain_name_server=' . $domain_name_server, + 'nogateway' + ]; + } else { + $config = $this->updateDhcpcdConfig( + $ap_iface, + $jsonData, + $ip_address, + $routers, + $domain_name_server + ); + } + $dhcp_cfg = file_get_contents(SELF::CONF_DEFAULT); + + if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) { + $skip_dhcp = true; + } elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) { + $dhcp_cfg = join(PHP_EOL, $config); + $status->addMessage(sprintf(_('DHCP configuration for %s enabled.'), $ap_iface), 'success'); + } elseif (!preg_match('/^interface\s'.$ap_iface.'$/m', $dhcp_cfg)) { + $config[] = PHP_EOL; + $config= join(PHP_EOL, $config); + $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); + $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); + $dhcp_cfg .= $config; + } else { + $config = join(PHP_EOL, $config); + $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); + $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); + if (!strpos($dhcp_cfg, 'metric')) { + $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1); + } else { + $metrics = true; + } + } + if ($repeaterEnable && $metrics) { + $status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning'); + } else if ($repeaterEnable && !$metrics) { + $status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success'); + $status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success'); + $this->saveConfig($dhcp_cfg, $ap_iface, $status); + } elseif (!$skip_dhcp) { + $this->saveConfig($dhcp_cfg, $ap_iface, $status); + } else { + $status->addMessage('WiFi hotspot settings saved.', 'success'); + } + return true; } /** - * Write or update interface section. + * Saves a dhcpcd configuration * - * @param string $iface - * @param array $kv + * @param string $config + * @param StatusMessage $status * @return bool + * @throws \RuntimeException */ - public function writeInterfaceSection(string $iface, array $kv): bool + public function saveConfig(string $config, string $iface, StatusMessage $status): bool { - // TODO: update config - return false; + if (file_put_contents(self::CONF_TMP, $config) === false) { + throw new \RuntimeException("Failed to write temporary dhcpcd config"); + } + + exec(sprintf('sudo cp %s %s', escapeshellarg(self::CONF_TMP), escapeshellarg(self::CONF_DEFAULT)), $o, $rc); + if ($rc !== 0) { + $status->addMessage('Unable to save DHCP configuration.', 'danger'); + return false; + } + $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $iface), 'success'); + return true; } /** - * Remove interface section from dhcpcd.conf. - * - * @param string $iface - * @return bool - */ - public function removeInterface(string $iface): bool + * Updates the dhcpcd configuration for a given interface, preserving existing settings + * + * @param string $ap_iface + * @param array $jsonData + * @param string $ip_address + * @param string $routers + * @param string $domain_name_server + * @return array updated configuration + */ + private function updateDhcpcdConfig( + string $ap_iface, + array $jsonData, + string $ip_address, + string $routers, + string $domain_name_server): array { - // TODO: delete section - return false; + $dhcp_cfg = file_get_contents(self::CONF_DEFAULT); + $existing_config = []; + $section_regex = '/^#\sRaspAP\s'.preg_quote($ap_iface, '/').'\s.*?(?=\s*^\s*$)/ms'; + + // extract existing interface configuration + if (preg_match($section_regex, $dhcp_cfg, $matches)) { + $lines = explode(PHP_EOL, $matches[0]); + foreach ($lines as $line) { + $line = trim($line); + if (preg_match('/^(interface|static|metric|nogateway|nohook)/', $line)) { + $existing_config[] = $line; + } + } + } + + // initialize with comment + $config = [ '# RaspAP '.$ap_iface.' configuration' ]; + $config[] = 'interface '.$ap_iface; + $static_settings = [ + 'static ip_address' => $ip_address, + 'static routers' => $routers, + 'static domain_name_server' => $domain_name_server + ]; + + // merge existing settings with updates + foreach ($existing_config as $line) { + $matched = false; + foreach ($static_settings as $key => $value) { + if (strpos($line, $key) === 0) { + $config[] = "$key=$value"; + $matched = true; + unset($static_settings[$key]); + break; + } + } + if (!$matched && !preg_match('/^interface/', $line)) { + $config[] = $line; + } + } + + // add any new static settings + foreach ($static_settings as $key => $value) { + $config[] = "$key=$value"; + } + + // add metric if provided + if (!empty($jsonData['Metric']) && !in_array('metric '.$jsonData['Metric'], $config)) { + $config[] = 'metric '.$jsonData['Metric']; + } + return $config; } /** @@ -61,6 +244,142 @@ class DhcpcdManager } } + /** + * Gets current dhcpcd info for an interface + * + * @param string $iface + * @return array + */ + public function getInterfaceConfig(string $iface): array + { + $result = [ + 'DHCPEnabled' => false, + 'RangeStart' => null, + 'RangeEnd' => null, + 'RangeMask' => null, + 'leaseTime' => null, + 'leaseTimeInterval' => null, + 'dhcpHost' => [], + 'upstreamServersEnabled' => false, + 'upstreamServers' => [], + 'DNS1' => null, + 'DNS2' => null, + 'Metric' => null, + 'StaticIP' => null, + 'SubnetMask' => null, + 'StaticRouters' => null, + 'StaticDNS' => null, + 'FallbackEnabled' => false, + 'DefaultRoute' => false, + 'NoHookWPASupplicant' => false, + ]; + + // dnsmasq + $dnsmasqFile = RASPI_DNSMASQ_PREFIX . $iface . '.conf'; + if (file_exists($dnsmasqFile) && is_readable($dnsmasqFile)) { + $lines = []; + exec('cat ' . escapeshellarg($dnsmasqFile), $lines); + if (!function_exists('ParseConfig')) { + // ensure legacy parser available + require_once RASPI_CONFIG . '/functions.php'; + } + $conf = ParseConfig($lines); + + if (!empty($conf)) { + $result['DHCPEnabled'] = true; + + // dhcp-range may be multi-value + $rangeRaw = $conf['dhcp-range'] ?? null; + if (is_array($rangeRaw)) { + $rangeRaw = $rangeRaw[0] ?? null; + } + if (is_string($rangeRaw)) { + $rangeParts = explode(',', $rangeRaw); + $result['RangeStart'] = $rangeParts[0] ?? null; + $result['RangeEnd'] = $rangeParts[1] ?? null; + $result['RangeMask'] = $rangeParts[2] ?? null; + $leaseSpec = $rangeParts[3] ?? null; + if ($leaseSpec) { + if (preg_match('/^(\d+)([smhd])?$/i', $leaseSpec, $m)) { + $result['leaseTime'] = $m[1]; + $result['leaseTimeInterval'] = $m[2] ?? 'h'; // default to hours if missing + } else { + $result['leaseTime'] = $leaseSpec; + $result['leaseTimeInterval'] = null; + } + } + } + + // dhcp-host entries (array or scalar) + $hosts = $conf['dhcp-host'] ?? []; + if (!is_array($hosts) && $hosts !== null) { + $hosts = [$hosts]; + } + $result['dhcpHost'] = array_values(array_filter($hosts)); + + // upstream DNS servers (server= lines) + $servers = $conf['server'] ?? []; + if (!is_array($servers) && !empty($servers)) { + $servers = [$servers]; + } + $servers = array_filter($servers); + if (!empty($servers)) { + $result['upstreamServersEnabled'] = true; + $result['upstreamServers'] = $servers; + } + + // dhcp-option=6,[,] + if (isset($conf['dhcp-option'])) { + $optsRaw = $conf['dhcp-option']; + // may be multiple dhcp-option lines; coalesce + $optLines = is_array($optsRaw) ? $optsRaw : [$optsRaw]; + foreach ($optLines as $optLine) { + $parts = explode(',', $optLine); + if ($parts[0] === '6') { + $result['DNS1'] = $parts[1] ?? null; + $result['DNS2'] = $parts[2] ?? null; + break; + } + } + } + } + } + + // dhcpcd + if (file_exists(self::CONF_DEFAULT) && is_readable(self::CONF_DEFAULT)) { + $dhcpcd = file_get_contents(self::CONF_DEFAULT); + + // match interface block starting with '# RaspAP configuration' + $sectionPattern = '/^#\sRaspAP\s' . preg_quote($iface, '/') . '\sconfiguration.*?(?=^(?:#\sRaspAP\s|\s*$))/ms'; + if (preg_match($sectionPattern, $dhcpcd, $match)) { + $block = $match[0]; + + $result['Metric'] = $this->matchFirst('/\bmetric\s+(\d+)/i', $block); + $staticIPLine = $this->matchFirst('/static\s+ip_address=([^\r\n]+)/i', $block); + $staticRouters = $this->matchFirst('/static\s+routers=([^\r\n]+)/i', $block); + $staticDNS = $this->matchFirst('/static\s+domain_name_server=([^\r\n]+)/i', $block); + + $result['StaticIP'] = $staticIPLine ? (strpos($staticIPLine,'/') !== false + ? substr($staticIPLine, 0, strpos($staticIPLine,'/')) + : $staticIPLine) : null; + $result['SubnetMask'] = $staticIPLine && function_exists('cidr2mask') && strpos($staticIPLine,'/') + ? cidr2mask($staticIPLine) + : ($result['SubnetMask'] ?? null); + $result['StaticRouters'] = $staticRouters; + $result['StaticDNS'] = $staticDNS; + + $result['FallbackEnabled'] = (bool) preg_match('/fallback\s+static_' . preg_quote($iface, '/') . '/i', $block); + $result['DefaultRoute'] = (bool) preg_match('/\bgateway\b/', $block); + $result['NoHookWPASupplicant'] = (bool) preg_match('/nohook\s+wpa_supplicant/i', $block); + } + } + return $result; + } + + private function matchFirst(string $pattern, string $subject): ?string + { + return preg_match($pattern, $subject, $m) ? trim($m[1]) : null; + } } From 5a57d542c5238c90a639fa4d594d0fc801d95727 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 19 Jul 2025 15:11:37 -0700 Subject: [PATCH 078/122] Normalize state flags, try-catch blocks for method calls, prune legacy dhcpcd routines --- includes/hostapd.php | 284 ++++++++----------------------------------- 1 file changed, 51 insertions(+), 233 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index c2e0d01b..f1aa0f39 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -21,7 +21,7 @@ function DisplayHostAPDConfig() $system = new Sysinfo(); $operatingSystem = $system->operatingSystem(); - $arrConfig = array(); + // set hostapd defaults $arr80211Standard = $hostapd->get80211Standards(); $arrSecurity = $hostapd->getSecurityModes(); $arrEncType = $hostapd->getEncTypes(); @@ -30,8 +30,6 @@ function DisplayHostAPDConfig() $countryCodes = getCountryCodes($languageCode); $reg_domain = $hostapd->getRegDomain(); $interfaces = $hostapd->getInterfaces(); - - // set defaults $arrTxPower = getDefaultNetOpts('txpower','dbm'); $managedModeEnabled = false; @@ -41,18 +39,7 @@ function DisplayHostAPDConfig() $interface = $_SESSION['ap_interface']; } $txpower = $hostapd->getTxPower($interface); - - if (!RASPI_MONITOR_ENABLED) { - if (isset($_POST['SaveHostAPDSettings'])) { - saveHostAPDConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); - } - } - - $arrHostapdConf = []; - $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; - if (file_exists($hostapdIni)) { - $arrHostapdConf = parse_ini_file($hostapdIni); - } + $arrHostapdConf = $hostapd->getHostapdIni(); if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['StartHotspot']) || isset($_POST['RestartHotspot'])) { @@ -73,6 +60,8 @@ function DisplayHostAPDConfig() foreach ($return as $line) { $status->addMessage($line, 'info'); } + } elseif (isset($_POST['SaveHostAPDSettings'])) { + saveHostapdConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); } elseif (isset($_POST['StopHotspot'])) { $status->addMessage('Attempting to stop hotspot', 'info'); exec('sudo /bin/systemctl stop hostapd.service', $return); @@ -111,7 +100,8 @@ function DisplayHostAPDConfig() $arrConfig['disassoc_low_ack_bool'] = isset($arrConfig['disassoc_low_ack']) ? 1 : 0; $hostapdstatus = $system->hostapdStatus(); $serviceStatus = $hostapdstatus[0] == 0 ? "down" : "up"; - + + // ensure log is writeable exec('sudo /bin/chmod o+r '.RASPI_HOSTAPD_LOG); $logdata = getLogLimited(RASPI_HOSTAPD_LOG); @@ -138,7 +128,7 @@ function DisplayHostAPDConfig() } /** - * Validate user input, save configs for hostapd, dnsmasq & dhcp + * Validates user input + saves configs for hostapd, dnsmasq & dhcp * * @param array $wpa_array * @param array $enc_types @@ -148,16 +138,14 @@ function DisplayHostAPDConfig() * @param object $status * @return boolean */ -function saveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) +function saveHostapdConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) { $hostapd = new HostapdManager(); $dnsmasq = new DnsmasqManager(); $dhcpcd = new DhcpcdManager(); + $arrHostapdConf = $hostapd->getHostapdIni(); $dualAPEnable = false; - $hostapdIniPath = RASPI_CONFIG . '/hostapd.ini'; - $arrHostapdConf = file_exists($hostapdIniPath) ? parse_ini_file($hostapdIniPath) : []; - // derive mode states $states = $hostapd->deriveModeStates($_POST, $arrHostapdConf); @@ -178,227 +166,57 @@ function saveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom if ($validated !== false) { try { + // normalize state flags $validated['interface'] = $apIface; - $validated['bridge'] = $states['BridgedEnable'] ? 'br0' : null; + $validated['bridge'] = !empty($states['BridgedEnable']); + $validated['apsta'] = !empty($states['WifiAPEnable']); + $validated['repeater'] = !empty($states['RepeaterEnable']); + $validated['dualmode'] = !empty($states['DualAPEnable']); $validated['txpower'] = $txpower; - // build and save configuration + + // hostapd $config = $hostapd->buildConfig($validated, $status); $hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); $status->addMessage('WiFi hotspot settings saved.', 'success'); - } catch (\RuntimeException $e) { + + // dnsmasq + try { + $syscfg = $dnsmasq->getConfig($ap_iface ?? RASPI_WIFI_AP_INTERFACE); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + + try { + $dnsmasqConfig = $dnsmasq->buildConfig( + $syscfg, + $validated['interface'], + $validated['apsta'], + $validated['bridge'] + ); + $dnsmasq->saveConfig($dnsmasqConfig, $validated['interface']); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + + // dhcpcd + try { + $return = $dhcpcd->buildConfig( + $validated['interface'], + $validated['bridge'], + $validated['repeater'], + $validated['apsta'], + $validated['dualmode'], + $status + ); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + } catch (\Throwable $e) { error_log('Error: ' . $e->getMessage()); + $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); } - } else { - $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); - return false; } - - /// TODO: build out DHCP class - /// finish processing save - /* - if (trim($country_code) != trim($reg_domain)) { - $return = $hostapd->iwRegSet($country_code, $status); - } - // Parse dnsmasq config for selected interface - try { - $syscfg = $dnsmasq->getConfig($ap_iface ?? RASPI_WIFI_AP_INTERFACE); - } catch (\RuntimeException $e) { - error_log('Error: ' . $e->getMessage()); - } - // Build and save dsnmasq config - try { - $config = $dnsmasq->buildConfig($syscfg, $ap_iface, $wifiAPEnable, $bridgedEnable); - $dnsmasq->saveConfig($config, $ap_iface); - } catch (\RuntimeException $e) { - error_log('Error: ' . $e->getMessage()); - } - - // Set dhcp values from system config, fallback to default if undefined - $jsonData = json_decode(getNetConfig($ap_iface), true); - $ip_address = empty($jsonData['StaticIP']) - ? getDefaultNetValue('dhcp', $ap_iface, 'static ip_address') : $jsonData['StaticIP']; - $domain_name_server = empty($jsonData['StaticDNS']) - ? getDefaultNetValue('dhcp', $ap_iface, 'static domain_name_server') : $jsonData['StaticDNS']; - $routers = empty($jsonData['StaticRouters']) - ? getDefaultNetValue('dhcp', $ap_iface, 'static routers') : $jsonData['StaticRouters']; - $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') - ? getDefaultNetValue('dhcp', $ap_iface, 'subnetmask') : $jsonData['SubnetMask']; - if (isset($ip_address) && !preg_match('/.*\/\d+/', $ip_address)) { - $ip_address.='/'.mask2cidr($netmask); - } - $hasDefaults = !( - empty($ip_address) || - empty($domain_name_server) || - empty($routers) || - empty($netmask) || - $netmask === '0.0.0.0' - ); - if (!$hasDefaults) { - $status->addMessage(sprintf(_('Interface %s has no default settings.'), $ap_iface), 'warning'); - $status->addMessage(('Configure settings in DHCP Server before starting AP.'), 'warning'); - } - if ($bridgedEnable == 1) { - $config = array_keys(getDefaultNetOpts('dhcp','options')); - $config[] = PHP_EOL.'# RaspAP br0 configuration'; - $config[] = 'denyinterfaces eth0 wlan0'; - $config[] = 'interface br0'; - $config[] = PHP_EOL; - } elseif ($repeaterEnable == 1) { - $config = [ '# RaspAP '.$ap_iface.' configuration' ]; - $config[] = 'interface '.$ap_iface; - $config[] = 'static ip_address='.$ip_address; - $config[] = 'static routers='.$routers; - $config[] = 'static domain_name_server='.$domain_name_server; - $client_metric = getIfaceMetric($_SESSION['wifi_client_interface']); - if (is_int($client_metric)) { - $ap_metric = (int)$client_metric + 1; - $config[] = 'metric '.$ap_metric; - } else { - $status->addMessage('Unable to obtain metric value for client interface. Repeater mode inactive.', 'warning'); - $repeaterEnable = false; - } - } elseif ($wifiAPEnable == 1) { - $config = array_keys(getDefaultNetOpts('dhcp','options')); - $config[] = PHP_EOL.'# RaspAP uap0 configuration'; - $config[] = 'interface uap0'; - $config[] = 'static ip_address='.$ip_address; - $config[] = 'nohook wpa_supplicant'; - $config[] = PHP_EOL; - } else { - $config = updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain_name_server); - } - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - - if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) { - $skip_dhcp = true; - } elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) { - $dhcp_cfg = join(PHP_EOL, $config); - $status->addMessage(sprintf(_('DHCP configuration for %s enabled.'), $ap_iface), 'success'); - } elseif (!preg_match('/^interface\s'.$ap_iface.'$/m', $dhcp_cfg)) { - $config[] = PHP_EOL; - $config= join(PHP_EOL, $config); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); - $dhcp_cfg .= $config; - } else { - $config = join(PHP_EOL, $config); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); - if (!strpos($dhcp_cfg, 'metric')) { - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1); - } else { - $metrics = true; - } - } - if ($repeaterEnable && $metrics) { - $status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning'); - } else if ($repeaterEnable && !$metrics) { - $status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success'); - $status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success'); - persistDHCPConfig($dhcp_cfg, $ap_iface, $status); - } elseif (!$skip_dhcp) { - persistDHCPConfig($dhcp_cfg, $ap_iface, $status); - } else { - $status->addMessage('WiFi hotspot settings saved.', 'success'); - } - */ return true; } -/** - * Persists a DHCP configuration - * - * @param string $dhcp_cfg - * @param string $ap_iface - * @param object $status - * @return $status - */ -function persistDHCPConfig($dhcp_cfg, $ap_iface, $status) -{ - file_put_contents("/tmp/dhcpddata", $dhcp_cfg); - system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return); - if ($return == 0) { - $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success'); - $status->addMessage('WiFi hotspot settings saved.', 'success'); - } else { - $status->addMessage('Unable to save WiFi hotspot settings.', 'danger'); - } - return $status; -} - -/** - * Returns a count of hostapd-.conf files - * - * @return int - */ -function countHostapdConfigs(): int -{ - $configs = glob('/etc/hostapd/hostapd-*.conf'); - return is_array($configs) ? count($configs) : 0; -} - -/** - * Updates the dhcpcd configuration for a given interface, preserving existing settings - * - * @param string $ap_iface - * @param array $jsonData - * @param string $ip_address - * @param string $routers - * @param string $domain_name_server - * @return array updated configuration - */ -function updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain_name_server) { - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - $existing_config = []; - $section_regex = '/^#\sRaspAP\s'.preg_quote($ap_iface, '/').'\s.*?(?=\s*^\s*$)/ms'; - - // extract existing interface configuration - if (preg_match($section_regex, $dhcp_cfg, $matches)) { - $lines = explode(PHP_EOL, $matches[0]); - foreach ($lines as $line) { - $line = trim($line); - if (preg_match('/^(interface|static|metric|nogateway|nohook)/', $line)) { - $existing_config[] = $line; - } - } - } - - // initialize with comment - $config = [ '# RaspAP '.$ap_iface.' configuration' ]; - $config[] = 'interface '.$ap_iface; - $static_settings = [ - 'static ip_address' => $ip_address, - 'static routers' => $routers, - 'static domain_name_server' => $domain_name_server - ]; - - // merge existing settings with updates - foreach ($existing_config as $line) { - $matched = false; - foreach ($static_settings as $key => $value) { - if (strpos($line, $key) === 0) { - $config[] = "$key=$value"; - $matched = true; - unset($static_settings[$key]); - break; - } - } - if (!$matched && !preg_match('/^interface/', $line)) { - $config[] = $line; - } - } - - // add any new static settings - foreach ($static_settings as $key => $value) { - $config[] = "$key=$value"; - } - - // add metric if provided - if (!empty($jsonData['Metric']) && !in_array('metric '.$jsonData['Metric'], $config)) { - $config[] = 'metric '.$jsonData['Metric']; - } - - return $config; -} - From 5f4469ab32102469378b6203fde586d61048e6d2 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 01:49:31 -0700 Subject: [PATCH 079/122] Update legacy handler w/ dhcpcdManager->getInterfaceConfig() --- ajax/networking/get_netcfg.php | 61 ++++------------------------------ 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/ajax/networking/get_netcfg.php b/ajax/networking/get_netcfg.php index 87411ba7..1520173d 100644 --- a/ajax/networking/get_netcfg.php +++ b/ajax/networking/get_netcfg.php @@ -1,4 +1,7 @@ 1) { - $dhcpdata['DNS1'] = $arrDns[1] ?? null; - } - if (count($arrDns) > 2) { - $dhcpdata['DNS2'] = $arrDns[2] ?? null; - } - } - } - - // fetch dhcpcd.conf settings for interface - $conf = file_get_contents(RASPI_DHCPCD_CONFIG); - preg_match('/^#\sRaspAP\s'.$interface.'\s.*?(?=\s*+$)/ms', $conf, $matched); - preg_match('/metric\s(\d*)/', $matched[0], $metric); - preg_match('/static\sip_address=(.*)/', $matched[0], $static_ip); - preg_match('/static\srouters=(.*)/', $matched[0], $static_routers); - preg_match('/static\sdomain_name_server=(.*)/', $matched[0], $static_dns); - preg_match('/fallback\sstatic_'.$interface.'/', $matched[0], $fallback); - preg_match('/(?:no)?gateway/', $matched[0], $gateway); - preg_match('/nohook\swpa_supplicant/', $matched[0], $nohook_wpa_supplicant); - $dhcpdata['Metric'] = $metric[1] ?? null; - $dhcpdata['StaticIP'] = isset($static_ip[1]) && strpos($static_ip[1], '/') !== false - ? substr($static_ip[1], 0, strpos($static_ip[1], '/')) - : ($static_ip[1] ?? ''); - $dhcpdata['SubnetMask'] = cidr2mask($static_ip[1] ?? ''); - $dhcpdata['StaticRouters'] = $static_routers[1] ?? null; - $dhcpdata['StaticDNS'] = $static_dns[1] ?? null; - $dhcpdata['FallbackEnabled'] = empty($fallback) ? false: true; - $dhcpdata['DefaultRoute'] = $gateway[0] == "gateway"; - $dhcpdata['NoHookWPASupplicant'] = ($nohook_wpa_supplicant[0] ?? '') == "nohook wpa_supplicant"; + $dhcpdata = $dhcpcdManager->getInterfaceConfig($interface); echo json_encode($dhcpdata); } From 636e04fa7833c12bbaf6c2fc786ce553f2c8a97a Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 02:05:05 -0700 Subject: [PATCH 080/122] Migrate non-hostapd methods to HotspotService class --- includes/hostapd.php | 119 +++++-------------------------------------- 1 file changed, 13 insertions(+), 106 deletions(-) diff --git a/includes/hostapd.php b/includes/hostapd.php index f1aa0f39..45004498 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -1,8 +1,7 @@ getWifiInterface(); function DisplayHostAPDConfig() { $hostapd = new HostapdManager(); + $hotspot = new HotspotService(); $status = new StatusMessage(); $system = new Sysinfo(); $operatingSystem = $system->operatingSystem(); // set hostapd defaults - $arr80211Standard = $hostapd->get80211Standards(); - $arrSecurity = $hostapd->getSecurityModes(); - $arrEncType = $hostapd->getEncTypes(); - $arr80211w = $hostapd->get80211wOptions(); + $arr80211Standard = $hotspot->get80211Standards(); + $arrSecurity = $hotspot->getSecurityModes(); + $arrEncType = $hotspot->getEncTypes(); + $arr80211w = $hotspot->get80211wOptions(); $languageCode = strtok($_SESSION['locale'], '_'); $countryCodes = getCountryCodes($languageCode); - $reg_domain = $hostapd->getRegDomain(); - $interfaces = $hostapd->getInterfaces(); + $reg_domain = $hotspot->getRegDomain(); + $interfaces = $hotspot->getInterfaces(); $arrTxPower = getDefaultNetOpts('txpower','dbm'); $managedModeEnabled = false; @@ -38,8 +38,8 @@ function DisplayHostAPDConfig() } else { $interface = $_SESSION['ap_interface']; } - $txpower = $hostapd->getTxPower($interface); - $arrHostapdConf = $hostapd->getHostapdIni(); + $txpower = $hotspot->getTxPower($interface); + $arrHostapdConf = $hotspot->getHostapdIni(); if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['StartHotspot']) || isset($_POST['RestartHotspot'])) { @@ -61,7 +61,7 @@ function DisplayHostAPDConfig() $status->addMessage($line, 'info'); } } elseif (isset($_POST['SaveHostAPDSettings'])) { - saveHostapdConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); + $hotspot->saveSettings($_POST, $arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); } elseif (isset($_POST['StopHotspot'])) { $status->addMessage('Attempting to stop hotspot', 'info'); exec('sudo /bin/systemctl stop hostapd.service', $return); @@ -82,9 +82,9 @@ function DisplayHostAPDConfig() if (isset($_POST['txpower'])) { if ($_POST['txpower'] != 'auto') { $txpower = intval($_POST['txpower']); - $hostapd->maybeSetTxPower($interface, $txpower, $status); + $hotspot->maybeSetTxPower($interface, $txpower, $status); } elseif ($_POST['txpower'] == 'auto') { - $hostapd->maybeSetTxPower($interface, 'auto', $status); + $hotspot->maybeSetTxPower($interface, 'auto', $status); } $txpower = $_POST['txpower']; } @@ -127,96 +127,3 @@ function DisplayHostAPDConfig() ); } -/** - * Validates user input + saves configs for hostapd, dnsmasq & dhcp - * - * @param array $wpa_array - * @param array $enc_types - * @param array $modes - * @param string $interface - * @param string $reg_domain - * @param object $status - * @return boolean - */ -function saveHostapdConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status) -{ - $hostapd = new HostapdManager(); - $dnsmasq = new DnsmasqManager(); - $dhcpcd = new DhcpcdManager(); - $arrHostapdConf = $hostapd->getHostapdIni(); - $dualAPEnable = false; - - // derive mode states - $states = $hostapd->deriveModeStates($_POST, $arrHostapdConf); - - // determine base interface (validated or fallback) - $baseIface = validateInterface($_POST['interface']) ? $_POST['interface'] : RASPI_WIFI_AP_INTERFACE; - - // derive interface roles - [$apIface, $cliIface, $sessionIface] = $hostapd->deriveInterfaces($baseIface, $states); - - // persist hostapd.ini - $hostapd->persistHostapdIni($states, $apIface, $cliIface, $arrHostapdConf); - - // store session (compatibility) - $_SESSION['ap_interface'] = $sessionIface; - - // validate config from $_POST - $validated = $hostapd->validate($_POST, $wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status); - - if ($validated !== false) { - try { - // normalize state flags - $validated['interface'] = $apIface; - $validated['bridge'] = !empty($states['BridgedEnable']); - $validated['apsta'] = !empty($states['WifiAPEnable']); - $validated['repeater'] = !empty($states['RepeaterEnable']); - $validated['dualmode'] = !empty($states['DualAPEnable']); - $validated['txpower'] = $txpower; - - // hostapd - $config = $hostapd->buildConfig($validated, $status); - $hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); - $status->addMessage('WiFi hotspot settings saved.', 'success'); - - // dnsmasq - try { - $syscfg = $dnsmasq->getConfig($ap_iface ?? RASPI_WIFI_AP_INTERFACE); - } catch (\RuntimeException $e) { - error_log('Error: ' . $e->getMessage()); - } - - try { - $dnsmasqConfig = $dnsmasq->buildConfig( - $syscfg, - $validated['interface'], - $validated['apsta'], - $validated['bridge'] - ); - $dnsmasq->saveConfig($dnsmasqConfig, $validated['interface']); - } catch (\RuntimeException $e) { - error_log('Error: ' . $e->getMessage()); - } - - // dhcpcd - try { - $return = $dhcpcd->buildConfig( - $validated['interface'], - $validated['bridge'], - $validated['repeater'], - $validated['apsta'], - $validated['dualmode'], - $status - ); - } catch (\RuntimeException $e) { - error_log('Error: ' . $e->getMessage()); - } - } catch (\Throwable $e) { - error_log('Error: ' . $e->getMessage()); - $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); - } - } - - return true; -} - From cf32a4ba01a2af33eee81c293124d20b2a617e2d Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 02:06:40 -0700 Subject: [PATCH 081/122] Implement saveSettings(), consolidate hotspot functions + static methods --- .../Networking/Hotspot/HotspotService.php | 322 ++++++++++++++++-- 1 file changed, 303 insertions(+), 19 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HotspotService.php b/src/RaspAP/Networking/Hotspot/HotspotService.php index cc331f2e..0b02af17 100644 --- a/src/RaspAP/Networking/Hotspot/HotspotService.php +++ b/src/RaspAP/Networking/Hotspot/HotspotService.php @@ -1,40 +1,324 @@ + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +declare(strict_types=1); + namespace RaspAP\Networking\Hotspot; use RaspAP\Networking\Hotspot\Validators\HostapdValidator; +use RaspAP\Messages\StatusMessage; + -/** - * Coordinates hotspot configuration and lifecycle - * - * Handles: - * - Hostapd & dnsmasq config updates - * - dhcpcd interface adjustments - * - Service control (start/stop/restart) - */ class HotspotService { - protected HostapdManager $hostapdManager; - protected DnsmasqManager $dnsmasqManager; - protected DhcpcdManager $dhcpcdManager; + protected HostapdManager $hostapd; + protected DnsmasqManager $dnsmasq; + protected DhcpcdManager $dhcpcd; + + // IEEE 802.11 standards + private const IEEE_80211_STANDARD = [ + 'a' => '802.11a - 5 GHz', + 'b' => '802.11b - 2.4 GHz', + 'g' => '802.11g - 2.4 GHz', + 'n' => '802.11n - 2.4/5 GHz', + 'ac' => '802.11ac - 5 GHz' + ]; + + // encryption types + private const ENC_TYPES = [ + 'TKIP' => 'TKIP', + 'CCMP' => 'CCMP', + 'TKIP CCMP' => 'TKIP+CCMP' + ]; + public function __construct() { - $this->hostapdManager = new HostapdManager(); - $this->dnsmasqManager = new DnsmasqManager(); - $this->dhcpcdManager = new DhcpcdManager(); + $this->hostapd = new HostapdManager(); + $this->dnsmasq = new DnsmasqManager(); + $this->dhcpcd = new DhcpcdManager(); } /** - * Apply configuration changes for hotspot. + * Returns IEEE 802.11 standards + */ + public static function get80211Standards(): array + { + return self::IEEE_80211_STANDARD; + } + + /** + * Returns encryption types + */ + public static function getEncTypes(): array + { + return self::ENC_TYPES; + } + + /** + * Returns translated security modes. + */ + public static function getSecurityModes(): array + { + // Build each call to ensure translation occurs under current locale. + return [ + 1 => 'WPA', + 2 => 'WPA2', + 3 => _('WPA and WPA2'), + 4 => _('WPA2 and WPA3-Personal (transitional mode)'), + 5 => 'WPA3-Personal (required)', + 'none' => _('None'), + ]; + } + + /** + * Returns translated 802.11w options + */ + public static function get80211wOptions(): array + { + return [ + 3 => _('Disabled'), + 1 => _('Enabled (for supported clients)'), + 2 => _('Required (for supported clients)'), + ]; + } + + + /** + * Validates user input + saves configs for hostapd, dnsmasq & dhcp * - * @param array $params + * @param array $wpa_array + * @param array $enc_types + * @param array $modes + * @param array $interfaces + * @param string $reg_domain + * @param StatusMessage $status * @return bool */ - public function configureHotspot(array $params): bool + public function saveSettings( + array $post_data, + array $wpa_array, + array $enc_types, + array $modes, + array $interfaces, + string $reg_domain, + StatusMessage $status): bool { - // TODO: validate params, orchestrate managers - return false; + $arrHostapdConf = $this->getHostapdIni(); + $dualAPEnable = false; + + // derive mode states + $states = $this->hostapd->deriveModeStates($post_data, $arrHostapdConf); + + // determine base interface (validated or fallback) + $baseIface = validateInterface($post_data['interface']) ? $post_data['interface'] : RASPI_WIFI_AP_INTERFACE; + + // derive interface roles + [$apIface, $cliIface, $sessionIface] = $this->hostapd->deriveInterfaces($baseIface, $states); + + // persist hostapd.ini + $this->hostapd->persistHostapdIni($states, $apIface, $cliIface, $arrHostapdConf); + + // store session (compatibility) + $_SESSION['ap_interface'] = $sessionIface; + + // validate config from post data + $validated = $this->hostapd->validate($post_data, $wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status); + + if ($validated !== false) { + try { + // normalize state flags + $validated['interface'] = $apIface; + $validated['bridge'] = !empty($states['BridgedEnable']); + $validated['apsta'] = !empty($states['WifiAPEnable']); + $validated['repeater'] = !empty($states['RepeaterEnable']); + $validated['dualmode'] = !empty($states['DualAPEnable']); + $validated['txpower'] = $post_data['txpower']; + + // hostapd + $config = $this->hostapd->buildConfig($validated, $status); + $this->hostapd->saveConfig($config, $dualAPEnable, $validated['interface']); + $this->maybeSetRegDomain($post_data['country_code'], $status); + + $status->addMessage('WiFi hotspot settings saved.', 'success'); + + // dnsmasq + try { + $syscfg = $this->dnsmasq->getConfig($validated['interface'] ?? RASPI_WIFI_AP_INTERFACE); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + + try { + $dnsmasqConfig = $this->dnsmasq->buildConfig( + $syscfg, + $validated['interface'], + $validated['apsta'], + $validated['bridge'] + ); + $this->dnsmasq->saveConfig($dnsmasqConfig, $validated['interface']); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + + // dhcpcd + try { + $return = $this->dhcpcd->buildConfig( + $validated['interface'], + $validated['bridge'], + $validated['repeater'], + $validated['apsta'], + $validated['dualmode'], + $status + ); + } catch (\RuntimeException $e) { + error_log('Error: ' . $e->getMessage()); + } + } catch (\Throwable $e) { + error_log(sprintf( + "Error: %s in %s on line %d\nStack trace:\n%s", + $e->getMessage(), + $e->getFile(), + $e->getLine(), + $e->getTraceAsString() + )); + $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); + } + } + + return true; + } + + /** + * Gets system hostapd.ini + * + * @return array $config + */ + public function getHostapdIni() + { + $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; + if (file_exists($hostapdIni)) { + $config = parse_ini_file($hostapdIni); + return $config; + } + } + + /** + * Sets transmit power for an interface + * + * @param string $iface + * @param int|string $dbm + * @param StatusMessage $status + * @return bool + */ + public function maybeSetTxPower(string $iface, $dbm, StatusMessage $status): bool + { + $currentTxPower = $this->getTxPower($iface); + + if ($currentTxPower === $dbm) { + return true; + } + + if ($dbm === 'auto') { + exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower auto', $return); + $status->addMessage('Setting transmit power to auto.', 'success'); + } else { + $sdBm = (int)$dbm * 100; + exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower fixed ' . $sdBm, $return); + $status->addMessage('Setting transmit power to ' . $dbm . ' dBm.', 'success'); + } + return true; + } + + /** + * Gets transmit power for an interface + * + * @param string $iface + * @return int + */ + public function getTxPower(string $iface): int + { + $cmd = "iw dev ".escapeshellarg($iface)." info | awk '$1==\"txpower\" {print $2}'"; + exec($cmd, $txpower); + return intval($txpower[0]); + } + + /** + * Sets a new regulatory domain if value has changed + * + * @param string $countryCode + * @return bool + */ + public function maybeSetRegDomain($countryCode, StatusMessage $status): bool + { + $currentDomain = $this->getRegDomain(); + if (trim($countryCode) !== trim($currentDomain)) { + $result = $this->setRegDomain($countryCode, $status); + if ($result !== true) { + return false; + } + } + return true; + } + + /** + * Gets the current regulatory domain + * + * @return string + */ + public function getRegDomain(): string + { + $domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); + return $domain; + } + + /** + * Sets the specified wireless regulatory domain + * + * @param string $country_code ISO 2-letter country code + * @param object $status StatusMessage object + * @return boolean $result + */ + public function setRegDomain(string $country_code, StatusMessage $status): bool + { + $country_code = escapeshellarg($country_code); + exec("sudo iw reg set $country_code", $output, $result); + if ($result !== 0) { + return false; + } else { + return true; + } + } + + /** + * Enumerates available network interfaces + * + * @return array $interfaces + */ + public function getInterfaces(): array + { + exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); + + // filter out loopback, docker, bridges + other virtual interfaces + // that are incapable of hosting an AP + $interfaces = array_filter($interfaces, function ($iface) { + return !preg_match('/^(lo|docker|br-|veth|tun|tap|tailscale)/', $iface); + }); + sort($interfaces); + + return array_values($interfaces); } /** From cbc6ee74c38b63e9c366d975140ef1c846e12c40 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 02:07:53 -0700 Subject: [PATCH 082/122] Consolidate non-hostapd concerns under HotspotService class --- .../Networking/Hotspot/HostapdManager.php | 212 ++---------------- 1 file changed, 15 insertions(+), 197 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index cbd9491b..593e93de 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -1,39 +1,26 @@ + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +declare(strict_types=1); + namespace RaspAP\Networking\Hotspot; use RaspAP\Networking\Hotspot\Validators\HostapdValidator; use RaspAP\Messages\StatusMessage; -/** - * Hostapd manager class for RaspAP - * - * @description Manages hostapd configurations and runtime settings - * @author Bill Zimmerman - * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE - */ class HostapdManager { private const CONF_DEFAULT = RASPI_HOSTAPD_CONFIG; private const CONF_PATH_PREFIX = '/etc/hostapd/hostapd-'; private const CONF_TMP = '/tmp/hostapddata'; - // IEEE 802.11 standards - private const IEEE_80211_STANDARD = [ - 'a' => '802.11a - 5 GHz', - 'b' => '802.11b - 2.4 GHz', - 'g' => '802.11g - 2.4 GHz', - 'n' => '802.11n - 2.4/5 GHz', - 'ac' => '802.11ac - 5 GHz' - ]; - - // encryption types - private const ENC_TYPES = [ - 'TKIP' => 'TKIP', - 'CCMP' => 'CCMP', - 'TKIP CCMP' => 'TKIP+CCMP' - ]; - /** @var HostapdValidator */ private $validator; @@ -42,50 +29,6 @@ class HostapdManager $this->validator = $validator ?: new HostapdValidator(); } - /** - * Returns IEEE 802.11 standards - */ - public static function get80211Standards(): array - { - return self::IEEE_80211_STANDARD; - } - - /** - * Returns encryption types - */ - public static function getEncTypes(): array - { - return self::ENC_TYPES; - } - - /** - * Returns translated security modes. - */ - public static function getSecurityModes(): array - { - // Build each call to ensure translation occurs under current locale. - return [ - 1 => 'WPA', - 2 => 'WPA2', - 3 => _('WPA and WPA2'), - 4 => _('WPA2 and WPA3-Personal (transitional mode)'), - 5 => 'WPA3-Personal (required)', - 'none' => _('None'), - ]; - } - - /** - * Returns translated 802.11w options - */ - public static function get80211wOptions(): array - { - return [ - 3 => _('Disabled'), - 1 => _('Enabled (for supported clients)'), - 2 => _('Required (for supported clients)'), - ]; - } - /** * Retrieves current hostapd config * @@ -141,7 +84,6 @@ class HostapdManager $config['wep_default_key'] ??= 0; return $config; - } /** @@ -288,8 +230,6 @@ class HostapdManager $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; } - $result = $this->maybeSetRegDomain($params['country_code'], $status); - // optional additional user config $config[] = $this->parseUserHostapdCfg(); @@ -469,20 +409,6 @@ class HostapdManager throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback)."); } - /** - * Gets system hostapd.ini - * - * @return array $config - */ - public function getHostapdIni() - { - $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; - if (file_exists($hostapdIni)) { - $config = parse_ini_file($hostapdIni); - return $config; - } - } - /** * Persist hostapd.ini with mode / interface user settings * @@ -499,11 +425,11 @@ class HostapdManager // compose new ini payload $cfg = [ 'WifiInterface' => $apIface, - 'LogEnable' => $states['LogEnable'], - 'WifiAPEnable' => $states['WifiAPEnable'], - 'BridgedEnable' => $states['BridgedEnable'], - 'RepeaterEnable' => $states['RepeaterEnable'], - 'DualAPEnable' => $states['DualAPEnable'], + 'LogEnable' => $states['LogEnable'] ?? false, + 'WifiAPEnable' => $states['WifiAPEnable'] ?? false, + 'BridgedEnable' => $states['BridgedEnable'] ?? false, + 'RepeaterEnable' => $states['RepeaterEnable'] ?? false, + 'DualAPEnable' => $states['DualAPEnable'] ?? false, 'WifiManaged' => $cliIface ]; foreach ($previousIni as $k => $v) { @@ -525,114 +451,6 @@ class HostapdManager exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script); } - /** - * Sets transmit power for an interface - * - * @param string $iface - * @param int|string $dbm - * @param StatusMessage $status - * @return bool - */ - public function maybeSetTxPower(string $iface, $dbm, StatusMessage $status): bool - { - $currentTxPower = $this->getTxPower($iface); - - if ($currentTxPower === $dbm) { - return true; - } - - if ($dbm === 'auto') { - exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower auto', $return); - $status->addMessage('Setting transmit power to auto.', 'success'); - } else { - $sdBm = (int)$dbm * 100; - exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower fixed ' . $sdBm, $return); - $status->addMessage('Setting transmit power to ' . $dbm . ' dBm.', 'success'); - } - return true; - } - - /** - * Gets transmit power for an interface - * - * @param string $iface - * @return string - */ - public function getTxPower(string $iface): string - { - $cmd = "iw dev ".escapeshellarg($iface)." info | awk '$1==\"txpower\" {print $2}'"; - exec($cmd, $txpower); - return intval($txpower[0]); - } - - /** - * Sets a new regulatory domain if value has changed - * - * @param string $countryCode - * @return bool - */ - public function maybeSetRegDomain($countryCode, StatusMessage $status): bool - { - $currentDomain = $this->getRegDomain(); - if (trim($countryCode) !== trim($currentDomain)) { - $result = $this->setRegDomain($countryCode, $status); - if ($result !== true) { - return false; - } - } - return true; - } - - /** - * Gets the current regulatory domain - * - * @return string - */ - public function getRegDomain(): string - { - $domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); - return $domain; - } - - /** - * Sets the specified wireless regulatory domain - * - * @param string $country_code ISO 2-letter country code - * @param object $status StatusMessage object - * @return boolean $result - */ - public function setRegDomain(string $country_code, StatusMessage $status): bool - { - $country_code = escapeshellarg($country_code); - exec("sudo iw reg set $country_code", $output, $result); - if ($result !== 0) { - $status->addMessage(sprintf(_('Unable to set wireless regulatory domain to %s'), $country_code, 'warning')); - return false; - } else { - $status->addMessage(sprintf(_('Setting wireless regulatory domain to %s'), $country_code, 'success')); - return true; - } - } - - /** - * Enumerates available network interfaces - * - * @return array $interfaces - */ - public function getInterfaces(): array - { - exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); - - // filter out loopback, docker, bridges + other virtual interfaces - // that are incapable of hosting an AP - $interfaces = array_filter($interfaces, function ($iface) { - return !preg_match('/^(lo|docker|br-|veth|tun|tap|tailscale)/', $iface); - }); - sort($interfaces); - - return array_values($interfaces); - } - /** * Returns a count of hostapd-.conf files * From 9df3baa5f1fc7c14bc6a17b55c32042d0c7becc8 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 02:08:33 -0700 Subject: [PATCH 083/122] Minor: standardize comment header, declare strict_types=1 --- src/RaspAP/Networking/Hotspot/DhcpcdManager.php | 17 ++++++++++++----- .../Networking/Hotspot/DnsmasqManager.php | 7 ++++--- .../Hotspot/Validators/HostapdValidator.php | 11 +++++++---- src/RaspAP/Networking/Hotspot/WiFiManager.php | 10 ++++++---- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php index 9e397112..50fc9433 100644 --- a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -1,12 +1,19 @@ + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +declare(strict_types=1); + namespace RaspAP\Networking\Hotspot; use RaspAP\Messages\StatusMessage; -/** - * Handles dhcpcd.conf interface configuration. - */ class DhcpcdManager { private const CONF_DEFAULT = RASPI_DHCPCD_CONFIG; @@ -99,6 +106,7 @@ class DhcpcdManager ); } $dhcp_cfg = file_get_contents(SELF::CONF_DEFAULT); + $skip_dhcp = false; if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) { $skip_dhcp = true; @@ -280,8 +288,7 @@ class DhcpcdManager $lines = []; exec('cat ' . escapeshellarg($dnsmasqFile), $lines); if (!function_exists('ParseConfig')) { - // ensure legacy parser available - require_once RASPI_CONFIG . '/functions.php'; + require_once 'includes/functions.php'; } $conf = ParseConfig($lines); diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 94a45803..0b4cd81b 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -1,7 +1,5 @@ * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE */ + +declare(strict_types=1); + +namespace RaspAP\Networking\Hotspot\Validators; + +use RaspAP\Messages\StatusMessage; + class HostapdValidator { diff --git a/src/RaspAP/Networking/Hotspot/WiFiManager.php b/src/RaspAP/Networking/Hotspot/WiFiManager.php index 4e2e4a9f..bc07fe1d 100644 --- a/src/RaspAP/Networking/Hotspot/WiFiManager.php +++ b/src/RaspAP/Networking/Hotspot/WiFiManager.php @@ -1,14 +1,16 @@ * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE */ +declare(strict_types=1); + +namespace RaspAP\Networking\Hotspot; + class WiFiManager { From ed1938d10b977e4b5238f235ee4aab7537b53b8a Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 06:35:19 -0700 Subject: [PATCH 084/122] Migrate validate, remove + removeIface methods from global function defs --- .../Networking/Hotspot/DhcpcdManager.php | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php index 50fc9433..19dd5678 100644 --- a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -116,13 +116,13 @@ class DhcpcdManager } elseif (!preg_match('/^interface\s'.$ap_iface.'$/m', $dhcp_cfg)) { $config[] = PHP_EOL; $config= join(PHP_EOL, $config); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); + $dhcp_cfg = $this->removeIface($dhcp_cfg,'br0'); + $dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0'); $dhcp_cfg .= $config; } else { $config = join(PHP_EOL, $config); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); - $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); + $dhcp_cfg = $this->removeIface($dhcp_cfg,'br0'); + $dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0'); if (!strpos($dhcp_cfg, 'metric')) { $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1); } else { @@ -143,6 +143,51 @@ class DhcpcdManager return true; } + /** + * Validates DHCP user input from $_POST data + * + * @param array $post_data + * @return array $errors + */ + public function validate(array $post_data): array + { + $errors = []; + define('IFNAMSIZ', 16); + $iface = $post_data['interface']; + if (!preg_match('/^[^\s\/\\0]+$/', $iface) + || strlen($iface) >= IFNAMSIZ + ) { + $errors[] = _('Invalid interface name.'); + } + if (!filter_var($post_data['StaticIP'], FILTER_VALIDATE_IP) && !empty($post_data['StaticIP'])) { + $errors[] = _('Invalid static IP address.'); + } + if (!filter_var($post_data['SubnetMask'], FILTER_VALIDATE_IP) && !empty($post_data['SubnetMask'])) { + $errors[] = _('Invalid subnet mask.'); + } + if (!filter_var($post_data['DefaultGateway'], FILTER_VALIDATE_IP) && !empty($post_data['DefaultGateway'])) { + $errors[] = _('Invalid default gateway.'); + } + if (($post_data['dhcp-iface'] == "1")) { + if (!filter_var($post_data['RangeStart'], FILTER_VALIDATE_IP) && !empty($post_data['RangeStart'])) { + $errors[] = _('Invalid DHCP range start.'); + } + if (!filter_var($post_data['RangeEnd'], FILTER_VALIDATE_IP) && !empty($post_data['RangeEnd'])) { + $errors[] = _('Invalid DHCP range end.'); + } + if (!ctype_digit($post_data['RangeLeaseTime']) && $post_data['RangeLeaseTimeUnits'] !== 'i') { + $errors[] = _('Invalid DHCP lease time, not a number.'); + } + if (!in_array($post_data['RangeLeaseTimeUnits'], array('m', 'h', 'd', 'i'))) { + $errors[] = _('Unknown DHCP lease time unit.'); + } + if ($post_data['Metric'] !== '' && !ctype_digit($post_data['Metric'])) { + $errors[] = _('Invalid metric value, not a number.'); + } + } + return $errors; + } + /** * Saves a dhcpcd configuration * @@ -166,6 +211,49 @@ class DhcpcdManager return true; } + /** + * Removes a dhcp configuration block for the specified interface + * + * @param string $iface + * @param StatusMessage $status + * @return bool $result + */ + public function remove(string $iface, StatusMessage $status): bool + { + $configFile = SELF::CONF_DEFAULT; + $tempFile = SELF::CONF_TMP; + + $dhcp_cfg = file_get_contents($configFile); + $modified_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1); + if ($modified_cfg !== $dhcp_cfg) { + file_put_contents($tempFile, $modified_cfg); + + $cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)); + exec($cmd, $output, $result); + + if ($result == 0) { + $status->addMessage('DHCP configuration for '.$iface.' removed', 'success'); + return true; + } else { + $status->addMessage('Failed to remove DHCP configuration for '.$iface, 'danger'); + return false; + } + } + } + + /** + * Removes a dhcp configuration block for the specified interface + * + * @param string $dhcp_cfg + * @param string $iface + * @return string $dhcp_cfg + */ + public function removeIface(string $dhcp_cfg, string $iface): string + { + $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1); + return $dhcp_cfg; + } + /** * Updates the dhcpcd configuration for a given interface, preserving existing settings * From 8f19d759f28e52e2eeb0826b822e7b877171434f Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 06:36:43 -0700 Subject: [PATCH 085/122] Consolidate functions in their respective dhcpcd + dnsmasq classes --- includes/functions.php | 71 ------------------------------------------ 1 file changed, 71 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 9541e307..8b388cf4 100755 --- a/includes/functions.php +++ b/includes/functions.php @@ -64,77 +64,6 @@ function cidr2mask($cidr) return $netmask; } -/** - * Removes a dhcp configuration block for the specified interface - * - * @param string $iface - * @param object $status - * @return boolean $result - */ -function removeDHCPConfig($iface,$status) -{ - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1); - file_put_contents("/tmp/dhcpddata", $dhcp_cfg); - system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $result); - if ($result == 0) { - $status->addMessage('DHCP configuration for '.$iface.' removed.', 'success'); - } else { - $status->addMessage('Failed to remove DHCP configuration for '.$iface.'.', 'danger'); - return $result; - } -} - -/** - * Removes a dhcp configuration block for the specified interface - * - * @param string $dhcp_cfg - * @param string $iface - * @return string $dhcp_cfg - */ -function removeDHCPIface($dhcp_cfg,$iface) -{ - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1); - return $dhcp_cfg; -} - -/** - * Removes a dnsmasq configuration block for the specified interface - * - * @param string $iface - * @param object $status - * @return boolean $result - */ -function removeDnsmasqConfig($iface,$status) -{ - system('sudo rm '.RASPI_DNSMASQ_PREFIX.$iface.'.conf', $result); - if ($result == 0) { - $status->addMessage('Dnsmasq configuration for '.$iface.' removed.', 'success'); - } else { - $status->addMessage('Failed to remove dnsmasq configuration for '.$iface.'.', 'danger'); - } - return $result; -} - -/** - * Scans dnsmasq configuration dir for the specified interface - * Non-matching configs are removed, optional adblock.conf is protected - * - * @param string $dir_conf - * @param string $interface - * @param object $status - */ -function scanConfigDir($dir_conf,$interface,$status) -{ - $syscnf = preg_grep('~\.(conf)$~', scandir($dir_conf)); - foreach ($syscnf as $cnf) { - if ($cnf !== '090_adblock.conf' && !preg_match('/.*_'.$interface.'.conf/', $cnf)) { - system('sudo rm /etc/dnsmasq.d/'.$cnf, $result); - } - } - return $status; -} - /** * Returns a default (fallback) value for the selected service, interface & setting * from /etc/raspap/networking/defaults.json From 1e2f77abcbde2bb9f3e313d530ad857773bd7d1d Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 06:38:33 -0700 Subject: [PATCH 086/122] Added buildEx, buildDefault, saveConfigDefault, remove, scanConfigDir methods --- .../Networking/Hotspot/DnsmasqManager.php | 203 +++++++++++++++++- 1 file changed, 197 insertions(+), 6 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 0b4cd81b..1fb1165c 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -12,10 +12,14 @@ declare(strict_types=1); namespace RaspAP\Networking\Hotspot; +use RaspAP\Messages\StatusMessage; + class DnsmasqManager { + private const CONF_DEFAULT = '/etc/dnsmasq.d/'; private const CONF_SUFFIX = '.conf'; private const CONF_TMP = '/tmp/dnsmasqdata'; + private const CONF_RASPAP = '090_raspap'; /** * Retrieves dnsmasq configuration for an interface @@ -74,7 +78,7 @@ class DnsmasqManager $config[] = 'dhcp-option='.$syscfg['dhcp-option']; } $config[] = PHP_EOL; - scanConfigDir('/etc/dnsmasq.d/','uap0',$status); + $this->scanConfigDir(SELF::CONF_DEFAULT,'uap0',$status); } elseif ($bridgedEnable !==1) { $dhcp_range = ($syscfg['dhcp-range'] =='') ? getDefaultNetValue('dnsmasq',$iface,'dhcp-range') : $syscfg['dhcp-range']; $config = [ '# RaspAP '.$_POST['interface'].' configuration' ]; @@ -105,24 +109,105 @@ class DnsmasqManager return $config; } + /** + * Builds an extended dnsmasq configuration + * + * @param string $iface + * @param array $post_data + * @return string $config //todo: standardize return type as array + */ + public function buildEx(string $iface, array $post_data): string + { + $config = '# RaspAP '. $iface .' configuration'.PHP_EOL; + $config .= 'interface='. $iface . PHP_EOL .'dhcp-range='.$post_data['RangeStart'].','.$post_data['RangeEnd'].','.$post_data['SubnetMask'].','; + if ($post_data['RangeLeaseTimeUnits'] !== 'i') { + $config .= $post_data['RangeLeaseTime']; + $config .= $post_data['RangeLeaseTimeUnits'].PHP_EOL; + } else { + $config .= 'infinite'.PHP_EOL; + } + // Static leases + $staticLeases = array(); + if (isset($post_data["static_leases"]["mac"])) { + for ($i=0; $i < count($post_data["static_leases"]["mac"]); $i++) { + $mac = trim($post_data["static_leases"]["mac"][$i]); + $ip = trim($post_data["static_leases"]["ip"][$i]); + $comment = trim($post_data["static_leases"]["comment"][$i]); + if ($mac != "" && $ip != "") { + $staticLeases[] = array('mac' => $mac, 'ip' => $ip, 'comment' => $comment); + } + } + } + // Sort ascending by IPs + usort($staticLeases, [$this, 'compareIPs']); + // Update config + for ($i = 0; $i < count($staticLeases); $i++) { + $mac = $staticLeases[$i]['mac']; + $ip = $staticLeases[$i]['ip']; + $comment = $staticLeases[$i]['comment']; + $config .= "dhcp-host=$mac,$ip # $comment".PHP_EOL; + } + if ($post_data['no-resolv'] == "1") { + $config .= "no-resolv".PHP_EOL; + } + foreach ($post_data['server'] as $server) { + $config .= "server=$server".PHP_EOL; + } + if ($post_data['DNS1']) { + $config .= "dhcp-option=6," . $post_data['DNS1']; + if ($post_data['DNS2']) { + $config .= ','.$post_data['DNS2']; + } + $config .= PHP_EOL; + } + if ($post_data['dhcp-ignore'] == "1") { + $config .= 'dhcp-ignore=tag:!known'.PHP_EOL; + } + + return $config; + } + + /** + * Builds a RaspAP default dnsmasq config + * Written to 090_raspap.conf + * + * @return string $config //todo: standardize return type as array + */ + public function buildDefault(): string + { + $config = '# RaspAP default config'. PHP_EOL; + $config .='log-facility='. RASPI_DHCPCD_LOG . PHP_EOL; + $config .='conf-dir=/etc/dnsmasq.d'. PHP_EOL; + // handle log option + if (($post_data['log-dhcp'] ?? '') == "1") { + $config .= "log-dhcp".PHP_EOL; + } + if (($post_data['log-queries'] ?? '') == "1") { + $config .= "log-queries".PHP_EOL; + } + $config .= PHP_EOL; + + return $config; + } + /** * Saves dnsmasq configuration for an interface * - * @param array $config + * @param string $config * @param string $iface * @return bool */ - public function saveConfig(array $config, string $iface = self::DEFAULT_IFACE): bool + public function saveConfig(string $config, string $iface): bool { - $configFile = RASPI_DNSMASQ_PREFIX . $iface . self::CONF_SUFFIX; + $configFile = RASPI_DNSMASQ_PREFIX . $iface . SELF::CONF_SUFFIX; $tempFile = SELF::CONF_TMP; - $config = join(PHP_EOL, $config); file_put_contents($tempFile, $config); $cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)); exec($cmd, $output, $status); if ($status !== 0) { throw new \RuntimeException("Failed to copy temp config to $configFile"); + return false; } // reload dnsmasq to apply changes @@ -134,6 +219,113 @@ class DnsmasqManager return true; } + /** + * Saves dnsmasq default configuration + * + * @param string $config + * @return bool + */ + public function saveConfigDefault(string $config): bool + { + $configFile = SELF::CONF_DEFAULT . SELF::CONF_RASPAP . SELF::CONF_SUFFIX; + $tempFile = SELF::CONF_TMP; + + file_put_contents($tempFile, $config); + $cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)); + exec($cmd, $output, $status); + if ($status !== 0) { + throw new \RuntimeException("Failed to copy temp config to $configFile"); + return false; + } + + // reload dnsmasq to apply changes + exec('sudo systemctl reload dnsmasq.service', $output, $status); + if ($status !== 0) { + throw new \RuntimeException("Failed to reload dnsmasq service"); + } + + return true; + } + + /** + * Validates dnsmasq user input from $_POST object + * + * @param array $post_data + * @return array $errors + */ + public function validate(array $post_data): array + { + $errors = []; + $encounteredIPs = []; + + if (isset($post_data["static_leases"]["mac"])) { + for ($i=0; $i < count($post_data["static_leases"]["mac"]); $i++) { + $mac = trim($post_data["static_leases"]["mac"][$i]); + $ip = trim($post_data["static_leases"]["ip"][$i]); + if (!validateMac($mac)) { + $errors[] = _('Invalid MAC address: '.$mac); + } + if (in_array($ip, $encounteredIPs)) { + $errors[] = _('Duplicate IP address entered: ' . $ip); + } else { + $encounteredIPs[] = $ip; + } + } + } + return $errors; + } + + /** + * Removes a configuration block for the specified interface + * + * @param string $iface + * @param StatusMessage $status + * @return bool $result + */ + public function remove(string $iface, StatusMessage $status): bool + { + system('sudo rm '.RASPI_DNSMASQ_PREFIX.$iface.'.conf', $result); + if ($result == 0) { + $status->addMessage('Dnsmasq configuration for '.$iface.' removed.', 'success'); + } else { + $status->addMessage('Failed to remove dnsmasq configuration for '.$iface.'.', 'danger'); + } + return $result; + } + + /** + * Scans configuration dir for the specified interface + * Non-matching configs are removed, optional adblock.conf is protected + * + * @param string $dir_conf + * @param string $interface + * @return bool + */ + public function scanConfigDir(string $dir_conf, string $interface): bool + { + $syscnf = preg_grep('~\.(conf)$~', scandir($dir_conf)); + foreach ($syscnf as $cnf) { + if ($cnf !== '090_adblock.conf' && !preg_match('/.*_'.$interface.'.conf/', $cnf)) { + system('sudo rm /etc/dnsmasq.d/'.$cnf, $result); + return true; + } + } + } + + /** + * Compares two IPs + * + * @param array $ip1 + * @param array $ip2 + * @return int + */ + private function compareIPs(array $ip1, array $ip2): int + { + $ipu1 = sprintf('%u', ip2long($ip1["ip"])) + 0; + $ipu2 = sprintf('%u', ip2long($ip2["ip"])) + 0; + return $ipu1 <=> $ipu2; + } + /** * Add static DHCP lease * @@ -145,7 +337,6 @@ class DnsmasqManager */ public function addStaticLease(string $iface, string $mac, string $ip, ?string $comment = null): bool { - // TODO: append to conf return false; } } From 83ab53dd87b84fa9c7f3b16b55828449816c68c1 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 06:39:59 -0700 Subject: [PATCH 087/122] WIP: refactored to use dnsmasq + dhcpcd manager classes --- includes/dhcp.php | 199 ++++------------------------------------------ 1 file changed, 17 insertions(+), 182 deletions(-) diff --git a/includes/dhcp.php b/includes/dhcp.php index 4c35fac1..3747fa59 100755 --- a/includes/dhcp.php +++ b/includes/dhcp.php @@ -2,11 +2,13 @@ require_once 'config.php'; +use RaspAP\Networking\Hotspot\DhcpcdManager; +use RaspAP\Networking\Hotspot\DnsmasqManager; use RaspAP\Networking\Hotspot\WiFiManager; use RaspAP\Messages\StatusMessage; /** - * Manage DHCP configuration + * Displays DHCP configuration */ function DisplayDHCPConfig() { @@ -91,32 +93,38 @@ function DisplayDHCPConfig() */ function saveDHCPConfig($status) { + $dhcpcd = new DhcpcdManager(); + $dnsmasq = new DnsmasqManager(); $iface = $_POST['interface']; $return = 1; - // handle disable dhcp option + // dhcp if (!isset($_POST['dhcp-iface']) && file_exists(RASPI_DNSMASQ_PREFIX.$iface.'.conf')) { // remove dhcp + dnsmasq configs for selected interface - $return = removeDHCPConfig($iface,$status); - $return = removeDnsmasqConfig($iface,$status); + $return = $dhcpcd->remove($iface, $status); + $return = $dnsmasq->remove($iface, $status); } else { - $errors = validateDHCPInput(); + $errors = $dhcpcd->validate($_POST); if (empty($errors)) { - $return = updateDHCPConfig($iface,$status); + $return = updateDHCPConfig($iface, $status); } else { foreach ($errors as $error) { $status->addMessage($error, 'danger'); } } if ($return == 1) { - $status->addMessage('Dnsmasq configuration failed to be updated.', 'danger'); + $status->addMessage('DHCP configuration failed to be updated.', 'danger'); return false; } + // dnsmasq if (($_POST['dhcp-iface'] == "1") || (isset($_POST['mac']))) { - $errors = validateDnsmasqInput(); + $errors = $dnsmasq->validate($_POST); if (empty($errors)) { - $return = updateDnsmasqConfig($iface,$status); + $config = $dnsmasq->buildEx($iface, $_POST); + $return = $dnsmasq->saveConfig($config, $iface); + $config = $dnsmasq->buildDefault(); + $return = $dnsmasq->saveConfigDefault($config); } else { foreach ($errors as $error) { $status->addMessage($error, 'danger'); @@ -124,183 +132,10 @@ function saveDHCPConfig($status) $return = 1; } } - - if ($return == 0) { - $status->addMessage('Dnsmasq configuration updated successfully.', 'success'); - } else { - $status->addMessage('Dnsmasq configuration failed to be updated.', 'danger'); - return false; - } return true; } } -/** - * Validates DHCP user input from the $_POST object - * - * @return array $errors - */ -function validateDHCPInput() -{ - $errors = []; - define('IFNAMSIZ', 16); - $iface = $_POST['interface']; - if (!preg_match('/^[^\s\/\\0]+$/', $iface) - || strlen($iface) >= IFNAMSIZ - ) { - $errors[] = _('Invalid interface name.'); - } - if (!filter_var($_POST['StaticIP'], FILTER_VALIDATE_IP) && !empty($_POST['StaticIP'])) { - $errors[] = _('Invalid static IP address.'); - } - if (!filter_var($_POST['SubnetMask'], FILTER_VALIDATE_IP) && !empty($_POST['SubnetMask'])) { - $errors[] = _('Invalid subnet mask.'); - } - if (!filter_var($_POST['DefaultGateway'], FILTER_VALIDATE_IP) && !empty($_POST['DefaultGateway'])) { - $errors[] = _('Invalid default gateway.'); - } - if (($_POST['dhcp-iface'] == "1")) { - if (!filter_var($_POST['RangeStart'], FILTER_VALIDATE_IP) && !empty($_POST['RangeStart'])) { - $errors[] = _('Invalid DHCP range start.'); - } - if (!filter_var($_POST['RangeEnd'], FILTER_VALIDATE_IP) && !empty($_POST['RangeEnd'])) { - $errors[] = _('Invalid DHCP range end.'); - } - if (!ctype_digit($_POST['RangeLeaseTime']) && $_POST['RangeLeaseTimeUnits'] !== 'i') { - $errors[] = _('Invalid DHCP lease time, not a number.'); - } - if (!in_array($_POST['RangeLeaseTimeUnits'], array('m', 'h', 'd', 'i'))) { - $errors[] = _('Unknown DHCP lease time unit.'); - } - if ($_POST['Metric'] !== '' && !ctype_digit($_POST['Metric'])) { - $errors[] = _('Invalid metric value, not a number.'); - } - } - return $errors; -} - -/** - * Compares to string IPs - * - * @param string $ip1 - * @param string $ip2 - * @return boolean $result - */ -function compareIPs($ip1, $ip2) -{ - $ipu1 = sprintf('%u', ip2long($ip1["ip"])) + 0; - $ipu2 = sprintf('%u', ip2long($ip2["ip"])) + 0; - return $ipu1 > $ipu2; -} - -/** - * Validates Dnsmasq user input from the $_POST object - * - * @return array $errors - */ -function validateDnsmasqInput() -{ - $errors = []; - $encounteredIPs = []; - - if (isset($_POST["static_leases"]["mac"])) { - for ($i=0; $i < count($_POST["static_leases"]["mac"]); $i++) { - $mac = trim($_POST["static_leases"]["mac"][$i]); - $ip = trim($_POST["static_leases"]["ip"][$i]); - if (!validateMac($mac)) { - $errors[] = _('Invalid MAC address: '.$mac); - } - if (in_array($ip, $encounteredIPs)) { - $errors[] = _('Duplicate IP address entered: ' . $ip); - } else { - $encounteredIPs[] = $ip; - } - } - } - return $errors; -} - - -/** - * Updates a dnsmasq configuration - * - * @param string $iface - * @param object $status - * @return boolean $result - */ -function updateDnsmasqConfig($iface,$status) -{ - - $config = '# RaspAP '.$iface.' configuration'.PHP_EOL; - $config .= 'interface='.$iface.PHP_EOL.'dhcp-range='.$_POST['RangeStart'].','.$_POST['RangeEnd'].','.$_POST['SubnetMask'].','; - if ($_POST['RangeLeaseTimeUnits'] !== 'i') { - $config .= $_POST['RangeLeaseTime']; - $config .= $_POST['RangeLeaseTimeUnits'].PHP_EOL; - } else { - $config .= 'infinite'.PHP_EOL; - } - // Static leases - $staticLeases = array(); - if (isset($_POST["static_leases"]["mac"])) { - for ($i=0; $i < count($_POST["static_leases"]["mac"]); $i++) { - $mac = trim($_POST["static_leases"]["mac"][$i]); - $ip = trim($_POST["static_leases"]["ip"][$i]); - $comment = trim($_POST["static_leases"]["comment"][$i]); - if ($mac != "" && $ip != "") { - $staticLeases[] = array('mac' => $mac, 'ip' => $ip, 'comment' => $comment); - } - } - } - // Sort ascending by IPs - usort($staticLeases, "compareIPs"); - // Update config - for ($i = 0; $i < count($staticLeases); $i++) { - $mac = $staticLeases[$i]['mac']; - $ip = $staticLeases[$i]['ip']; - $comment = $staticLeases[$i]['comment']; - $config .= "dhcp-host=$mac,$ip # $comment".PHP_EOL; - } - if ($_POST['no-resolv'] == "1") { - $config .= "no-resolv".PHP_EOL; - } - foreach ($_POST['server'] as $server) { - $config .= "server=$server".PHP_EOL; - } - if ($_POST['DNS1']) { - $config .= "dhcp-option=6," . $_POST['DNS1']; - if ($_POST['DNS2']) { - $config .= ','.$_POST['DNS2']; - } - $config .= PHP_EOL; - } - if ($_POST['dhcp-ignore'] == "1") { - $config .= 'dhcp-ignore=tag:!known'.PHP_EOL; - } - file_put_contents("/tmp/dnsmasqdata", $config); - $msg = file_exists(RASPI_DNSMASQ_PREFIX.$iface.'.conf') ? 'updated' : 'added'; - system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$iface.'.conf', $result); - if ($result == 0) { - $status->addMessage('Dnsmasq configuration for '.$iface.' '.$msg.'.', 'success'); - } - - // write default 090_raspap.conf - $config = '# RaspAP default config'.PHP_EOL; - $config .='log-facility='.RASPI_DHCPCD_LOG.PHP_EOL; - $config .='conf-dir=/etc/dnsmasq.d'.PHP_EOL; - // handle log option - if (($_POST['log-dhcp'] ?? '') == "1") { - $config .= "log-dhcp".PHP_EOL; - } - if (($_POST['log-queries'] ?? '') == "1") { - $config .= "log-queries".PHP_EOL; - } - $config .= PHP_EOL; - file_put_contents("/tmp/dnsmasqdata", $config); - system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.'raspap.conf', $result); - - return $result; -} - /** * Updates a dhcp configuration * From 98922434f27dafca497e0fb317177ddd5130ebe2 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 08:07:51 -0700 Subject: [PATCH 088/122] Added buildConfigEx(), migrated from legacy controller --- .../Networking/Hotspot/DhcpcdManager.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php index 19dd5678..9b8f58db 100644 --- a/src/RaspAP/Networking/Hotspot/DhcpcdManager.php +++ b/src/RaspAP/Networking/Hotspot/DhcpcdManager.php @@ -143,6 +143,53 @@ class DhcpcdManager return true; } + /** + * (Re)builds an existing dhcp configuration + * + * @param string $iface + * @param StatusMessage $status + * @param array $post_data + * @return string $dhcp_cfg + */ + public function buildConfigEx(string $iface, array $post_data, StatusMessage $status): string + { + $cfg[] = '# RaspAP '.$iface.' configuration'; + $cfg[] = 'interface '.$iface; + if (isset($post_data['StaticIP']) && $post_data['StaticIP'] !== '') { + $mask = ($post_data['SubnetMask'] !== '' && $post_data['SubnetMask'] !== '0.0.0.0') ? '/'.mask2cidr($post_data['SubnetMask']) : null; + $cfg[] = 'static ip_address='.$post_data['StaticIP'].$mask; + } + if (isset($post_data['DefaultGateway']) && $post_data['DefaultGateway'] !== '') { + $cfg[] = 'static routers='.$post_data['DefaultGateway']; + } + if ($post_data['DNS1'] !== '' || $post_data['DNS2'] !== '') { + $cfg[] = 'static domain_name_server='.$post_data['DNS1'].' '.$post_data['DNS2']; + } + if ($post_data['Metric'] !== '') { + $cfg[] = 'metric '.$post_data['Metric']; + } + if (($post_data['Fallback'] ?? 0) == 1) { + $cfg[] = 'profile static_'.$iface; + $cfg[] = 'fallback static_'.$iface; + } + $cfg[] = ($post_data['DefaultRoute'] ?? '') == '1' ? 'gateway' : 'nogateway'; + if (substr($iface, 0, 2) === "wl" && ($post_data['NoHookWPASupplicant'] ?? '') == '1') { + $cfg[] = 'nohook wpa_supplicant'; + } + $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); + if (!preg_match('/^interface\s'.$iface.'$/m', $dhcp_cfg)) { + $cfg[] = PHP_EOL; + $cfg = join(PHP_EOL, $cfg); + $dhcp_cfg .= $cfg; + $status->addMessage('DHCP configuration for '.$iface.' added.', 'success'); + } else { + $cfg = join(PHP_EOL, $cfg); + $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)/ms', $cfg, $dhcp_cfg, 1); + } + + return $dhcp_cfg; + } + /** * Validates DHCP user input from $_POST data * From 87f55c8b1e55738d47e33d86f942df1ead892504 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 09:58:56 -0700 Subject: [PATCH 089/122] Standardize array return/input type for build + save config --- .../Networking/Hotspot/DnsmasqManager.php | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 1fb1165c..b55ebac4 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -85,6 +85,7 @@ class DnsmasqManager $config[] = 'interface='.$_POST['interface']; $config[] = 'domain-needed'; $config[] = 'dhcp-range='.$dhcp_range; + // handle multiple dhcp-host + option entries if (!empty($syscfg['dhcp-host'])) { if (is_array($syscfg['dhcp-host'])) { @@ -114,18 +115,19 @@ class DnsmasqManager * * @param string $iface * @param array $post_data - * @return string $config //todo: standardize return type as array + * @return array $config */ - public function buildEx(string $iface, array $post_data): string + public function buildConfigEx(string $iface, array $post_data): array { - $config = '# RaspAP '. $iface .' configuration'.PHP_EOL; - $config .= 'interface='. $iface . PHP_EOL .'dhcp-range='.$post_data['RangeStart'].','.$post_data['RangeEnd'].','.$post_data['SubnetMask'].','; - if ($post_data['RangeLeaseTimeUnits'] !== 'i') { - $config .= $post_data['RangeLeaseTime']; - $config .= $post_data['RangeLeaseTimeUnits'].PHP_EOL; - } else { - $config .= 'infinite'.PHP_EOL; - } + $config[] = '# RaspAP '. $iface .' configuration'; + $config[] = 'interface='. $iface; + $leaseTime = ($post_data['RangeLeaseTimeUnits'] !== 'i') + ? $post_data['RangeLeaseTime'] . $post_data['RangeLeaseTimeUnits'] + : 'infinite'; + $config[] = 'dhcp-range=' . $post_data['RangeStart'] . ',' . + $post_data['RangeEnd'] . ',' . + $post_data['SubnetMask'] . ',' . + $leaseTime; // Static leases $staticLeases = array(); if (isset($post_data["static_leases"]["mac"])) { @@ -145,23 +147,23 @@ class DnsmasqManager $mac = $staticLeases[$i]['mac']; $ip = $staticLeases[$i]['ip']; $comment = $staticLeases[$i]['comment']; - $config .= "dhcp-host=$mac,$ip # $comment".PHP_EOL; + $config[] = "dhcp-host=$mac,$ip # $comment"; } if ($post_data['no-resolv'] == "1") { - $config .= "no-resolv".PHP_EOL; + $config[] = "no-resolv"; } foreach ($post_data['server'] as $server) { - $config .= "server=$server".PHP_EOL; + $config[] = "server=$server"; } if ($post_data['DNS1']) { - $config .= "dhcp-option=6," . $post_data['DNS1']; + $config[] = "dhcp-option=6," . $post_data['DNS1']; if ($post_data['DNS2']) { - $config .= ','.$post_data['DNS2']; + $config[] = ','.$post_data['DNS2']; } - $config .= PHP_EOL; + $config[]= PHP_EOL; } if ($post_data['dhcp-ignore'] == "1") { - $config .= 'dhcp-ignore=tag:!known'.PHP_EOL; + $config[] = 'dhcp-ignore=tag:!known'; } return $config; @@ -171,21 +173,24 @@ class DnsmasqManager * Builds a RaspAP default dnsmasq config * Written to 090_raspap.conf * - * @return string $config //todo: standardize return type as array + * @param array $post_data + * @return array $config */ - public function buildDefault(): string + public function buildDefault(array $post_data): array { - $config = '# RaspAP default config'. PHP_EOL; - $config .='log-facility='. RASPI_DHCPCD_LOG . PHP_EOL; - $config .='conf-dir=/etc/dnsmasq.d'. PHP_EOL; + // preamble + $config[] = '# RaspAP default config'; + $config[] = 'log-facility='. RASPI_DHCPCD_LOG; + $config[] = 'conf-dir=/etc/dnsmasq.d'; + // handle log option if (($post_data['log-dhcp'] ?? '') == "1") { - $config .= "log-dhcp".PHP_EOL; + $config[] = "log-dhcp"; } if (($post_data['log-queries'] ?? '') == "1") { - $config .= "log-queries".PHP_EOL; + $config[] = "log-queries"; } - $config .= PHP_EOL; + $config[] = PHP_EOL; return $config; } @@ -193,15 +198,16 @@ class DnsmasqManager /** * Saves dnsmasq configuration for an interface * - * @param string $config + * @param array $config * @param string $iface * @return bool */ - public function saveConfig(string $config, string $iface): bool + public function saveConfig(array $config, string $iface): bool { $configFile = RASPI_DNSMASQ_PREFIX . $iface . SELF::CONF_SUFFIX; $tempFile = SELF::CONF_TMP; + $config = join(PHP_EOL, $config); file_put_contents($tempFile, $config); $cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)); exec($cmd, $output, $status); @@ -222,14 +228,15 @@ class DnsmasqManager /** * Saves dnsmasq default configuration * - * @param string $config + * @param array $config * @return bool */ - public function saveConfigDefault(string $config): bool + public function saveConfigDefault(array $config): bool { $configFile = SELF::CONF_DEFAULT . SELF::CONF_RASPAP . SELF::CONF_SUFFIX; $tempFile = SELF::CONF_TMP; + $config = join(PHP_EOL, $config); file_put_contents($tempFile, $config); $cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)); exec($cmd, $output, $status); From 7cc436fbaa3b0c10d0cdc014f6280cf7ed76bffa Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 10:00:15 -0700 Subject: [PATCH 090/122] Add missing param in dnsmasq->saveConfig() --- .../Networking/Hotspot/HotspotService.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HotspotService.php b/src/RaspAP/Networking/Hotspot/HotspotService.php index 0b02af17..e86185eb 100644 --- a/src/RaspAP/Networking/Hotspot/HotspotService.php +++ b/src/RaspAP/Networking/Hotspot/HotspotService.php @@ -20,7 +20,6 @@ namespace RaspAP\Networking\Hotspot; use RaspAP\Networking\Hotspot\Validators\HostapdValidator; use RaspAP\Messages\StatusMessage; - class HotspotService { protected HostapdManager $hostapd; @@ -168,7 +167,7 @@ class HotspotService $validated['apsta'], $validated['bridge'] ); - $this->dnsmasq->saveConfig($dnsmasqConfig, $validated['interface']); + $this->dnsmasq->saveConfig($dnsmasqConfig, $validated['interface'], $status); } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); } @@ -181,7 +180,7 @@ class HotspotService $validated['repeater'], $validated['apsta'], $validated['dualmode'], - $status + $status, ); } catch (\RuntimeException $e) { error_log('Error: ' . $e->getMessage()); @@ -322,48 +321,44 @@ class HotspotService } /** - * Start hotspot services for given interface. + * Starts services for given interface * * @param string $iface * @return bool */ public function start(string $iface): bool { - // TODO: implement systemctl or service logic return false; } /** - * Stop hotspot services. + * Stops hotspot services * * @return bool */ public function stop(): bool { - // TODO: implement return false; } /** - * Restart hotspot services for given interface. + * Restart hotspot services for given interface * * @param string $iface * @return bool */ public function restart(string $iface): bool { - // TODO: implement return false; } /** - * Get current hotspot status. + * Get current hotspot status * * @return array */ public function getStatus(): array { - // TODO: query service state + configs return []; } } From 7a7bdda708ff89a4d7d60501f28f4a6fbc53ff67 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 10:02:58 -0700 Subject: [PATCH 091/122] Delegate dnsmasq + dhcpcd config build/save to manager classes --- includes/dhcp.php | 62 +++-------------------------------------------- 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/includes/dhcp.php b/includes/dhcp.php index 3747fa59..f209813e 100755 --- a/includes/dhcp.php +++ b/includes/dhcp.php @@ -96,7 +96,6 @@ function saveDHCPConfig($status) $dhcpcd = new DhcpcdManager(); $dnsmasq = new DnsmasqManager(); $iface = $_POST['interface']; - $return = 1; // dhcp if (!isset($_POST['dhcp-iface']) && file_exists(RASPI_DNSMASQ_PREFIX.$iface.'.conf')) { @@ -106,82 +105,29 @@ function saveDHCPConfig($status) } else { $errors = $dhcpcd->validate($_POST); if (empty($errors)) { - $return = updateDHCPConfig($iface, $status); + $dhcp_cfg = $dhcpcd->buildConfigEx($iface, $_POST, $status); + $dhcpcd->saveConfig($dhcp_cfg, $iface, $status); } else { foreach ($errors as $error) { $status->addMessage($error, 'danger'); } } - if ($return == 1) { - $status->addMessage('DHCP configuration failed to be updated.', 'danger'); - return false; - } // dnsmasq if (($_POST['dhcp-iface'] == "1") || (isset($_POST['mac']))) { $errors = $dnsmasq->validate($_POST); if (empty($errors)) { - $config = $dnsmasq->buildEx($iface, $_POST); + $config = $dnsmasq->buildConfigEx($iface, $_POST); $return = $dnsmasq->saveConfig($config, $iface); - $config = $dnsmasq->buildDefault(); + $config = $dnsmasq->buildDefault($_POST); $return = $dnsmasq->saveConfigDefault($config); } else { foreach ($errors as $error) { $status->addMessage($error, 'danger'); } - $return = 1; } } return true; } } -/** - * Updates a dhcp configuration - * - * @param string $iface - * @param object $status - * @return boolean $result - */ -function updateDHCPConfig($iface,$status) -{ - $cfg[] = '# RaspAP '.$iface.' configuration'; - $cfg[] = 'interface '.$iface; - if (isset($_POST['StaticIP']) && $_POST['StaticIP'] !== '') { - $mask = ($_POST['SubnetMask'] !== '' && $_POST['SubnetMask'] !== '0.0.0.0') ? '/'.mask2cidr($_POST['SubnetMask']) : null; - $cfg[] = 'static ip_address='.$_POST['StaticIP'].$mask; - } - if (isset($_POST['DefaultGateway']) && $_POST['DefaultGateway'] !== '') { - $cfg[] = 'static routers='.$_POST['DefaultGateway']; - } - if ($_POST['DNS1'] !== '' || $_POST['DNS2'] !== '') { - $cfg[] = 'static domain_name_server='.$_POST['DNS1'].' '.$_POST['DNS2']; - } - if ($_POST['Metric'] !== '') { - $cfg[] = 'metric '.$_POST['Metric']; - } - if (($_POST['Fallback'] ?? 0) == 1) { - $cfg[] = 'profile static_'.$iface; - $cfg[] = 'fallback static_'.$iface; - } - $cfg[] = ($_POST['DefaultRoute'] ?? '') == '1' ? 'gateway' : 'nogateway'; - if (substr($iface, 0, 2) === "wl" && ($_POST['NoHookWPASupplicant'] ?? '') == '1') { - $cfg[] = 'nohook wpa_supplicant'; - } - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - if (!preg_match('/^interface\s'.$iface.'$/m', $dhcp_cfg)) { - $cfg[] = PHP_EOL; - $cfg = join(PHP_EOL, $cfg); - $dhcp_cfg .= $cfg; - $status->addMessage('DHCP configuration for '.$iface.' added.', 'success'); - } else { - $cfg = join(PHP_EOL, $cfg); - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)/ms', $cfg, $dhcp_cfg, 1); - $status->addMessage('DHCP configuration for '.$iface.' updated.', 'success'); - } - file_put_contents("/tmp/dhcpddata", $dhcp_cfg); - system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $result); - - return $result; -} - From c19bd6024186ad85be4a24e7b379e30ee096ac45 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 20 Jul 2025 14:46:33 -0700 Subject: [PATCH 092/122] Fix: set return value (bool) --- src/RaspAP/Networking/Hotspot/DnsmasqManager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index b55ebac4..4942ffe2 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -294,10 +294,11 @@ class DnsmasqManager system('sudo rm '.RASPI_DNSMASQ_PREFIX.$iface.'.conf', $result); if ($result == 0) { $status->addMessage('Dnsmasq configuration for '.$iface.' removed.', 'success'); + return true; } else { $status->addMessage('Failed to remove dnsmasq configuration for '.$iface.'.', 'danger'); + return false; } - return $result; } /** From 522b204bb94e1dcd01dba14cd6987b1524403a94 Mon Sep 17 00:00:00 2001 From: ruralmeltdown Date: Wed, 23 Jul 2025 15:21:02 -0700 Subject: [PATCH 093/122] comment out sbadmin code for sidebar overlay --- dist/sb-admin/js/scripts.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/sb-admin/js/scripts.js b/dist/sb-admin/js/scripts.js index 873276da..39067c08 100644 --- a/dist/sb-admin/js/scripts.js +++ b/dist/sb-admin/js/scripts.js @@ -13,9 +13,9 @@ window.addEventListener('DOMContentLoaded', event => { const sidebarToggle = document.body.querySelector('#sidebarToggle'); if (sidebarToggle) { // Uncomment below to persist sidebar toggle between refreshes - if (localStorage.getItem('sb|sidebar-toggle') === 'true') { - document.body.classList.toggle('sb-sidenav-toggled'); - } + // if (localStorage.getItem('sb|sidebar-toggle') === 'true') { + // document.body.classList.toggle('sb-sidenav-toggled'); + // } sidebarToggle.addEventListener('click', event => { event.preventDefault(); document.body.classList.toggle('sb-sidenav-toggled'); From 33476098c1623ae2dd9a70e670e2f48eeed02694 Mon Sep 17 00:00:00 2001 From: ruralmeltdown Date: Thu, 24 Jul 2025 07:36:30 -0700 Subject: [PATCH 094/122] add auto-close to Bootstrap alerts --- app/js/ui/main.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/js/ui/main.js b/app/js/ui/main.js index 97824fcc..406b3620 100644 --- a/app/js/ui/main.js +++ b/app/js/ui/main.js @@ -4,7 +4,7 @@ function msgShow(retcode,msg) { } else if(retcode == 2 || retcode == 1) { var alertType = 'danger'; } - var htmlMsg = ''; + var htmlMsg = ''; return htmlMsg; } @@ -581,3 +581,9 @@ $(document) .ready(contentLoaded) .ready(loadWifiStations()); +// To auto-close Bootstrap alerts; time is in milliseconds +window.setTimeout(function() { + $(".alert").fadeTo(500, 0).slideUp(500, function(){ + $(this).remove(); + }); +}, 5000); \ No newline at end of file From bb76eb86a4748cae4efa1c9a86d0c3015b44b72f Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 25 Jul 2025 11:55:18 -0700 Subject: [PATCH 095/122] Append PostUpEx/PreDown rules to WG config --- config/defaults.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/defaults.json b/config/defaults.json index a0e1b190..093715fb 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -49,7 +49,9 @@ "ListenPort": [ "51820" ], "DNS": [ "9.9.9.9" ], "PostUp": [ "iptables -A FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -A FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE" ], - "PostDown": [ "iptables -D FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -D FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE" ] + "PostDown": [ "iptables -D FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -D FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE" ], + "PostUpEx": [ "iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ], + "PreDown": [ "iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ] }, "peer": { "Address": [ "10.8.1.2/24" ], From 2967f5b692262e7c0622b6739b6212898134e2f0 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 25 Jul 2025 19:34:31 -0700 Subject: [PATCH 096/122] Implement user-definable alert timeout option --- app/js/ui/main.js | 11 ++++++++--- includes/system.php | 23 ++++++++++++++++++++++- templates/system/advanced.php | 16 ++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/js/ui/main.js b/app/js/ui/main.js index 406b3620..c387ea50 100644 --- a/app/js/ui/main.js +++ b/app/js/ui/main.js @@ -582,8 +582,13 @@ $(document) .ready(loadWifiStations()); // To auto-close Bootstrap alerts; time is in milliseconds -window.setTimeout(function() { +const alertTimeout = parseInt(getCookie('alert_timeout'), 10); + +if (!isNaN(alertTimeout) && alertTimeout > 0) { + window.setTimeout(function() { $(".alert").fadeTo(500, 0).slideUp(500, function(){ - $(this).remove(); + $(this).remove(); }); -}, 5000); \ No newline at end of file + }, alertTimeout); +} + diff --git a/includes/system.php b/includes/system.php index 6bc73875..5bace1ef 100755 --- a/includes/system.php +++ b/includes/system.php @@ -12,6 +12,10 @@ function DisplaySystem(&$extraFooterScripts) $dashboard = new \RaspAP\UI\Dashboard; $pluginInstaller = \RaspAP\Plugins\PluginInstaller::getInstance(); + // set defaults + $optAutoclose = true; + $alertTimeout = 5000; + if (isset($_POST['SaveLanguage'])) { if (isset($_POST['locale'])) { $_SESSION['locale'] = $_POST['locale']; @@ -51,6 +55,21 @@ function DisplaySystem(&$extraFooterScripts) $status->addMessage(sprintf(_('Changing log limit size to %s KB'), $_SESSION['log_limit']), 'info'); } } + // Validate alert timout + if (isset($_POST['autoClose'])) { + $alertTimeout = trim($_POST['alertTimeout'] ?? ''); + if (strlen($alertTimeout) > 7 || !is_numeric($alertTimeout)) { + $status->addMessage('Invalid value for alert close timeout', 'danger'); + $good_input = false; + } else { + setcookie('alert_timeout', (int) $alertTimeout); + $status->addMessage(sprintf(_('Changing alert close timeout to %s ms'), $alertTimeout), 'info'); + } + } else { + setcookie('alert_timeout', '', time() - 3600, '/'); + $optAutoclose = false; + } + // Save settings if ($good_input) { exec("sudo /etc/raspap/lighttpd/configport.sh $serverPort $serverBind " .RASPI_LIGHTTPD_CONFIG. " ".$_SERVER['SERVER_NAME'], $return); @@ -155,7 +174,9 @@ function DisplaySystem(&$extraFooterScripts) "themes", "selectedTheme", "logLimit", - "pluginsTable" + "pluginsTable", + "optAutoclose", + "alertTimeout" )); } diff --git a/templates/system/advanced.php b/templates/system/advanced.php index 66612f16..3ea11efc 100644 --- a/templates/system/advanced.php +++ b/templates/system/advanced.php @@ -22,6 +22,22 @@
+
+
+
+ + /> + +
+
+
+
+
+ + +
+
+ " /> " /> From f558d02e68c23de8dbda70846fe11ecaf2e84650 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 25 Jul 2025 19:37:34 -0700 Subject: [PATCH 097/122] Add messages to en_US locale --- locale/en_US/LC_MESSAGES/messages.mo | Bin 65371 -> 65579 bytes locale/en_US/LC_MESSAGES/messages.po | 6 ++++++ 2 files changed, 6 insertions(+) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index e2720b14d0f3c1a1664497ab06c8a0808894ac39..d5711ed99dd92d04cfb856ce0b5d4e0ab2b305e0 100644 GIT binary patch delta 15764 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 delta 15545 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 Date: Fri, 25 Jul 2025 21:10:20 -0700 Subject: [PATCH 098/122] Move alert message options to theme tab --- templates/system/advanced.php | 16 ------------- templates/system/theme.php | 43 +++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/templates/system/advanced.php b/templates/system/advanced.php index 3ea11efc..66612f16 100644 --- a/templates/system/advanced.php +++ b/templates/system/advanced.php @@ -22,22 +22,6 @@
-
-
-
- - /> - -
-
-
-
-
- - -
-
- " /> " /> diff --git a/templates/system/theme.php b/templates/system/theme.php index 483a1764..045bbd8b 100644 --- a/templates/system/theme.php +++ b/templates/system/theme.php @@ -1,19 +1,38 @@
-

-
-
- - -
-
- - -
-
+

-
From 01a441c687627cd19a7a7b9515771b0fb5cbc756 Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 25 Jul 2025 21:12:42 -0700 Subject: [PATCH 099/122] Separate system settings save functions, read alert_timeout --- includes/system.php | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/includes/system.php b/includes/system.php index 5bace1ef..912b3750 100755 --- a/includes/system.php +++ b/includes/system.php @@ -15,7 +15,20 @@ function DisplaySystem(&$extraFooterScripts) // set defaults $optAutoclose = true; $alertTimeout = 5000; + $good_input = true; + $config_port = false; + // set alert_timeout from cookie if valid + if (isset($_COOKIE['alert_timeout']) && is_numeric($_COOKIE['alert_timeout'])) { + $cookieTimeout = (int) $_COOKIE['alert_timeout']; + + if ($cookieTimeout > 0) { + $alertTimeout = $cookieTimeout; + } else { + // A value of 0 means auto-close is disabled + $optAutoclose = false; + } + } if (isset($_POST['SaveLanguage'])) { if (isset($_POST['locale'])) { $_SESSION['locale'] = $_POST['locale']; @@ -25,7 +38,6 @@ function DisplaySystem(&$extraFooterScripts) if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveServerSettings'])) { - $good_input = true; // Validate server port if (isset($_POST['serverPort'])) { if (strlen($_POST['serverPort']) > 4 || !is_numeric($_POST['serverPort'])) { @@ -36,13 +48,13 @@ function DisplaySystem(&$extraFooterScripts) } } // Validate server bind address - $serverBind = escapeshellarg(''); - if ($_POST['serverBind'] && $_POST['serverBind'] !== null ) { - if (!filter_var($_POST['serverBind'], FILTER_VALIDATE_IP)) { + if (isset($_POST['serverBind']) && $_POST['serverBind'] !== '') { + $inputBind = trim($_POST['serverBind']); + if (!filter_var($inputBind, FILTER_VALIDATE_IP)) { $status->addMessage('Invalid value for bind address', 'danger'); $good_input = false; } else { - $serverBind = escapeshellarg($_POST['serverBind']); + $serverBind = escapeshellarg($inputBind); } } // Validate log limit @@ -55,6 +67,14 @@ function DisplaySystem(&$extraFooterScripts) $status->addMessage(sprintf(_('Changing log limit size to %s KB'), $_SESSION['log_limit']), 'info'); } } + // Save settings + if ($good_input) { + exec("sudo /etc/raspap/lighttpd/configport.sh $serverPort $serverBind " .RASPI_LIGHTTPD_CONFIG. " ".$_SERVER['SERVER_NAME'], $return); + foreach ($return as $line) { + $status->addMessage($line, 'info'); + } + } + } elseif (isset($_POST['savethemeSettings'])) { // Validate alert timout if (isset($_POST['autoClose'])) { $alertTimeout = trim($_POST['alertTimeout'] ?? ''); @@ -69,14 +89,6 @@ function DisplaySystem(&$extraFooterScripts) setcookie('alert_timeout', '', time() - 3600, '/'); $optAutoclose = false; } - - // Save settings - if ($good_input) { - exec("sudo /etc/raspap/lighttpd/configport.sh $serverPort $serverBind " .RASPI_LIGHTTPD_CONFIG. " ".$_SERVER['SERVER_NAME'], $return); - foreach ($return as $line) { - $status->addMessage($line, 'info'); - } - } } } From 59e7a9d859d779bd1267501f046f0b3a4e73b97b Mon Sep 17 00:00:00 2001 From: billz Date: Fri, 25 Jul 2025 21:15:16 -0700 Subject: [PATCH 100/122] Add messages to en_US locale --- locale/en_US/LC_MESSAGES/messages.mo | Bin 65579 -> 65583 bytes locale/en_US/LC_MESSAGES/messages.po | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index d5711ed99dd92d04cfb856ce0b5d4e0ab2b305e0..cb04e00b3315b941a4073bf3cd0a1d9a7912e325 100644 GIT binary patch delta 11892 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 delta 11875 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 Date: Mon, 28 Jul 2025 12:56:36 -0700 Subject: [PATCH 101/122] Populate static-lease container from jsonData.dhcpHost --- app/js/ajax/main.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/app/js/ajax/main.js b/app/js/ajax/main.js index c9dae9ea..f58ffa51 100644 --- a/app/js/ajax/main.js +++ b/app/js/ajax/main.js @@ -94,12 +94,41 @@ function loadInterfaceDHCPSelect() { $('#dhcp-iface').removeAttr('disabled'); } else { $('#chkdhcp').closest('.btn').addClass('active'); - $('#chkdhcp').closest('.btn').button.blur(); + $('#chkdhcp').closest('.btn').blur(); } if (jsonData.FallbackEnabled || $('#chkdhcp').is(':checked')) { $('#dhcp-iface').prop('disabled', true); setDhcpFieldsDisabled(); } + + const leaseContainer = $('.js-dhcp-static-lease-container'); + leaseContainer.empty(); + + if (jsonData.dhcpHost && jsonData.dhcpHost.length > 0) { + const leases = jsonData.dhcpHost || []; + leases.forEach((entry, index) => { + const [mainPart, commentPart] = entry.split('#'); + const comment = commentPart ? commentPart.trim() : ''; + const [mac, ip] = mainPart.split(',').map(part => part.trim()); + console.log(`Lease ${index}: MAC=${mac}, IP=${ip}, Comment=${comment}`); + const row = ` +
+
+ +
+
+ +
+
+ +
+
+ +
+
`; + leaseContainer.append(row); + }); + } }); } From f30abc4bd7e6133521f44994e5654cd164fe4961 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 28 Jul 2025 13:34:26 -0700 Subject: [PATCH 102/122] Remove debug output --- app/js/ajax/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/js/ajax/main.js b/app/js/ajax/main.js index f58ffa51..f8bf997a 100644 --- a/app/js/ajax/main.js +++ b/app/js/ajax/main.js @@ -110,7 +110,6 @@ function loadInterfaceDHCPSelect() { const [mainPart, commentPart] = entry.split('#'); const comment = commentPart ? commentPart.trim() : ''; const [mac, ip] = mainPart.split(',').map(part => part.trim()); - console.log(`Lease ${index}: MAC=${mac}, IP=${ip}, Comment=${comment}`); const row = `
From 3c1d4325f24b2cd042a5eb0c4718d920bb1c9e28 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 28 Jul 2025 13:34:45 -0700 Subject: [PATCH 103/122] Implement dnsmasq restart button --- includes/dhcp.php | 12 ++++++++++++ templates/dhcp.php | 1 + 2 files changed, 13 insertions(+) diff --git a/includes/dhcp.php b/includes/dhcp.php index f209813e..a4b06f76 100755 --- a/includes/dhcp.php +++ b/includes/dhcp.php @@ -37,6 +37,18 @@ function DisplayDHCPConfig() $status->addMessage('Failed to start dnsmasq', 'danger'); } } + } elseif (isset($_POST['restartdhcpd'])) { + if ($dnsmasq_state) { + exec('sudo /bin/systemctl restart dnsmasq.service', $dnsmasq, $return); + if ($return == 0) { + $status->addMessage('Successfully restarted dnsmasq', 'success'); + $dnsmasq_state = false; + } else { + $status->addMessage('Failed to restart dnsmasq', 'danger'); + } + } else { + $status->addMessage('dnsmasq already stopped', 'info'); + } } elseif (isset($_POST['stopdhcpd'])) { if ($dnsmasq_state) { exec('sudo /bin/systemctl stop dnsmasq.service', $dnsmasq, $return); diff --git a/templates/dhcp.php b/templates/dhcp.php index b83a6444..9e5803f9 100755 --- a/templates/dhcp.php +++ b/templates/dhcp.php @@ -3,6 +3,7 @@ " name="savedhcpdsettings" /> " name="stopdhcpd" /> + " name="restartdhcpd" /> " name="startdhcpd" /> From dadc4e4fb4aac25b8d37c3f50859249428b291e0 Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 28 Jul 2025 15:56:01 -0700 Subject: [PATCH 104/122] Refactor setKnownStationsWPA(), add helper method addWpaNetwork() --- src/RaspAP/Networking/Hotspot/WiFiManager.php | 153 ++++++++++++++---- 1 file changed, 119 insertions(+), 34 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/WiFiManager.php b/src/RaspAP/Networking/Hotspot/WiFiManager.php index bc07fe1d..bf4e4e32 100644 --- a/src/RaspAP/Networking/Hotspot/WiFiManager.php +++ b/src/RaspAP/Networking/Hotspot/WiFiManager.php @@ -304,55 +304,140 @@ class WiFiManager 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 + * @throws Exception on wpa_cli command failure */ public function setKnownStationsWPA($networks) { $iface = escapeshellarg($_SESSION['wifi_client_interface']); - $output = shell_exec("sudo wpa_cli -i $iface list_networks"); - $lines = explode("\n", $output); - array_shift($lines); - $wpaCliNetworks = []; + $output = shell_exec("sudo wpa_cli -i $iface list_networks 2>&1"); - 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; + if ($output === null) { + throw new \Exception("Failed to execute wpa_cli command - command returned null"); + } + + // check for common wpa_cli errors and try to fix them + if (strpos($output, 'Failed to connect') !== false || strpos($output, 'No such file or directory') !== false) { + error_log("wpa_supplicant not available for interface, attempting to start it"); + + // try starting wpa_supplicant for this interface + $unescapedIface = trim($iface, "'\""); + $startCmd = "sudo /sbin/wpa_supplicant -i $unescapedIface -c /etc/wpa_supplicant/wpa_supplicant.conf -B 2>&1"; + $startResult = shell_exec($startCmd); + sleep(2); + + // retry + $output = shell_exec("sudo wpa_cli -i $iface list_networks 2>&1"); + + // tf it still fails, throw an exception + if ($output === null || strpos($output, 'Failed to connect') !== false) { + throw new \Exception("Failed to start wpa_supplicant for interface: " . trim($startResult ?? 'unknown error')); } } - foreach ($networks as $network) { - $ssid = $network['ssid']; - if (!$this->networkExists($ssid, $wpaCliNetworks)) { - $ssid = escapeshellarg('"'.$network['ssid'].'"'); - $psk = escapeshellarg('"'.$network['passphrase'].'"'); - $protocol = $network['protocol']; - $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" - ]; - if ($protocol === 'Open') { - $commands[1] = "sudo wpa_cli -i $iface set_network $netid key_mgmt NONE"; - } - foreach ($commands as $cmd) { - exec($cmd); - usleep(1000); + + // split output into lines + $lines = explode("\n", trim($output)); + + // check for header line + if (empty($lines) || count($lines) < 1) { + error_log("wpa_cli list_networks returned no output"); + $wpaCliNetworks = []; + } else { + // remove header line if it exists + $headerLine = trim($lines[0]); + if (strpos($headerLine, 'network id') !== false || strpos($headerLine, 'id') !== false) { + array_shift($lines); + } + + $wpaCliNetworks = []; + foreach ($lines as $line) { + $trimmedLine = trim($line); + + // skip empty lines + if (empty($trimmedLine)) { + continue; + } + + $data = explode("\t", $trimmedLine); + if (count($data) >= 2) { + $id = trim($data[0]); + $ssid = trim($data[1]); + + // add if we have valid data + if ($id !== '' && $ssid !== '') { + $wpaCliNetworks[] = [ + 'id' => $id, + 'ssid' => $ssid + ]; } } } } + + // process networks to add + foreach ($networks as $network) { + if (!isset($network['ssid']) || empty($network['ssid'])) { + error_log("Skipping network with missing or empty SSID"); + continue; + } + + $ssid = $network['ssid']; + if (!$this->networkExists($ssid, $wpaCliNetworks)) { + $this->addWpaNetwork($network, $iface); + } + } + } + + /** + * Helper method to add a single network to wpa_supplicant + * + * @param array $network Network configuration + * @param string $iface Escaped shell argument for interface + */ + private function addWpaNetwork($network, $iface) + { + $ssid = escapeshellarg('"' . $network['ssid'] . '"'); + $psk = escapeshellarg('"' . $network['passphrase'] . '"'); + $protocol = $network['protocol'] ?? 'WPA'; + + // add network and get its ID + $netid = trim(shell_exec("sudo wpa_cli -i $iface add_network 2>&1")); + + // validate network ID + if (!$netid || !is_numeric($netid)) { + error_log("Failed to add network '{$network['ssid']}': Invalid network ID returned: '$netid'"); + return; + } + + // prepare command based on protocol + $commands = [ + "sudo wpa_cli -i $iface set_network $netid ssid $ssid", + ]; + + if (strtolower($protocol) === 'open') { + $commands[] = "sudo wpa_cli -i $iface set_network $netid key_mgmt NONE"; + } else { + $commands[] = "sudo wpa_cli -i $iface set_network $netid psk $psk"; + } + + $commands[] = "sudo wpa_cli -i $iface enable_network $netid"; + + // execute commands, checking errors + foreach ($commands as $cmd) { + $result = shell_exec("$cmd 2>&1"); + if ($result === null || strpos($result, 'FAIL') !== false) { + error_log("Command failed: $cmd - Result: " . ($result ?? 'null')); + // remove the failed network + shell_exec("sudo wpa_cli -i $iface remove_network $netid 2>&1"); + return; + } + usleep(1000); + } + error_log("Successfully added network: {$network['ssid']}"); } /* From 3922832b5330181df11227b7e338b4f79c74cf8e Mon Sep 17 00:00:00 2001 From: billz Date: Mon, 28 Jul 2025 15:56:31 -0700 Subject: [PATCH 105/122] Fix: update w/ CSRF::hiddenField() --- templates/wifi_stations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/wifi_stations.php b/templates/wifi_stations.php index 2816d71a..fb884cca 100755 --- a/templates/wifi_stations.php +++ b/templates/wifi_stations.php @@ -5,7 +5,7 @@

wpa_supplicant.") ?>

- +
" />
From 58e0867c1e2f1a461c3cb4675927b0e53221aad2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:00:21 +0000 Subject: [PATCH 106/122] Bump brace-expansion from 1.1.11 to 1.1.12 Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12. - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ebfbf189..7dfe1b20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -484,9 +484,9 @@ bootstrap@4.3.1: integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" From 63491b17d64ce94de994545a3ddd03cb97624234 Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Aug 2025 13:12:39 -0700 Subject: [PATCH 107/122] Run wpa_supplicant in background mode (-B) --- installers/raspap.sudoers | 1 + src/RaspAP/Networking/Hotspot/WiFiManager.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index e3985826..bd073f33 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -5,6 +5,7 @@ www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-[a-zA-Z0 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 -i[a-zA-Z0-9]* +www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -i [a-zA-Z0-9]* -c /etc/wpa_supplicant/wpa_supplicant.conf -B 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 diff --git a/src/RaspAP/Networking/Hotspot/WiFiManager.php b/src/RaspAP/Networking/Hotspot/WiFiManager.php index bf4e4e32..316ba074 100644 --- a/src/RaspAP/Networking/Hotspot/WiFiManager.php +++ b/src/RaspAP/Networking/Hotspot/WiFiManager.php @@ -183,7 +183,7 @@ class WiFiManager if (preg_match('/ESSID:\"([^"]+)\"/i', $line, $iwconfig_ssid)) { $ssid=hexSequence2lower($iwconfig_ssid[1]); $networks[$ssid]['connected'] = true; - $check=detectCaptivePortal($_SESSION['wifi_client_interface']); + //$check=detectCaptivePortal($_SESSION['wifi_client_interface']); $networks[$ssid]["portal-url"]=$check["URL"]; } } @@ -248,9 +248,9 @@ class WiFiManager */ public function reinitializeWPA($force) { - $iface = escapeshellarg($_SESSION['wifi_client_interface']); + $iface = $_SESSION['wifi_client_interface']; if ($force == true) { - $cmd = "sudo wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i$iface"; + $cmd = "sudo /sbin/wpa_supplicant -i $unescapedIface -c /etc/wpa_supplicant/wpa_supplicant.conf -B 2>&1"; $result = shell_exec($cmd); } $cmd = "sudo wpa_cli -i $iface reconfigure"; @@ -333,7 +333,7 @@ class WiFiManager // retry $output = shell_exec("sudo wpa_cli -i $iface list_networks 2>&1"); - // tf it still fails, throw an exception + // if it still fails, throw an exception if ($output === null || strpos($output, 'Failed to connect') !== false) { throw new \Exception("Failed to start wpa_supplicant for interface: " . trim($startResult ?? 'unknown error')); } From 7f2eb6e88fcf3546363e4d86a241c4301970e15d Mon Sep 17 00:00:00 2001 From: billz Date: Sat, 2 Aug 2025 13:13:41 -0700 Subject: [PATCH 108/122] Coalesce dhcp-option=6 lines, prevents invalid config --- .../Networking/Hotspot/DnsmasqManager.php | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php index 4942ffe2..49bb8ee7 100644 --- a/src/RaspAP/Networking/Hotspot/DnsmasqManager.php +++ b/src/RaspAP/Networking/Hotspot/DnsmasqManager.php @@ -97,12 +97,19 @@ class DnsmasqManager } } if (!empty($syscfg['dhcp-option'])) { - if (is_array($syscfg['dhcp-option'])) { - foreach ($syscfg['dhcp-option'] as $opt) { - $config[] = 'dhcp-option=' . $opt; + $dhcpOptions = (array) $syscfg['dhcp-option']; + $grouped = []; + + foreach ($dhcpOptions as $opt) { + $parts = explode(',', $opt, 2); + if (count($parts) < 2) { + continue; // skip malformed option } - } else { - $config[] = 'dhcp-option=' . $syscfg['dhcp-option']; + list($code, $value) = $parts; + $grouped[$code][] = $value; + } + foreach ($grouped as $code => $values) { + $config[] = 'dhcp-option=' . $code . ',' . implode(',', $values); } } $config[] = PHP_EOL; @@ -155,17 +162,17 @@ class DnsmasqManager foreach ($post_data['server'] as $server) { $config[] = "server=$server"; } - if ($post_data['DNS1']) { - $config[] = "dhcp-option=6," . $post_data['DNS1']; - if ($post_data['DNS2']) { - $config[] = ','.$post_data['DNS2']; + if (!empty($post_data['DNS1'])) { + $dnsOption = "dhcp-option=6," . $post_data['DNS1']; + if (!empty($post_data['DNS2'])) { + $dnsOption .= ',' . $post_data['DNS2']; } - $config[]= PHP_EOL; + $config[] = $dnsOption; } if ($post_data['dhcp-ignore'] == "1") { $config[] = 'dhcp-ignore=tag:!known'; } - + $config[]= PHP_EOL; return $config; } From cbc6221420c6e4fa52e505e8d6e3014096b9cd8b Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 12 Aug 2025 10:34:24 -0700 Subject: [PATCH 109/122] Update release version --- README.md | 2 +- includes/defaults.php | 2 +- index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f3c38fe6..c550a029 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![RaspAP Custom OS images](https://github.com/user-attachments/assets/e871adf1-123c-450b-94eb-80a185c242cc) -[![Release 3.3.8](https://img.shields.io/badge/release-v3.3.8-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) +[![Release 3.3.9](https://img.shields.io/badge/release-v3.3.9-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. diff --git a/includes/defaults.php b/includes/defaults.php index 39d26491..21ef115e 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) { $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel', - 'RASPI_VERSION' => '3.3.8', + 'RASPI_VERSION' => '3.3.9', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', diff --git a/index.php b/index.php index 8250a153..83f34b46 100755 --- a/index.php +++ b/index.php @@ -14,7 +14,7 @@ * @author Lawrence Yau * @author Bill Zimmerman * @license GNU General Public License, version 3 (GPL-3.0) - * @version 3.3.8 + * @version 3.3.9 * @link https://github.com/RaspAP/raspap-webgui/ * @link https://raspap.com/ * @see http://sirlagz.net/2013/02/08/raspap-webgui/ From e39d35a395251792020ae7b0f0f5c807fdaa3b4a Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 12 Aug 2025 13:11:59 -0700 Subject: [PATCH 110/122] Re-add js/vendor ignored in .gitignore --- .gitignore | 1 - app/js/vendor/bandwidthcharts.js | 147 ++++++++++++++++++++++++ app/js/vendor/dashboardchart.js | 106 +++++++++++++++++ app/js/vendor/huebee.js | 22 ++++ app/js/vendor/speedtestUI.js | 189 +++++++++++++++++++++++++++++++ 5 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 app/js/vendor/bandwidthcharts.js create mode 100644 app/js/vendor/dashboardchart.js create mode 100644 app/js/vendor/huebee.js create mode 100644 app/js/vendor/speedtestUI.js diff --git a/.gitignore b/.gitignore index 25983d20..ddfb310f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ yarn-error.log *.swp includes/config.php rootCA.pem -vendor .env locale/**/*.mo app/net_activity diff --git a/app/js/vendor/bandwidthcharts.js b/app/js/vendor/bandwidthcharts.js new file mode 100644 index 00000000..2bc63d72 --- /dev/null +++ b/app/js/vendor/bandwidthcharts.js @@ -0,0 +1,147 @@ +(function($, _t) { + "use strict"; + + /** + * Create a Chart.js barchart. + */ + function CreateChart(ctx, labels) { + var barchart = new Chart(ctx,{ + type: 'line', + options: { + responsive: true, + scales: { + xAxes: [{ + scaleLabel: { + display: true, + labelString: 'date', + }, + ticks: { + maxRotation: 90, + minRotation: 80 + } + }], + yAxes: [{ + id: 'y-axis-1', + type: 'linear', + display: true, + position: 'left', + ticks: { + beginAtZero: true + } + }] + } + }, + data: { + labels: labels, + datasets: [] + } + }); + + return barchart; + } + + /** + * Create a jquery bootstrap datatable. + */ + function CreateDataTable(placeholder, timeunits) { + $("#"+placeholder).append(''+ + '
daterxtx
'); + } + + /** + * Figure out which tab is selected and remove all existing charts and then + * construct the proper barchart. + */ + function ShowBandwidthChartHandler(e) { + // Remove all chartjs charts + $('#divChartBandwidthhourly').empty(); + $('#divChartBandwidthdaily').empty(); + $('#divChartBandwidthmonthly').empty(); + // Remove all datatables + $('#divTableBandwidthhourly').empty(); + $('#divTableBandwidthdaily').empty(); + $('#divTableBandwidthmonthly').empty(); + // Construct ajax uri for getting the proper data. + var timeunit = $('ul#tabbarBandwidth li.nav-item a.nav-link.active').attr('href').substr(1); + var uri = 'ajax/bandwidth/get_bandwidth.php?'; + uri += 'inet='; + uri += encodeURIComponent($('#cbxInterface'+timeunit+' option:selected').text()); + uri += '&tu='; + uri += encodeURIComponent(timeunit.substr(0, 1)); + var datasizeunits = 'mb'; + uri += '&dsu='+encodeURIComponent(datasizeunits); + // Init. datatable html + var datatable = CreateDataTable('divTableBandwidth'+timeunit, timeunit); + // Get data for chart + $.ajax({ + url: uri, + dataType: 'json', + beforeSend: function() { + $('#divLoaderBandwidth'+timeunit).show(); + } + }).done(function(jsondata) { + $('#divLoaderBandwidth'+timeunit).hide(); + // Map json values to label array + var labels = jsondata.map(function(e) { + return e.date; + }); + // Init. chart with label series + var barchart = CreateChart('divChartBandwidth'+timeunit, labels); + var dataRx = jsondata.map(function(e) { + return e.rx; + }); + var dataTx = jsondata.map(function(e) { + return e.tx; + }); + + addData(barchart, dataRx, dataTx, datasizeunits); + $('#tableBandwidth'+timeunit).DataTable({ + 'searching': false, + 'paging': false, + 'data': jsondata, + 'order': [[ 0, 'ASC' ]], + 'columns': [ + { 'data': 'date' }, + { 'data': 'rx', "title": _t['receive']+' '+datasizeunits.toUpperCase() }, + { 'data': 'tx', "title": _t['send']+' '+datasizeunits.toUpperCase() }] + }); + }).fail(function(xhr, textStatus) { + if (window.console) { + console.error('server error'); + } else { + alert("server error"); + } + }); + } + /** + * Add data array to datasets of current chart. + */ + function addData(chart, dataRx, dataTx, datasizeunits) { + chart.data.datasets.push({ + label: 'Receive'+' '+datasizeunits.toUpperCase(), + yAxisID: 'y-axis-1', + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + data: dataRx + }); + chart.data.datasets.push({ + label: 'Send'+' '+datasizeunits.toUpperCase(), + yAxisID: 'y-axis-1', + borderColor: 'rgba(192, 192, 192, 1)', + backgroundColor: 'rgba(192, 192, 192, 0.2)', + data: dataTx + }); + chart.update(); + } + + $(document).ready(function() { + $('#tabbarBandwidth a[data-toggle="tab"]').on('shown.bs.tab', ShowBandwidthChartHandler); + $('#cbxInterfacehourly').on('change', ShowBandwidthChartHandler); + $('#cbxInterfacedaily').on('change', ShowBandwidthChartHandler); + $('#cbxInterfacemonthly').on('change', ShowBandwidthChartHandler); + ShowBandwidthChartHandler(); + }); + +})(jQuery, t); + diff --git a/app/js/vendor/dashboardchart.js b/app/js/vendor/dashboardchart.js new file mode 100644 index 00000000..e9b039fa --- /dev/null +++ b/app/js/vendor/dashboardchart.js @@ -0,0 +1,106 @@ +(function($, _t) { + "use strict"; + + /** + * Create a Chart.js barchart. + */ + function CreateChart(ctx, labels) { + var barchart = new Chart(ctx,{ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + xAxes: [{ + scaleLabel: { + display: true + }, + ticks: { + maxRotation: 0, + minRotation: 0 + } + }], + yAxes: [{ + id: 'y-axis-1', + type: 'linear', + display: true, + position: 'left', + ticks: { + beginAtZero: true + } + }] + } + }, + data: { + labels: labels, + datasets: [] + } + }); + return barchart; + } + + function ShowBandwidthChartHandler(e) { + // Remove hourly chartjs chart + $('#divDBChartBandwidthhourly').empty(); + // Construct ajax uri for getting the proper data + var timeunit = 'hourly'; + var uri = 'ajax/bandwidth/get_bandwidth.php?'; + uri += 'inet='; + uri += encodeURIComponent($('#divInterface').text()); + uri += '&tu='; + uri += encodeURIComponent(timeunit.substr(0, 1)); + var datasizeunits = 'mb'; + uri += '&dsu='+encodeURIComponent(datasizeunits); + // Get data for chart + $.ajax({ + url: uri, + dataType: 'json', + beforeSend: function() {} + }).done(function(jsondata) { + // Map json values to label array + var labels = jsondata.map(function(e) { + return e.date; + }); + // Init. chart with label series + var barchart = CreateChart('divDBChartBandwidth'+timeunit, labels); + var dataRx = jsondata.map(function(e) { + return e.rx; + }); + var dataTx = jsondata.map(function(e) { + return e.tx; + }); + addData(barchart, dataTx, dataRx, datasizeunits); + }).fail(function(xhr, textStatus) { + if (window.console) { + console.error('server error'); + } else { + alert("server error"); + } + }); + } + /** + * Add data array to datasets of current chart. + */ + function addData(chart, dataTx, dataRx, datasizeunits) { + chart.data.datasets.push({ + label: 'Send'+' '+datasizeunits.toUpperCase(), + yAxisID: 'y-axis-1', + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + data: dataTx + }); + chart.data.datasets.push({ + label: 'Receive'+' '+datasizeunits.toUpperCase(), + yAxisID: 'y-axis-1', + borderColor: 'rgba(192, 192, 192, 1)', + backgroundColor: 'rgba(192, 192, 192, 0.2)', + data: dataRx + }); + chart.update(); + } + $(document).ready(function() { + ShowBandwidthChartHandler(); + }); + +})(jQuery, t); + diff --git a/app/js/vendor/huebee.js b/app/js/vendor/huebee.js new file mode 100644 index 00000000..486d28b2 --- /dev/null +++ b/app/js/vendor/huebee.js @@ -0,0 +1,22 @@ +// Initialize Huebee color picker +var elem = document.querySelector('.color-input'); +var hueb = new Huebee( elem, { + notation: 'hex', + saturations: 2, + customColors: [ '#d8224c', '#dd4814', '#ea0', '#19f', '#333' ], + className: 'light-picker', + hue0: 210 +}); + +// Set custom color if defined +var color = getCookie('color'); +if (color == null || color == '') { + color = '#2b8080'; +} +hueb.setColor(color); + +// Change event +hueb.on( 'change', function( color, hue, sat, lum ) { + setCookie('color',color,90); +}) + diff --git a/app/js/vendor/speedtestUI.js b/app/js/vendor/speedtestUI.js new file mode 100644 index 00000000..e97b7c09 --- /dev/null +++ b/app/js/vendor/speedtestUI.js @@ -0,0 +1,189 @@ +function I(i){return document.getElementById(i);} + +const origin=window.location.origin; +const host=window.location.host; +var SPEEDTEST_SERVERS=[ + { + "name":"RaspAP Speedtest server (US)", + "server":"https://speedtest.raspap.com/", + "dlURL":"backend/garbage.php", + "ulURL":"backend/empty.php", + "pingURL":"backend/empty.php", + "getIpURL":"backend/getIP.php" + }, + { + "name":"RaspAP ("+host+")", + "server":origin, + "dlURL":"dist/speedtest/backend/garbage.php", + "ulURL":"dist/speedtest/backend/empty.php", + "pingURL":"dist/speedtest/backend/empty.php", + "getIpURL":"dist/speedtest/backend/getIP.php" + } +]; + +//INITIALIZE SPEEDTEST +var s=new Speedtest(); //create speedtest object +s.setParameter("telemetry_level","basic"); //enable telemetry + +//SERVER AUTO SELECTION +function initServers(){ + var noServersAvailable=function(){ + I("message").innerHTML="No servers available"; + } + var runServerSelect=function(){ + s.selectServer(function(server){ + if(server!=null){ //at least 1 server is available + I("loading").className="hidden"; //hide loading message + //populate server list for manual selection + for(var i=0;i Date: Tue, 12 Aug 2025 13:26:00 -0700 Subject: [PATCH 111/122] Fix: Return expected datatype from getHostapdIni() --- src/RaspAP/Networking/Hotspot/HotspotService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HotspotService.php b/src/RaspAP/Networking/Hotspot/HotspotService.php index e86185eb..e1f7b46f 100644 --- a/src/RaspAP/Networking/Hotspot/HotspotService.php +++ b/src/RaspAP/Networking/Hotspot/HotspotService.php @@ -205,13 +205,13 @@ class HotspotService * * @return array $config */ - public function getHostapdIni() + public function getHostapdIni(): array { $hostapdIni = RASPI_CONFIG . '/hostapd.ini'; if (file_exists($hostapdIni)) { - $config = parse_ini_file($hostapdIni); - return $config; + return parse_ini_file($hostapdIni) ?: []; } + return []; } /** From b1d776aa64f3d11f064db615ede0450f4d0b08c4 Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 12 Aug 2025 13:32:53 -0700 Subject: [PATCH 112/122] Update release version --- README.md | 2 +- includes/defaults.php | 2 +- index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c550a029..523539fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ![RaspAP Custom OS images](https://github.com/user-attachments/assets/e871adf1-123c-450b-94eb-80a185c242cc) -[![Release 3.3.9](https://img.shields.io/badge/release-v3.3.9-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) +[![Release 3.4.0](https://img.shields.io/badge/release-v3.4.0-green)](https://github.com/raspap/raspap-webgui/releases) [![Awesome](https://awesome.re/badge.svg)](https://github.com/thibmaek/awesome-raspberry-pi) [![Join Insiders](https://img.shields.io/static/v1?label=Insiders&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/RaspAP) [![Build Status](https://app.travis-ci.com/RaspAP/raspap-webgui.svg?branch=master)](https://app.travis-ci.com/RaspAP/raspap-webgui) [![Crowdin](https://badges.crowdin.net/raspap/localized.svg)](https://crowdin.com/project/raspap) [![Twitter URL](https://img.shields.io/twitter/url?label=%40RaspAP&logoColor=%23d8224c&url=https%3A%2F%2Ftwitter.com%2Frasp_ap)](https://twitter.com/rasp_ap) [![Reddit](https://img.shields.io/badge/%2Fr%2FRaspAP-e05d44?style=flat&logo=Reddit&logoColor=white&labelColor=e05d44&color=b14835)](https://reddit.com/r/RaspAP) [![Discord](https://img.shields.io/discord/642436993451819018?color=7289DA&label=Discord&logo=discord&style=flat)](https://discord.gg/KVAsaAR) RaspAP is feature-rich wireless router software that _just works_ on many popular [Debian-based devices](#supported-operating-systems), including the Raspberry Pi. Our [custom OS images](#pre-built-image), [Quick installer](#quick-installer) and [Docker container](#docker-support) create a known-good default configuration for all current Raspberry Pis with onboard wireless. A fully responsive, mobile-ready interface gives you control over the relevant services and networking options. Advanced DHCP settings, [WireGuard](https://docs.raspap.com/wireguard/), [Tailscale](https://docs.raspap.com/tailscale/) and [OpenVPN](https://docs.raspap.com/openvpn/) support, [SSL certificates](https://docs.raspap.com/ssl/), [ad blocking](#ad-blocking), security audits, [captive portal integration](https://docs.raspap.com/captive/), themes and [multilingual options](https://docs.raspap.com/translations/) are included. diff --git a/includes/defaults.php b/includes/defaults.php index 21ef115e..a07f5f3f 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -7,7 +7,7 @@ if (!defined('RASPI_CONFIG')) { $defaults = [ 'RASPI_BRAND_TEXT' => 'RaspAP', 'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel', - 'RASPI_VERSION' => '3.3.9', + 'RASPI_VERSION' => '3.4.0', 'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json', 'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json', 'RASPI_CONFIG_API' => RASPI_CONFIG.'/api', diff --git a/index.php b/index.php index 83f34b46..2e15ab58 100755 --- a/index.php +++ b/index.php @@ -14,7 +14,7 @@ * @author Lawrence Yau * @author Bill Zimmerman * @license GNU General Public License, version 3 (GPL-3.0) - * @version 3.3.9 + * @version 3.4.0 * @link https://github.com/RaspAP/raspap-webgui/ * @link https://raspap.com/ * @see http://sirlagz.net/2013/02/08/raspap-webgui/ From c3cc4ff9dbd30ebc3b28dea4eb7d1a6aa9aa8faa Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 13 Aug 2025 09:49:46 -0700 Subject: [PATCH 113/122] Update plugins submodule pointer --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index a9bfe516..b8e51de4 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit a9bfe51603e87c86982bb0ac9f68af7522ed8dbf +Subproject commit b8e51de4480fa502ab88e861604f13a91c2233a4 From 7976d77ac1dc4752b9f87ab280dc60830a648201 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 17 Aug 2025 13:00:19 -0700 Subject: [PATCH 114/122] Define hostapd settings per hw_mode --- config/defaults.json | 184 +++++++++++++++++++++++++++++-------------- 1 file changed, 124 insertions(+), 60 deletions(-) diff --git a/config/defaults.json b/config/defaults.json index 093715fb..383c1c30 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -1,68 +1,132 @@ { - "dhcp": { - "wlan0": { - "static ip_address": [ "10.3.141.1/24" ], - "static routers": [ "10.3.141.1" ], - "static domain_name_server": [ "1.1.1.1 8.8.8.8" ], - "subnetmask": [ "255.255.255.0" ] + "hostapd":{ + "modes":{ + "n":{ + "settings":[ + "hw_mode=g", + "ieee80211n=1", + "wmm_enabled=1" + ] + }, + "ac":{ + "settings":[ + "hw_mode=a", + "# N", + "ieee80211n=1", + "require_ht=1", + "ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]", + "# AC", + "ieee80211ac=1", + "require_vht=1", + "ieee80211d=0", + "ieee80211h=0", + "vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]", + "vht_oper_chwidth=1", + "vht_oper_centr_freq_seg0_idx={VHT_FREQ_IDX}" + ] + }, + "g":{ + "settings":[ + "hw_mode=g", + "ieee80211n=0" + ] + }, + "a":{ + "settings":[ + "hw_mode=a", + "ieee80211n=0" + ] + } }, - "wlan1": { - "static ip_address": [ "10.9.141.1/24" ], - "static routers": [ "10.9.141.1" ], - "static domain_name_server": [ "1.1.1.1 8.8.8.8" ], - "subnetmask": [ "255.255.255.0" ] + "mappings":{ + "wpa_key_mgmt":{ + "0":"NONE", + "1":"WPA-PSK", + "2":"WPA-PSK WPA-PSK-SHA256 SAE", + "3":"SAE" + }, + "ieee80211w_wpa":{ + "1":"0", + "2":"0", + "3":"2", + "4":"1", + "5":"2" + }, + "wpa_numeric":{ + "0":"0", + "1":1, + "2":2, + "3":2, + "4":2, + "5":2, + "none":"none" + } }, - "uap0": { - "static ip_address": [ "192.168.50.1/24" ], - "static routers": [ "192.168.50.1" ], - "static domain_name_server": [ "1.1.1.1 8.8.8.8" ], - "subnetmask": [ "255.255.255.0" ] + "dhcp":{ + "wlan0":{ + "static ip_address":[ "10.3.141.1/24" ], + "static routers":[ "10.3.141.1" ], + "static domain_name_server":[ "1.1.1.1 8.8.8.8" ], + "subnetmask":[ "255.255.255.0" ] + }, + "wlan1":{ + "static ip_address":[ "10.9.141.1/24" ], + "static routers":[ "10.9.141.1" ], + "static domain_name_server":[ "1.1.1.1 8.8.8.8" ], + "subnetmask":[ "255.255.255.0" ] + }, + "uap0":{ + "static ip_address":[ "192.168.50.1/24" ], + "static routers":[ "192.168.50.1" ], + "static domain_name_server":[ "1.1.1.1 8.8.8.8" ], + "subnetmask":[ "255.255.255.0" ] + }, + "options":{ + "# RaspAP default configuration":null, + "hostname":null, + "clientid":null, + "persistent":null, + "option rapid_commit":null, + "option domain_name_servers, domain_name, domain_search, host_name":null, + "option classless_static_routes":null, + "option ntp_servers":null, + "require dhcp_server_identifier":null, + "slaac private":null, + "nohook lookup-hostname":null + } }, - "options": { - "# RaspAP default configuration": null, - "hostname": null, - "clientid": null, - "persistent": null, - "option rapid_commit": null, - "option domain_name_servers, domain_name, domain_search, host_name": null, - "option classless_static_routes": null, - "option ntp_servers": null, - "require dhcp_server_identifier": null, - "slaac private": null, - "nohook lookup-hostname": null - } - }, - "dnsmasq": { - "wlan0": { - "dhcp-range": [ "10.3.141.50,10.3.141.254,255.255.255.0,12h" ] + "dnsmasq":{ + "wlan0":{ + "dhcp-range":[ "10.3.141.50,10.3.141.254,255.255.255.0,12h" ] + }, + "wlan1":{ + "dhcp-range":[ "10.9.141.50,10.9.141.254,255.255.255.0,12h" ] + }, + "uap0":{ + "dhcp-range":[ "192.168.50.50,192.168.50.150,12h" ] + } }, - "wlan1": { - "dhcp-range": [ "10.9.141.50,10.9.141.254,255.255.255.0,12h" ] - }, - "uap0": { - "dhcp-range": [ "192.168.50.50,192.168.50.150,12h" ] - } - }, - "wireguard": { - "server": { - "Address": [ "10.8.2.1/24" ], - "ListenPort": [ "51820" ], - "DNS": [ "9.9.9.9" ], - "PostUp": [ "iptables -A FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -A FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE" ], - "PostDown": [ "iptables -D FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -D FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE" ], - "PostUpEx": [ "iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ], - "PreDown": [ "iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ] - }, - "peer": { - "Address": [ "10.8.1.2/24" ], - "Endpoint": [ "10.8.2.1:51820" ], - "ListenPort": [ "21841" ], - "AllowedIPs": ["10.8.2.0/24"], - "PersistentKeepalive": [ "15" ] - } - }, - "txpower": { - "dbm": [ "auto", "30", "20", "17", "10", "6", "3", "1", "0" ] + "wireguard":{ + "server":{ + "Address":[ "10.8.2.1/24" ], + "ListenPort":[ "51820" ], + "DNS":[ "9.9.9.9" ], + "PostUp":[ "iptables -A FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -A FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE" ], + "PostDown":[ "iptables -D FORWARD -i wlan0 -o wg0 -j ACCEPT; iptables -D FORWARD -i wg0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE" ], + "PostUpEx":[ "iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ], + "PreDown":[ "iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL ! -d %s -j REJECT" ] + }, + "peer":{ + "Address":[ "10.8.1.2/24" ], + "Endpoint":[ "10.8.2.1:51820" ], + "ListenPort":[ "21841" ], + "AllowedIPs":[ "10.8.2.0/24" ], + "PersistentKeepalive":[ "15" ] + } + }, + "txpower": { + "dbm": [ "auto", "30", "20", "17", "10", "6", "3", "1", "0" ] + } } } From d1c2e0d3bab7733ce3a09fb98f19b290608d35d5 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 17 Aug 2025 13:01:58 -0700 Subject: [PATCH 115/122] Revise buildConfig() to parse settings from getDefaultNetValue --- .../Networking/Hotspot/HostapdManager.php | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index 593e93de..c06649fc 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -143,78 +143,87 @@ class HostapdManager public function buildConfig(array $params, StatusMessage $status): string { $config = []; + + // core static values $config[] = 'driver=nl80211'; $config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE; $config[] = 'ctrl_interface_group=0'; $config[] = 'auth_algs=1'; - $wpa = $params['wpa']; - $wpa_key_mgmt = 'WPA-PSK'; + $mappings = getDefaultNetValue('hostapd', 'mappings', 'all') ?? []; - if ($wpa == 4) { - $config[] = 'ieee80211w=1'; - $wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE'; - $wpa = 2; - } elseif ($wpa == 5) { - $config[] = 'ieee80211w=2'; - $wpa_key_mgmt = 'SAE'; - $wpa = 2; + $wpa = isset($params['wpa']) ? $params['wpa'] : 'none'; + + $ieee80211w = null; + if (isset($params['80211w']) && $params['80211w'] !== '') { + $ieee80211w = (string)$params['80211w']; + } elseif (!empty($mappings['ieee80211w_wpa']) && isset($mappings['ieee80211w_wpa'][(string)$wpaCode])) { + $ieee80211w = (string)$mappings['ieee80211w_wpa'][(string)$wpaCode]; } - if ($params['80211w'] == 1) { - $config[] = 'ieee80211w=1'; + if ($ieee80211w !== null && $ieee80211w !== '' && $ieee80211w !== '0') { + $config[] = 'ieee80211w=' . $ieee80211w; + } + + $wpa_key_mgmt = null; + if (!empty($params['wpa_key_mgmt'])) { + $wpa_key_mgmt = $params['wpa_key_mgmt']; + } elseif (!empty($mappings['wpa_key_mgmt']) && isset($mappings['wpa_key_mgmt'][(string)$wpaCode])) { + $wpa_key_mgmt = $mappings['wpa_key_mgmt'][(string)$wpaCode]; + } elseif ($wpaCode !== 'none') { + // fallback sensible default $wpa_key_mgmt = 'WPA-PSK'; - } elseif ($params['80211w'] == 2) { - $config[] = 'ieee80211w=2'; - $wpa_key_mgmt = 'WPA-PSK-SHA256'; } - $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; + if ($wpa_key_mgmt !== null) { + $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; + } + + $wpa_numeric = $wpa; + if (!empty($mappings['wpa_numeric']) && isset($mappings['wpa_numeric'][(string)$wpa])) { + $wpa_numeric = (int)$mappings['wpa_numeric'][(string)$wpa]; + } else { + // ensure int or none + $wpa_numeric = ($wpa === 'none') ? 'none' : (int)$wpa; + } if (!empty($params['beacon_interval'])) { - $config[] = 'beacon_int=' . $params['beacon_interval']; + $config[] = 'beacon_int=' . intval($params['beacon_interval']); } if (!empty($params['disassoc_low_ack'])) { $config[] = 'disassoc_low_ack=0'; } + // SSID and channel (required) $config[] = 'ssid=' . $params['ssid']; $config[] = 'channel=' . $params['channel']; - // Choose VHT segment index (fallback only if required) + // choose VHT segment index (fallback only if required) $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; - switch ($params['hw_mode']) { - case 'n': - $config[] = 'hw_mode=g'; - $config[] = 'ieee80211n=1'; - $config[] = 'wmm_enabled=1'; - break; - case 'ac': - $config[] = 'hw_mode=a'; - $config[] = '# N'; - $config[] = 'ieee80211n=1'; - $config[] = 'require_ht=1'; - $config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'; - $config[] = '# AC'; - $config[] = 'ieee80211ac=1'; - $config[] = 'require_vht=1'; - $config[] = 'ieee80211d=0'; - $config[] = 'ieee80211h=0'; - $config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'; - $config[] = 'vht_oper_chwidth=1'; - $config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx; - break; - default: - $config[] = 'hw_mode=' . $params['hw_mode']; - $config[] = 'ieee80211n=0'; + $hwMode = isset($params['hw_mode']) ? $params['hw_mode'] : ''; + + // fetch settings for selected mode + $modeSettings = getDefaultNetOpts('hostapd', 'modes', $hwMode); + $settings = $modeSettings[$hwMode]['settings'] ?? []; + + if (!empty($settings)) { + foreach ($settings as $line) { + if (!is_string($line)) { + continue; + } + $replaced = str_replace('{VHT_FREQ_IDX}', (string) $vht_freq_idx ?? '',$line); + $config[] = $replaced; + } } - if ($params['wpa'] !== 'none') { + // WPA passphrase + if ($wpa_numeric !== 'none' && !empty($params['wpa_passphrase'])) { $config[] = 'wpa_passphrase=' . $params['wpa_passphrase']; } + // bridge handling if (!empty($params['bridge'])) { $config[] = 'interface=' . $params['interface']; $config[] = 'bridge=' . $params['bridge']; @@ -222,10 +231,11 @@ class HostapdManager $config[] = 'interface=' . $params['interface']; } - $config[] = 'wpa=' . $wpa; - $config[] = 'wpa_pairwise=' . $params['wpa_pairwise']; - $config[] = 'country_code=' . $params['country_code']; - $config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID']; + $config[] = 'wpa=' . $wpa_numeric; + $config[] = 'wpa_pairwise=' . ($params['wpa_pairwise'] ?? ''); + $config[] = 'country_code=' . ($params['country_code'] ?? ''); + $config[] = 'ignore_broadcast_ssid=' . ($params['hiddenSSID'] ?? 0); + if (!empty($params['max_num_sta'])) { $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; } @@ -233,7 +243,7 @@ class HostapdManager // optional additional user config $config[] = $this->parseUserHostapdCfg(); - return implode(PHP_EOL, $config) . PHP_EOL; + return implode(PHP_EOL, array_filter($config, function ($v) { return $v !== null && $v !== ''; })) . PHP_EOL; } /** From c77fc254f6b2c661ab0db1791381146a7bec56f4 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 17 Aug 2025 14:45:06 -0700 Subject: [PATCH 116/122] Remove mappings, add hw_mode b settings --- config/defaults.json | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/config/defaults.json b/config/defaults.json index 383c1c30..8713d5dd 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -36,30 +36,12 @@ "hw_mode=a", "ieee80211n=0" ] - } - }, - "mappings":{ - "wpa_key_mgmt":{ - "0":"NONE", - "1":"WPA-PSK", - "2":"WPA-PSK WPA-PSK-SHA256 SAE", - "3":"SAE" }, - "ieee80211w_wpa":{ - "1":"0", - "2":"0", - "3":"2", - "4":"1", - "5":"2" - }, - "wpa_numeric":{ - "0":"0", - "1":1, - "2":2, - "3":2, - "4":2, - "5":2, - "none":"none" + "b":{ + "settings":[ + "hw_mode=b", + "ieee80211n=0" + ] } }, "dhcp":{ From 75577ecd1d08856276b0c9e2cb64dcbd1cb082d3 Mon Sep 17 00:00:00 2001 From: billz Date: Sun, 17 Aug 2025 14:46:15 -0700 Subject: [PATCH 117/122] Simplify wpa key management, ieee80211w settings --- .../Networking/Hotspot/HostapdManager.php | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/RaspAP/Networking/Hotspot/HostapdManager.php b/src/RaspAP/Networking/Hotspot/HostapdManager.php index c06649fc..c9d0a5ef 100644 --- a/src/RaspAP/Networking/Hotspot/HostapdManager.php +++ b/src/RaspAP/Networking/Hotspot/HostapdManager.php @@ -150,42 +150,28 @@ class HostapdManager $config[] = 'ctrl_interface_group=0'; $config[] = 'auth_algs=1'; - $mappings = getDefaultNetValue('hostapd', 'mappings', 'all') ?? []; + $wpa = $params['wpa']; + $wpa_key_mgmt = 'WPA-PSK'; - $wpa = isset($params['wpa']) ? $params['wpa'] : 'none'; - - $ieee80211w = null; - if (isset($params['80211w']) && $params['80211w'] !== '') { - $ieee80211w = (string)$params['80211w']; - } elseif (!empty($mappings['ieee80211w_wpa']) && isset($mappings['ieee80211w_wpa'][(string)$wpaCode])) { - $ieee80211w = (string)$mappings['ieee80211w_wpa'][(string)$wpaCode]; + if ($wpa == 4) { + $config[] = 'ieee80211w=1'; + $wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE'; + $wpa = 2; + } elseif ($wpa == 5) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'SAE'; + $wpa = 2; } - if ($ieee80211w !== null && $ieee80211w !== '' && $ieee80211w !== '0') { - $config[] = 'ieee80211w=' . $ieee80211w; - } - - $wpa_key_mgmt = null; - if (!empty($params['wpa_key_mgmt'])) { - $wpa_key_mgmt = $params['wpa_key_mgmt']; - } elseif (!empty($mappings['wpa_key_mgmt']) && isset($mappings['wpa_key_mgmt'][(string)$wpaCode])) { - $wpa_key_mgmt = $mappings['wpa_key_mgmt'][(string)$wpaCode]; - } elseif ($wpaCode !== 'none') { - // fallback sensible default + if ($params['80211w'] == 1) { + $config[] = 'ieee80211w=1'; $wpa_key_mgmt = 'WPA-PSK'; + } elseif ($params['80211w'] == 2) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'WPA-PSK-SHA256'; } - if ($wpa_key_mgmt !== null) { - $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; - } - - $wpa_numeric = $wpa; - if (!empty($mappings['wpa_numeric']) && isset($mappings['wpa_numeric'][(string)$wpa])) { - $wpa_numeric = (int)$mappings['wpa_numeric'][(string)$wpa]; - } else { - // ensure int or none - $wpa_numeric = ($wpa === 'none') ? 'none' : (int)$wpa; - } + $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; if (!empty($params['beacon_interval'])) { $config[] = 'beacon_int=' . intval($params['beacon_interval']); @@ -201,7 +187,6 @@ class HostapdManager // choose VHT segment index (fallback only if required) $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; - $hwMode = isset($params['hw_mode']) ? $params['hw_mode'] : ''; // fetch settings for selected mode @@ -231,7 +216,7 @@ class HostapdManager $config[] = 'interface=' . $params['interface']; } - $config[] = 'wpa=' . $wpa_numeric; + $config[] = 'wpa=' . $wpa; $config[] = 'wpa_pairwise=' . ($params['wpa_pairwise'] ?? ''); $config[] = 'country_code=' . ($params['country_code'] ?? ''); $config[] = 'ignore_broadcast_ssid=' . ($params['hiddenSSID'] ?? 0); From de9a3b1fc4fc0245812f2aca18aa3bdf28c31b4a Mon Sep 17 00:00:00 2001 From: billz Date: Tue, 19 Aug 2025 17:17:27 -0700 Subject: [PATCH 118/122] Update class methods + js handler for custom path locations --- app/js/ui/main.js | 2 +- plugins | 2 +- src/RaspAP/Auth/HTTPAuth.php | 3 ++- src/RaspAP/Exceptions/HtmlErrorRenderer.php | 2 +- src/RaspAP/Plugins/PluginInstaller.php | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/js/ui/main.js b/app/js/ui/main.js index c387ea50..008765c6 100644 --- a/app/js/ui/main.js +++ b/app/js/ui/main.js @@ -542,7 +542,7 @@ function disableValidation(form) { function updateActivityLED() { const threshold_bytes = 300; - fetch('/app/net_activity') + fetch('app/net_activity') .then(res => res.text()) .then(data => { const activity = parseInt(data.trim()); diff --git a/plugins b/plugins index b8e51de4..054f6bc0 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit b8e51de4480fa502ab88e861604f13a91c2233a4 +Subproject commit 054f6bc0abe6999ddd629cc6878d5ada1749a6e8 diff --git a/src/RaspAP/Auth/HTTPAuth.php b/src/RaspAP/Auth/HTTPAuth.php index d5fcf215..fe141ae0 100755 --- a/src/RaspAP/Auth/HTTPAuth.php +++ b/src/RaspAP/Auth/HTTPAuth.php @@ -82,9 +82,10 @@ class HTTPAuth session_regenerate_id(true); // generate a new session id session_unset(); // unset all session variables session_destroy(); // destroy the session + $basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); $redirectUrl = $_SERVER['REQUEST_URI']; if (strpos($redirectUrl, '/login') === false) { - header('Location: /login?action=' . urlencode($redirectUrl)); + header('Location: ' . $basePath . '/login?action=' . urlencode(basename($redirectUrl))); exit(); } } diff --git a/src/RaspAP/Exceptions/HtmlErrorRenderer.php b/src/RaspAP/Exceptions/HtmlErrorRenderer.php index 058a2478..9d6b5612 100755 --- a/src/RaspAP/Exceptions/HtmlErrorRenderer.php +++ b/src/RaspAP/Exceptions/HtmlErrorRenderer.php @@ -19,7 +19,7 @@ class HtmlErrorRenderer public function __construct() { $this->charset = 'UTF-8'; - $this->projectDir = $_SERVER['DOCUMENT_ROOT']; + $this->projectDir = dirname(__DIR__, 3); $this->template = '/templates/exception.php'; $this->debug = true; } diff --git a/src/RaspAP/Plugins/PluginInstaller.php b/src/RaspAP/Plugins/PluginInstaller.php index 3df0b067..e6043c8e 100644 --- a/src/RaspAP/Plugins/PluginInstaller.php +++ b/src/RaspAP/Plugins/PluginInstaller.php @@ -32,7 +32,7 @@ class PluginInstaller $this->tempSudoers = '/tmp/090_'; $this->destSudoers = '/etc/sudoers.d/'; $this->refModules = '/refs/heads/master/.gitmodules'; - $this->rootPath = $_SERVER['DOCUMENT_ROOT']; + $this->rootPath = dirname(__DIR__, 3); $this->pluginsManifest = '/plugins/manifest.json'; $this->repoPublic = $this->getRepository(); $this->helperScriptPath = RASPI_CONFIG.'/plugins/plugin_helper.sh'; From 5319b9dbbd9ba58a481e4c970fbc10aac229f297 Mon Sep 17 00:00:00 2001 From: billz Date: Wed, 20 Aug 2025 08:11:51 -0700 Subject: [PATCH 119/122] Handle undefined PATH_INFO value --- includes/page_actions.php | 2 +- plugins | 2 +- src/RaspAP/Plugins/PluginManager.php | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/page_actions.php b/includes/page_actions.php index 4d218850..2b76355c 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -4,7 +4,7 @@ $pluginManager = \RaspAP\Plugins\PluginManager::getInstance(); // Get the requested page $extraFooterScripts = array(); -$page = $_SERVER['PATH_INFO']; +$page = $_SERVER['PATH_INFO'] ?? ''; // Check if any plugin wants to handle the request if (!$pluginManager->handlePageAction($page)) { diff --git a/plugins b/plugins index b8e51de4..054f6bc0 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit b8e51de4480fa502ab88e861604f13a91c2233a4 +Subproject commit 054f6bc0abe6999ddd629cc6878d5ada1749a6e8 diff --git a/src/RaspAP/Plugins/PluginManager.php b/src/RaspAP/Plugins/PluginManager.php index d1de40d5..5fae5fc1 100644 --- a/src/RaspAP/Plugins/PluginManager.php +++ b/src/RaspAP/Plugins/PluginManager.php @@ -76,8 +76,10 @@ class PluginManager * Iterates over registered plugins and calls its associated method * @param string $page */ - public function handlePageAction(string $page): bool + public function handlePageAction(?string $page): bool { + $page = $page ?? ''; + foreach ($this->getInstalledPlugins() as $pluginClass) { $plugin = new $pluginClass($this->pluginPath, $pluginClass); From eca174a20b82a7a443f55b703cd8493c1a72560b Mon Sep 17 00:00:00 2001 From: Bill Zimmerman Date: Wed, 20 Aug 2025 18:47:12 +0200 Subject: [PATCH 120/122] Create torrent.yml --- .github/workflows/torrent.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/torrent.yml diff --git a/.github/workflows/torrent.yml b/.github/workflows/torrent.yml new file mode 100644 index 00000000..ac5531ad --- /dev/null +++ b/.github/workflows/torrent.yml @@ -0,0 +1,27 @@ +name: Torrent Generation + +on: + workflow_dispatch + +jobs: + torrent: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create torrents + uses: devopsx/action-torrent@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + files: | + raspap-bookworm-arm64-lite-*.img.zip + raspap-bookworm-armhf-lite-*.img.zip + + - name: Upload to release + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + files: torrents/*.torrent From d21b1345bb15bd854d5587fa9bd7af7dd9be7e8d Mon Sep 17 00:00:00 2001 From: Bill Zimmerman Date: Wed, 20 Aug 2025 18:50:43 +0200 Subject: [PATCH 121/122] Update torrent.yml --- .github/workflows/torrent.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/torrent.yml b/.github/workflows/torrent.yml index ac5531ad..f2a689f2 100644 --- a/.github/workflows/torrent.yml +++ b/.github/workflows/torrent.yml @@ -1,7 +1,12 @@ name: Torrent Generation on: - workflow_dispatch + workflow_dispatch: + inputs: + ref: + description: 'Specify tag to run on' + required: true + default: 'master' jobs: torrent: @@ -9,8 +14,10 @@ jobs: permissions: contents: write steps: - - name: Checkout + - name: Checkout with ref uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref }} - name: Create torrents uses: devopsx/action-torrent@v1 From 451c76afe8b2e1c89d78c053d115100af4e671e4 Mon Sep 17 00:00:00 2001 From: Bill Zimmerman Date: Wed, 20 Aug 2025 18:58:33 +0200 Subject: [PATCH 122/122] Update torrent.yml --- .github/workflows/torrent.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/torrent.yml b/.github/workflows/torrent.yml index f2a689f2..849bcc98 100644 --- a/.github/workflows/torrent.yml +++ b/.github/workflows/torrent.yml @@ -1,12 +1,8 @@ -name: Torrent Generation +name: Generate torrents on: - workflow_dispatch: - inputs: - ref: - description: 'Specify tag to run on' - required: true - default: 'master' + release: + types: [published] jobs: torrent: @@ -14,10 +10,8 @@ jobs: permissions: contents: write steps: - - name: Checkout with ref + - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.ref }} - name: Create torrents uses: devopsx/action-torrent@v1