Merge pull request #1907 from RaspAP/maint/hostapd-refactor

Maintenance: Refactor legacy procedural code into class methods
This commit is contained in:
Bill Zimmerman
2025-08-02 14:31:56 -07:00
committed by GitHub
23 changed files with 2501 additions and 1455 deletions

View File

@@ -1,4 +1,7 @@
<?php
use RaspAP\Networking\Hotspot\DhcpcdManager;
require_once '../../includes/autoload.php';
require_once '../../includes/CSRF.php';
require_once '../../includes/session.php';
@@ -6,63 +9,11 @@ require_once '../../includes/config.php';
require_once '../../includes/authenticate.php';
require_once '../../includes/functions.php';
$dhcpcdManager = new DhcpcdManager();
$interface = $_POST['iface'];
if (isset($interface)) {
// fetch dnsmasq.conf settings for interface
exec('cat '. escapeshellarg(RASPI_DNSMASQ_PREFIX.$interface.'.conf'), $return);
$conf = ParseConfig($return);
$dhcpdata['DHCPEnabled'] = empty($conf) ? false : true;
if (is_string($conf['dhcp-range'])) {
$arrRange = explode(",", $conf['dhcp-range']);
} else {
$arrRange = explode(",", $conf['dhcp-range'][0]);
}
$dhcpdata['RangeStart'] = $arrRange[0] ?? null;
$dhcpdata['RangeEnd'] = $arrRange[1] ?? null;
$dhcpdata['RangeMask'] = $arrRange[2] ?? null;
$dhcpdata['leaseTime'] = $arrRange[3] ?? null;
$dhcpHost = $conf["dhcp-host"] ?? null;
$dhcpHost = empty($dhcpHost) ? [] : $dhcpHost;
$dhcpdata['dhcpHost'] = is_array($dhcpHost) ? $dhcpHost : [ $dhcpHost ];
$upstreamServers = is_array($conf['server'] ?? null) ? $conf['server'] : [ $conf['server'] ?? '' ];
$dhcpdata['upstreamServersEnabled'] = empty($conf['server']) ? false: true;
$dhcpdata['upstreamServers'] = array_filter($upstreamServers);
preg_match('/([0-9]*)([a-z])/i', $dhcpdata['leaseTime'], $arrRangeLeaseTime);
$dhcpdata['leaseTime'] = $arrRangeLeaseTime[1];
$dhcpdata['leaseTimeInterval'] = $arrRangeLeaseTime[2];
if (isset($conf['dhcp-option'])) {
$arrDns = explode(",", $conf['dhcp-option']);
if ($arrDns[0] == '6') {
if (count($arrDns) > 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);
}

View File

@@ -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']; } );

View File

@@ -94,12 +94,40 @@ 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 = `
<div class="row dhcp-static-lease-row js-dhcp-static-lease-row">
<div class="col-md-4 col-xs-3">
<input type="text" name="static_leases[mac][]" value="${mac}" placeholder="MAC address" class="form-control">
</div>
<div class="col-md-3 col-xs-3">
<input type="text" name="static_leases[ip][]" value="${ip}" placeholder="IP address" class="form-control">
</div>
<div class="col-md-3 col-xs-3">
<input type="text" name="static_leases[comment][]" value="${comment || ''}" placeholder="Optional comment" class="form-control">
</div>
<div class="col-md-2 col-xs-3">
<button type="button" class="btn btn-outline-danger js-remove-dhcp-static-lease"><i class="far fa-trash-alt"></i></button>
</div>
</div>`;
leaseContainer.append(row);
});
}
});
}

View File

