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/app/js/custom.js b/app/js/custom.js index 626ce796..090cc826 100644 --- a/app/js/custom.js +++ b/app/js/custom.js @@ -43,7 +43,7 @@ function setupTabs() { var target = $(e.target).attr('href'); if(!target.match('summary')) { var int = target.replace("#",""); - loadCurrentSettings(int); + // loadCurrentSettings(int); } }); } diff --git a/app/js/speedtestUI.js b/app/js/speedtestUI.js new file mode 100644 index 00000000..e97b7c09 --- /dev/null +++ b/app/js/speedtestUI.js @@ -0,0 +1,189 @@ +function I(i){return document.getElementById(i);} + +const origin=window.location.origin; +const host=window.location.host; +var SPEEDTEST_SERVERS=[ + { + "name":"RaspAP Speedtest server (US)", + "server":"https://speedtest.raspap.com/", + "dlURL":"backend/garbage.php", + "ulURL":"backend/empty.php", + "pingURL":"backend/empty.php", + "getIpURL":"backend/getIP.php" + }, + { + "name":"RaspAP ("+host+")", + "server":origin, + "dlURL":"dist/speedtest/backend/garbage.php", + "ulURL":"dist/speedtest/backend/empty.php", + "pingURL":"dist/speedtest/backend/empty.php", + "getIpURL":"dist/speedtest/backend/getIP.php" + } +]; + +//INITIALIZE SPEEDTEST +var s=new Speedtest(); //create speedtest object +s.setParameter("telemetry_level","basic"); //enable telemetry + +//SERVER AUTO SELECTION +function initServers(){ + var noServersAvailable=function(){ + I("message").innerHTML="No servers available"; + } + var runServerSelect=function(){ + s.selectServer(function(server){ + if(server!=null){ //at least 1 server is available + I("loading").className="hidden"; //hide loading message + //populate server list for manual selection + for(var i=0;i + + + + <?php echo _("Printable Wi-Fi sign"); ?> + + + + + + + + + + +
+
+
+
+
+
+

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

+ +

+
+
+
+
+ +
+
+
+ + 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/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/defaults.php b/includes/defaults.php index f658a51e..dc7683ec 100755 --- a/includes/defaults.php +++ b/includes/defaults.php @@ -37,11 +37,17 @@ $defaults = [ 'RASPI_OPENVPN_CLIENT_LOGIN' => '/etc/openvpn/client/login.conf', 'RASPI_WIREGUARD_PATH' => '/etc/wireguard/', 'RASPI_WIREGUARD_CONFIG' => RASPI_WIREGUARD_PATH.'wg0.conf', + 'RASPI_IPTABLES_CONF' => RASPI_CONFIG.'/networking/iptables_rules.json', + 'RASPI_TORPROXY_ENABLED' => false, 'RASPI_TORPROXY_CONFIG' => '/etc/tor/torrc', 'RASPI_LIGHTTPD_CONFIG' => '/etc/lighttpd/lighttpd.conf', 'RASPI_ACCESS_CHECK_IP' => '1.1.1.1', 'RASPI_ACCESS_CHECK_DNS' => 'one.one.one.one', + // Captive portal detection - returns 204 or 200 is successful + 'RASPI_ACCESS_CHECK_URL' => 'http://detectportal.firefox.com', + 'RASPI_ACCESS_CHECK_URL_CODE' => 200, + // Constants for the 5GHz wireless regulatory domain 'RASPI_5GHZ_CHANNEL_MIN' => 100, 'RASPI_5GHZ_CHANNEL_MAX' => 192, @@ -58,7 +64,6 @@ $defaults = [ 'RASPI_OPENVPN_ENABLED' => false, 'RASPI_VPN_PROVIDER_ENABLED' => false, 'RASPI_WIREGUARD_ENABLED' => false, - 'RASPI_TORPROXY_ENABLED' => false, 'RASPI_CONFAUTH_ENABLED' => true, 'RASPI_CHANGETHEME_ENABLED' => true, 'RASPI_VNSTAT_ENABLED' => true, diff --git a/includes/hostapd.php b/includes/hostapd.php index e4c7e69c..bf44a020 100755 --- a/includes/hostapd.php +++ b/includes/hostapd.php @@ -25,15 +25,18 @@ function DisplayHostAPDConfig() $languageCode = strtok($_SESSION['locale'], '_'); $countryCodes = getCountryCodes($languageCode); - $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => 'WPA+WPA2', 'none' => _("None")); + $arrSecurity = array(1 => 'WPA', 2 => 'WPA2', 3 => _("WPA and WPA2")); + $arrSecurity += [4 => _("WPA2 and WPA3-Personal (transitional mode)")]; + $arrSecurity += [5 => 'WPA3-Personal (required)']; + $arrSecurity += ['none' => _("None")]; $arrEncType = array('TKIP' => 'TKIP', 'CCMP' => 'CCMP', 'TKIP CCMP' => 'TKIP+CCMP'); + $arr80211w = array(3 => _("Disabled"), 1 => _("Enabled (for supported clients)"), 2 => _("Required (for supported clients)")); $arrTxPower = getDefaultNetOpts('txpower','dbm'); $managedModeEnabled = false; exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces); sort($interfaces); $reg_domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'"); - $cmd = "iw dev ".escapeshellarg($_SESSION['ap_interface'])." info | awk '$1==\"txpower\" {print $2}'"; exec($cmd, $txpower); $txpower = intval($txpower[0]); @@ -41,6 +44,7 @@ function DisplayHostAPDConfig() if (isset($_POST['interface'])) { $interface = escapeshellarg($_POST['interface']); } + if (!RASPI_MONITOR_ENABLED) { if (isset($_POST['SaveHostAPDSettings'])) { SaveHostAPDConfig($arrSecurity, $arrEncType, $arr80211Standard, $interfaces, $reg_domain, $status); @@ -61,7 +65,7 @@ function DisplayHostAPDConfig() } elseif ($arrHostapdConf['WifiAPEnable'] == 1) { exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface uap0 --seconds 1', $return); } else { - // systemctl expects a unit name like raspap-network-activity@wlan0.service, no extra quotes + // systemctl expects a unit name like raspap-network-activity@wlan0.service $iface_nonescaped = $_POST['interface']; if (preg_match('/^[a-zA-Z0-9_-]+$/', $iface_nonescaped)) { // validate interface name exec('sudo '.RASPI_CONFIG.'/hostapd/servicestart.sh --interface ' .$iface_nonescaped. ' --seconds 1', $return); @@ -110,12 +114,10 @@ function DisplayHostAPDConfig() } else { $arrConfig['disassoc_low_ack_bool'] = 0; } - // assign country_code from iw reg if not set in config if (empty($arrConfig['country_code']) && isset($country_code[0])) { $arrConfig['country_code'] = $country_code[0]; } - // set txpower with iw if value is non-default ('auto') if (isset($_POST['txpower'])) { if ($_POST['txpower'] != 'auto') { @@ -129,6 +131,12 @@ function DisplayHostAPDConfig() $status->addMessage('Setting transmit power to '.$_POST['txpower'].'.', 'success'); $txpower = $_POST['txpower']; } + } + // map wpa_key_mgmt to security types + if ($arrConfig['wpa_key_mgmt'] == 'WPA-PSK WPA-PSK-SHA256 SAE') { + $arrConfig['wpa'] = 4; + } elseif ($arrConfig['wpa_key_mgmt'] == 'SAE') { + $arrConfig['wpa'] = 5; } $selectedHwMode = $arrConfig['hw_mode']; @@ -166,6 +174,7 @@ function DisplayHostAPDConfig() "selectedHwMode", "arrSecurity", "arrEncType", + "arr80211w", "arrTxPower", "txpower", "arrHostapdConf", @@ -226,6 +235,19 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $bridgedEnable = 1; } } + // Check for WiFi repeater mode checkbox + $repeaterEnable = 0; + if ($bridgedEnable == 0) { // enable client mode actions when not bridged + if ($arrHostapdConf['RepeaterEnable'] == 0) { + if (isset($_POST['repeaterEnable'])) { + $repeaterEnable = 1; + } + } else { + if (isset($_POST['repeaterEnable'])) { + $repeaterEnable = 1; + } + } + } // Check for WiFi client AP mode checkbox $wifiAPEnable = 0; if ($bridgedEnable == 0) { // enable client mode actions when not bridged @@ -277,6 +299,7 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom // Save previous Client mode status when Bridged $cfg['WifiAPEnable'] = ($bridgedEnable == 1 ? $arrHostapdConf['WifiAPEnable'] : $wifiAPEnable); $cfg['BridgedEnable'] = $bridgedEnable; + $cfg['RepeaterEnable'] = $repeaterEnable; $cfg['WifiManaged'] = $cli_iface; write_php_ini($cfg, RASPI_CONFIG.'/hostapd.ini'); $_SESSION['ap_interface'] = $session_iface; @@ -305,7 +328,6 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $status->addMessage('Parameter hiddenSSID contains an invalid configuration value.', 'danger'); $good_input = false; } - if (! in_array($_POST['interface'], $interfaces)) { $status->addMessage('Unknown interface '.htmlspecialchars($_POST['interface'], ENT_QUOTES), 'danger'); $good_input = false; @@ -330,7 +352,29 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $_POST['max_num_sta'] = $_POST['max_num_sta'] < 1 ? null : $_POST['max_num_sta']; if ($good_input) { - $return = updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable); + $config = buildHostapdConfig([ + 'interface' => $_POST['interface'], + 'ssid' => $_POST['ssid'], + 'channel' => $_POST['channel'], + 'wpa' => $_POST['wpa'], + '80211w' => $_POST['80211w'] ?? 0, + 'wpa_passphrase' => $_POST['wpa_passphrase'], + 'wpa_pairwise' => $_POST['wpa_pairwise'], + 'hw_mode' => $_POST['hw_mode'], + 'country_code' => $_POST['country_code'], + 'hiddenSSID' => $_POST['hiddenSSID'], + 'max_num_sta' => $_POST['max_num_sta'] ?? null, + 'beacon_interval' => $_POST['beacon_interval'] ?? null, + 'disassoc_low_ack' => $_POST['disassoc_low_ackEnable'] ?? null, + 'bridge' => $bridgedEnable ? 'br0' : null + ]); + + file_put_contents('/tmp/hostapddata', $config); + if ($dualAPEnable) { + system("sudo cp /tmp/hostapddata " . '/etc/hostapd/hostapd-'.$_POST['interface'].'.conf', $result); + } else { + system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result); + } if (trim($country_code) != trim($reg_domain)) { $return = iwRegSet($country_code, $status); @@ -377,21 +421,46 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $jsonData = json_decode(getNetConfig($ap_iface), true); $ip_address = empty($jsonData['StaticIP']) ? getDefaultNetValue('dhcp', $ap_iface, 'static ip_address') : $jsonData['StaticIP']; - $domain_name_server = empty($jsonData['StaticDNS']) + $domain_name_server = empty($jsonData['StaticDNS']) ? getDefaultNetValue('dhcp', $ap_iface, 'static domain_name_server') : $jsonData['StaticDNS']; - $routers = empty($jsonData['StaticRouters']) + $routers = empty($jsonData['StaticRouters']) ? getDefaultNetValue('dhcp', $ap_iface, 'static routers') : $jsonData['StaticRouters']; - $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') + $netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0') ? getDefaultNetValue('dhcp', $ap_iface, 'subnetmask') : $jsonData['SubnetMask']; if (isset($ip_address) && !preg_match('/.*\/\d+/', $ip_address)) { $ip_address.='/'.mask2cidr($netmask); } + $hasDefaults = !( + empty($ip_address) || + empty($domain_name_server) || + empty($routers) || + empty($netmask) || + $netmask === '0.0.0.0' + ); + if (!$hasDefaults) { + $status->addMessage(sprintf(_('Interface %s has no default settings.'), $ap_iface), 'warning'); + $status->addMessage(('Configure settings in DHCP Server before starting AP.'), 'warning'); + } if ($bridgedEnable == 1) { $config = array_keys(getDefaultNetOpts('dhcp','options')); $config[] = PHP_EOL.'# RaspAP br0 configuration'; $config[] = 'denyinterfaces eth0 wlan0'; $config[] = 'interface br0'; $config[] = PHP_EOL; + } elseif ($repeaterEnable == 1) { + $config = [ '# RaspAP '.$ap_iface.' configuration' ]; + $config[] = 'interface '.$ap_iface; + $config[] = 'static ip_address='.$ip_address; + $config[] = 'static routers='.$routers; + $config[] = 'static domain_name_server='.$domain_name_server; + $client_metric = getIfaceMetric($_SESSION['wifi_client_interface']); + if (is_int($client_metric)) { + $ap_metric = (int)$client_metric + 1; + $config[] = 'metric '.$ap_metric; + } else { + $status->addMessage('Unable to obtain metric value for client interface. Repeater mode inactive.', 'warning'); + $repeaterEnable = false; + } } elseif ($wifiAPEnable == 1) { $config = array_keys(getDefaultNetOpts('dhcp','options')); $config[] = PHP_EOL.'# RaspAP uap0 configuration'; @@ -399,15 +468,12 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $config[] = 'static ip_address='.$ip_address; $config[] = 'nohook wpa_supplicant'; $config[] = PHP_EOL; - } else { $config = updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain_name_server); } - $dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG); - $skip_dhcp = false; - if (preg_match('/wlan[2-9]\d*|wlan[1-9]\d+/', $ap_iface)) { + if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) { $skip_dhcp = true; } elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) { $dhcp_cfg = join(PHP_EOL, $config); @@ -418,114 +484,183 @@ function SaveHostAPDConfig($wpa_array, $enc_types, $modes, $interfaces, $reg_dom $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); $dhcp_cfg .= $config; - $status->addMessage(sprintf(_('DHCP configuration for %s added.'), $ap_iface), 'success'); } else { $config = join(PHP_EOL, $config); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0'); $dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0'); - $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=\s*^\s*$)/ms', $config, $dhcp_cfg, 1); - $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success'); - } - if (!$skip_dhcp) { - file_put_contents("/tmp/dhcpddata", $dhcp_cfg); - system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return); - if ($return == 0) { - $status->addMessage('Wifi Hotspot settings saved', 'success'); + if (!strpos($dhcp_cfg, 'metric')) { + $dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1); } else { - $status->addMessage('Unable to save wifi hotspot settings', 'danger'); + $metrics = true; } + } + if ($repeaterEnable && $metrics) { + $status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning'); + } else if ($repeaterEnable && !$metrics) { + $status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success'); + $status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success'); + persistDHCPConfig($dhcp_cfg, $ap_iface, $status); + } elseif (!$skip_dhcp) { + persistDHCPConfig($dhcp_cfg, $ap_iface, $status); } else { - $status->addMessage(sprintf(_('Interface %s has no default settings.'), $ap_iface), 'warning'); - $status->addMessage(('Configure settings in DHCP Server before starting AP.'), 'warning'); - $status->addMessage('Wifi Hotspot settings saved', 'success'); + $status->addMessage('WiFi hotspot settings saved.', 'success'); } } else { - $status->addMessage('Unable to save wifi hotspot settings', 'danger'); + $status->addMessage('Unable to save WiFi hotspot settings', 'danger'); return false; } return true; } /** - * Updates a hostapd configuration + * Persists a DHCP configuration * - * @return boolean $result + * @param string $dhcp_cfg + * @param string $ap_iface + * @param object $status + * @return $status */ -function updateHostapdConfig($ignore_broadcast_ssid,$wifiAPEnable,$bridgedEnable) +function persistDHCPConfig($dhcp_cfg, $ap_iface, $status) { - // Fixed values - $country_code = $_POST['country_code']; - $config = 'driver=nl80211'.PHP_EOL; - $config.= 'ctrl_interface='.RASPI_HOSTAPD_CTRL_INTERFACE.PHP_EOL; - $config.= 'ctrl_interface_group=0'.PHP_EOL; - $config.= 'auth_algs=1'.PHP_EOL; - $config.= 'wpa_key_mgmt=WPA-PSK'.PHP_EOL; - if (isset($_POST['beaconintervalEnable'])) { - $config.= 'beacon_int='.$_POST['beacon_interval'].PHP_EOL; - } - if (isset($_POST['disassoc_low_ackEnable'])) { - $config.= 'disassoc_low_ack=0'.PHP_EOL; - } - $config.= 'ssid='.$_POST['ssid'].PHP_EOL; - $config.= 'channel='.$_POST['channel'].PHP_EOL; - - // Set VHT center frequency segment value - if ((int)$_POST['channel'] < RASPI_5GHZ_CHANNEL_MIN) { - $vht_freq_idx = 42; + file_put_contents("/tmp/dhcpddata", $dhcp_cfg); + system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return); + if ($return == 0) { + $status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success'); + $status->addMessage('WiFi hotspot settings saved.', 'success'); } else { - $vht_freq_idx = 155; + $status->addMessage('Unable to save WiFi hotspot settings.', 'danger'); } + return $status; +} - if ($_POST['hw_mode'] === 'n') { - $config.= 'hw_mode=g'.PHP_EOL; - $config.= 'ieee80211n=1'.PHP_EOL; - // Enable basic Quality of service - $config.= 'wmm_enabled=1'.PHP_EOL; - } elseif ($_POST['hw_mode'] === 'ac') { - $config.= 'hw_mode=a'.PHP_EOL.PHP_EOL; - $config.= '# N'.PHP_EOL; - $config.= 'ieee80211n=1'.PHP_EOL; - $config.= 'require_ht=1'.PHP_EOL; - $config.= 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'.PHP_EOL.PHP_EOL; - $config.= '# AC'.PHP_EOL; - $config.= 'ieee80211ac=1'.PHP_EOL; - $config.= 'require_vht=1'.PHP_EOL; - $config.= 'ieee80211d=0'.PHP_EOL; - $config.= 'ieee80211h=0'.PHP_EOL; - $config.= 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'.PHP_EOL; - $config.= 'vht_oper_chwidth=1'.PHP_EOL; - $config.= 'vht_oper_centr_freq_seg0_idx='.$vht_freq_idx.PHP_EOL.PHP_EOL; - } elseif ($_POST['hw_mode'] === 'w') { - $config.= 'ieee80211w=2'.PHP_EOL; - $config.= 'wpa_key_mgmt=WPA-EAP-SHA256'.PHP_EOL; +/** + * Returns a count of hostapd-.conf files + * + * @return int + */ +function countHostapdConfigs(): int +{ + $configs = glob('/etc/hostapd/hostapd-*.conf'); + return is_array($configs) ? count($configs) : 0; +} + +/** + * Retrieves the metric value for a given interface + * + * @param string $iface + * @return int $metric + */ +function getIfaceMetric($iface) +{ + $metric = shell_exec("ip -o -4 route show dev ".$iface." | awk '/metric/ {print \$NF; exit}'"); + if (isset($metric)) { + $metric = (int)$metric; + return $metric; } else { - $config.= 'hw_mode='.$_POST['hw_mode'].PHP_EOL; - $config.= 'ieee80211n=0'.PHP_EOL; + return false; } - if ($_POST['wpa'] !== 'none') { - $config.= 'wpa_passphrase='.$_POST['wpa_passphrase'].PHP_EOL; +} + +/** + * Builds a hostapd configuration string for a given interface + * + * @param array $params Associative array of config values + * @return string + */ +function buildHostapdConfig(array $params): string +{ + $config = []; + $config[] = 'driver=nl80211'; + $config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE; + $config[] = 'ctrl_interface_group=0'; + $config[] = 'auth_algs=1'; + + $wpa = $params['wpa']; + $wpa_key_mgmt = 'WPA-PSK'; + + if ($wpa == 4) { + $config[] = 'ieee80211w=1'; + $wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE'; + $wpa = 2; + } elseif ($wpa == 5) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'SAE'; + $wpa = 2; } - if ($wifiAPEnable == 1) { - $config.= 'interface=uap0'.PHP_EOL; - } elseif ($bridgedEnable == 1) { - $config.='interface='.$_POST['interface'].PHP_EOL; - $config.= 'bridge=br0'.PHP_EOL; + + if ($params['80211w'] == 1) { + $config[] = 'ieee80211w=1'; + $wpa_key_mgmt = 'WPA-PSK'; + } elseif ($params['80211w'] == 2) { + $config[] = 'ieee80211w=2'; + $wpa_key_mgmt = 'WPA-PSK-SHA256'; + } + + $config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt; + + if (!empty($params['beacon_interval'])) { + $config[] = 'beacon_int=' . $params['beacon_interval']; + } + + if (!empty($params['disassoc_low_ack'])) { + $config[] = 'disassoc_low_ack=0'; + } + + $config[] = 'ssid=' . $params['ssid']; + $config[] = 'channel=' . $params['channel']; + + // Choose VHT segment index (fallback only if required) + $vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155; + + switch ($params['hw_mode']) { + case 'n': + $config[] = 'hw_mode=g'; + $config[] = 'ieee80211n=1'; + $config[] = 'wmm_enabled=1'; + break; + case 'ac': + $config[] = 'hw_mode=a'; + $config[] = '# N'; + $config[] = 'ieee80211n=1'; + $config[] = 'require_ht=1'; + $config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]'; + $config[] = '# AC'; + $config[] = 'ieee80211ac=1'; + $config[] = 'require_vht=1'; + $config[] = 'ieee80211d=0'; + $config[] = 'ieee80211h=0'; + $config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]'; + $config[] = 'vht_oper_chwidth=1'; + $config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx; + break; + default: + $config[] = 'hw_mode=' . $params['hw_mode']; + $config[] = 'ieee80211n=0'; + } + + if ($params['wpa'] !== 'none') { + $config[] = 'wpa_passphrase=' . $params['wpa_passphrase']; + } + + if (!empty($params['bridge'])) { + $config[] = 'interface=' . $params['interface']; + $config[] = 'bridge=' . $params['bridge']; } else { - $config.= 'interface='.$_SESSION['ap_interface'].PHP_EOL; - } - $config.= 'wpa='.$_POST['wpa'].PHP_EOL; - $config.= 'wpa_pairwise='.$_POST['wpa_pairwise'].PHP_EOL; - $config.= 'country_code='.$_POST['country_code'].PHP_EOL; - $config.= 'ignore_broadcast_ssid='.$ignore_broadcast_ssid.PHP_EOL; - if (isset($_POST['max_num_sta'])) { - $config.= 'max_num_sta='.$_POST['max_num_sta'].PHP_EOL; + $config[] = 'interface=' . $params['interface']; } - $config.= parseUserHostapdCfg(); + $config[] = 'wpa=' . $wpa; + $config[] = 'wpa_pairwise=' . $params['wpa_pairwise']; + $config[] = 'country_code=' . $params['country_code']; + $config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID']; + if (!empty($params['max_num_sta'])) { + $config[] = 'max_num_sta=' . (int)$params['max_num_sta']; + } + + // Optional additional user config + $config[] = parseUserHostapdCfg(); - file_put_contents("/tmp/hostapddata", $config); - system("sudo cp /tmp/hostapddata " . RASPI_HOSTAPD_CONFIG, $result); - return $result; + return implode(PHP_EOL, $config) . PHP_EOL; } /** @@ -628,4 +763,3 @@ function parseUserHostapdCfg() return $tmp; } } - 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/networking.php b/includes/networking.php index 9669fca4..83c587ed 100755 --- a/includes/networking.php +++ b/includes/networking.php @@ -1,12 +1,12 @@ 'app/js/speedtestUI.js', 'defer'=>false); } diff --git a/includes/page_actions.php b/includes/page_actions.php index c9649792..4d218850 100755 --- a/includes/page_actions.php +++ b/includes/page_actions.php @@ -32,7 +32,7 @@ function handleCorePageAction(string $page, array &$extraFooterScripts): void DisplayWPAConfig(); break; case "/network_conf": - DisplayNetworkingConfig(); + DisplayNetworkingConfig($extraFooterScripts); break; case "/hostapd_conf": DisplayHostAPDConfig(); diff --git a/includes/wireguard.php b/includes/wireguard.php index 233dd33b..ea2b1fe9 100755 --- a/includes/wireguard.php +++ b/includes/wireguard.php @@ -1,28 +1,33 @@ addMessage('Attempting to start WireGuard', 'info'); + exec('sudo /bin/systemctl enable wg-quick@wg0', $return); exec('sudo /bin/systemctl start wg-quick@wg0', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); @@ -30,6 +35,7 @@ function DisplayWireGuardConfig() } elseif (isset($_POST['stopwg'])) { $status->addMessage('Attempting to stop WireGuard', 'info'); exec('sudo /bin/systemctl stop wg-quick@wg0', $return); + exec('sudo /bin/systemctl disable wg-quick@wg0', $return); foreach ($return as $line) { $status->addMessage($line, 'info'); } @@ -70,11 +76,18 @@ function DisplayWireGuardConfig() $wg_state = ($wgstatus[0] == 'active' ? true : false ); $public_ip = get_public_ip(); - // retrieve wg log - $wg_log = ""; + // fetch uploaded file configs + exec("sudo ls ".RASPI_WIREGUARD_PATH, $clist); + $configs = preg_grep('/^((?!wg0).)*\.conf/', $clist); + exec("sudo readlink ".RASPI_WIREGUARD_CONFIG." | xargs basename", $ret); + $conf_default = empty($ret) ? "none" : $ret[0]; + + // fetch wg log + exec('sudo chmod o+r /tmp/wireguard.log'); if (file_exists('/tmp/wireguard.log')) { - exec('sudo chmod o+r /tmp/wireguard.log'); - $wg_log = file_get_contents('/tmp/wireguard.log'); + $log = file_get_contents('/tmp/wireguard.log'); + } else { + $log = ''; } $peer_id = $peer_id ?? "1"; @@ -90,6 +103,7 @@ function DisplayWireGuardConfig() "public_ip", "interfaces", "optRules", + "optKSwitch", "optLogEnable", "peer_id", "wg_srvpubkey", @@ -104,7 +118,9 @@ function DisplayWireGuardConfig() "wg_pendpoint", "wg_pallowedips", "wg_pkeepalive", - "wg_log" + "configs", + "conf_default", + "log" ) ); } @@ -116,10 +132,11 @@ function DisplayWireGuardConfig() * @param object $status * @param object $file * @param boolean $optRules + * @param boolean $optKSwitch * @param string $optInterface * @return object $status */ -function SaveWireGuardUpload($status, $file, $optRules, $optInterface) +function SaveWireGuardUpload($status, $file, $optRules, $optKSwitch, $optInterface) { define('KB', 1024); $tmp_destdir = '/tmp/'; @@ -148,19 +165,56 @@ function SaveWireGuardUpload($status, $file, $optRules, $optInterface) $tmp_wgconfig = $results['full_path']; $tmp_contents = file_get_contents($tmp_wgconfig); - // Set iptables rules - if (isset($optRules) && !preg_match('/PostUp|PostDown/m',$tmp_contents)) { - $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUp'); - $rules[] = 'PostDown = '.getDefaultNetValue('wireguard','server','PostDown'); - $rules[] = ''; - $rules = join(PHP_EOL, $rules); - $rules = preg_replace('/wlan0/m', $optInterface, $rules); - $tmp_contents = preg_replace('/^\s*$/ms', $rules, $tmp_contents, 1); - file_put_contents($tmp_wgconfig, $tmp_contents); + // Check for existing iptables rules + if ((isset($optRules) || isset($optKSwitch)) && preg_match('/PostUp|PostDown|PreDown/m',$tmp_contents)) { + $status->addMessage('Existing iptables rules found in WireGuard configuration - not added', 'info'); + } else { + // Set rules from default config + if (isset($optRules)) { + $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUp'); + $rules[] = 'PostDown = '.getDefaultNetValue('wireguard','server','PostDown'); + $rules = preg_replace('/wlan0/m', $optInterface, $rules); + } + if (isset($optKSwitch)) { + // Get ap static ip_addr from system config, fallback to default if undefined + $jsonData = json_decode(getNetConfig($optInterface), true); + $ip_addr = ($jsonData['StaticIP'] == '') ? getDefaultNetValue('dhcp', $optInterface, 'static ip_address') : $jsonData['StaticIP']; + $mask = ($jsonData['SubnetMask'] == '') ? getDefaultNetValue('dhcp', $optInterface, 'subnetmask') : $jsonData['SubnetMask']; + + // if empty, try to detect IP/mask from system + if (empty($ip_addr) || empty($mask)) { + $ipDetails = shell_exec("ip -4 -o addr show dev " . escapeshellarg($optInterface)); + if (preg_match('/inet (\d+\.\d+\.\d+\.\d+)\/(\d+)/', $ipDetails, $matches)) { + $ip_addr = $matches[1]; + $cidr = $matches[2]; + } else { + $ip_addr = '0.0.0.0'; + $cidr = '24'; + } + } else { + $cidr = mask2cidr($mask); + } + $cidr_ip = strpos($ip_addr, '/') === false ? "$ip_addr/$cidr" : $ip_addr; + + $rules[] = 'PostUp = '.getDefaultNetValue('wireguard','server','PostUpEx'); + $rules[] = 'PreDown = '.getDefaultNetValue('wireguard','server','PreDown'); + $rules = preg_replace('/%s/m', $cidr_ip, $rules); + } + if ((isset($rules) && count($rules) > 0)) { + $rules[] = ''; + $rules = join(PHP_EOL, $rules); + $tmp_contents = preg_replace('/^\s*$/ms', $rules, $tmp_contents, 1); + file_put_contents($tmp_wgconfig, $tmp_contents); + $status->addMessage('iptables rules added to WireGuard configuration', 'info'); + } } - // Move processed file from tmp to destination - system("sudo mv $tmp_wgconfig ". RASPI_WIREGUARD_CONFIG, $return); + // Move processed file from /tmp and create symlink + $client_wg = RASPI_WIREGUARD_PATH.pathinfo($file['name'], PATHINFO_FILENAME).'.conf'; + chmod($tmp_wgconfig, 0644); + system("sudo mv $tmp_wgconfig $client_wg", $return); + system("sudo rm ".RASPI_WIREGUARD_CONFIG, $return); + system("sudo ln -s $client_wg ".RASPI_WIREGUARD_CONFIG, $return); if ($return ==0) { $status->addMessage('WireGuard configuration uploaded successfully', 'info'); @@ -225,7 +279,7 @@ function SaveWireGuardConfig($status) $wg_pendpoint_seg = substr($_POST['wg_pendpoint'],0,strpos($_POST['wg_pendpoint'],':')); $host_port = explode(':', $wg_pendpoint_seg); $hostname = $host_port[0]; - if (!filter_var($hostname, FILTER_VALIDATE_IP) && + if (!filter_var($hostname, FILTER_VALIDATE_IP) && !filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { $status->addMessage('Invalid value for endpoint address', 'danger'); $good_input = false; @@ -295,11 +349,10 @@ function SaveWireGuardConfig($status) } $config[] = ''; $config = join(PHP_EOL, $config); - file_put_contents("/tmp/wgdata", $config); system('sudo cp /tmp/wgdata '.RASPI_WIREGUARD_PATH.'client.conf', $return); } else { - # remove selected conf + keys + # remove selected conf + keys system('sudo rm '. RASPI_WIREGUARD_PATH .'wg-peer-private.key', $return); system('sudo rm '. RASPI_WIREGUARD_PATH .'wg-peer-public.key', $return); system('sudo rm '. RASPI_WIREGUARD_PATH.'client.conf', $return); diff --git a/index.php b/index.php index 5958cf8e..3ca02507 100755 --- a/index.php +++ b/index.php @@ -79,6 +79,9 @@ initializeApp(); + + + @@ -141,6 +144,9 @@ initializeApp(); + + + diff --git a/installers/raspap.sudoers b/installers/raspap.sudoers index 9933cf18..3b7fd055 100644 --- a/installers/raspap.sudoers +++ b/installers/raspap.sudoers @@ -59,10 +59,13 @@ www-data ALL=(ALL) NOPASSWD:/bin/chmod o+r /var/log/dnsmasq.log www-data ALL=(ALL) NOPASSWD:/bin/chmod o+r /tmp/wireguard.log www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/dnsmasqdata /etc/dnsmasq.d/090_adblock.conf www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/dnsmasq_custom /etc/raspap/adblock/custom.txt +www-data ALL=(ALL) NOPASSWD:/etc/raspap/adblock/update_blocklist.sh www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wgdata /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/wg-*.key /etc/wireguard/wg-*.key www-data ALL=(ALL) NOPASSWD:/bin/mv /tmp/wg/* /etc/wireguard/*.conf -www-data ALL=(ALL) NOPASSWD:/etc/raspap/adblock/update_blocklist.sh +www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/wg0.conf +www-data ALL=(ALL) NOPASSWD:/usr/bin/ls /etc/wireguard/ +www-data ALL=(ALL) NOPASSWD:/usr/bin/ln -s /etc/wireguard/*.conf /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/usr/bin/socat - /dev/ttyUSB[0-9] www-data ALL=(ALL) NOPASSWD:/usr/local/sbin/onoff_huawei_hilink.sh * www-data ALL=(ALL) NOPASSWD:/bin/sed -i * /etc/wvdial.conf @@ -75,8 +78,10 @@ www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wireguard/wg-*.key www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/bin/rm /etc/wireguard/wg-*.key +www-data ALL=(ALL) NOPASSWD:/usr/bin/readlink /etc/wireguard/*.conf www-data ALL=(ALL) NOPASSWD:/usr/sbin/netplan www-data ALL=(ALL) NOPASSWD:/bin/truncate -s 0 /tmp/*.log,/bin/truncate -s 0 /var/log/dnsmasq.log +www-data ALL=(ALL) NOPASSWD:/usr/bin/nmap --script=broadcast-dhcp-discover -e [a-zA-Z0-9]* www-data ALL=(ALL) NOPASSWD:/usr/bin/vnstat * www-data ALL=(ALL) NOPASSWD:/usr/sbin/visudo -cf * www-data ALL=(ALL) NOPASSWD:/etc/raspap/plugins/plugin_helper.sh diff --git a/locale/en_US/LC_MESSAGES/messages.mo b/locale/en_US/LC_MESSAGES/messages.mo index 039abaa1..e2720b14 100644 Binary files a/locale/en_US/LC_MESSAGES/messages.mo and b/locale/en_US/LC_MESSAGES/messages.mo differ diff --git a/locale/en_US/LC_MESSAGES/messages.po b/locale/en_US/LC_MESSAGES/messages.po index 70a5c706..5986aa27 100644 --- a/locale/en_US/LC_MESSAGES/messages.po +++ b/locale/en_US/LC_MESSAGES/messages.po @@ -581,12 +581,15 @@ msgstr "Clients with a particular hardware MAC address can always be allocated t msgid "This option adds dhcp-host entries to the dnsmasq configuration." msgstr "This option adds dhcp-host entries to the dnsmasq configuration." -msgid "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." -msgstr "This toggles the gateway/nogateway option for this interface in the DHCPCD configuration." +msgid "This toggles the gateway/nogateway option for this interface in the dhcpcd.conf file." +msgstr "This toggles the gateway/nogateway option for this interface in the dhcpcd.conf file." msgid "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." msgstr "This toggles the nohook wpa_supplicant option for this interface in the DHCPCD configuration." +msgid "Enable this only if you want your device to use this interface as its primary route to the internet." +msgstr "Enable this only if you want your device to use this interface as its primary route to the internet." + msgid "Disable wpa_supplicant dhcp hook for this interface" msgstr "Disable wpa_supplicant dhcp hook for this interface" diff --git a/src/RaspAP/Networking/DeviceScanner.php b/src/RaspAP/Networking/DeviceScanner.php new file mode 100644 index 00000000..d3a812f3 --- /dev/null +++ b/src/RaspAP/Networking/DeviceScanner.php @@ -0,0 +1,109 @@ + + * @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE + */ + +namespace RaspAP\Networking; + +class DeviceScanner +{ + public function listDevices(): array + { + $devices = []; + + foreach (glob('/sys/class/net/*') as $ifacePath) { + $iface = basename($ifacePath); + if ($iface === 'lo') { + continue; // skip loopback + } + + $device = [ + 'name' => $iface, + 'mac' => $this->readFile("$ifacePath/address"), + 'ipaddress' => $this->getIPAddress($iface), + 'vendor' => '', + 'model' => '', + 'vid' => '', + 'pid' => '', + 'driver' => '', + 'type' => $this->getInterfaceType($iface), + 'isAP' => false, + 'connected' => 'y', // placeholder + 'signal' => '0 dB (100%)' // placeholder + ]; + + $udev = $this->getUdevAttributes($iface); + $device['vendor'] = $this->getVendorName($udev); + $device['model'] = $udev['ID_MODEL_FROM_DATABASE'] ?? $udev['ID_MODEL'] ?? ''; + $device['vid'] = $udev['ID_VENDOR_ID'] ?? ''; + $device['pid'] = $udev['ID_MODEL_ID'] ?? ''; + $device['driver'] = $udev['ID_NET_DRIVER'] ?? ''; + + $devices[] = $device; + } + + return $devices; + } + + private function readFile(string $path): string + { + return is_readable($path) ? trim(file_get_contents($path)) : ''; + } + + private function getIPAddress(string $iface): string + { + $cmd = "ip -4 -o addr show dev " . escapeshellarg($iface) . " | awk '{print $4}' | cut -d/ -f1"; + $result = []; + exec($cmd, $result); + return $result[0] ?? ''; + } + + private function getInterfaceType(string $iface): string + { + $wirelessPath = "/sys/class/net/{$iface}/wireless"; + if (is_dir($wirelessPath)) { + return 'wlan'; + } + + $typeFile = "/sys/class/net/{$iface}/type"; + $type = $this->readFile($typeFile); + + return match ($type) { + '1' => 'eth', // ARPHRD_ETHER + '772' => 'loopback', + '512' => 'ppp', + default => 'unknown' + }; + } + + private function getUdevAttributes(string $iface): array + { + $attributes = []; + $output = []; + $path = escapeshellarg("/sys/class/net/{$iface}"); + + exec("udevadm info {$path}", $output); + + foreach ($output as $line) { + if (preg_match('/E: (\w+)=([^\n]+)/', $line, $matches)) { + $attributes[$matches[1]] = $matches[2]; + } + } + + return $attributes; + } + + private function getVendorName(array $udev): ?string + { + return $udev['ID_VENDOR_FROM_DATABASE'] + ?? $udev['ID_VENDOR'] + ?? $udev['ID_OUI_FROM_DATABASE'] + ?? null; + } +} + diff --git a/templates/dhcp/general.php b/templates/dhcp/general.php index b788c87a..13bc7b52 100644 --- a/templates/dhcp/general.php +++ b/templates/dhcp/general.php @@ -69,9 +69,10 @@
+ ">

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

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

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

diff --git a/templates/hostapd/security.php b/templates/hostapd/security.php index 1decef3d..cc7eb141 100644 --- a/templates/hostapd/security.php +++ b/templates/hostapd/security.php @@ -4,15 +4,21 @@
- +
- +
+ + + +
+ +
- +
@@ -22,7 +28,12 @@
RaspAP Wifi QR code -
+
+ ', + '', + ''); ?> +
diff --git a/templates/networking.php b/templates/networking.php index f1cd45c3..1767878d 100755 --- a/templates/networking.php +++ b/templates/networking.php @@ -1,7 +1,7 @@ +
-
@@ -9,96 +9,24 @@
-
+ showMessages(); ?> +
+
+ + +
-
-

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

-

- "> -

-

- "> -

-
-
-
-
- -

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

-
- - - -
-
-
-
-

-                      
-
-
- - -
- -
-
+ + +
diff --git a/templates/networking/diagnostics.php b/templates/networking/diagnostics.php new file mode 100755 index 00000000..011ae9c2 --- /dev/null +++ b/templates/networking/diagnostics.php @@ -0,0 +1,95 @@ +
+ +

+
+
+ +
+

...

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

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

+

Access point

+
+

"> + " > +

+

"> + " > +

+

+ " > +

+
+
+
+
+ +

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

+
+ + + +
+
+
+
+

+              
+
+
+ + +
+ +
+ diff --git a/templates/wg/general.php b/templates/wg/general.php index 9b8b8246..e7e0399a 100644 --- a/templates/wg/general.php +++ b/templates/wg/general.php @@ -39,9 +39,18 @@ ">

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

+

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

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