mirror of
https://github.com/billz/raspap-webgui.git
synced 2025-12-27 07:31:09 +01:00
Create getConfig(), saveConfig() class methods
This commit is contained in:
89
src/RaspAP/Networking/Hotspot/DnsmasqManager.php
Normal file
89
src/RaspAP/Networking/Hotspot/DnsmasqManager.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace RaspAP\Networking\Hotspot;
|
||||
|
||||
/**
|
||||
* Manages dnsmasq configuration for DHCP/DNS services
|
||||
*/
|
||||
class DnsmasqManager
|
||||
{
|
||||
private const CONF_SUFFIX = '.conf';
|
||||
private const CONF_TMP = '/tmp/dnsmasqdata';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves dnsmasq configuration for an interface
|
||||
*
|
||||
* @param array $config
|
||||
* @param string $iface
|
||||
* @return bool
|
||||
*/
|
||||
public function saveConfig(array $config, string $iface = self::DEFAULT_IFACE): bool
|
||||
{
|
||||
$configFile = RASPI_DNSMASQ_PREFIX . $iface . self::CONF_SUFFIX;
|
||||
$tempFile = SELF::CONF_TMP;
|
||||
$config = join(PHP_EOL, $config);
|
||||
error_log('[DnsmasqManager::saveConfig] $config = ' . var_export($config, true));
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// TODO: append to conf
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
315
src/RaspAP/Networking/Hotspot/HostapdManager.php
Normal file
315
src/RaspAP/Networking/Hotspot/HostapdManager.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace RaspAP\Networking\Hotspot;
|
||||
|
||||
/**
|
||||
* Manages hostapd configurations and runtime settings
|
||||
*/
|
||||
class HostapdManager
|
||||
{
|
||||
private const CONF_DEFAULT = RASPI_HOSTAPD_CONFIG;
|
||||
private const CONF_PATH_PREFIX = '/etc/hostapd/hostapd-';
|
||||
private const CONF_TMP = '/tmp/hostapddata';
|
||||
|
||||
/**
|
||||
* Retrieves current hostapd config
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
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");
|
||||
}
|
||||
//error_log("HostapdManager::getConfig() hostapdconfig =" . print_r($hostapdconfig, true));
|
||||
|
||||
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;
|
||||
}
|
||||
$selectedHwMode = $config['hw_mode'];
|
||||
if (isset($config['ieee80211n'])) {
|
||||
if (strval($config['ieee80211n']) === '1') {
|
||||
$selectedHwMode = 'n';
|
||||
}
|
||||
}
|
||||
if (isset($config['ieee80211ac'])) {
|
||||
if (strval($config['ieee80211ac']) === '1') {
|
||||
$selectedHwMode = 'ac';
|
||||
}
|
||||
}
|
||||
if (isset($config['ieee80211w'])) {
|
||||
if (strval($config['ieee80211w']) === '2') {
|
||||
$selectedHwMode = 'w';
|
||||
}
|
||||
}
|
||||
$config['ignore_broadcast_ssid'] ??= 0;
|
||||
$config['max_num_sta'] ??= 0;
|
||||
$config['wep_default_key'] ??= 0;
|
||||
|
||||
return $config;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds hostapd configuration text from array
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public function buildConfig(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
//$configFile = self::CONF_DEFAULT;
|
||||
$tempFile = self::CONF_TMP;
|
||||
|
||||
|
||||
if (file_put_contents($tempFile, $config) === false) {
|
||||
throw new \RuntimeException("Failed to write temp hostapd config");
|
||||
}
|
||||
|
||||
exec(sprintf('sudo cp %s %s', escapeshellarg($tempFile), escapeshellarg($configFile)), $o, $status);
|
||||
if ($status !== 0) {
|
||||
throw new \RuntimeException("Failed to apply new hostapd config");
|
||||
}
|
||||
|
||||
if ($restart) {
|
||||
$this->restartService($iface);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets transmit power for an interface
|
||||
*
|
||||
* @param string $iface
|
||||
* @param int|string $dbm
|
||||
* @return bool
|
||||
*/
|
||||
public function setTxPower(string $iface, $dbm): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets regulatory domain
|
||||
*
|
||||
* @param string $countryCode
|
||||
* @return bool
|
||||
*/
|
||||
public function setRegDomain(string $countryCode): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses optional /etc/hostapd/hostapd.conf.users file
|
||||
*
|
||||
* @return string $tmp
|
||||
*/
|
||||
function parseUserHostapdCfg()
|
||||
{
|
||||
if (file_exists(CONF_DEFAULT . '.users')) {
|
||||
exec('cat '. CONF_DEFAULT . '.users', $hostapdconfigusers);
|
||||
foreach ($hostapdconfigusers as $hostapdconfigusersline) {
|
||||
if (strlen($hostapdconfigusersline) === 0) {
|
||||
continue;
|
||||
}
|
||||
if ($hostapdconfigusersline[0] != "#") {
|
||||
$arrLine = explode("=", $hostapdconfigusersline);
|
||||
$tmp.= $arrLine[0]."=".$arrLine[1].PHP_EOL;;
|
||||
}
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the hostapd config file for a given interface
|
||||
*
|
||||
* @param string $iface
|
||||
* @param bool $dualMode
|
||||
* @return string
|
||||
*/
|
||||
private function resolveConfigPath(string $iface, bool $dualMode): string
|
||||
{
|
||||
if ($dualMode) {
|
||||
return SELF::CONF_PATH_PREFIX . $iface . '.conf';
|
||||
}
|
||||
// primary interface uses the canonical config path
|
||||
return self::CONF_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts hostapd systemd instance
|
||||
*
|
||||
* @param string $iface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function restartService(string $iface): void
|
||||
{
|
||||
// sanitize
|
||||
if (!preg_match('/^[A-Za-z0-9_-]+$/', $iface)) {
|
||||
throw new \RuntimeException("Invalid interface name: $iface");
|
||||
}
|
||||
|
||||
// use instance unit (preferred) if available
|
||||
$cmds = [
|
||||
sprintf('sudo systemctl restart hostapd@%s', $iface),
|
||||
// fallback to singleton service
|
||||
'sudo systemctl restart hostapd.service'
|
||||
];
|
||||
|
||||
foreach ($cmds as $cmd) {
|
||||
exec($cmd, $out, $rc);
|
||||
if ($rc === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Failed to restart hostapd (tried instance + fallback).");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user