diff --git a/.gitignore b/.gitignore
index 71641545..ddfb310f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ yarn-error.log
*.swp
includes/config.php
rootCA.pem
-vendor
.env
locale/**/*.mo
app/net_activity
+app/js/plugins/
diff --git a/BACKERS.md b/BACKERS.md
index 964785a7..f43b880e 100644
--- a/BACKERS.md
+++ b/BACKERS.md
@@ -13,38 +13,47 @@ 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/)
+ ✅ [Inspect network adapters](https://docs.raspap.com/troubleshooting/#inspect-network-adapters)
-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.
[](https://www.youtube.com/watch?v=dEzg92g1LHw)
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).
diff --git a/README.md b/README.md
index 849e2f38..523539fb 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@

-[](https://github.com/raspap/raspap-webgui/releases) [](https://github.com/thibmaek/awesome-raspberry-pi) [](https://github.com/sponsors/RaspAP) [](https://app.travis-ci.com/RaspAP/raspap-webgui) [](https://crowdin.com/project/raspap) [](https://twitter.com/rasp_ap) [](https://reddit.com/r/RaspAP) [](https://discord.gg/KVAsaAR)
+[](https://github.com/raspap/raspap-webgui/releases) [](https://github.com/thibmaek/awesome-raspberry-pi) [](https://github.com/sponsors/RaspAP) [](https://app.travis-ci.com/RaspAP/raspap-webgui) [](https://crowdin.com/project/raspap) [](https://twitter.com/rasp_ap) [](https://reddit.com/r/RaspAP) [](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.
-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).
diff --git a/ajax/adblock/update_blocklist.php b/ajax/adblock/update_blocklist.php
index f21ed4cd..458369ab 100644
--- a/ajax/adblock/update_blocklist.php
+++ b/ajax/adblock/update_blocklist.php
@@ -5,50 +5,57 @@ 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);
+ $scriptPath = RASPI_CONFIG . '/adblock/update_blocklist.sh';
+
+ 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
+ ]);
- 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;
+}
+
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);
}
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)) {
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']; } );
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%;
}
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/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/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/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 "";
}
}
?>
-
+
-
+
-
+
-
+
-
+
diff --git a/app/js/custom.js b/app/js/ajax/main.js
similarity index 50%
rename from app/js/custom.js
rename to app/js/ajax/main.js
index d95b5405..f8bf997a 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 = '
'+msg+'
';
- 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'); }
@@ -248,35 +94,43 @@ 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());
+ const row = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+ leaseContainer.append(row);
+ });
+ }
});
}
-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 +181,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 +222,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 +252,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 +282,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 +348,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: "___.___.___.___"
- });
- $('.date').mask('FF:FF:FF:FF:FF:FF', {
- translation: {
- "F": {
- pattern: /[0-9a-z]/, optional: true
- }
- },
- 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);
@@ -730,20 +438,65 @@ 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() {
@@ -782,22 +535,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() {
@@ -816,249 +553,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/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('')}("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''+a+"
"}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(''+jsonData.output.join("
")+"
")})}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'+msg+'';
+ 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());
+
+// To auto-close Bootstrap alerts; time is in milliseconds
+const alertTimeout = parseInt(getCookie('alert_timeout'), 10);
+
+if (!isNaN(alertTimeout) && alertTimeout > 0) {
+ window.setTimeout(function() {
+ $(".alert").fadeTo(500, 0).slideUp(500, function(){
+ $(this).remove();
+ });
+ }, alertTimeout);
+}
+
diff --git a/app/js/bandwidthcharts.js b/app/js/vendor/bandwidthcharts.js
similarity index 100%
rename from app/js/bandwidthcharts.js
rename to app/js/vendor/bandwidthcharts.js
diff --git a/app/js/dashboardchart.js b/app/js/vendor/dashboardchart.js
similarity index 100%
rename from app/js/dashboardchart.js
rename to app/js/vendor/dashboardchart.js
diff --git a/app/js/huebee.js b/app/js/vendor/huebee.js
similarity index 100%
rename from app/js/huebee.js
rename to app/js/vendor/huebee.js
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
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"
+ }
+ }
}
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/config/defaults.json b/config/defaults.json
index a0e1b190..8713d5dd 100644
--- a/config/defaults.json
+++ b/config/defaults.json
@@ -1,66 +1,114 @@
{
- "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"
+ ]
+ },
+ "b":{
+ "settings":[
+ "hw_mode=b",
+ "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" ]
+ "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
+ }
},
- "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" ]
+ "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" ]
+ }
},
- "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" ]
- },
- "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" ]
- },
- "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" ]
+ }
}
}
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/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');
diff --git a/dist/speedtest/backend/empty.php b/dist/speedtest/backend/empty.php
new file mode 100755
index 00000000..3c4547bf
--- /dev/null
+++ b/dist/speedtest/backend/empty.php
@@ -0,0 +1,14 @@
+ 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);
+ }
+}
+
diff --git a/includes/CSRF.php b/includes/CSRF.php
old mode 100644
new mode 100755
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 @@
+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 f0f6e3e8..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'];
@@ -123,7 +129,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/defaults.php b/includes/defaults.php
index 0eaa05b6..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.5',
+ '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',
@@ -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,
diff --git a/includes/dhcp.php b/includes/dhcp.php
index fd6a47ac..a4b06f76 100755
--- a/includes/dhcp.php
+++ b/includes/dhcp.php
@@ -2,12 +2,20 @@
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()
{
- $status = new \RaspAP\Messages\StatusMessage;
+ $status = new StatusMessage();
+ $wifi = new WiFiManager();
+ $wifi->getWifiInterface();
+
if (!RASPI_MONITOR_ENABLED) {
if (isset($_POST['savedhcpdsettings'])) {
saveDHCPConfig($status);
@@ -29,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);
@@ -43,7 +63,6 @@ function DisplayDHCPConfig()
}
}
}
- getWifiInterface();
$ap_iface = $_SESSION['ap_interface'];
$serviceStatus = $dnsmasq_state ? "up" : "down";
exec('cat '. RASPI_DNSMASQ_PREFIX.'raspap.conf', $return);
@@ -86,262 +105,41 @@ 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);
+ $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('Dnsmasq 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->buildConfigEx($iface, $_POST);
+ $return = $dnsmasq->saveConfig($config, $iface);
+ $config = $dnsmasq->buildDefault($_POST);
+ $return = $dnsmasq->saveConfigDefault($config);
} else {
foreach ($errors as $error) {
$status->addMessage($error, 'danger');
}
- $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
- *
- * @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;
-}
-
diff --git a/includes/functions.php b/includes/functions.php
index f4e26eca..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
@@ -570,8 +499,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)
@@ -659,6 +593,21 @@ 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})" . "|" . // 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)) {
+ // Return a default color if validation fails
+ $color = "#2b8080";
+ }
+
return $color;
}
@@ -1005,4 +954,3 @@ function callbackTimeout(callable $callback, int $interval)
return $result;
}
-
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);
- }
- }
-}
diff --git a/includes/hostapd.php b/includes/hostapd.php
index e4c7e69c..45004498 100755
--- a/includes/hostapd.php
+++ b/includes/hostapd.php
@@ -1,9 +1,13 @@
getWifiInterface();
/**
* Initialize hostapd values, display interface
@@ -11,47 +15,31 @@ getWifiInterface();
*/
function DisplayHostAPDConfig()
{
- $status = new \RaspAP\Messages\StatusMessage;
- $system = new \RaspAP\System\Sysinfo;
+ $hostapd = new HostapdManager();
+ $hotspot = new HotspotService();
+ $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'
- ];
+
+ // set hostapd defaults
+ $arr80211Standard = $hotspot->get80211Standards();
+ $arrSecurity = $hotspot->getSecurityModes();
+ $arrEncType = $hotspot->getEncTypes();
+ $arr80211w = $hotspot->get80211wOptions();
$languageCode = strtok($_SESSION['locale'], '_');
$countryCodes = getCountryCodes($languageCode);
-
- $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => 'WPA+WPA2', 'none' => _("None"));
- $arrEncType = array('TKIP' => 'TKIP', 'CCMP' => 'CCMP', 'TKIP CCMP' => 'TKIP+CCMP');
+ $reg_domain = $hotspot->getRegDomain();
+ $interfaces = $hotspot->getInterfaces();
$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']);
- }
- 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);
+ $interface = $_POST['interface'];
+ } else {
+ $interface = $_SESSION['ap_interface'];
}
+ $txpower = $hotspot->getTxPower($interface);
+ $arrHostapdConf = $hotspot->getHostapdIni();
if (!RASPI_MONITOR_ENABLED) {
if (isset($_POST['StartHotspot']) || isset($_POST['RestartHotspot'])) {
@@ -61,7 +49,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);
@@ -72,6 +60,8 @@ function DisplayHostAPDConfig()
foreach ($return as $line) {
$status->addMessage($line, 'info');
}
+ } elseif (isset($_POST['SaveHostAPDSettings'])) {
+ $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);
@@ -81,76 +71,37 @@ 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;
}
}
- $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')
+ // process txpower user input
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'];
+ $hotspot->maybeSetTxPower($interface, $txpower, $status);
} 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'];
+ $hotspot->maybeSetTxPower($interface, 'auto', $status);
}
+ $txpower = $_POST['txpower'];
}
- $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';
- }
+ // parse hostapd configuration
+ try {
+ $arrConfig = $hostapd->getConfig();
+ } catch (\RuntimeException $e) {
+ error_log('Error: ' . $e->getMessage());
}
- $arrConfig['ignore_broadcast_ssid'] ??= 0;
- $arrConfig['max_num_sta'] ??= 0;
- $arrConfig['wep_default_key'] ??= 0;
+ // 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";
+
+ // ensure log is writeable
exec('sudo /bin/chmod o+r '.RASPI_HOSTAPD_LOG);
$logdata = getLogLimited(RASPI_HOSTAPD_LOG);
@@ -163,469 +114,16 @@ function DisplayHostAPDConfig()
"interfaces",
"arrConfig",
"arr80211Standard",
- "selectedHwMode",
"arrSecurity",
"arrEncType",
+ "arr80211w",
"arrTxPower",
"txpower",
"arrHostapdConf",
"operatingSystem",
- "selectedHwMode",
"countryCodes",
"logdata"
)
);
}
-/**
- * Validate user input, save 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)
-{
- // It should not be possible to send bad data for these fields.
- // 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');
-
- // 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 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');
- }
- }
-
- // set AP interface default, override for ap-sta & bridged options
- $iface = validateInterface($_POST['interface']) ? $_POST['interface'] : RASPI_WIFI_AP_INTERFACE;
-
- $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';
- }
-
- // 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['WifiManaged'] = $cli_iface;
- write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini');
- $_SESSION['ap_interface'] = $session_iface;
-
- // Verify input
- if (empty($_POST['ssid']) || strlen($_POST['ssid']) > 32) {
- $status->addMessage('SSID must be between 1 and 32 characters', 'danger');
- $good_input = false;
- }
-
- # 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) {
- $return = updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable);
-
- 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);
-
- 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;
- if (!empty($syscfg['dhcp-option'])) {
- $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);
- }
-
- // 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);
- }
- 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 ($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);
-
- $skip_dhcp = false;
- if (preg_match('/wlan[2-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;
- $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');
- } else {
- $status->addMessage('Unable to save wifi hotspot settings', 'danger');
- }
- } 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');
- }
- } else {
- $status->addMessage('Unable to save wifi hotspot settings', 'danger');
- return false;
- }
- return true;
-}
-
-/**
- * Updates a hostapd configuration
- *
- * @return boolean $result
- */
-function updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable)
-{
- // 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;
- } else {
- $vht_freq_idx = 155;
- }
-
- 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;
- } else {
- $config.= 'hw_mode='.$_POST['hw_mode'].PHP_EOL;
- $config.= 'ieee80211n=0'.PHP_EOL;
- }
- if ($_POST['wpa'] !== 'none') {
- $config.= 'wpa_passphrase='.$_POST['wpa_passphrase'].PHP_EOL;
- }
- if ($wifiAPEnable == 1) {
- $config.= 'interface=uap0'.PHP_EOL;
- } elseif ($bridgedEnable == 1) {
- $config.='interface='.$_POST['interface'].PHP_EOL;
- $config.= 'bridge=br0'.PHP_EOL;
- } 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.= parseUserHostapdCfg();
-
- file_put_contents("/tmp/hostapddata", $config);
- system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result);
- return $result;
-}
-
-/**
- * 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;
-}
-
-/**
- * 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;
- }
-}
-
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;
}
-
diff --git a/includes/locale.php b/includes/locale.php
index 9f83b5a6..1b9d97bd 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,65 @@ 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';
+ }
+
+ $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';
+ }
+}
+
diff --git a/includes/networking.php b/includes/networking.php
index 9669fca4..5ebfffe4 100755
--- a/includes/networking.php
+++ b/includes/networking.php
@@ -1,12 +1,12 @@
'app/js/vendor/speedtestUI.js', 'defer'=>false);
}
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/page_actions.php b/includes/page_actions.php
index c9649792..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)) {
@@ -32,7 +32,7 @@ function handleCorePageAction(string $page, array &$extraFooterScripts): void
DisplayWPAConfig();
break;
case "/network_conf":
- DisplayNetworkingConfig();
+ DisplayNetworkingConfig($extraFooterScripts);
break;
case "/hostapd_conf":
DisplayHostAPDConfig();
diff --git a/includes/system.php b/includes/system.php
index d37a5450..912b3750 100755
--- a/includes/system.php
+++ b/includes/system.php
@@ -12,6 +12,23 @@ function DisplaySystem(&$extraFooterScripts)
$dashboard = new \RaspAP\UI\Dashboard;
$pluginInstaller = \RaspAP\Plugins\PluginInstaller::getInstance();
+ // 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'];
@@ -21,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'])) {
@@ -32,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
@@ -58,6 +74,21 @@ function DisplaySystem(&$extraFooterScripts)
$status->addMessage($line, 'info');
}
}
+ } elseif (isset($_POST['savethemeSettings'])) {
+ // 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;
+ }
}
}
@@ -91,10 +122,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);
@@ -116,7 +153,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();
@@ -138,6 +175,9 @@ function DisplaySystem(&$extraFooterScripts)
"memused",
"memused_status",
"memused_led",
+ "diskused",
+ "diskused_status",
+ "diskused_led",
"cpuload",
"cpuload_status",
"cputemp",
@@ -146,29 +186,31 @@ function DisplaySystem(&$extraFooterScripts)
"themes",
"selectedTheme",
"logLimit",
- "pluginsTable"
+ "pluginsTable",
+ "optAutoclose",
+ "alertTimeout"
));
}
-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
];
}
diff --git a/includes/wifi_functions.php b/includes/wifi_functions.php
deleted file mode 100755
index ad0982a6..00000000
--- a/includes/wifi_functions.php
+++ /dev/null
@@ -1,328 +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;
- }
- }
- }
- }
-}
-
-function nearbyWifiStations(&$networks, $cached = true)
-{
- $cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG);
- $cacheKey = "nearby_wifi_stations_$cacheTime";
-
- if ($cached == false) {
- deleteCache($cacheKey);
- }
-
- $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');
- 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];
-
- $index = 0;
- 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];
- }
- }
- }
- }
-}
-
-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 233dd33b..af6dbe28 100755
--- a/includes/wireguard.php
+++ b/includes/wireguard.php
@@ -1,28 +1,35 @@
getWifiInterface();
/**
* Displays wireguard server & peer configuration
*/
function DisplayWireGuardConfig()
{
- $status = new \RaspAP\Messages\StatusMessage;
$parseFlag = true;
+ $status = new \RaspAP\Messages\StatusMessage;
if (!RASPI_MONITOR_ENABLED) {
$optRules = isset($_POST['wgRules']) ? $_POST['wgRules'] : null;
$optInterface = isset($_POST['wgInterface']) ? $_POST['wgInterface'] : null;
$optConf = isset($_POST['wgCnfOpt']) ? $_POST['wgCnfOpt'] : null;
$optSrvEnable = isset($_POST['wgSrvEnable']) ? $_POST['wgSrvEnable'] : null;
$optLogEnable = isset($_POST['wgLogEnable']) ? $_POST['wgLogEnable'] : null;
+ $optKSwitch = isset($_POST['wgKSwitch']) ? $_POST['wgKSwitch'] : null;
if (isset($_POST['savewgsettings']) && $optConf == 'manual' && $optSrvEnable == 1 ) {
SaveWireGuardConfig($status);
} elseif (isset($_POST['savewgsettings']) && $optConf == 'upload' && is_uploaded_file($_FILES["wgFile"]["tmp_name"])) {
- SaveWireGuardUpload($status, $_FILES['wgFile'], $optRules, $optInterface);
+ SaveWireGuardUpload($status, $_FILES['wgFile'], $optRules, $optKSwitch, $optInterface);
} elseif (isset($_POST['savewgsettings']) && isset($_POST['wg_penabled']) ) {
SaveWireGuardConfig($status);
} elseif (isset($_POST['startwg'])) {
$status->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 +37,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 +78,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 +105,7 @@ function DisplayWireGuardConfig()
"public_ip",
"interfaces",
"optRules",
+ "optKSwitch",
"optLogEnable",
"peer_id",
"wg_srvpubkey",
@@ -104,7 +120,9 @@ function DisplayWireGuardConfig()
"wg_pendpoint",
"wg_pallowedips",
"wg_pkeepalive",
- "wg_log"
+ "configs",
+ "conf_default",
+ "log"
)
);
}
@@ -116,10 +134,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 +167,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 +281,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 +351,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/index.php b/index.php
index e4884dc5..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.5
+ * @version 3.4.0
* @link https://github.com/RaspAP/raspap-webgui/
* @link https://raspap.com/
* @see http://sirlagz.net/2013/02/08/raspap-webgui/
@@ -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,22 @@ initializeApp();
-
+
-
+
-
+
-
+
+
+
+
-
+
" title="main" rel="stylesheet">
@@ -123,25 +127,29 @@ initializeApp();
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
+
+
+
+