@@ -1,6 +1,6 @@
<?php
require_once 'includes/wifi_functions.php';
use RaspAP\Networking\Hotspot\WiFiManager;
/**
*
@@ -8,12 +8,13 @@ require_once 'includes/wifi_functions.php';
*/
function DisplayWPAConfig()
{
$wifi = new WiFiManager();
$status = new \RaspAP\Messages\StatusMessage;
$networks = [];
getWifiInterface();
knownWifiStations($networks);
setKnownStationsWPA($networks);
$wifi->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)) {

View File

@@ -1,22 +1,28 @@
<?php
require_once 'includes/config.php';
require_once 'includes/wifi_functions.php';
require_once 'includes/functions.php';
use RaspAP\System\Sysinfo;
use RaspAP\UI\Dashboard;
use RaspAP\Messages\StatusMessage;
use RaspAP\Plugins\PluginManager;
use RaspAP\Networking\Hotspot\WiFiManager;
/**
* Displays the dashboard
*/
function DisplayDashboard(&$extraFooterScripts): void
{
// instantiate RaspAP objects
$system = new \RaspAP\System\Sysinfo;
$dashboard = new \RaspAP\UI\Dashboard;
$status = new \RaspAP\Messages\StatusMessage;
$pluginManager = \RaspAP\Plugins\PluginManager::getInstance();
$system = new Sysinfo();
$dashboard = new Dashboard();
$status = new StatusMessage();
$pluginManager = PluginManager::getInstance();
$wifi = new WiFiManager();
// set AP and client interface session vars
getWifiInterface();
$wifi->getWifiInterface();
$interface = $_SESSION['ap_interface'] ?? 'wlan0';
$clientInterface = $_SESSION['wifi_client_interface'];

View File

@@ -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;
}

View File

@@ -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

View File

@@ -1,9 +1,13 @@
<?php
require_once 'includes/wifi_functions.php';
require_once 'includes/config.php';
use RaspAP\Networking\Hotspot\HostapdManager;
use RaspAP\Networking\Hotspot\HotspotService;
use RaspAP\Networking\Hotspot\WiFiManager;
use RaspAP\Messages\StatusMessage;
use RaspAP\System\Sysinfo;
getWifiInterface();
$wifi = new WiFiManager();
$wifi->getWifiInterface();
/**
* Initialize hostapd values, display interface
@@ -11,51 +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 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)"));
$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'])) {
@@ -76,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);
@@ -85,80 +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);
}
}
// 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;
$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);
@@ -171,7 +114,6 @@ function DisplayHostAPDConfig()
"interfaces",
"arrConfig",
"arr80211Standard",
"selectedHwMode",
"arrSecurity",
"arrEncType",
"arr80211w",
@@ -179,607 +121,9 @@ function DisplayHostAPDConfig()
"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 repeater mode checkbox
$repeaterEnable = 0;
if ($bridgedEnable == 0) { // enable client mode actions when not bridged
if ($arrHostapdConf['RepeaterEnable'] == 0) {
if (isset($_POST['repeaterEnable'])) {
$repeaterEnable = 1;
}
} else {
if (isset($_POST['repeaterEnable'])) {
$repeaterEnable = 1;
}
}
}
// Check for WiFi client AP mode checkbox
$wifiAPEnable = 0;
if ($bridgedEnable == 0) { // enable client mode actions when not bridged
if ($arrHostapdConf['WifiAPEnable'] == 0) {
if (isset($_POST['wifiAPEnable'])) {
$wifiAPEnable = 1;
}
} else {
if (isset($_POST['wifiAPEnable'])) {
$wifiAPEnable = 1;
}
}
}
// Check for Logfile output checkbox
$logEnable = 0;
if ($arrHostapdConf['LogEnable'] == 0) {
if (isset($_POST['logEnable'])) {
$logEnable = 1;
exec('sudo '.RASPI_CONFIG.'/hostapd/enablelog.sh');
} else {
exec('sudo '.RASPI_CONFIG.'/hostapd/disablelog.sh');
}
} else {
if (isset($_POST['logEnable'])) {
$logEnable = 1;
exec('sudo '.RASPI_CONFIG.'/hostapd/enablelog.sh');
} else {
exec('sudo '.RASPI_CONFIG.'/hostapd/disablelog.sh');
}
}
// 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['RepeaterEnable'] = $repeaterEnable;
$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) {
$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);
}
// Parse dnsmasq config for selected interface
exec('cat '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $lines);
$syscfg = ParseConfig($lines);
if ($wifiAPEnable == 1) {
// Enable uap0 configuration for ap-sta mode
// Set dhcp-range from system config, fallback to default if undefined
$dhcp_range = ($syscfg['dhcp-range'] == '') ? getDefaultNetValue('dnsmasq','uap0','dhcp-range') : $syscfg['dhcp-range'];
$config = [ '# RaspAP uap0 configuration' ];
$config[] = 'interface=lo,uap0 # Enable uap0 interface for wireless client AP mode';
$config[] = 'bind-dynamic # Hybrid between --bind-interfaces and default';
$config[] = 'server=8.8.8.8 # Forward DNS requests to Google DNS';
$config[] = 'domain-needed # Don\'t forward short names';
$config[] = 'bogus-priv # Never forward addresses in the non-routed address spaces';
$config[] = 'dhcp-range='.$dhcp_range;
if (!empty($syscfg['dhcp-option'])) {
$config[] = 'dhcp-option='.$syscfg['dhcp-option'];
}
$config[] = PHP_EOL;
scanConfigDir('/etc/dnsmasq.d/','uap0',$status);
$config = join(PHP_EOL, $config);
file_put_contents("/tmp/dnsmasqdata", $config);
system('sudo cp /tmp/dnsmasqdata '.RASPI_DNSMASQ_PREFIX.$ap_iface.'.conf', $return);
} elseif ($bridgedEnable !== 1) {
$dhcp_range = ($syscfg['dhcp-range'] == '')
? getDefaultNetValue('dnsmasq', $ap_iface, 'dhcp-range')
: $syscfg['dhcp-range'];
$config = [ '# RaspAP ' . $_POST['interface'] . ' configuration' ];
$config[] = 'interface=' . $_POST['interface'];
$config[] = 'domain-needed';
$config[] = 'dhcp-range=' . $dhcp_range;
// handle multiple dhcp-host + option entries
if (!empty($syscfg['dhcp-host'])) {
if (is_array($syscfg['dhcp-host'])) {
foreach ($syscfg['dhcp-host'] as $host) {
$config[] = 'dhcp-host=' . $host;
}
} else {
$config[] = 'dhcp-host=' . $syscfg['dhcp-host'];
}
}
if (!empty($syscfg['dhcp-option'])) {
if (is_array($syscfg['dhcp-option'])) {
foreach ($syscfg['dhcp-option'] as $opt) {
$config[] = 'dhcp-option=' . $opt;
}
} else {
$config[] = 'dhcp-option=' . $syscfg['dhcp-option'];
}
}
$config[] = PHP_EOL;
$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);
}
$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 <strong>DHCP Server</strong> before starting AP.'), 'warning');
}
if ($bridgedEnable == 1) {
$config = array_keys(getDefaultNetOpts('dhcp','options'));
$config[] = PHP_EOL.'# RaspAP br0 configuration';
$config[] = 'denyinterfaces eth0 wlan0';
$config[] = 'interface br0';
$config[] = PHP_EOL;
} elseif ($repeaterEnable == 1) {
$config = [ '# RaspAP '.$ap_iface.' configuration' ];
$config[] = 'interface '.$ap_iface;
$config[] = 'static ip_address='.$ip_address;
$config[] = 'static routers='.$routers;
$config[] = 'static domain_name_server='.$domain_name_server;
$client_metric = getIfaceMetric($_SESSION['wifi_client_interface']);
if (is_int($client_metric)) {
$ap_metric = (int)$client_metric + 1;
$config[] = 'metric '.$ap_metric;
} else {
$status->addMessage('Unable to obtain metric value for client interface. Repeater mode inactive.', 'warning');
$repeaterEnable = false;
}
} elseif ($wifiAPEnable == 1) {
$config = array_keys(getDefaultNetOpts('dhcp','options'));
$config[] = PHP_EOL.'# RaspAP uap0 configuration';
$config[] = 'interface uap0';
$config[] = 'static ip_address='.$ip_address;
$config[] = 'nohook wpa_supplicant';
$config[] = PHP_EOL;
} else {
$config = updateDhcpcdConfig($ap_iface, $jsonData, $ip_address, $routers, $domain_name_server);
}
$dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG);
if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) {
$skip_dhcp = true;
} elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) {
$dhcp_cfg = join(PHP_EOL, $config);
$status->addMessage(sprintf(_('DHCP configuration for %s enabled.'), $ap_iface), 'success');
} elseif (!preg_match('/^interface\s'.$ap_iface.'$/m', $dhcp_cfg)) {
$config[] = PHP_EOL;
$config= join(PHP_EOL, $config);
$dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0');
$dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0');
$dhcp_cfg .= $config;
} else {
$config = join(PHP_EOL, $config);
$dhcp_cfg = removeDHCPIface($dhcp_cfg,'br0');
$dhcp_cfg = removeDHCPIface($dhcp_cfg,'uap0');
if (!strpos($dhcp_cfg, 'metric')) {
$dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1);
} else {
$metrics = true;
}
}
if ($repeaterEnable && $metrics) {
$status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning');
} else if ($repeaterEnable && !$metrics) {
$status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success');
$status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success');
persistDHCPConfig($dhcp_cfg, $ap_iface, $status);
} elseif (!$skip_dhcp) {
persistDHCPConfig($dhcp_cfg, $ap_iface, $status);
} else {
$status->addMessage('WiFi hotspot settings saved.', 'success');
}
} else {
$status->addMessage('Unable to save WiFi hotspot settings', 'danger');
return false;
}
return true;
}
/**
* Persists a DHCP configuration
*
* @param string $dhcp_cfg
* @param string $ap_iface
* @param object $status
* @return $status
*/
function persistDHCPConfig($dhcp_cfg, $ap_iface, $status)
{
file_put_contents("/tmp/dhcpddata", $dhcp_cfg);
system('sudo cp /tmp/dhcpddata '.RASPI_DHCPCD_CONFIG, $return);
if ($return == 0) {
$status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $ap_iface), 'success');
$status->addMessage('WiFi hotspot settings saved.', 'success');
} else {
$status->addMessage('Unable to save WiFi hotspot settings.', 'danger');
}
return $status;
}
/**
* Returns a count of hostapd-<interface>.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 {
return false;
}
}
/**
* Builds a hostapd configuration string for a given interface
*
* @param array $params Associative array of config values
* @return string
*/
function buildHostapdConfig(array $params): string
{
$config = [];
$config[] = 'driver=nl80211';
$config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE;
$config[] = 'ctrl_interface_group=0';
$config[] = 'auth_algs=1';
$wpa = $params['wpa'];
$wpa_key_mgmt = 'WPA-PSK';
if ($wpa == 4) {
$config[] = 'ieee80211w=1';
$wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE';
$wpa = 2;
} elseif ($wpa == 5) {
$config[] = 'ieee80211w=2';
$wpa_key_mgmt = 'SAE';
$wpa = 2;
}
if ($params['80211w'] == 1) {
$config[] = 'ieee80211w=1';
$wpa_key_mgmt = 'WPA-PSK';
} elseif ($params['80211w'] == 2) {
$config[] = 'ieee80211w=2';
$wpa_key_mgmt = 'WPA-PSK-SHA256';
}
$config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt;
if (!empty($params['beacon_interval'])) {
$config[] = 'beacon_int=' . $params['beacon_interval'];
}
if (!empty($params['disassoc_low_ack'])) {
$config[] = 'disassoc_low_ack=0';
}
$config[] = 'ssid=' . $params['ssid'];
$config[] = 'channel=' . $params['channel'];
// Choose VHT segment index (fallback only if required)
$vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155;
switch ($params['hw_mode']) {
case 'n':
$config[] = 'hw_mode=g';
$config[] = 'ieee80211n=1';
$config[] = 'wmm_enabled=1';
break;
case 'ac':
$config[] = 'hw_mode=a';
$config[] = '# N';
$config[] = 'ieee80211n=1';
$config[] = 'require_ht=1';
$config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]';
$config[] = '# AC';
$config[] = 'ieee80211ac=1';
$config[] = 'require_vht=1';
$config[] = 'ieee80211d=0';
$config[] = 'ieee80211h=0';
$config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]';
$config[] = 'vht_oper_chwidth=1';
$config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx;
break;
default:
$config[] = 'hw_mode=' . $params['hw_mode'];
$config[] = 'ieee80211n=0';
}
if ($params['wpa'] !== 'none') {
$config[] = 'wpa_passphrase=' . $params['wpa_passphrase'];
}
if (!empty($params['bridge'])) {
$config[] = 'interface=' . $params['interface'];
$config[] = 'bridge=' . $params['bridge'];
} else {
$config[] = 'interface=' . $params['interface'];
}
$config[] = 'wpa=' . $wpa;
$config[] = 'wpa_pairwise=' . $params['wpa_pairwise'];
$config[] = 'country_code=' . $params['country_code'];
$config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID'];
if (!empty($params['max_num_sta'])) {
$config[] = 'max_num_sta=' . (int)$params['max_num_sta'];
}
// Optional additional user config
$config[] = parseUserHostapdCfg();
return implode(PHP_EOL, $config) . PHP_EOL;
}
/**
* Updates the dhcpcd configuration for a given interface, preserving existing settings
*
* @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;
}
}

View File

@@ -1,9 +1,10 @@
<?php
require_once 'includes/config.php';
require_once 'includes/wifi_functions.php';
getWifiInterface();
use RaspAP\Networking\Hotspot\WiFiManager;
$wifi = new WiFiManager();
$wifi->getWifiInterface();
/**
* Manage OpenVPN configuration

View File

@@ -1,366 +0,0 @@
<?php
require_once 'functions.php';
const MIN_RSSI = -100;
const MAX_RSSI = -55;
function knownWifiStations(&$networks)
{
// Find currently configured networks
exec(' sudo cat ' . RASPI_WPA_SUPPLICANT_CONFIG, $known_return);
//$index = 0;
foreach ($known_return as $line) {
if (preg_match('/network\s*=/', $line)) {
$network = array('visible' => false, 'configured' => true, 'connected' => false, 'index' => null);
++$index;
} elseif (isset($network) && $network !== null) {
if (preg_match('/^\s*}\s*$/', $line)) {
$networks[$ssid] = $network;
$network = null;
$ssid = null;
} elseif ($lineArr = preg_split('/\s*=\s*/', trim($line), 2)) {
switch (strtolower($lineArr[0])) {
case 'ssid':
$ssid = trim($lineArr[1], '"');
$ssid = str_replace('P"','',$ssid);
$network['ssid'] = $ssid;
$index = getNetworkIdBySSID($ssid);
$network['index'] = $index;
break;
case 'psk':
$network['passkey'] = trim($lineArr[1]);
$network['protocol'] = 'WPA';
break;
case '#psk':
$network['protocol'] = 'WPA';
case 'wep_key0': // Untested
$network['passphrase'] = trim($lineArr[1], '"');
break;
case 'key_mgmt':
if (! array_key_exists('passphrase', $network) && $lineArr[1] === 'NONE') {
$network['protocol'] = 'Open';
}
break;
case 'priority':
$network['priority'] = trim($lineArr[1], '"');
break;
}
}
}
}
}
/**
* Scans for nearby WiFi networks using `iw` and updates the reference array
*
* @param array $networks Reference to the array of known and discovered networks.
* @param bool $cached If false, bypasses the cache and performs a fresh scan.
*/
function nearbyWifiStations(&$networks, $cached = true)
{
$cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG);
$cacheKey = "nearby_wifi_stations_$cacheTime";
if ($cached == false) {
deleteCache($cacheKey);
}
$iface = escapeshellarg($_SESSION['wifi_client_interface']);
$scan_results = cache(
$cacheKey,
function () use ($iface) {
$stdout = shell_exec("sudo iw dev $iface scan");
return preg_split("/\n/", $stdout);
}
);
// exclude the AP from nearby networks
exec('sed -rn "s/ssid=(.*)\s*$/\1/p" ' . escapeshellarg(RASPI_HOSTAPD_CONFIG), $ap_ssid);
$ap_ssid = $ap_ssid[0] ?? '';
$index = 0;
if (!empty($networks)) {
$lastnet = end($networks);
if (isset($lastnet['index'])) {
$index = $lastnet['index'] + 1;
}
}
$current = [];
$commitCurrent = function () use (&$current, &$networks, &$index, $ap_ssid) {
if (empty($current['ssid'])) {
return;
}
$ssid = $current['ssid'];
// unprintable 7bit ASCII control codes, delete or quotes -> ignore network
if ($ssid === $ap_ssid || preg_match('/[\x00-\x1f\x7f\'`\´"]/', $ssid)) {
return;
}
$channel = ConvertToChannel($current['freq'] ?? 0);
$rssi = $current['signal'] ?? -100;
// if network is saved
if (array_key_exists($ssid, $networks)) {
$networks[$ssid]['visible'] = true;
$networks[$ssid]['channel'] = $channel;
if (!isset($networks[$ssid]['RSSI']) || $networks[$ssid]['RSSI'] < $rssi) {
$networks[$ssid]['RSSI'] = $rssi;
}
} else {
$networks[$ssid] = [
'ssid' => $ssid,
'configured' => false,
'protocol' => $current['security'] ?? 'OPEN',
'channel' => $channel,
'passphrase' => '',
'visible' => true,
'connected' => false,
'RSSI' => $rssi,
'index' => $index
];
++$index;
}
};
foreach ($scan_results as $line) {
$line = trim($line);
if (preg_match('/^BSS\s+([0-9a-f:]{17})/', $line, $match)) {
$commitCurrent(); // commit previous
$current = [
'bssid' => $match[1],
'ssid' => '',
'signal' => null,
'freq' => null,
'security' => 'OPEN'
];
continue;
}
if (preg_match('/^SSID:\s*(.*)$/', $line, $match)) {
$current['ssid'] = $match[1];
continue;
}
if (preg_match('/^signal:\s*(-?\d+\.\d+)/', $line, $match)) {
$current['signal'] = (float)$match[1];
continue;
}
if (preg_match('/^freq:\s*(\d+)/', $line, $match)) {
$current['freq'] = (int)$match[1];
continue;
}
if (preg_match('/^RSN:/', $line) || preg_match('/^WPA:/', $line)) {
$current['security'] = 'WPA/WPA2';
continue;
}
}
$commitCurrent();
}
function connectedWifiStations(&$networks)
{
exec('iwconfig ' .$_SESSION['wifi_client_interface'], $iwconfig_return);
foreach ($iwconfig_return as $line) {
if (preg_match('/ESSID:\"([^"]+)\"/i', $line, $iwconfig_ssid)) {
$networks[hexSequence2lower($iwconfig_ssid[1])]['connected'] = true;
}
}
}
function sortNetworksByRSSI(&$networks)
{
$valRSSI = array();
foreach ($networks as $SSID => $net) {
if (!array_key_exists('RSSI', $net)) {
$net['RSSI'] = -1000;
}
$valRSSI[$SSID] = $net['RSSI'];
}
$nets = $networks;
arsort($valRSSI);
$networks = array();
foreach ($valRSSI as $SSID => $RSSI) {
$networks[$SSID] = $nets[$SSID];
$networks[$SSID]['RSSI'] = $RSSI;
}
}
/*
* Determines the configured wireless AP interface
*
* If not saved in /etc/raspap/hostapd.ini, check for a second
* wireless interface with iw dev. Fallback to the constant
* value defined in config.php
*/
function getWifiInterface()
{
$hostapdIni = RASPI_CONFIG . '/hostapd.ini';
$arrHostapdConf = file_exists($hostapdIni) ? parse_ini_file($hostapdIni) : [];
$iface = $_SESSION['ap_interface'] = $arrHostapdConf['WifiInterface'] ?? RASPI_WIFI_AP_INTERFACE;
if (!validateInterface($iface)) {
$iface = RASPI_WIFI_AP_INTERFACE;
}
// check for 2nd wifi interface -> wifi client on different interface
exec("iw dev | awk '$1==\"Interface\" && $2!=\"$iface\" {print $2}'", $iface2);
$client_iface = $_SESSION['wifi_client_interface'] = empty($iface2) ? $iface : trim($iface2[0]);
// handle special case for RPi Zero W in AP-STA mode
if ($client_iface === "uap0" && ($arrHostapdConf['WifiAPEnable'] ?? 0)) {
$_SESSION['wifi_client_interface'] = $iface;
$_SESSION['ap_interface'] = $client_iface;
}
}
/*
* Reinitializes wpa_supplicant for the wireless client interface
* The 'force' parameter deletes the socket in /var/run/wpa_supplicant/
*
* @param boolean $force
*/
function reinitializeWPA($force)
{
$iface = escapeshellarg($_SESSION['wifi_client_interface']);
if ($force == true) {
$cmd = "sudo /bin/rm /var/run/wpa_supplicant/$iface";
$result = shell_exec($cmd);
}
$cmd = "sudo wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i$iface";
$result = shell_exec($cmd);
sleep(1);
return $result;
}
/*
* Replace escaped bytes (hex) by binary - assume UTF8 encoding
*
* @param string $ssid
*/
function ssid2utf8($ssid)
{
return evalHexSequence($ssid);
}
/*
* Returns a signal strength indicator based on RSSI value
*
* @param string $rssi
*/
function getSignalBars($rssi)
{
// assign css class based on RSSI value
if ($rssi >= MAX_RSSI) {
$class = 'strong';
} elseif ($rssi >= -56) {
$class = 'medium';
} elseif ($rssi >= -67) {
$class = 'weak';
} elseif ($rssi >= -89) {
$class = '';
}
// calculate percent strength
if ($rssi >= -50) {
$pct = 100;
} elseif ($rssi <= MIN_RSSI) {
$pct = 0;
} else {
$pct = 2*($rssi + 100);
}
$elem = '<div data-toggle="tooltip" title="' . _("Signal strength"). ': ' .$pct. '%" class="signal-icon ' .$class. '">'.PHP_EOL;
for ($n = 0; $n < 3; $n++ ) {
$elem .= '<div class="signal-bar"></div>'.PHP_EOL;
}
$elem .= '</div>'.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;
}

