Auto hotspot implementation

This commit is contained in:
Robin Davies
2024-09-11 00:06:52 -04:00
parent bbfd0a07fd
commit 448979e7fe
61 changed files with 5234 additions and 178 deletions
+851
View File
@@ -0,0 +1,851 @@
// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "HotspotManager.hpp"
#include <thread>
#include "Lv2Log.hpp"
#include "DBusLog.hpp"
#include "DBusEvent.hpp"
#include "DBusDispatcher.hpp"
#include "NetworkManagerInterfaces.hpp"
#include "ss.hpp"
#include "WifiConfigSettings.hpp"
#include "ServiceConfiguration.hpp"
#include <unordered_set>
#include <mutex>
using namespace pipedal;
using namespace dbus::networkmanager;
namespace pipedal::impl
{
constexpr std::string PIPEDAL_HOTSPOT_NAME = "PiPedal Hotspot";
class HotspotManagerImpl : public HotspotManager
{
public:
using ssid_t = WifiConfigSettings::ssid_t;
HotspotManagerImpl();
virtual ~HotspotManagerImpl() noexcept;
virtual void Open() override;
virtual void Reload() override;
virtual void Close() override;
virtual PostHandle Post(PostCallback&&fn) override;
virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn) override;
virtual bool CancelPost(PostHandle handle) override;
virtual void SetNetworkChangingListener(NetworkChangingListener &&listener) override;
private:
enum class State
{
Initial,
WaitingForNetworkManager,
Disabled,
Monitoring,
HotspotConnecting,
HotspotConnected,
Error,
Closed
};
void SetState(State state);
State state = State::Initial;
void onClose();
void onError(const std::string &message);
void onInitialize();
void onDevicesChanged();
void onDisconnect();
void onReload();
void onDisableHotspot();
void DisableHotspot();
void EnableHotspot();
void StartHotspot();
void StopHotspot();
void onStartMonitoring();
void onAccessPointChanged();
void onAccessPointsChanged();
void CancelAccessPointsChangedTimer();
void WaitForNetworkManager();
void StartWaitForNetworkManagerTimer();
void CancelWaitForNetworkManagerTimer();
void UpdateNetworkManagerStatus();
void CancelDeviceChangedTimer();
void ReleaseNetworkManager();
void OnEthernetStateChanged(uint32_t state);
void OnWlanStateChanged(uint32_t state);
std::vector<ssid_t> GetAutoConnectSsids();
std::vector<ssid_t> GetKnownVisibleAccessPoints();
void FireNetworkChanging();
void MaybeStartHotspot();
void StartScanTimer();
void StopScanTimer();
void ScanNow();
Connection::ptr FindExistingConnection();
Device::ptr GetDevice(uint32_t nmDeviceType);
std::atomic<bool> closed;
DBusDispatcher dbusDispatcher;
std::unique_ptr<std::thread> thread;
void ThreadProc();
WifiConfigSettings wifiConfigSettings;
NetworkManager::ptr networkManager;
Device::ptr ethernetDevice;
Device::ptr wlanDevice;
DeviceWireless::ptr wlanWirelessDevice;
ActiveConnection::ptr activeConnection;
std::recursive_mutex networkChangingListenerMutex;
NetworkChangingListener networkChangingListener;
bool ethernetConnected = true;
bool wlanConnected = true;
bool isAutoWlanConnectionVisible = true;
DBusEventHandle onDeviceAddedHandle = INVALID_DBUS_EVENT_HANDLE;
DBusEventHandle onDeviceRemovedHandle = INVALID_DBUS_EVENT_HANDLE;
DBusDispatcher::PostHandle devicesChangedTimerHandle = 0;
DBusDispatcher::PostHandle networkManagerTimerHandle = 0;
DBusDispatcher::PostHandle accessPointsChangedTimerHandle = 0;
DBusDispatcher::PostHandle scanTimerHandle = 0;
};
}
using namespace pipedal::impl;
HotspotManagerImpl::HotspotManagerImpl()
{
SetDBusLogLevel(DBusLogLevel::None);
}
void HotspotManagerImpl::Open()
{
dbusDispatcher.Run();
dbusDispatcher.Post(
[this]()
{
UpdateNetworkManagerStatus(); // waits for valid network manager state, and calls onInitialize when ready.
});
}
void HotspotManagerImpl::onClose()
{
onDisableHotspot();
ReleaseNetworkManager();
Lv2Log::debug("HotspotManager: state=Closed");
SetState(State::Closed);
}
void HotspotManagerImpl::onError(const std::string &message)
{
Lv2Log::error(message);
ReleaseNetworkManager();
Lv2Log::debug("HotspotManager: state=Error");
SetState(State::Error);
}
void HotspotManagerImpl::SetState(State state)
{
this->state = state;
}
void HotspotManagerImpl::onInitialize()
{
if (closed)
return;
if (state != State::Initial && state != State::WaitingForNetworkManager)
{
onError("Invalid state (onInitialize).");
return;
}
try
{
wifiConfigSettings.Load();
if (wifiConfigSettings.valid_ && wifiConfigSettings.enable_)
{
onStartMonitoring();
}
else
{
onDisableHotspot();
}
}
catch (const std::exception &e)
{
onError(SS("HotspotManager: " << e.what()));
}
}
void HotspotManagerImpl::onDisconnect()
{
if (state != State::Initial && state != State::WaitingForNetworkManager)
{
WaitForNetworkManager();
}
}
void HotspotManagerImpl::UpdateNetworkManagerStatus()
{
try
{
if (!networkManager)
{
networkManager = NetworkManager::Create(dbusDispatcher);
}
// turn on wifi if required.
auto ethernetDevice = GetDevice(NM_DEVICE_TYPE_ETHERNET);
auto wlanDevice = GetDevice(NM_DEVICE_TYPE_WIFI);
if (ethernetDevice && wlanDevice)
{
if (state == State::Initial || state == State::WaitingForNetworkManager)
{
CancelWaitForNetworkManagerTimer();
this->onInitialize();
}
return;
}
// check to see whether we have BOTH an eth0 device and a wlan0 device.
}
catch (const std::exception &e)
{
}
if (state != State::WaitingForNetworkManager)
{
this->WaitForNetworkManager();
}
else
{
this->StartWaitForNetworkManagerTimer();
}
}
void HotspotManagerImpl::CancelDeviceChangedTimer()
{
if (devicesChangedTimerHandle)
{
dbusDispatcher.CancelPost(devicesChangedTimerHandle);
devicesChangedTimerHandle = 0;
}
}
void HotspotManagerImpl::ReleaseNetworkManager()
{
StopScanTimer();
CancelAccessPointsChangedTimer();
CancelWaitForNetworkManagerTimer();
CancelDeviceChangedTimer();
DisableHotspot();
ethernetDevice = nullptr;
wlanDevice = nullptr;
wlanWirelessDevice = nullptr;
networkManager = nullptr;
this->onDeviceRemovedHandle = INVALID_DBUS_EVENT_HANDLE;
this->onDeviceAddedHandle = INVALID_DBUS_EVENT_HANDLE;
}
void HotspotManagerImpl::onDevicesChanged()
{
UpdateNetworkManagerStatus();
}
Device::ptr HotspotManagerImpl::GetDevice(uint32_t nmDeviceType)
{
const auto &allDevices = networkManager->GetAllDevices();
for (const auto &devicePath : allDevices)
{
auto device = Device::Create(dbusDispatcher, devicePath);
if (device->DeviceType() == nmDeviceType)
{
return device;
}
}
return nullptr;
}
void HotspotManagerImpl::OnEthernetStateChanged(uint32_t state)
{
bool newValue = (state == 100);
if (newValue != this->ethernetConnected)
{
this->ethernetConnected = newValue;
Lv2Log::debug(SS("HotspotMonitor: ethernetConnected=" << ethernetConnected));
FireNetworkChanging();
MaybeStartHotspot();
}
}
void HotspotManagerImpl::OnWlanStateChanged(uint32_t state)
{
this->wlanConnected = (state == 100);
Lv2Log::debug(SS("HotspotMonitor: OnWlanStateChanged"));
MaybeStartHotspot();
}
void HotspotManagerImpl::onStartMonitoring()
{
try
{
ReleaseNetworkManager();
this->networkManager = NetworkManager::Create(dbusDispatcher);
if (!networkManager->WirelessEnabled())
{
networkManager->WirelessEnabled(true);
}
this->onDeviceAddedHandle = this->networkManager->OnDeviceAdded.add(
[this](const sdbus::ObjectPath &)
{
onDevicesChanged();
});
this->onDeviceRemovedHandle = this->networkManager->OnDeviceRemoved.add(
[this](const sdbus::ObjectPath &objectPath)
{
if (this->ethernetDevice && this->ethernetDevice->getObjectPath() == objectPath)
{
onDisconnect();
}
else if (this->wlanDevice && this->wlanDevice->getObjectPath() == objectPath)
{
onDisconnect();
}
});
ethernetDevice = GetDevice(NM_DEVICE_TYPE_ETHERNET);
if (!ethernetDevice)
{
throw std::runtime_error("eth0 device not found.");
}
this->ethernetDevice->OnStateChanged.add(
[this](uint32_t, uint32_t, uint32_t)
{
OnEthernetStateChanged(ethernetDevice->State());
});
wlanDevice = GetDevice(NM_DEVICE_TYPE_WIFI);
if (!wlanDevice)
{
throw std::runtime_error("wlan0 device not found.");
}
wlanWirelessDevice = DeviceWireless::Create(dbusDispatcher, wlanDevice->getObjectPath());
wlanWirelessDevice->OnAccessPointAdded.add(
[this](const sdbus::ObjectPath &accessPoint)
{
onAccessPointChanged();
});
wlanWirelessDevice->OnAccessPointRemoved.add(
[this](const sdbus::ObjectPath &accessPoint)
{
onAccessPointChanged();
});
this->wlanDevice->OnStateChanged.add(
[this](uint32_t, uint32_t, uint32_t)
{
OnWlanStateChanged(ethernetDevice->State());
});
Lv2Log::debug("HotspotManager: state=Monitoring");
SetState(State::Monitoring);
Lv2Log::info("HotspotManager: Monitoring network status.");
OnEthernetStateChanged(ethernetDevice->State());
OnWlanStateChanged(wlanDevice->State());
StartScanTimer();
onAccessPointChanged();
}
catch (const std::exception &e)
{
Lv2Log::error(SS("HotspotManager: " << e.what()));
WaitForNetworkManager();
}
}
void HotspotManagerImpl::StartWaitForNetworkManagerTimer()
{
CancelWaitForNetworkManagerTimer();
networkManagerTimerHandle = dbusDispatcher.PostDelayed(
std::chrono::seconds(5),
[this]()
{
networkManagerTimerHandle = 0;
this->UpdateNetworkManagerStatus();
});
}
void HotspotManagerImpl::CancelWaitForNetworkManagerTimer()
{
if (networkManagerTimerHandle)
{
dbusDispatcher.CancelPost(networkManagerTimerHandle);
networkManagerTimerHandle = 0;
}
}
void HotspotManagerImpl::WaitForNetworkManager()
{
Lv2Log::debug("HotspotManager: state=WaitingForNetworkManager");
SetState(State::WaitingForNetworkManager);
ReleaseNetworkManager();
StartWaitForNetworkManagerTimer();
}
void HotspotManagerImpl::onDisableHotspot()
{
if (this->state != State::Disabled)
{
ReleaseNetworkManager();
Lv2Log::debug("HotspotManager: state=Disabled");
SetState(State::Disabled);
Lv2Log::info("HotspotManager: Hotspot disabled.");
}
}
void HotspotManagerImpl::onReload()
{
if (closed)
return;
WifiConfigSettings oldSettings = this->wifiConfigSettings;
;
wifiConfigSettings.Load();
switch (state)
{
case State::Initial:
// ignore.
return;
case State::Disabled:
if (wifiConfigSettings.valid_ && wifiConfigSettings.enable_)
{
onStartMonitoring();
}
return;
default:
this->onDisableHotspot();
break;
}
}
HotspotManagerImpl::~HotspotManagerImpl()
{
Close();
}
void HotspotManagerImpl::ThreadProc()
{
try
{
}
catch (const std::exception &e)
{
Lv2Log::error(SS("Hotspot Manager thread terminated. " << e.what()));
}
}
void HotspotManagerImpl::Reload()
{
dbusDispatcher.Post([this]()
{ onReload(); });
}
void HotspotManagerImpl::Close()
{
if (!closed)
{
closed = true;
dbusDispatcher.Post([this]
{ onClose(); });
dbusDispatcher.Stop();
}
}
HotspotManager::ptr HotspotManager::Create()
{
return std::make_unique<HotspotManagerImpl>();
}
template <typename T>
class VectorHash
{
public:
std::size_t operator()(const std::vector<T> &vec) const
{
std::size_t hash = vec.size();
for (auto &i : vec)
{
hash += static_cast<size_t>(i);
hash = ((hash >> 32) ^ hash) * 0x119de1F3;
}
return hash;
}
};
static std::string ssidToString(const std::vector<uint8_t> &ssid)
{
std::stringstream s;
for (auto v: ssid)
{
if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that.
s << (char)v;
}
return s.str();
}
std::vector<std::vector<uint8_t>> HotspotManagerImpl::GetKnownVisibleAccessPoints()
{
std::vector<std::vector<uint8_t>> knownSsids = this->GetAutoConnectSsids();
std::unordered_set<std::vector<uint8_t>, VectorHash<uint8_t>> index{knownSsids.begin(), knownSsids.end()};
std::vector<ssid_t> result;
for (const auto &accessPointPath : wlanWirelessDevice->AccessPoints())
{
auto accessPoint = AccessPoint::Create(dbusDispatcher, accessPointPath);
auto ssid = accessPoint->Ssid();
if (index.contains(ssid))
{
result.push_back(ssid);
}
}
return result;
}
std::vector<std::vector<uint8_t>> HotspotManagerImpl::GetAutoConnectSsids()
{
auto availableConnections = wlanDevice->AvailableConnections();
std::vector<std::vector<uint8_t>> ssids;
for (const auto &connectionPath : availableConnections)
{
auto connection = Connection::Create(dbusDispatcher, connectionPath);
try
{
auto settings = connection->GetSettings();
bool autoConnect = true;
if (settings["connect"].count("autoconnect") > 0)
{
autoConnect = settings["connect"]["autoconnect"];
}
bool isInfrastructure =
settings["802-11-wireless"].count("mode") > 0 && settings["802-11-wireless"]["mode"].get<std::string>() == "infrastructure";
if (isInfrastructure && autoConnect)
{
std::vector<uint8_t> ssid =
settings["802-11-wireless"]["ssid"];
ssids.push_back(std::move(ssid));
}
}
catch (const std::exception &ignored)
{
// not totally sure of structure of all connection types.
}
}
return ssids;
}
void HotspotManagerImpl::onAccessPointsChanged()
{
MaybeStartHotspot();
}
void HotspotManagerImpl::CancelAccessPointsChangedTimer()
{
if (accessPointsChangedTimerHandle)
{
dbusDispatcher.CancelPost(accessPointsChangedTimerHandle);
accessPointsChangedTimerHandle = 0;
}
}
void HotspotManagerImpl::onAccessPointChanged()
{
// coalesce large bursts of AccessPointChanged calls using a timer.
if (!accessPointsChangedTimerHandle)
{
accessPointsChangedTimerHandle = dbusDispatcher.PostDelayed(
std::chrono::milliseconds(512),
[this]()
{
accessPointsChangedTimerHandle = 0;
onAccessPointsChanged(); // coalesced handling of one or more changes.
});
}
}
void HotspotManagerImpl::MaybeStartHotspot()
{
std::vector<std::vector<uint8_t>> connectableSsids = GetKnownVisibleAccessPoints(); // all the ssids currently visible for which we have credentials.
bool wantsHotspot = this->wifiConfigSettings.WantsHotspot(
this->ethernetConnected,connectableSsids);
this->isAutoWlanConnectionVisible = connectableSsids.size() != 0;
if (this->state == State::Monitoring && wantsHotspot)
{
StartHotspot();
}
else
{
if ((!wantsHotspot) && (this->state == State::HotspotConnected || this->state == State::HotspotConnecting))
{
StopHotspot();
}
}
}
void HotspotManagerImpl::EnableHotspot()
{
}
void HotspotManagerImpl::DisableHotspot()
{
StopHotspot();
}
Connection::ptr HotspotManagerImpl::FindExistingConnection()
{
if (wlanDevice)
{
for (const auto &connectionPath : wlanDevice->AvailableConnections())
{
auto connection = Connection::Create(dbusDispatcher, connectionPath);
auto settings = connection->GetSettings();
bool isHotspot =
settings["802-11-wireless"].count("mode") > 0 && settings["802-11-wireless"]["mode"].get<std::string>() == "ap";
if (isHotspot)
{
std::string id;
if (settings["connection"].count("id") > 0)
{
id = settings["connection"]["id"].get<std::string>();
}
if (id.starts_with(PIPEDAL_HOTSPOT_NAME))
{
return connection;
}
}
}
}
return nullptr;
}
void HotspotManagerImpl::StartHotspot()
{
if (this->state == State::Monitoring)
{
try
{
ServiceConfiguration serviceConfiguration;
serviceConfiguration.Load();
Lv2Log::debug("HotspotManager: state=HotspotConnecting");
SetState(State::HotspotConnecting);
Lv2Log::info("HotspotManager: Enabling PiPedal hotspot.");
// do it ONLY if we're in monitoring state.
// Create a proxy for NetworkManager
// Create connection settings for the hotspot
std::map<std::string, std::map<std::string, sdbus::Variant>> settings;
std::map<std::string, sdbus::Variant> &connection = settings["connection"];
connection["type"] = "802-11-wireless";
connection["autoconnect"] = false;
connection["id"] = PIPEDAL_HOTSPOT_NAME;
connection["interface-name"] = wlanDevice->Interface();
connection["uuid"] = serviceConfiguration.uuid;
std::map<std::string, sdbus::Variant> &wireless = settings["802-11-wireless"];
std::string ssid = this->wifiConfigSettings.hotspotName_;
std::vector<uint8_t> vSsid{ssid.begin(), ssid.end()};
wireless["ssid"] = vSsid;
wireless["mode"] = "ap";
wireless["band"] = "a";
wireless["band"] = "bg";
uint32_t iChannel;
auto channel = this->wifiConfigSettings.channel_;
if (channel.length() != 0)
{
std::stringstream ss{channel};
ss >> iChannel;
}
if (iChannel != 0)
{
wireless["channel"] = iChannel;
}
std::map<std::string, sdbus::Variant> &wirelessSecurity = settings["802-11-wireless-security"];
wirelessSecurity["key-mgmt"] = "wpa-psk";
wirelessSecurity["psk"] = wifiConfigSettings.password_;
settings["ipv4"]["method"] = "shared";
settings["ipv6"]["method"] = "shared";
// settings["ipv6"]["addr-gen-mode"] = "stable-privacy";
std::map<std::string, sdbus::Variant> options;
options["persist"] = "disk";
Connection::ptr existingConnection = FindExistingConnection();
if (existingConnection)
{
existingConnection->Update(settings);
auto activeConnectionPath = networkManager->ActivateConnection(
existingConnection->getObjectPath(),
wlanDevice->getObjectPath(),
"/");
this->activeConnection = ActiveConnection::Create(dbusDispatcher, activeConnectionPath);
}
else
{
// Call AddAndActivateConnection2 method to create and activate the hotspot
sdbus::ObjectPath nullPath("/");
auto result = networkManager->AddAndActivateConnection2(
settings, wlanDevice->getObjectPath(), "/", options
);
auto connectionPath = std::get<0>(result);
auto activeConnectionPath = std::get<1>(result);
// auto resultArgs = std::get<2>(result);
this->activeConnection = ActiveConnection::Create(dbusDispatcher, activeConnectionPath);
}
SetState(State::HotspotConnected);
Lv2Log::info("HotspotManager: Hotspot activated.");
FireNetworkChanging();
}
catch (const std::exception &e)
{
onError(SS("HotspotManager: Activation failed: " << e.what()));
}
}
else
{
onError("HotspotManager: Illegal state (StartHotspot)");
}
}
void HotspotManagerImpl::StopHotspot()
{
// do it regardless of state.
try
{
if (activeConnection)
{
networkManager->DeactivateConnection(activeConnection->getObjectPath());
activeConnection = nullptr;
FireNetworkChanging();
}
}
catch (const std::exception &e)
{
Lv2Log::error("HotspotManager: Failed to deactivate hotspot.");
activeConnection = nullptr;
}
if (this->state == State::HotspotConnected || this->state == State::HotspotConnecting)
{
Lv2Log::info("HotspotManager: state=HotspotMonitoring");
SetState(State::Monitoring);
Lv2Log::info("HotspotManager: PiPedal hotspot disabled.");
}
}
static const std::chrono::seconds scanInterval { 60};
void HotspotManagerImpl::StartScanTimer()
{
StopScanTimer();
ScanNow();
}
void HotspotManagerImpl::StopScanTimer()
{
if (this->scanTimerHandle) {
dbusDispatcher.CancelPost(this->scanTimerHandle);
this->scanTimerHandle = 0;
}
}
void HotspotManagerImpl::ScanNow()
{
this->scanTimerHandle = 0;
if (wlanWirelessDevice) {
std::map<std::string, sdbus::Variant> options;
try {
Lv2Log::debug("Scanning");
wlanWirelessDevice->RequestScan(options);
} catch (const std::exception &e)
{
Lv2Log::error(SS("HotspotMonitor: Wi-Fi RequestScan failed." << e.what()));
return;
}
this->scanTimerHandle = this->dbusDispatcher.PostDelayed(
std::chrono::duration_cast<std::chrono::steady_clock::duration>(scanInterval),
[this]() {
ScanNow();
}
);
}
}
HotspotManagerImpl::PostHandle HotspotManagerImpl::Post(PostCallback&&fn)
{
return dbusDispatcher.Post(std::move(fn));
}
HotspotManagerImpl::PostHandle HotspotManagerImpl::PostDelayed(const clock::duration&delay,PostCallback&&fn)
{
return dbusDispatcher.PostDelayed(delay,std::move(fn));
}
bool HotspotManagerImpl::CancelPost(PostHandle handle) {
return dbusDispatcher.CancelPost(handle);
}
void HotspotManagerImpl::SetNetworkChangingListener(NetworkChangingListener &&listener)
{
std::lock_guard<std::recursive_mutex> lock { this->networkChangingListenerMutex};
this->networkChangingListener = std::move(listener);
}
void HotspotManagerImpl::FireNetworkChanging() {
std::lock_guard<std::recursive_mutex> lock { this->networkChangingListenerMutex};
if (this->networkChangingListener)
{
this->networkChangingListener(this->ethernetConnected,!!activeConnection);
}
}