View File

@@ -1,9 +1,11 @@
<?php
require_once 'includes/wifi_functions.php';
require_once 'includes/config.php';
getWifiInterface();
use RaspAP\Networking\Hotspot\WiFiManager;
$wifi = new WiFiManager();
$wifi->getWifiInterface();
/**
* Displays wireguard server & peer configuration

View File

@@ -5,6 +5,7 @@ www-data ALL=(ALL) NOPASSWD:/bin/cat /etc/wpa_supplicant/wpa_supplicant-[a-zA-Z0
www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant.conf
www-data ALL=(ALL) NOPASSWD:/bin/cp /tmp/wifidata /etc/wpa_supplicant/wpa_supplicant-wl*.conf
www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -B -Dnl80211 -c/etc/wpa_supplicant/wpa_supplicant.conf -i[a-zA-Z0-9]*
www-data ALL=(ALL) NOPASSWD:/sbin/wpa_supplicant -i [a-zA-Z0-9]* -c /etc/wpa_supplicant/wpa_supplicant.conf -B
www-data ALL=(ALL) NOPASSWD:/bin/rm /var/run/wpa_supplicant/[a-zA-Z0-9]*
www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* scan_results
www-data ALL=(ALL) NOPASSWD:/sbin/wpa_cli -i [a-zA-Z0-9]* scan
@@ -22,6 +23,7 @@ www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop hostapd.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl start dnsmasq.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop dnsmasq.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl restart dnsmasq.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl reload dnsmasq.service
www-data ALL=(ALL) NOPASSWD:/bin/systemctl start openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl enable openvpn-client@client
www-data ALL=(ALL) NOPASSWD:/bin/systemctl stop openvpn-client@client
@@ -44,7 +46,7 @@ www-data ALL=(ALL) NOPASSWD:/sbin/ip link set wl* up
www-data ALL=(ALL) NOPASSWD:/sbin/ip -s a f label wl*
www-data ALL=(ALL) NOPASSWD:/sbin/ifup *
www-data ALL=(ALL) NOPASSWD:/sbin/ifdown *
www-data ALL=(ALL) NOPASSWD:/sbin/iw
www-data ALL=(ALL) NOPASSWD:/sbin/iw dev*
www-data ALL=(ALL) NOPASSWD:/bin/cp /etc/raspap/networking/dhcpcd.conf /etc/dhcpcd.conf
www-data ALL=(ALL) NOPASSWD:/etc/raspap/hostapd/enablelog.sh
www-data ALL=(ALL) NOPASSWD:/etc/raspap/hostapd/disablelog.sh

View File

@@ -0,0 +1,527 @@
<?php
/**
* A dhcpcd configuration class for RaspAP
*
* @description Handles building, saving and safe updating of dhcpcd configs
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot;
use RaspAP\Messages\StatusMessage;
class DhcpcdManager
{
private const CONF_DEFAULT = RASPI_DHCPCD_CONFIG;
private const CONF_TMP = '/tmp/dhcpddata';
/**
* Builds a dhcpcd config for an interface
*
* @param string $ap_iface
* @param bool $bridgedEnable
* @param bool $repeaterEnable
* @param bool $wifiAPEnable
* @param bool $dualAPEnable
* @param StatusMessage $status
* @return string
*/
public function buildConfig(
string $ap_iface,
bool $bridgedEnable,
bool $repeaterEnable,
bool $wifiAPEnable,
bool $dualAPEnable,
StatusMessage $status
): bool
{
// determine static IP, routers, DNS
$jsonData = $this->getInterfaceConfig($ap_iface);
//error_log("DhcpcdManager::buildConfig() jsonData =" . print_r($jsonData, true));
$ip_address = empty($jsonData['StaticIP'])
? getDefaultNetValue('dhcp', $ap_iface, 'static ip_address')
: $jsonData['StaticIP'];
$domain_name_server = empty($jsonData['StaticDNS'])
? getDefaultNetValue('dhcp', $ap_iface, 'static domain_name_server')
: $jsonData['StaticDNS'];
$routers = empty($jsonData['StaticRouters'])
? getDefaultNetValue('dhcp', $ap_iface, 'static routers')
: $jsonData['StaticRouters'];
$netmask = (empty($jsonData['SubnetMask']) || $jsonData['SubnetMask'] === '0.0.0.0')
? getDefaultNetValue('dhcp', $ap_iface, 'subnetmask')
: $jsonData['SubnetMask'];
if (!preg_match('/.*\/\d+/', $ip_address)) {
$ip_address .= '/' . mask2cidr($netmask);
}
$config = [];
if ($bridgedEnable) {
$config = array_keys(getDefaultNetOpts('dhcp', 'options'));
$config[] = '# RaspAP br0 configuration';
$config[] = 'denyinterfaces eth0 wlan0';
$config[] = 'interface br0';
} elseif ($repeaterEnable) {
$config = [
'# RaspAP ' . $ap_iface . ' configuration',
'interface ' . $ap_iface,
'static ip_address=' . $ip_address,
'static routers=' . $routers,
'static domain_name_server=' . $domain_name_server
];
$client_metric = getIfaceMetric($_SESSION['wifi_client_interface']);
if (is_int($client_metric)) {
$config[] = 'metric ' . ((int)$client_metric + 1);
} else {
$status->addMessage(
'Unable to obtain metric value for client interface. Repeater mode inactive.',
'warning'
);
}
} elseif ($wifiAPEnable) {
$config = array_keys(getDefaultNetOpts('dhcp', 'options'));
$config[] = '# RaspAP uap0 configuration';
$config[] = 'interface uap0';
$config[] = 'static ip_address=' . $ip_address;
$config[] = 'nohook wpa_supplicant';
} elseif ($dualAPEnable) {
$config = [
'# RaspAP ' . $ap_iface . ' configuration',
'interface ' . $ap_iface,
'static ip_address=' . $ip_address,
'static routers=' . $routers,
'static domain_name_server=' . $domain_name_server,
'nogateway'
];
} else {
$config = $this->updateDhcpcdConfig(
$ap_iface,
$jsonData,
$ip_address,
$routers,
$domain_name_server
);
}
$dhcp_cfg = file_get_contents(SELF::CONF_DEFAULT);
$skip_dhcp = false;
if (preg_match('/wlan[3-9]\d*|wlan[1-9]\d+/', $ap_iface)) {
$skip_dhcp = true;
} elseif ($bridgedEnable == 1 || $wifiAPEnable == 1) {
$dhcp_cfg = join(PHP_EOL, $config);
$status->addMessage(sprintf(_('DHCP configuration for %s enabled.'), $ap_iface), 'success');
} elseif (!preg_match('/^interface\s'.$ap_iface.'$/m', $dhcp_cfg)) {
$config[] = PHP_EOL;
$config= join(PHP_EOL, $config);
$dhcp_cfg = $this->removeIface($dhcp_cfg,'br0');
$dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0');
$dhcp_cfg .= $config;
} else {
$config = join(PHP_EOL, $config);
$dhcp_cfg = $this->removeIface($dhcp_cfg,'br0');
$dhcp_cfg = $this->removeIface($dhcp_cfg,'uap0');
if (!strpos($dhcp_cfg, 'metric')) {
$dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$ap_iface.'\s.*?(?=(?:\s*^\s*$|\s*nogateway))/ms', $config, $dhcp_cfg, 1);
} else {
$metrics = true;
}
}
if ($repeaterEnable && $metrics) {
$status->addMessage(_('WiFi repeater mode: A metric value is already defined for DHCP.'), 'warning');
} else if ($repeaterEnable && !$metrics) {
$status->addMessage(sprintf(_('Metric value configured for the %s interface.'), $ap_iface), 'success');
$status->addMessage('Restart hotspot to enable WiFi repeater mode.', 'success');
$this->saveConfig($dhcp_cfg, $ap_iface, $status);
} elseif (!$skip_dhcp) {
$this->saveConfig($dhcp_cfg, $ap_iface, $status);
} else {
$status->addMessage('WiFi hotspot settings saved.', 'success');
}
return true;
}
/**
* (Re)builds an existing dhcp configuration
*
* @param string $iface
* @param StatusMessage $status
* @param array $post_data
* @return string $dhcp_cfg
*/
public function buildConfigEx(string $iface, array $post_data, StatusMessage $status): string
{
$cfg[] = '# RaspAP '.$iface.' configuration';
$cfg[] = 'interface '.$iface;
if (isset($post_data['StaticIP']) && $post_data['StaticIP'] !== '') {
$mask = ($post_data['SubnetMask'] !== '' && $post_data['SubnetMask'] !== '0.0.0.0') ? '/'.mask2cidr($post_data['SubnetMask']) : null;
$cfg[] = 'static ip_address='.$post_data['StaticIP'].$mask;
}
if (isset($post_data['DefaultGateway']) && $post_data['DefaultGateway'] !== '') {
$cfg[] = 'static routers='.$post_data['DefaultGateway'];
}
if ($post_data['DNS1'] !== '' || $post_data['DNS2'] !== '') {
$cfg[] = 'static domain_name_server='.$post_data['DNS1'].' '.$post_data['DNS2'];
}
if ($post_data['Metric'] !== '') {
$cfg[] = 'metric '.$post_data['Metric'];
}
if (($post_data['Fallback'] ?? 0) == 1) {
$cfg[] = 'profile static_'.$iface;
$cfg[] = 'fallback static_'.$iface;
}
$cfg[] = ($post_data['DefaultRoute'] ?? '') == '1' ? 'gateway' : 'nogateway';
if (substr($iface, 0, 2) === "wl" && ($post_data['NoHookWPASupplicant'] ?? '') == '1') {
$cfg[] = 'nohook wpa_supplicant';
}
$dhcp_cfg = file_get_contents(RASPI_DHCPCD_CONFIG);
if (!preg_match('/^interface\s'.$iface.'$/m', $dhcp_cfg)) {
$cfg[] = PHP_EOL;
$cfg = join(PHP_EOL, $cfg);
$dhcp_cfg .= $cfg;
$status->addMessage('DHCP configuration for '.$iface.' added.', 'success');
} else {
$cfg = join(PHP_EOL, $cfg);
$dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)/ms', $cfg, $dhcp_cfg, 1);
}
return $dhcp_cfg;
}
/**
* Validates DHCP user input from $_POST data
*
* @param array $post_data
* @return array $errors
*/
public function validate(array $post_data): array
{
$errors = [];
define('IFNAMSIZ', 16);
$iface = $post_data['interface'];
if (!preg_match('/^[^\s\/\\0]+$/', $iface)
|| strlen($iface) >= IFNAMSIZ
) {
$errors[] = _('Invalid interface name.');
}
if (!filter_var($post_data['StaticIP'], FILTER_VALIDATE_IP) && !empty($post_data['StaticIP'])) {
$errors[] = _('Invalid static IP address.');
}
if (!filter_var($post_data['SubnetMask'], FILTER_VALIDATE_IP) && !empty($post_data['SubnetMask'])) {
$errors[] = _('Invalid subnet mask.');
}
if (!filter_var($post_data['DefaultGateway'], FILTER_VALIDATE_IP) && !empty($post_data['DefaultGateway'])) {
$errors[] = _('Invalid default gateway.');
}
if (($post_data['dhcp-iface'] == "1")) {
if (!filter_var($post_data['RangeStart'], FILTER_VALIDATE_IP) && !empty($post_data['RangeStart'])) {
$errors[] = _('Invalid DHCP range start.');
}
if (!filter_var($post_data['RangeEnd'], FILTER_VALIDATE_IP) && !empty($post_data['RangeEnd'])) {
$errors[] = _('Invalid DHCP range end.');
}
if (!ctype_digit($post_data['RangeLeaseTime']) && $post_data['RangeLeaseTimeUnits'] !== 'i') {
$errors[] = _('Invalid DHCP lease time, not a number.');
}
if (!in_array($post_data['RangeLeaseTimeUnits'], array('m', 'h', 'd', 'i'))) {
$errors[] = _('Unknown DHCP lease time unit.');
}
if ($post_data['Metric'] !== '' && !ctype_digit($post_data['Metric'])) {
$errors[] = _('Invalid metric value, not a number.');
}
}
return $errors;
}
/**
* Saves a dhcpcd configuration
*
* @param string $config
* @param StatusMessage $status
* @return bool
* @throws \RuntimeException
*/
public function saveConfig(string $config, string $iface, StatusMessage $status): bool
{
if (file_put_contents(self::CONF_TMP, $config) === false) {
throw new \RuntimeException("Failed to write temporary dhcpcd config");
}
exec(sprintf('sudo cp %s %s', escapeshellarg(self::CONF_TMP), escapeshellarg(self::CONF_DEFAULT)), $o, $rc);
if ($rc !== 0) {
$status->addMessage('Unable to save DHCP configuration.', 'danger');
return false;
}
$status->addMessage(sprintf(_('DHCP configuration for %s updated.'), $iface), 'success');
return true;
}
/**
* Removes a dhcp configuration block for the specified interface
*
* @param string $iface
* @param StatusMessage $status
* @return bool $result
*/
public function remove(string $iface, StatusMessage $status): bool
{
$configFile = SELF::CONF_DEFAULT;
$tempFile = SELF::CONF_TMP;
$dhcp_cfg = file_get_contents($configFile);
$modified_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1);
if ($modified_cfg !== $dhcp_cfg) {
file_put_contents($tempFile, $modified_cfg);
$cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile));
exec($cmd, $output, $result);
if ($result == 0) {
$status->addMessage('DHCP configuration for '.$iface.' removed', 'success');
return true;
} else {
$status->addMessage('Failed to remove DHCP configuration for '.$iface, 'danger');
return false;
}
}
}
/**
* Removes a dhcp configuration block for the specified interface
*
* @param string $dhcp_cfg
* @param string $iface
* @return string $dhcp_cfg
*/
public function removeIface(string $dhcp_cfg, string $iface): string
{
$dhcp_cfg = preg_replace('/^#\sRaspAP\s'.$iface.'\s.*?(?=\s*^\s*$)([\s]+)/ms', '', $dhcp_cfg, 1);
return $dhcp_cfg;
}
/**
* Updates the dhcpcd configuration for a given interface, preserving existing settings
*
* @param string $ap_iface
* @param array $jsonData
* @param string $ip_address
* @param string $routers
* @param string $domain_name_server
* @return array updated configuration
*/
private function updateDhcpcdConfig(
string $ap_iface,
array $jsonData,
string $ip_address,
string $routers,
string $domain_name_server): array
{
$dhcp_cfg = file_get_contents(self::CONF_DEFAULT);
$existing_config = [];
$section_regex = '/^#\sRaspAP\s'.preg_quote($ap_iface, '/').'\s.*?(?=\s*^\s*$)/ms';
// extract existing interface configuration
if (preg_match($section_regex, $dhcp_cfg, $matches)) {
$lines = explode(PHP_EOL, $matches[0]);
foreach ($lines as $line) {
$line = trim($line);
if (preg_match('/^(interface|static|metric|nogateway|nohook)/', $line)) {
$existing_config[] = $line;
}
}
}
// initialize with comment
$config = [ '# RaspAP '.$ap_iface.' configuration' ];
$config[] = 'interface '.$ap_iface;
$static_settings = [
'static ip_address' => $ip_address,
'static routers' => $routers,
'static domain_name_server' => $domain_name_server
];
// merge existing settings with updates
foreach ($existing_config as $line) {
$matched = false;
foreach ($static_settings as $key => $value) {
if (strpos($line, $key) === 0) {
$config[] = "$key=$value";
$matched = true;
unset($static_settings[$key]);
break;
}
}
if (!$matched && !preg_match('/^interface/', $line)) {
$config[] = $line;
}
}
// add any new static settings
foreach ($static_settings as $key => $value) {
$config[] = "$key=$value";
}
// add metric if provided
if (!empty($jsonData['Metric']) && !in_array('metric '.$jsonData['Metric'], $config)) {
$config[] = 'metric '.$jsonData['Metric'];
}
return $config;
}
/**
* Retrieves the metric value for a given interface
*
* @param string $iface
* @return int $metric| bool false on failure
*/
public function getIfaceMetric($iface)
{
$metric = shell_exec("ip -o -4 route show dev ".$iface." | awk '/metric/ {print \$NF; exit}'");
if (isset($metric)) {
$metric = (int)$metric;
return $metric;
} else {
return false;
}
}
/**
* Gets current dhcpcd info for an interface
*
* @param string $iface
* @return array
*/
public function getInterfaceConfig(string $iface): array
{
$result = [
'DHCPEnabled' => false,
'RangeStart' => null,
'RangeEnd' => null,
'RangeMask' => null,
'leaseTime' => null,
'leaseTimeInterval' => null,
'dhcpHost' => [],
'upstreamServersEnabled' => false,
'upstreamServers' => [],
'DNS1' => null,
'DNS2' => null,
'Metric' => null,
'StaticIP' => null,
'SubnetMask' => null,
'StaticRouters' => null,
'StaticDNS' => null,
'FallbackEnabled' => false,
'DefaultRoute' => false,
'NoHookWPASupplicant' => false,
];
// dnsmasq
$dnsmasqFile = RASPI_DNSMASQ_PREFIX . $iface . '.conf';
if (file_exists($dnsmasqFile) && is_readable($dnsmasqFile)) {
$lines = [];
exec('cat ' . escapeshellarg($dnsmasqFile), $lines);
if (!function_exists('ParseConfig')) {
require_once 'includes/functions.php';
}
$conf = ParseConfig($lines);
if (!empty($conf)) {
$result['DHCPEnabled'] = true;
// dhcp-range may be multi-value
$rangeRaw = $conf['dhcp-range'] ?? null;
if (is_array($rangeRaw)) {
$rangeRaw = $rangeRaw[0] ?? null;
}
if (is_string($rangeRaw)) {
$rangeParts = explode(',', $rangeRaw);
$result['RangeStart'] = $rangeParts[0] ?? null;
$result['RangeEnd'] = $rangeParts[1] ?? null;
$result['RangeMask'] = $rangeParts[2] ?? null;
$leaseSpec = $rangeParts[3] ?? null;
if ($leaseSpec) {
if (preg_match('/^(\d+)([smhd])?$/i', $leaseSpec, $m)) {
$result['leaseTime'] = $m[1];
$result['leaseTimeInterval'] = $m[2] ?? 'h'; // default to hours if missing
} else {
$result['leaseTime'] = $leaseSpec;
$result['leaseTimeInterval'] = null;
}
}
}
// dhcp-host entries (array or scalar)
$hosts = $conf['dhcp-host'] ?? [];
if (!is_array($hosts) && $hosts !== null) {
$hosts = [$hosts];
}
$result['dhcpHost'] = array_values(array_filter($hosts));
// upstream DNS servers (server= lines)
$servers = $conf['server'] ?? [];
if (!is_array($servers) && !empty($servers)) {
$servers = [$servers];
}
$servers = array_filter($servers);
if (!empty($servers)) {
$result['upstreamServersEnabled'] = true;
$result['upstreamServers'] = $servers;
}
// dhcp-option=6,<dns1>[,<dns2>]
if (isset($conf['dhcp-option'])) {
$optsRaw = $conf['dhcp-option'];
// may be multiple dhcp-option lines; coalesce
$optLines = is_array($optsRaw) ? $optsRaw : [$optsRaw];
foreach ($optLines as $optLine) {
$parts = explode(',', $optLine);
if ($parts[0] === '6') {
$result['DNS1'] = $parts[1] ?? null;
$result['DNS2'] = $parts[2] ?? null;
break;
}
}
}
}
}
// dhcpcd
if (file_exists(self::CONF_DEFAULT) && is_readable(self::CONF_DEFAULT)) {
$dhcpcd = file_get_contents(self::CONF_DEFAULT);
// match interface block starting with '# RaspAP <iface> configuration'
$sectionPattern = '/^#\sRaspAP\s' . preg_quote($iface, '/') . '\sconfiguration.*?(?=^(?:#\sRaspAP\s|\s*$))/ms';
if (preg_match($sectionPattern, $dhcpcd, $match)) {
$block = $match[0];
$result['Metric'] = $this->matchFirst('/\bmetric\s+(\d+)/i', $block);
$staticIPLine = $this->matchFirst('/static\s+ip_address=([^\r\n]+)/i', $block);
$staticRouters = $this->matchFirst('/static\s+routers=([^\r\n]+)/i', $block);
$staticDNS = $this->matchFirst('/static\s+domain_name_server=([^\r\n]+)/i', $block);
$result['StaticIP'] = $staticIPLine ? (strpos($staticIPLine,'/') !== false
? substr($staticIPLine, 0, strpos($staticIPLine,'/'))
: $staticIPLine) : null;
$result['SubnetMask'] = $staticIPLine && function_exists('cidr2mask') && strpos($staticIPLine,'/')
? cidr2mask($staticIPLine)
: ($result['SubnetMask'] ?? null);
$result['StaticRouters'] = $staticRouters;
$result['StaticDNS'] = $staticDNS;
$result['FallbackEnabled'] = (bool) preg_match('/fallback\s+static_' . preg_quote($iface, '/') . '/i', $block);
$result['DefaultRoute'] = (bool) preg_match('/\bgateway\b/', $block);
$result['NoHookWPASupplicant'] = (bool) preg_match('/nohook\s+wpa_supplicant/i', $block);
}
}
return $result;
}
private function matchFirst(string $pattern, string $subject): ?string
{
return preg_match($pattern, $subject, $m) ? trim($m[1]) : null;
}
}

View File

@@ -0,0 +1,358 @@
<?php
/**
* A dnsmasq configuration manager for RaspAP
*
* @description Class methods to get, build and save dnsmasq configs
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot;
use RaspAP\Messages\StatusMessage;
class DnsmasqManager
{
private const CONF_DEFAULT = '/etc/dnsmasq.d/';
private const CONF_SUFFIX = '.conf';
private const CONF_TMP = '/tmp/dnsmasqdata';
private const CONF_RASPAP = '090_raspap';
/**
* Retrieves dnsmasq configuration for an interface
*
* @param string $iface
* @return array
* @throws \RuntimeException
*/
public function getConfig(string $iface): array
{
$configFile = RASPI_DNSMASQ_PREFIX . "$iface.conf";
$lines = [];
if (!file_exists($configFile)) {
throw new \RuntimeException("dnsmasq config not found: $configFile");
}
if (!is_readable($configFile)) {
throw new \RuntimeException("Unable to read dnsmasq config: $configFile");
}
if (!function_exists('ParseConfig')) {
throw new \RuntimeException("Unable to execute ParseConfig()");
}
exec('cat ' . escapeshellarg($configFile), $lines, $status);
if ($status !== 0 || empty($lines)) {
throw new \RuntimeException("Failed to read dnsmasq config for $iface");
}
$config = ParseConfig($lines);
return $config;
}
/**
* Builds a dnsmasq configuration
* @param array $syscfg
* @param string $iface
* @param bool $wifiAPEnable
* @param bool $bridgedEnable
* @return array $config
* @throws \RuntimeException
*/
public function buildConfig(array $syscfg, string $iface, bool $wifiAPEnable, bool $bridgedEnable): array
{
if ($wifiAPEnable == 1) {
// Enable uap0 configuration for ap-sta mode
// Set dhcp-range from system config, fallback to default if undefined
$dhcp_range = ($syscfg['dhcp-range'] == '') ? getDefaultNetValue('dnsmasq','uap0','dhcp-range') : $syscfg['dhcp-range'];
$config = [ '# RaspAP uap0 configuration' ];
$config[] = 'interface=lo,uap0 # Enable uap0 interface for wireless client AP mode';
$config[] = 'bind-dynamic # Hybrid between --bind-interfaces and default';
$config[] = 'server=8.8.8.8 # Forward DNS requests to Google DNS';
$config[] = 'domain-needed # Don\'t forward short names';
$config[] = 'bogus-priv # Never forward addresses in the non-routed address spaces';
$config[] = 'dhcp-range='.$dhcp_range;
if (!empty($syscfg['dhcp-option'])) {
$config[] = 'dhcp-option='.$syscfg['dhcp-option'];
}
$config[] = PHP_EOL;
$this->scanConfigDir(SELF::CONF_DEFAULT,'uap0',$status);
} elseif ($bridgedEnable !==1) {
$dhcp_range = ($syscfg['dhcp-range'] =='') ? getDefaultNetValue('dnsmasq',$iface,'dhcp-range') : $syscfg['dhcp-range'];
$config = [ '# RaspAP '.$_POST['interface'].' configuration' ];
$config[] = 'interface='.$_POST['interface'];
$config[] = 'domain-needed';
$config[] = 'dhcp-range='.$dhcp_range;
// handle multiple dhcp-host + option entries
if (!empty($syscfg['dhcp-host'])) {
if (is_array($syscfg['dhcp-host'])) {
foreach ($syscfg['dhcp-host'] as $host) {
$config[] = 'dhcp-host=' . $host;
}
} else {
$config[] = 'dhcp-host=' . $syscfg['dhcp-host'];
}
}
if (!empty($syscfg['dhcp-option'])) {
$dhcpOptions = (array) $syscfg['dhcp-option'];
$grouped = [];
foreach ($dhcpOptions as $opt) {
$parts = explode(',', $opt, 2);
if (count($parts) < 2) {
continue; // skip malformed option
}
list($code, $value) = $parts;
$grouped[$code][] = $value;
}
foreach ($grouped as $code => $values) {
$config[] = 'dhcp-option=' . $code . ',' . implode(',', $values);
}
}
$config[] = PHP_EOL;
}
return $config;
}
/**
* Builds an extended dnsmasq configuration
*
* @param string $iface
* @param array $post_data
* @return array $config
*/
public function buildConfigEx(string $iface, array $post_data): array
{
$config[] = '# RaspAP '. $iface .' configuration';
$config[] = 'interface='. $iface;
$leaseTime = ($post_data['RangeLeaseTimeUnits'] !== 'i')
? $post_data['RangeLeaseTime'] . $post_data['RangeLeaseTimeUnits']
: 'infinite';
$config[] = 'dhcp-range=' . $post_data['RangeStart'] . ',' .
$post_data['RangeEnd'] . ',' .
$post_data['SubnetMask'] . ',' .
$leaseTime;
// Static leases
$staticLeases = array();
if (isset($post_data["static_leases"]["mac"])) {
for ($i=0; $i < count($post_data["static_leases"]["mac"]); $i++) {
$mac = trim($post_data["static_leases"]["mac"][$i]);
$ip = trim($post_data["static_leases"]["ip"][$i]);
$comment = trim($post_data["static_leases"]["comment"][$i]);
if ($mac != "" && $ip != "") {
$staticLeases[] = array('mac' => $mac, 'ip' => $ip, 'comment' => $comment);
}
}
}
// Sort ascending by IPs
usort($staticLeases, [$this, 'compareIPs']);
// Update config
for ($i = 0; $i < count($staticLeases); $i++) {
$mac = $staticLeases[$i]['mac'];
$ip = $staticLeases[$i]['ip'];
$comment = $staticLeases[$i]['comment'];
$config[] = "dhcp-host=$mac,$ip # $comment";
}
if ($post_data['no-resolv'] == "1") {
$config[] = "no-resolv";
}
foreach ($post_data['server'] as $server) {
$config[] = "server=$server";
}
if (!empty($post_data['DNS1'])) {
$dnsOption = "dhcp-option=6," . $post_data['DNS1'];
if (!empty($post_data['DNS2'])) {
$dnsOption .= ',' . $post_data['DNS2'];
}
$config[] = $dnsOption;
}
if ($post_data['dhcp-ignore'] == "1") {
$config[] = 'dhcp-ignore=tag:!known';
}
$config[]= PHP_EOL;
return $config;
}
/**
* Builds a RaspAP default dnsmasq config
* Written to 090_raspap.conf
*
* @param array $post_data
* @return array $config
*/
public function buildDefault(array $post_data): array
{
// preamble
$config[] = '# RaspAP default config';
$config[] = 'log-facility='. RASPI_DHCPCD_LOG;
$config[] = 'conf-dir=/etc/dnsmasq.d';
// handle log option
if (($post_data['log-dhcp'] ?? '') == "1") {
$config[] = "log-dhcp";
}
if (($post_data['log-queries'] ?? '') == "1") {
$config[] = "log-queries";
}
$config[] = PHP_EOL;
return $config;
}
/**
* Saves dnsmasq configuration for an interface
*
* @param array $config
* @param string $iface
* @return bool
*/
public function saveConfig(array $config, string $iface): bool
{
$configFile = RASPI_DNSMASQ_PREFIX . $iface . SELF::CONF_SUFFIX;
$tempFile = SELF::CONF_TMP;
$config = join(PHP_EOL, $config);
file_put_contents($tempFile, $config);
$cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile));
exec($cmd, $output, $status);
if ($status !== 0) {
throw new \RuntimeException("Failed to copy temp config to $configFile");
return false;
}
// reload dnsmasq to apply changes
exec('sudo systemctl reload dnsmasq.service', $output, $status);
if ($status !== 0) {
throw new \RuntimeException("Failed to reload dnsmasq service");
}
return true;
}
/**
* Saves dnsmasq default configuration
*
* @param array $config
* @return bool
*/
public function saveConfigDefault(array $config): bool
{
$configFile = SELF::CONF_DEFAULT . SELF::CONF_RASPAP . SELF::CONF_SUFFIX;
$tempFile = SELF::CONF_TMP;
$config = join(PHP_EOL, $config);
file_put_contents($tempFile, $config);
$cmd = sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile));
exec($cmd, $output, $status);
if ($status !== 0) {
throw new \RuntimeException("Failed to copy temp config to $configFile");
return false;
}
// reload dnsmasq to apply changes
exec('sudo systemctl reload dnsmasq.service', $output, $status);
if ($status !== 0) {
throw new \RuntimeException("Failed to reload dnsmasq service");
}
return true;
}
/**
* Validates dnsmasq user input from $_POST object
*
* @param array $post_data
* @return array $errors
*/
public function validate(array $post_data): array
{
$errors = [];
$encounteredIPs = [];
if (isset($post_data["static_leases"]["mac"])) {
for ($i=0; $i < count($post_data["static_leases"]["mac"]); $i++) {
$mac = trim($post_data["static_leases"]["mac"][$i]);
$ip = trim($post_data["static_leases"]["ip"][$i]);
if (!validateMac($mac)) {
$errors[] = _('Invalid MAC address: '.$mac);
}
if (in_array($ip, $encounteredIPs)) {
$errors[] = _('Duplicate IP address entered: ' . $ip);
} else {
$encounteredIPs[] = $ip;
}
}
}
return $errors;
}
/**
* Removes a configuration block for the specified interface
*
* @param string $iface
* @param StatusMessage $status
* @return bool $result
*/
public function remove(string $iface, StatusMessage $status): bool
{
system('sudo rm '.RASPI_DNSMASQ_PREFIX.$iface.'.conf', $result);
if ($result == 0) {
$status->addMessage('Dnsmasq configuration for '.$iface.' removed.', 'success');
return true;
} else {
$status->addMessage('Failed to remove dnsmasq configuration for '.$iface.'.', 'danger');
return false;
}
}
/**
* Scans configuration dir for the specified interface
* Non-matching configs are removed, optional adblock.conf is protected
*
* @param string $dir_conf
* @param string $interface
* @return bool
*/
public function scanConfigDir(string $dir_conf, string $interface): bool
{
$syscnf = preg_grep('~\.(conf)$~', scandir($dir_conf));
foreach ($syscnf as $cnf) {
if ($cnf !== '090_adblock.conf' && !preg_match('/.*_'.$interface.'.conf/', $cnf)) {
system('sudo rm /etc/dnsmasq.d/'.$cnf, $result);
return true;
}
}
}
/**
* Compares two IPs
*
* @param array $ip1
* @param array $ip2
* @return int
*/
private function compareIPs(array $ip1, array $ip2): int
{
$ipu1 = sprintf('%u', ip2long($ip1["ip"])) + 0;
$ipu2 = sprintf('%u', ip2long($ip2["ip"])) + 0;
return $ipu1 <=> $ipu2;
}
/**
* Add static DHCP lease
*
* @param string $iface
* @param string $mac
* @param string $ip
* @param string|null $comment
* @return bool
*/
public function addStaticLease(string $iface, string $mac, string $ip, ?string $comment = null): bool
{
return false;
}
}

View File

@@ -0,0 +1,466 @@
<?php
/**
* A hostapd manager class for RaspAP
*
* @description Manages hostapd configurations and runtime settings
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot;
use RaspAP\Networking\Hotspot\Validators\HostapdValidator;
use RaspAP\Messages\StatusMessage;
class HostapdManager
{
private const CONF_DEFAULT = RASPI_HOSTAPD_CONFIG;
private const CONF_PATH_PREFIX = '/etc/hostapd/hostapd-';
private const CONF_TMP = '/tmp/hostapddata';
/** @var HostapdValidator */
private $validator;
public function __construct(?HostapdValidator $validator = null)
{
$this->validator = $validator ?: new HostapdValidator();
}
/**
* Retrieves current hostapd config
*
* @return array
* @throws \RuntimeException
*/
public function getConfig(): array
{
$configFile = SELF::CONF_DEFAULT;
if (!file_exists($configFile)) {
throw new \RuntimeException("hostapd config not found: $configFile");
}
if (!is_readable($configFile)) {
throw new \RuntimeException("Unable to read hostapd config: $configFile");
}
exec('cat ' . escapeshellarg($configFile), $hostapdconfig, $status);
if ($status !== 0 || empty($hostapdconfig)) {
throw new \RuntimeException("Failed to read hostapd config: $configFile");
}
foreach ($hostapdconfig as $hostapdconfigline) {
if (strlen($hostapdconfigline) === 0) {
continue;
}
if ($hostapdconfigline[0] != "#") {
$line = explode("=", $hostapdconfigline);
$config[$line[0]]=$line[1];
}
};
// assign beacon_int boolean if value is set
if (isset($config['beacon_int'])) {
$config['beacon_interval_bool'] = 1;
}
// assign disassoc_low_ack boolean if value is set
if (isset($config['disassoc_low_ack'])) {
$config['disassoc_low_ack_bool'] = 1;
}
// assign country_code from iw reg if not set in config
if (empty($config['country_code']) && isset($country_code[0])) {
$config['country_code'] = $country_code[0];
}
// map wpa_key_mgmt to security types
if ($config['wpa_key_mgmt'] == 'WPA-PSK WPA-PSK-SHA256 SAE') {
$config['wpa'] = 4;
} elseif ($config['wpa_key_mgmt'] == 'SAE') {
$config['wpa'] = 5;
}
$config['selected_hw_mode'] = $this->resolveHwMode($config);
$config['ignore_broadcast_ssid'] ??= 0;
$config['max_num_sta'] ??= 0;
$config['wep_default_key'] ??= 0;
return $config;
}
/**
* Determines the selected hardware mode based on config
*
* @param array $config
* @return string
*/
private function resolveHwMode(array $config): string
{
$selected = $config['hw_mode'] ?? 'g'; // default fallback
if (!empty($config['ieee80211n']) && strval($config['ieee80211n']) === '1') {
$selected = 'n';
}
if (!empty($config['ieee80211ac']) && strval($config['ieee80211ac']) === '1') {
$selected = 'ac';
}
if (!empty($config['ieee80211w']) && strval($config['ieee80211w']) === '2') {
$selected = 'w';
}
return $selected;
}
/**
* Validates a hostapd configuration
*
* @param array $post raw $_POST object
* @param array $wpaArray allowed WPA values
* @param array $encTypes allowed encryption types
* @param array $modes allowed hardware modes
* @param array $interfaces valid interface list
* @param string $regDomain regulatory domain
* @param StatusMessage $status Status message collector
* @return array|false validated configuration array or false on failure
*/
public function validate(
array $post,
array $wpaArray,
array $encTypes,
array $modes,
array $interfaces,
string $regDomain,
StatusMessage $status
) {
return $this->validator->validate($post, $wpaArray, $encTypes, $modes, $interfaces, $regDomain, $status);
}
/**
* Builds hostapd configuration text from array
*
* @param array $params
* @param StatusMessage $status
* @return string
*/
public function buildConfig(array $params, StatusMessage $status): string
{
$config = [];
$config[] = 'driver=nl80211';
$config[] = 'ctrl_interface=' . RASPI_HOSTAPD_CTRL_INTERFACE;
$config[] = 'ctrl_interface_group=0';
$config[] = 'auth_algs=1';
$wpa = $params['wpa'];
$wpa_key_mgmt = 'WPA-PSK';
if ($wpa == 4) {
$config[] = 'ieee80211w=1';
$wpa_key_mgmt = 'WPA-PSK WPA-PSK-SHA256 SAE';
$wpa = 2;
} elseif ($wpa == 5) {
$config[] = 'ieee80211w=2';
$wpa_key_mgmt = 'SAE';
$wpa = 2;
}
if ($params['80211w'] == 1) {
$config[] = 'ieee80211w=1';
$wpa_key_mgmt = 'WPA-PSK';
} elseif ($params['80211w'] == 2) {
$config[] = 'ieee80211w=2';
$wpa_key_mgmt = 'WPA-PSK-SHA256';
}
$config[] = 'wpa_key_mgmt=' . $wpa_key_mgmt;
if (!empty($params['beacon_interval'])) {
$config[] = 'beacon_int=' . $params['beacon_interval'];
}
if (!empty($params['disassoc_low_ack'])) {
$config[] = 'disassoc_low_ack=0';
}
$config[] = 'ssid=' . $params['ssid'];
$config[] = 'channel=' . $params['channel'];
// Choose VHT segment index (fallback only if required)
$vht_freq_idx = ($params['channel'] < RASPI_5GHZ_CHANNEL_MIN) ? 42 : 155;
switch ($params['hw_mode']) {
case 'n':
$config[] = 'hw_mode=g';
$config[] = 'ieee80211n=1';
$config[] = 'wmm_enabled=1';
break;
case 'ac':
$config[] = 'hw_mode=a';
$config[] = '# N';
$config[] = 'ieee80211n=1';
$config[] = 'require_ht=1';
$config[] = 'ht_capab=[MAX-AMSDU-3839][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]';
$config[] = '# AC';
$config[] = 'ieee80211ac=1';
$config[] = 'require_vht=1';
$config[] = 'ieee80211d=0';
$config[] = 'ieee80211h=0';
$config[] = 'vht_capab=[MAX-AMSDU-3839][SHORT-GI-80]';
$config[] = 'vht_oper_chwidth=1';
$config[] = 'vht_oper_centr_freq_seg0_idx=' . $vht_freq_idx;
break;
default:
$config[] = 'hw_mode=' . $params['hw_mode'];
$config[] = 'ieee80211n=0';
}
if ($params['wpa'] !== 'none') {
$config[] = 'wpa_passphrase=' . $params['wpa_passphrase'];
}
if (!empty($params['bridge'])) {
$config[] = 'interface=' . $params['interface'];
$config[] = 'bridge=' . $params['bridge'];
} else {
$config[] = 'interface=' . $params['interface'];
}
$config[] = 'wpa=' . $wpa;
$config[] = 'wpa_pairwise=' . $params['wpa_pairwise'];
$config[] = 'country_code=' . $params['country_code'];
$config[] = 'ignore_broadcast_ssid=' . $params['hiddenSSID'];
if (!empty($params['max_num_sta'])) {
$config[] = 'max_num_sta=' . (int)$params['max_num_sta'];
}
// optional additional user config
$config[] = $this->parseUserHostapdCfg();
return implode(PHP_EOL, $config) . PHP_EOL;
}
/**
* Saves a hostapd configuration
*
* @param string $config, rendered hostapd.conf
* @param string $interface, named interface
* @param bool $dualMode, dual-band AP mode enabled
* @param bool $restart, option to restart hostapd@<iface> after save
* @return bool
* @throws \RuntimeException
*/
public function saveConfig(string $config, bool $dualMode, string $iface, bool $restart = false): bool
{
$configFile = $this->resolveConfigPath($iface, $dualMode);
$tempFile = SELF::CONF_TMP;
if (file_put_contents($tempFile, $config) === false) {
throw new \RuntimeException("Failed to write temp hostapd config");
}
exec(sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)), $o, $status);
if ($status !== 0) {
throw new \RuntimeException("Failed to apply new hostapd config");
}
if ($restart) {
$this->restartService($iface);
}
return true;
}
/**
* Derives mode checkbox states from POST + existing ini
*
* @param array $post raw $_POST
* @param array $currentIni parsed hostapd.ini
* @return array normalized states
*/
public function deriveModeStates(array $post, array $currentIni): array
{
$prevWifiAPEnable = (int)($currentIni['WifiAPEnable'] ?? 0);
$bridgedEnable = isset($post['bridgedEnable']) ? 1 : 0;
$repeaterEnable = 0;
$wifiAPEnable = 0;
if ($bridgedEnable === 0) {
// only meaningful when not bridged
$repeaterEnable = isset($post['repeaterEnable']) ? 1 : 0;
$wifiAPEnable = isset($post['wifiAPEnable']) ? 1 : 0;
}
$logEnable = isset($post['logEnable']) ? 1 : 0;
$effectiveWifiAPEnable = $bridgedEnable === 1 ? $prevWifiAPEnable : $wifiAPEnable;
return [
'BridgedEnable' => $bridgedEnable,
'RepeaterEnable' => $repeaterEnable,
'WifiAPEnable' => $effectiveWifiAPEnable,
'LogEnable' => $logEnable
];
}
/**
* Determine AP interface, client (managed) interface and session/monitor interface
* Uses these semantics:
* - Base interface = user selection (validated) or RASPI_WIFI_AP_INTERFACE
* - AP-STA mode (WifiAPEnable=1): AP is 'uap0', client is base iface
* - Bridged mode: client/session use 'br0', AP remains base iface
*
* @param string $baseIface Selected interface from form
* @param array $states Output from deriveModeStates()
* @return array [ap_iface, cli_iface, session_iface]
*/
public function deriveInterfaces(string $baseIface, array $states): array
{
$apIface = $baseIface;
$cliIface = $baseIface;
$sessionIface = $baseIface;
if ($states['WifiAPEnable'] === 1 && $states['BridgedEnable'] === 0) {
// client AP (AP-STA) uap0 is AP, base iface remains client
$apIface = 'uap0';
$sessionIface = 'uap0';
$cliIface = $baseIface;
} elseif ($states['BridgedEnable'] === 1) {
// bridged mode monitor br0, AP stays as base wireless iface
$cliIface = 'br0';
$sessionIface = 'br0';
}
return [$apIface, $cliIface, $sessionIface];
}
/**
* Enables or disables hostapd logging
*
* @param int $logEnable
*/
private function handleLogState(int $logEnable): void
{
$script = $logEnable === 1 ? 'enablelog.sh' : 'disablelog.sh';
exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script);
}
/**
* Parses optional /etc/hostapd/hostapd.conf.users file
*
* @return string $tmp
*/
private function parseUserHostapdCfg()
{
if (file_exists(SELF::CONF_DEFAULT . '.users')) {
exec('cat '. SELF::CONF_DEFAULT . '.users', $hostapdconfigusers);
foreach ($hostapdconfigusers as $hostapdconfigusersline) {
if (strlen($hostapdconfigusersline) === 0) {
continue;
}
if ($hostapdconfigusersline[0] != "#") {
$arrLine = explode("=", $hostapdconfigusersline);
$tmp.= $arrLine[0]."=".$arrLine[1].PHP_EOL;;
}
}
return $tmp;
}
}
/**
* Determines the hostapd config file for a given interface
*
* @param string $iface
* @param bool $dualMode
* @return string
*/
private function resolveConfigPath(string $iface, bool $dualMode): string
{
if ($dualMode) {
return SELF::CONF_PATH_PREFIX . $iface . '.conf';
}
// primary interface uses the canonical config path
return self::CONF_DEFAULT;
}
/**
* Restarts hostapd systemd instance
*
* @param string $iface
* @throws \RuntimeException
*/
private function restartService(string $iface): void
{
// sanitize
if (!preg_match('/^[A-Za-z0-9_-]+$/', $iface)) {
throw new \RuntimeException("Invalid interface name: $iface");
}
// use instance unit (preferred) if available
$cmds = [
sprintf('sudo systemctl restart hostapd@%s', $iface),
// fallback to singleton service
'sudo systemctl restart hostapd.service'
];
foreach ($cmds as $cmd) {
exec($cmd, $out, $rc);
if ($rc === 0) {
return;
}
}
throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback).");
}
/**
* Persist hostapd.ini with mode / interface user settings
*
* @param array $states states from deriveModeStates()
* @param string $apIface the AP interface
* @param string $cliIface the managed interface
* @param array $previousIni existing ini
* @return bool
*/
public function persistHostapdIni(array $states, string $apIface, string $cliIface, array $previousIni = []): bool
{
$this->applyLogState($states['LogEnable']);
// compose new ini payload
$cfg = [
'WifiInterface' => $apIface,
'LogEnable' => $states['LogEnable'] ?? false,
'WifiAPEnable' => $states['WifiAPEnable'] ?? false,
'BridgedEnable' => $states['BridgedEnable'] ?? false,
'RepeaterEnable' => $states['RepeaterEnable'] ?? false,
'DualAPEnable' => $states['DualAPEnable'] ?? false,
'WifiManaged' => $cliIface
];
foreach ($previousIni as $k => $v) {
if (!array_key_exists($k, $cfg)) {
$cfg[$k] = $v;
}
}
return write_php_ini($cfg, RASPI_CONFIG . '/hostapd.ini');
}
/**
* Enables or disables hostapd logging
*
* @param int $logEnable 1 = enable, 0 = disable
*/
private function applyLogState(int $logEnable): void
{
$script = $logEnable === 1 ? 'enablelog.sh' : 'disablelog.sh';
exec('sudo ' . RASPI_CONFIG . '/hostapd/' . $script);
}
/**
* Returns a count of hostapd-<interface>.conf files
*
* @return int
*/
private function countHostapdConfigs(): int
{
$configs = glob('/etc/hostapd/hostapd-*.conf');
return is_array($configs) ? count($configs) : 0;
}
}

View File

@@ -0,0 +1,365 @@
<?php
/**
* A class to manage hotspot service configurations
*
* Consolidates:
* hostapd, dnsmasq and dhcpcd config updates
* dhcpcd interface adjustments
* service control (start/stop/restart)
*
* @description Manages wireless configurations and services
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot;
use RaspAP\Networking\Hotspot\Validators\HostapdValidator;
use RaspAP\Messages\StatusMessage;
class HotspotService
{
protected HostapdManager $hostapd;
protected DnsmasqManager $dnsmasq;
protected DhcpcdManager $dhcpcd;
// IEEE 802.11 standards
private const IEEE_80211_STANDARD = [
'a' => '802.11a - 5 GHz',
'b' => '802.11b - 2.4 GHz',
'g' => '802.11g - 2.4 GHz',
'n' => '802.11n - 2.4/5 GHz',
'ac' => '802.11ac - 5 GHz'
];
// encryption types
private const ENC_TYPES = [
'TKIP' => 'TKIP',
'CCMP' => 'CCMP',
'TKIP CCMP' => 'TKIP+CCMP'
];
public function __construct()
{
$this->hostapd = new HostapdManager();
$this->dnsmasq = new DnsmasqManager();
$this->dhcpcd = new DhcpcdManager();
}
/**
* Returns IEEE 802.11 standards
*/
public static function get80211Standards(): array
{
return self::IEEE_80211_STANDARD;
}
/**
* Returns encryption types
*/
public static function getEncTypes(): array
{
return self::ENC_TYPES;
}
/**
* Returns translated security modes.
*/
public static function getSecurityModes(): array
{
// Build each call to ensure translation occurs under current locale.
return [
1 => 'WPA',
2 => 'WPA2',
3 => _('WPA and WPA2'),
4 => _('WPA2 and WPA3-Personal (transitional mode)'),
5 => 'WPA3-Personal (required)',
'none' => _('None'),
];
}
/**
* Returns translated 802.11w options
*/
public static function get80211wOptions(): array
{
return [
3 => _('Disabled'),
1 => _('Enabled (for supported clients)'),
2 => _('Required (for supported clients)'),
];
}
/**
* Validates user input + saves configs for hostapd, dnsmasq & dhcp
*
* @param array $wpa_array
* @param array $enc_types
* @param array $modes
* @param array $interfaces
* @param string $reg_domain
* @param StatusMessage $status
* @return bool
*/
public function saveSettings(
array $post_data,
array $wpa_array,
array $enc_types,
array $modes,
array $interfaces,
string $reg_domain,
StatusMessage $status): bool
{
$arrHostapdConf = $this->getHostapdIni();
$dualAPEnable = false;
// derive mode states
$states = $this->hostapd->deriveModeStates($post_data, $arrHostapdConf);
// determine base interface (validated or fallback)
$baseIface = validateInterface($post_data['interface']) ? $post_data['interface'] : RASPI_WIFI_AP_INTERFACE;
// derive interface roles
[$apIface, $cliIface, $sessionIface] = $this->hostapd->deriveInterfaces($baseIface, $states);
// persist hostapd.ini
$this->hostapd->persistHostapdIni($states, $apIface, $cliIface, $arrHostapdConf);
// store session (compatibility)
$_SESSION['ap_interface'] = $sessionIface;
// validate config from post data
$validated = $this->hostapd->validate($post_data, $wpa_array, $enc_types, $modes, $interfaces, $reg_domain, $status);
if ($validated !== false) {
try {
// normalize state flags
$validated['interface'] = $apIface;
$validated['bridge'] = !empty($states['BridgedEnable']);
$validated['apsta'] = !empty($states['WifiAPEnable']);
$validated['repeater'] = !empty($states['RepeaterEnable']);
$validated['dualmode'] = !empty($states['DualAPEnable']);
$validated['txpower'] = $post_data['txpower'];
// hostapd
$config = $this->hostapd->buildConfig($validated, $status);
$this->hostapd->saveConfig($config, $dualAPEnable, $validated['interface']);
$this->maybeSetRegDomain($post_data['country_code'], $status);
$status->addMessage('WiFi hotspot settings saved.', 'success');
// dnsmasq
try {
$syscfg = $this->dnsmasq->getConfig($validated['interface'] ?? RASPI_WIFI_AP_INTERFACE);
} catch (\RuntimeException $e) {
error_log('Error: ' . $e->getMessage());
}
try {
$dnsmasqConfig = $this->dnsmasq->buildConfig(
$syscfg,
$validated['interface'],
$validated['apsta'],
$validated['bridge']
);
$this->dnsmasq->saveConfig($dnsmasqConfig, $validated['interface'], $status);
} catch (\RuntimeException $e) {
error_log('Error: ' . $e->getMessage());
}
// dhcpcd
try {
$return = $this->dhcpcd->buildConfig(
$validated['interface'],
$validated['bridge'],
$validated['repeater'],
$validated['apsta'],
$validated['dualmode'],
$status,
);
} catch (\RuntimeException $e) {
error_log('Error: ' . $e->getMessage());
}
} catch (\Throwable $e) {
error_log(sprintf(
"Error: %s in %s on line %d\nStack trace:\n%s",
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
));
$status->addMessage('Unable to save WiFi hotspot settings', 'danger');
}
}
return true;
}
/**
* Gets system hostapd.ini
*
* @return array $config
*/
public function getHostapdIni()
{
$hostapdIni = RASPI_CONFIG . '/hostapd.ini';
if (file_exists($hostapdIni)) {
$config = parse_ini_file($hostapdIni);
return $config;
}
}
/**
* Sets transmit power for an interface
*
* @param string $iface
* @param int|string $dbm
* @param StatusMessage $status
* @return bool
*/
public function maybeSetTxPower(string $iface, $dbm, StatusMessage $status): bool
{
$currentTxPower = $this->getTxPower($iface);
if ($currentTxPower === $dbm) {
return true;
}
if ($dbm === 'auto') {
exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower auto', $return);
$status->addMessage('Setting transmit power to auto.', 'success');
} else {
$sdBm = (int)$dbm * 100;
exec('sudo /sbin/iw dev ' . escapeshellarg($iface) . ' set txpower fixed ' . $sdBm, $return);
$status->addMessage('Setting transmit power to ' . $dbm . ' dBm.', 'success');
}
return true;
}
/**
* Gets transmit power for an interface
*
* @param string $iface
* @return int
*/
public function getTxPower(string $iface): int
{
$cmd = "iw dev ".escapeshellarg($iface)." info | awk '$1==\"txpower\" {print $2}'";
exec($cmd, $txpower);
return intval($txpower[0]);
}
/**
* Sets a new regulatory domain if value has changed
*
* @param string $countryCode
* @return bool
*/
public function maybeSetRegDomain($countryCode, StatusMessage $status): bool
{
$currentDomain = $this->getRegDomain();
if (trim($countryCode) !== trim($currentDomain)) {
$result = $this->setRegDomain($countryCode, $status);
if ($result !== true) {
return false;
}
}
return true;
}
/**
* Gets the current regulatory domain
*
* @return string
*/
public function getRegDomain(): string
{
$domain = shell_exec("iw reg get | grep -o 'country [A-Z]\{2\}' | awk 'NR==1{print $2}'");
return $domain;
}
/**
* Sets the specified wireless regulatory domain
*
* @param string $country_code ISO 2-letter country code
* @param object $status StatusMessage object
* @return boolean $result
*/
public function setRegDomain(string $country_code, StatusMessage $status): bool
{
$country_code = escapeshellarg($country_code);
exec("sudo iw reg set $country_code", $output, $result);
if ($result !== 0) {
return false;
} else {
return true;
}
}
/**
* Enumerates available network interfaces
*
* @return array $interfaces
*/
public function getInterfaces(): array
{
exec("ip -o link show | awk -F': ' '{print $2}'", $interfaces);
// filter out loopback, docker, bridges + other virtual interfaces
// that are incapable of hosting an AP
$interfaces = array_filter($interfaces, function ($iface) {
return !preg_match('/^(lo|docker|br-|veth|tun|tap|tailscale)/', $iface);
});
sort($interfaces);
return array_values($interfaces);
}
/**
* Starts services for given interface
*
* @param string $iface
* @return bool
*/
public function start(string $iface): bool
{
return false;
}
/**
* Stops hotspot services
*
* @return bool
*/
public function stop(): bool
{
return false;
}
/**
* Restart hotspot services for given interface
*
* @param string $iface
* @return bool
*/
public function restart(string $iface): bool
{
return false;
}
/**
* Get current hotspot status
*
* @return array
*/
public function getStatus(): array
{
return [];
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* Hostapd validator class for RaspAP
*
* @description Validates hostapd configuration input
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot\Validators;
use RaspAP\Messages\StatusMessage;
class HostapdValidator
{
/**
* Validates full hostapd parameter set
*
* @param array $post raw $_POST object
* @param array $wpaArray allowed WPA values
* @param array $encTypes allowed encryption types
* @param array $modes allowed hardware modes
* @param array $interfaces valid interface list
* @param string $regDomain regulatory domain
* @param StatusMessage $status Status message collector
* @return array|false validated configuration array or false on failure
*/
public function validate(
array $post,
array $wpaArray,
array $encTypes,
array $modes,
array $interfaces,
string $regDomain,
?StatusMessage $status = null
) {
$goodInput = true;
// check WPA and encryption
if (
!array_key_exists($post['wpa'], $wpaArray) ||
!array_key_exists($post['wpa_pairwise'], $encTypes) ||
!array_key_exists($post['hw_mode'], $modes)
) {
$err = "Invalid WPA or encryption settings: "
. "wpa='{$post['wpa']}', "
. "wpa_pairwise='{$post['wpa_pairwise']}', "
. "hw_mode='{$post['hw_mode']}'";
error_log($err);
return false;
}
// validate channel
if (!filter_var($post['channel'], FILTER_VALIDATE_INT)) {
$status->addMessage('Attempting to set channel to invalid number.', 'danger');
$goodInput = false;
}
if ((int)$post['channel'] < 1 || (int)$post['channel'] > RASPI_5GHZ_CHANNEL_MAX) {
$status->addMessage('Attempting to set channel outside of permitted range', 'danger');
$goodInput = false;
}
// validate SSID
if (empty($post['ssid']) || strlen($post['ssid']) > 32) {
$status->addMessage('SSID must be between 1 and 32 characters', 'danger');
$goodInput = false;
}
// validate WPA passphrase
if ($post['wpa'] !== 'none') {
if (strlen($post['wpa_passphrase']) < 8 || strlen($post['wpa_passphrase']) > 63) {
$status->addMessage('WPA passphrase must be between 8 and 63 characters', 'danger');
$goodInput = false;
} elseif (!ctype_print($post['wpa_passphrase'])) {
$status->addMessage('WPA passphrase must be comprised of printable ASCII characters', 'danger');
$goodInput = false;
}
}
// hidden SSID
$ignoreBroadcastSSID = $post['hiddenSSID'] ?? '0';
if (!ctype_digit($ignoreBroadcastSSID) || (int)$ignoreBroadcastSSID < 0 || (int)$ignoreBroadcastSSID >= 3) {
$status->addMessage('Invalid hiddenSSID parameter.', 'danger');
$goodInput = false;
}
// validate interface
if (!in_array($post['interface'], $interfaces, true)) {
$status->addMessage('Unknown interface '.htmlspecialchars($post['interface'], ENT_QUOTES), 'danger');
$goodInput = false;
}
// country code
$countryCode = $post['country_code'];
if (strlen($countryCode) !== 0 && strlen($countryCode) !== 2) {
$status->addMessage('Country code must be blank or two characters', 'danger');
$goodInput = false;
}
// beacon Interval
if (!empty($post['beaconintervalEnable'])) {
if (!is_numeric($post['beacon_interval'])) {
$status->addMessage('Beacon interval must be numeric', 'danger');
$goodInput = false;
} elseif ($post['beacon_interval'] < 15 || $post['beacon_interval'] > 65535) {
$status->addMessage('Beacon interval must be between 15 and 65535', 'danger');
$goodInput = false;
}
}
// max number of clients
$post['max_num_sta'] = (int) ($post['max_num_sta'] ?? 0);
$post['max_num_sta'] = $post['max_num_sta'] > 2007 ? 2007 : $post['max_num_sta'];
$post['max_num_sta'] = $post['max_num_sta'] < 1 ? null : $post['max_num_sta'];
if (!$goodInput) {
return false;
}
// return normalized config array
return [
'interface' => $post['interface'],
'ssid' => $post['ssid'],
'channel' => (int)$post['channel'],
'wpa' => $post['wpa'],
'80211w' => $post['80211w'] ?? 0,
'wpa_passphrase' => $post['wpa_passphrase'],
'wpa_pairwise' => $post['wpa_pairwise'],
'hw_mode' => $post['hw_mode'],
'country_code' => $countryCode,
'hiddenSSID' => (int)$ignoreBroadcastSSID,
'max_num_sta' => $post['max_num_sta'],
'beacon_interval' => $post['beacon_interval'] ?? null,
'disassoc_low_ack' => $post['disassoc_low_ackEnable'] ?? null,
'bridge' => ($post['bridgedEnable'] ?? false) ? 'br0' : null
];
}
}

View File

@@ -0,0 +1,479 @@
<?php
/**
* A wireless utility class for RaspAP
* @description A collection of wireless utility methods
* @author Bill Zimmerman <billzimmerman@gmail.com>
* @license https://github.com/raspap/raspap-webgui/blob/master/LICENSE
*/
declare(strict_types=1);
namespace RaspAP\Networking\Hotspot;
class WiFiManager
{
private const MIN_RSSI = -100;
private const MAX_RSSI = -55;
public function knownWifiStations(&$networks)
{
// find currently configured networks
exec(' sudo cat ' . RASPI_WPA_SUPPLICANT_CONFIG, $known_return);
$index = 0;
foreach ($known_return as $line) {
if (preg_match('/network\s*=/', $line)) {
$network = array('visible' => false, 'configured' => true, 'connected' => false, 'index' => null);
++$index;
} elseif (isset($network) && $network !== null) {
if (preg_match('/^\s*}\s*$/', $line)) {
$networks[$ssid] = $network;
$network = null;
$ssid = null;
} elseif ($lineArr = preg_split('/\s*=\s*/', trim($line), 2)) {
switch (strtolower($lineArr[0])) {
case 'ssid':
$ssid = trim($lineArr[1], '"');
$ssid = str_replace('P"','',$ssid);
$network['ssid'] = $ssid;
$index = $this->getNetworkIdBySSID($ssid);
$network['index'] = $index;
break;
case 'psk':
$network['passkey'] = trim($lineArr[1]);
$network['protocol'] = 'WPA';
break;
case '#psk':
$network['protocol'] = 'WPA';
case 'wep_key0': // Untested
$network['passphrase'] = trim($lineArr[1], '"');
break;
case 'key_mgmt':
if (! array_key_exists('passphrase', $network) && $lineArr[1] === 'NONE') {
$network['protocol'] = 'Open';
}
break;
case 'priority':
$network['priority'] = trim($lineArr[1], '"');
break;
}
}
}
}
}
/**
* Scans for nearby WiFi networks using `iw` and updates the reference array
*
* @param array $networks Reference to the array of known and discovered networks
* @param bool $cached If false, bypasses the cache and performs a fresh scan
*/
public function nearbyWifiStations(&$networks, $cached = true)
{
$cacheTime = filemtime(RASPI_WPA_SUPPLICANT_CONFIG);
$cacheKey = "nearby_wifi_stations_$cacheTime";
if ($cached == false) {
deleteCache($cacheKey);
}
$iface = escapeshellarg($_SESSION['wifi_client_interface']);
$scan_results = cache(
$cacheKey,
function () use ($iface) {
$stdout = shell_exec("sudo iw dev $iface scan");
return preg_split("/\n/", $stdout);
}
);
// exclude the AP from nearby networks
exec('sed -rn "s/ssid=(.*)\s*$/\1/p" ' . escapeshellarg(RASPI_HOSTAPD_CONFIG), $ap_ssid);
$ap_ssid = $ap_ssid[0] ?? '';
$index = 0;
if (!empty($networks)) {
$lastnet = end($networks);
if (isset($lastnet['index'])) {
$index = $lastnet['index'] + 1;
}
}
$current = [];
$commitCurrent = function () use (&$current, &$networks, &$index, $ap_ssid) {
if (empty($current['ssid'])) {
return;
}
$ssid = $current['ssid'];
// unprintable 7bit ASCII control codes, delete or quotes -> ignore network
if ($ssid === $ap_ssid || preg_match('/[\x00-\x1f\x7f\'`\´"]/', $ssid)) {
return;
}
$channel = ConvertToChannel($current['freq'] ?? 0);
$rssi = $current['signal'] ?? -100;
// if network is saved
if (array_key_exists($ssid, $networks)) {
$networks[$ssid]['visible'] = true;
$networks[$ssid]['channel'] = $channel;
if (!isset($networks[$ssid]['RSSI']) || $networks[$ssid]['RSSI'] < $rssi) {
$networks[$ssid]['RSSI'] = $rssi;
}
} else {
$networks[$ssid] = [
'ssid' => $ssid,
'configured' => false,
'protocol' => $current['security'] ?? 'OPEN',
'channel' => $channel,
'passphrase' => '',
'visible' => true,
'connected' => false,
'RSSI' => $rssi,
'index' => $index
];
++$index;
}
};
foreach ($scan_results as $line) {
$line = trim($line);
if (preg_match('/^BSS\s+([0-9a-f:]{17})/', $line, $match)) {
$commitCurrent(); // commit previous
$current = [
'bssid' => $match[1],
'ssid' => '',
'signal' => null,
'freq' => null,
'security' => 'OPEN'
];
continue;
}
if (preg_match('/^SSID:\s*(.*)$/', $line, $match)) {
$current['ssid'] = $match[1];
continue;
}
if (preg_match('/^signal:\s*(-?\d+\.\d+)/', $line, $match)) {
$current['signal'] = (float)$match[1];
continue;
}
if (preg_match('/^freq:\s*(\d+)/', $line, $match)) {
$current['freq'] = (int)$match[1];
continue;
}
if (preg_match('/^RSN:/', $line) || preg_match('/^WPA:/', $line)) {
$current['security'] = 'WPA/WPA2';
continue;
}
}
$commitCurrent();
}
/**
*
*/
public function connectedWifiStations(&$networks)
{
exec('iwconfig ' .$_SESSION['wifi_client_interface'], $iwconfig_return);
foreach ($iwconfig_return as $line) {
if (preg_match('/ESSID:\"([^"]+)\"/i', $line, $iwconfig_ssid)) {
$ssid=hexSequence2lower($iwconfig_ssid[1]);
$networks[$ssid]['connected'] = true;
//$check=detectCaptivePortal($_SESSION['wifi_client_interface']);
$networks[$ssid]["portal-url"]=$check["URL"];
}
}
}
/**
*
*
*/
public function sortNetworksByRSSI(&$networks)
{
$valRSSI = array();
foreach ($networks as $SSID => $net) {
if (!array_key_exists('RSSI', $net)) {
$net['RSSI'] = -1000;
}
$valRSSI[$SSID] = $net['RSSI'];
}
$nets = $networks;
arsort($valRSSI);
$networks = array();
foreach ($valRSSI as $SSID => $RSSI) {
$networks[$SSID] = $nets[$SSID];
$networks[$SSID]['RSSI'] = $RSSI;
}
}
/*
* Determines the configured wireless AP interface
*
* If not saved in /etc/raspap/hostapd.ini, check for a second
* wireless interface with iw dev. Fallback to the constant
* value defined in config.php
*/
public function getWifiInterface()
{
$hostapdIni = RASPI_CONFIG . '/hostapd.ini';
$arrHostapdConf = file_exists($hostapdIni) ? parse_ini_file($hostapdIni) : [];
$iface = $_SESSION['ap_interface'] = $arrHostapdConf['WifiInterface'] ?? RASPI_WIFI_AP_INTERFACE;
if (!validateInterface($iface)) {
$iface = RASPI_WIFI_AP_INTERFACE;
}
// check for 2nd wifi interface -> wifi client on different interface
exec("iw dev | awk '$1==\"Interface\" && $2!=\"$iface\" {print $2}'", $iface2);
$client_iface = $_SESSION['wifi_client_interface'] = empty($iface2) ? $iface : trim($iface2[0]);
// handle special case for RPi Zero W in AP-STA mode
if ($client_iface === "uap0" && ($arrHostapdConf['WifiAPEnable'] ?? 0)) {
$_SESSION['wifi_client_interface'] = $iface;
$_SESSION['ap_interface'] = $client_iface;
}
}
/*
* Reinitializes wpa_supplicant for the wireless client interface
* The 'force' parameter deletes the socket in /var/run/wpa_supplicant/
*
* @param boolean $force
*/
public function reinitializeWPA($force)
{
$iface = $_SESSION['wifi_client_interface'];
if ($force == true) {
$cmd = "sudo /sbin/wpa_supplicant -i $unescapedIface -c /etc/wpa_supplicant/wpa_supplicant.conf -B 2>&1";
$result = shell_exec($cmd);
}
$cmd = "sudo wpa_cli -i $iface reconfigure";
$result = shell_exec($cmd);
sleep(1);
return $result;
}
/*
* Replace escaped bytes (hex) by binary - assume UTF8 encoding
*
* @param string $ssid
*/
public function ssid2utf8($ssid)
{
return evalHexSequence($ssid);
}
/*
* Returns a signal strength indicator based on RSSI value
*
* @param string $rssi
*/
public function getSignalBars($rssi)
{
// assign css class based on RSSI value
$class = '';
if ($rssi >= SELF::MAX_RSSI) {
$class = 'strong';
} elseif ($rssi >= -56) {
$class = 'medium';
} elseif ($rssi >= -67) {
$class = 'weak';
} elseif ($rssi >= -89) {
$class = '';
}
// calculate percent strength
if ($rssi >= -50) {
$pct = 100;
} elseif ($rssi <= SELF::MIN_RSSI) {
$pct = 0;
} else {
$pct = 2*($rssi + 100);
}
$elem = '<div data-toggle="tooltip" title="' . _("Signal strength"). ': ' .$pct. '%" class="signal-icon ' .$class. '">'.PHP_EOL;
for ($n = 0; $n < 3; $n++ ) {
$elem .= '<div class="signal-bar"></div>'.PHP_EOL;
}
$elem .= '</div>'.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
* @throws Exception on wpa_cli command failure
*/
public function setKnownStationsWPA($networks)
{
$iface = escapeshellarg($_SESSION['wifi_client_interface']);
$output = shell_exec("sudo wpa_cli -i $iface list_networks 2>&1");
if ($output === null) {
throw new \Exception("Failed to execute wpa_cli command - command returned null");
}
// check for common wpa_cli errors and try to fix them
if (strpos($output, 'Failed to connect') !== false || strpos($output, 'No such file or directory') !== false) {
error_log("wpa_supplicant not available for interface, attempting to start it");
// try starting wpa_supplicant for this interface
$unescapedIface = trim($iface, "'\"");
$startCmd = "sudo /sbin/wpa_supplicant -i $unescapedIface -c /etc/wpa_supplicant/wpa_supplicant.conf -B 2>&1";
$startResult = shell_exec($startCmd);
sleep(2);
// retry
$output = shell_exec("sudo wpa_cli -i $iface list_networks 2>&1");
// if it still fails, throw an exception
if ($output === null || strpos($output, 'Failed to connect') !== false) {
throw new \Exception("Failed to start wpa_supplicant for interface: " . trim($startResult ?? 'unknown error'));
}
}
// split output into lines
$lines = explode("\n", trim($output));
// check for header line
if (empty($lines) || count($lines) < 1) {
error_log("wpa_cli list_networks returned no output");
$wpaCliNetworks = [];
} else {
// remove header line if it exists
$headerLine = trim($lines[0]);
if (strpos($headerLine, 'network id') !== false || strpos($headerLine, 'id') !== false) {
array_shift($lines);
}
$wpaCliNetworks = [];
foreach ($lines as $line) {
$trimmedLine = trim($line);
// skip empty lines
if (empty($trimmedLine)) {
continue;
}
$data = explode("\t", $trimmedLine);
if (count($data) >= 2) {
$id = trim($data[0]);
$ssid = trim($data[1]);
// add if we have valid data
if ($id !== '' && $ssid !== '') {
$wpaCliNetworks[] = [
'id' => $id,
'ssid' => $ssid
];
}
}
}
}
// process networks to add
foreach ($networks as $network) {
if (!isset($network['ssid']) || empty($network['ssid'])) {
error_log("Skipping network with missing or empty SSID");
continue;
}
$ssid = $network['ssid'];
if (!$this->networkExists($ssid, $wpaCliNetworks)) {
$this->addWpaNetwork($network, $iface);
}
}
}
/**
* Helper method to add a single network to wpa_supplicant
*
* @param array $network Network configuration
* @param string $iface Escaped shell argument for interface
*/
private function addWpaNetwork($network, $iface)
{
$ssid = escapeshellarg('"' . $network['ssid'] . '"');
$psk = escapeshellarg('"' . $network['passphrase'] . '"');
$protocol = $network['protocol'] ?? 'WPA';
// add network and get its ID
$netid = trim(shell_exec("sudo wpa_cli -i $iface add_network 2>&1"));
// validate network ID
if (!$netid || !is_numeric($netid)) {
error_log("Failed to add network '{$network['ssid']}': Invalid network ID returned: '$netid'");
return;
}
// prepare command based on protocol
$commands = [
"sudo wpa_cli -i $iface set_network $netid ssid $ssid",
];
if (strtolower($protocol) === 'open') {
$commands[] = "sudo wpa_cli -i $iface set_network $netid key_mgmt NONE";
} else {
$commands[] = "sudo wpa_cli -i $iface set_network $netid psk $psk";
}
$commands[] = "sudo wpa_cli -i $iface enable_network $netid";
// execute commands, checking errors
foreach ($commands as $cmd) {
$result = shell_exec("$cmd 2>&1");
if ($result === null || strpos($result, 'FAIL') !== false) {
error_log("Command failed: $cmd - Result: " . ($result ?? 'null'));
// remove the failed network
shell_exec("sudo wpa_cli -i $iface remove_network $netid 2>&1");
return;
}
usleep(1000);
}
error_log("Successfully added network: {$network['ssid']}");
}
/*
* Parses wpa_cli list_networks output and returns the id
* of a corresponding network SSID
*
* @param string $ssid
* @return integer id
*/
public function getNetworkIdBySSID($ssid) {
$iface = escapeshellarg($_SESSION['wifi_client_interface']);
$cmd = "sudo wpa_cli -i $iface list_networks";
$output = [];
exec($cmd, $output);
array_shift($output);
foreach ($output as $line) {
$columns = preg_split('/\t/', $line);
if (count($columns) >= 4 && trim($columns[1]) === trim($ssid)) {
return $columns[0]; // return network ID
}
}
return null;
}
/**
*
*/
public function networkExists($ssid, $collection)
{
foreach ($collection as $network) {
if ($network['ssid'] === $ssid) {
return true;
}
}
return false;
}
}

View File

@@ -3,6 +3,7 @@
<input type="submit" class="btn btn-outline btn-primary" value="<?php echo _("Save settings"); ?>" name="savedhcpdsettings" />
<?php if ($dnsmasq_state) : ?>
<input type="submit" class="btn btn-warning" value="<?php echo _("Stop dnsmasq") ?>" name="stopdhcpd" />
<input type="submit" class="btn btn-warning" value="<?php echo _("Restart dnsmasq") ?>" name="restartdhcpd" />
<?php else : ?>
<input type="submit" class="btn btn-success" value="<?php echo _("Start dnsmasq") ?>" name="startdhcpd" />
<?php endif ?>

View File

@@ -19,7 +19,7 @@
<div class="mb-3 col-md-6">
<label for="cbxhwmode"><?php echo _("Wireless Mode") ;?></label>
<i class="fas fa-question-circle text-muted" data-bs-toggle="tooltip" data-bs-placement="auto" title="<?php echo _("The 802.11ac 5 GHz option is disabled until a compatible wireless regulatory domain is set."); ?>"></i>
<?php SelectorOptions('hw_mode', $arr80211Standard, $selectedHwMode, 'cbxhwmode', 'getChannel'); ?>
<?php SelectorOptions('hw_mode', $arr80211Standard, $arrConfig['selected_hw_mode'], 'cbxhwmode', 'getChannel'); ?>
</div>
</div>
<div class="row">

View File

@@ -13,7 +13,7 @@
<div class="mb-3">
<label for="cbx80211w"><?php echo _("802.11w"); ?></label>
<i class="fas fa-question-circle text-muted" data-bs-toggle="tooltip" data-bs-placement="auto" title="802.11w extends strong cryptographic protection to a select set of robust management frames, including Deauthentication, Disassociation and certain categories of Action Management frames. Collectively, this is known as Management Frame Protection (MFP)."></i>
<?php SelectorOptions('80211w', $arr80211w, $arrConfig['ieee80211w'], 'cbx80211w'); ?>
<?php SelectorOptions('80211w', $arr80211w, $arrConfig['ieee80211w'] ?? 0, 'cbx80211w'); ?>
</div>
<label for="txtwpapassphrase"><?php echo _("Pre-shared key (PSK)"); ?></label>

View File

@@ -5,7 +5,7 @@
<?php if (!RASPI_MONITOR_ENABLED) : ?>
<p class="text-center"><?php echo _("Click 'Reinitialize' to force reinitialize <code>wpa_supplicant</code>.") ?></p>
<form method="POST" action="wpa_conf" name="wpa_conf_form" class="row">
<?php echo CSRFTokenFieldTag() ?>
<?php echo \RaspAP\Tokens\CSRF::hiddenField(); ?>
<div class="col-xs me-3 mb-3">
<input type="submit" class="btn btn-warning btn-block float-end" name="wpa_reinit" value="<?php echo _("Reinitialize"); ?>" />
</div>

View File

@@ -1,3 +1,9 @@
<?php
use RaspAP\Networking\Hotspot\WiFiManager;
$wifi = new WiFiManager();
?>
<div class="card">
<div class="card-body">
<input type="hidden" name="ssid<?php echo $index ?>" value="<?php echo htmlentities($network['ssid'], ENT_QUOTES) ?>" />
@@ -31,7 +37,7 @@
<div>
<?php if (isset($network['RSSI']) && $network['RSSI'] >= -200) {
echo '<div class="d-flex justify-content-start">';
echo getSignalBars($network['RSSI']);
echo $wifi->getSignalBars($network['RSSI']);
echo '<div class="ms-2">' .htmlspecialchars($network['RSSI'], ENT_QUOTES) . "dB" . "</div>";
echo '</div>';
} else {