diff --git a/CMakeLists.txt b/CMakeLists.txt index 7161c4e4..d85a2db5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ boption(SERVICE_GREETD "Greetd" ON) boption(SERVICE_UPOWER "UPower" ON) boption(SERVICE_NOTIFICATIONS "Notifications" ON) boption(BLUETOOTH "Bluetooth" ON) +boption(NETWORK "Network" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) @@ -117,7 +118,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS OR SERVICE_UPOWER OR SERVICE_NOTIFICATIONS OR BLUETOOTH OR NETWORK) set(DBUS ON) endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a5..c95ecf71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,3 +33,7 @@ add_subdirectory(services) if (BLUETOOTH) add_subdirectory(bluetooth) endif() + +if (NETWORK) + add_subdirectory(network) +endif() diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 00000000..f511772f --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,96 @@ +# NetworkManager DBus +set_source_files_properties(nm/org.freedesktop.NetworkManager.xml PROPERTIES + CLASSNAME DBusNetworkManagerProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nm/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.xml + nm/dbus_nm_backend +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.xml PROPERTIES + CLASSNAME DBusNMDeviceProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.xml + nm/dbus_nm_device +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Device.Wireless.xml PROPERTIES + CLASSNAME DBusNMWirelessProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Device.Wireless.xml + nm/dbus_nm_wireless +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.AccessPoint.xml PROPERTIES + CLASSNAME DBusNMAccessPointProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.AccessPoint.xml + nm/dbus_nm_accesspoint +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Settings.Connection.xml PROPERTIES + CLASSNAME DBusNMConnectionSettingsProxy + NO_NAMESPACE TRUE + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/nm/dbus_types.hpp +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Settings.Connection.xml + nm/dbus_nm_connection_settings +) + +set_source_files_properties(nm/org.freedesktop.NetworkManager.Connection.Active.xml PROPERTIES + CLASSNAME DBusNMActiveConnectionProxy + NO_NAMESPACE TRUE +) + +qt_add_dbus_interface(NM_DBUS_INTERFACES + nm/org.freedesktop.NetworkManager.Connection.Active.xml + nm/dbus_nm_active_connection +) + +qt_add_library(quickshell-network STATIC + network.cpp + device.cpp + wifi.cpp + nm/backend.cpp + nm/device.cpp + nm/connection.cpp + nm/accesspoint.cpp + nm/wireless.cpp + nm/utils.cpp + nm/enums.hpp + ${NM_DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-network PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} +) + +qt_add_qml_module(quickshell-network + URI Quickshell.Network + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-network Quickshell) +install_qml_module(quickshell-network) + +target_link_libraries(quickshell-network PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-network quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-networkplugin) + +qs_module_pch(quickshell-network SET dbus) diff --git a/src/network/device.cpp b/src/network/device.cpp new file mode 100644 index 00000000..c4db6ed8 --- /dev/null +++ b/src/network/device.cpp @@ -0,0 +1,67 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkDevice, "quickshell.network.device", QtWarningMsg); +} // namespace + +NetworkDevice::NetworkDevice(QObject* parent): QObject(parent) {}; + +void NetworkDevice::setName(const QString& name) { + if (name == this->mName) return; + this->mName = name; + emit this->nameChanged(); +} + +void NetworkDevice::setAddress(const QString& address) { + if (address == this->mAddress) return; + this->mAddress = address; + emit this->addressChanged(); +} + +void NetworkDevice::setState(NetworkConnectionState::Enum state) { + if (state == this->mState) return; + this->mState = state; + emit this->stateChanged(); +} + +void NetworkDevice::setNmState(NMDeviceState::Enum nmState) { + if (nmState == this->mNmState) return; + this->mNmState = nmState; + emit this->nmStateChanged(); +} + +void NetworkDevice::disconnect() { + if (this->mState == NetworkConnectionState::Disconnected) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnected"; + return; + } + if (this->mState == NetworkConnectionState::Disconnecting) { + qCCritical(logNetworkDevice) << "Device" << this << "is already disconnecting"; + return; + } + qCDebug(logNetworkDevice) << "Disconnecting from device" << this; + this->requestDisconnect(); +} + +QString NetworkConnectionState::toString(NetworkConnectionState::Enum state) { + switch (state) { + case Unknown: return "Unknown"; + case Connecting: return "Connecting"; + case Connected: return "Connected"; + case Disconnecting: return "Disconnecting"; + case Disconnected: return "Disconnected"; + } + return {}; +} + +} // namespace qs::network diff --git a/src/network/device.hpp b/src/network/device.hpp new file mode 100644 index 00000000..f0d92c85 --- /dev/null +++ b/src/network/device.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nm/enums.hpp" + +namespace qs::network { + +///! Connection state. +class NetworkConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Connecting = 1, + Connected = 2, + Disconnecting = 3, + Disconnected = 4, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NetworkConnectionState::Enum state); +}; + +///! A Network device. +class NetworkDevice: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Devices can only be acquired through Network"); + + /// The name of the device's interface. + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// The hardware address of the device's interface in the XX:XX:XX:XX:XX:XX format. + Q_PROPERTY(QString address READ address NOTIFY addressChanged); + /// Connection state of the device. + Q_PROPERTY(NetworkConnectionState::Enum state READ state NOTIFY stateChanged); + /// A more specific device state when the backend is NetworkManager. + Q_PROPERTY(NMDeviceState::Enum nmState READ nmState NOTIFY nmStateChanged); + +signals: + void nameChanged(); + void addressChanged(); + void stateChanged(); + void nmStateChanged(); + void requestDisconnect(); + +public slots: + void setName(const QString& name); + void setAddress(const QString& address); + void setState(NetworkConnectionState::Enum state); + void setNmState(NMDeviceState::Enum state); + +public: + explicit NetworkDevice(QObject* parent = nullptr); + + [[nodiscard]] QString name() const { return this->mName; }; + [[nodiscard]] QString address() const { return this->mAddress; }; + [[nodiscard]] NetworkConnectionState::Enum state() const { return this->mState; }; + [[nodiscard]] NMDeviceState::Enum nmState() const { return this->mNmState; }; + + /// Disconnects the device and prevents it from automatically activating further connections. + Q_INVOKABLE void disconnect(); + +private: + QString mName; + QString mAddress; + NetworkConnectionState::Enum mState = NetworkConnectionState::Unknown; + NMDeviceState::Enum mNmState = NMDeviceState::Unknown; +}; + +} // namespace qs::network diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 00000000..746572ec --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,40 @@ +#include "network.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nm/backend.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetwork, "quickshell.network", QtWarningMsg); +} // namespace + +Network::Network(QObject* parent): QObject(parent), mWifi(new Wifi(this)) { + // NetworkManager + auto* nm = new NetworkManager(this); + if (nm->isAvailable()) { + // clang-format off + QObject::connect(nm, &NetworkManager::wifiDeviceAdded, this->wifi(), &Wifi::onDeviceAdded); + QObject::connect(nm, &NetworkManager::wifiDeviceRemoved, this->wifi(), &Wifi::onDeviceRemoved); + QObject::connect(nm, &NetworkManager::wifiEnabledChanged, this->wifi(), &Wifi::onEnabledSet); + QObject::connect(nm, &NetworkManager::wifiHardwareEnabledChanged, this->wifi(), &Wifi::onHardwareEnabledSet); + QObject::connect(this->wifi(), &Wifi::requestSetEnabled, nm, &NetworkManager::setWifiEnabled); + // clang-format on + this->mBackend = nm; + this->mBackendType = NetworkBackendType::NetworkManager; + return; + } else { + delete nm; + } + + qCCritical(logNetwork) << "Network will not work. Could not find an available backend."; +} + +} // namespace qs::network diff --git a/src/network/network.hpp b/src/network/network.hpp new file mode 100644 index 00000000..c022c262 --- /dev/null +++ b/src/network/network.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wifi.hpp" + +namespace qs::network { + +///! The backend supplying the Network service. +class NetworkBackendType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// There is no available Network backend. + None = 0, + /// The backend is NetworkManager. + NetworkManager = 1, + }; + Q_ENUM(Enum); +}; + +class NetworkBackend: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] virtual bool isAvailable() const = 0; + +protected: + explicit NetworkBackend(QObject* parent = nullptr): QObject(parent) {}; +}; + +///! The Network service. +/// An interface to a network backend (currently only NetworkManager), +/// which can be used to view, configure, and connect to various networks. +class Network: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Network); + QML_SINGLETON; + + /// The wifi device service. + Q_PROPERTY(Wifi* wifi READ wifi CONSTANT); + /// The backend being used to power the Network service. + Q_PROPERTY(NetworkBackendType::Enum backend READ backend); + +public: + explicit Network(QObject* parent = nullptr); + + [[nodiscard]] Wifi* wifi() { return this->mWifi; }; + [[nodiscard]] NetworkBackendType::Enum backend() { return this->mBackendType; }; + +private: + Wifi* mWifi; + NetworkBackend* mBackend = nullptr; + NetworkBackendType::Enum mBackendType = NetworkBackendType::None; +}; + +} // namespace qs::network diff --git a/src/network/nm/accesspoint.cpp b/src/network/nm/accesspoint.cpp new file mode 100644 index 00000000..9b7e2374 --- /dev/null +++ b/src/network/nm/accesspoint.cpp @@ -0,0 +1,73 @@ +#include "accesspoint.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "nm/dbus_nm_accesspoint.h" +#include "utils.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMAccessPoint::NMAccessPoint( + const QString& path, + NMWirelessCapabilities::Enum caps, + QObject* parent +) + : QObject(parent) + , mCaps(caps) { + this->proxy = new DBusNMAccessPointProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for AP at" << path; + return; + } + + // clang-format off + QObject::connect(&this->accessPointProperties, &DBusPropertyGroup::getAllFinished, this, [this]() { emit this->ready(); }, Qt::SingleShotConnection); + bSecurity.setBinding([&] { return findBestWirelessSecurity( this->mCaps, true, this->bMode == NM80211Mode::Adhoc, this->bFlags, this->bWpaFlags, this->bRsnFlags); }); + // clang-format on + + this->accessPointProperties.setInterface(this->proxy); + this->accessPointProperties.updateAllViaGetAll(); +} + +bool NMAccessPoint::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMAccessPoint::address() const { return this->proxy ? this->proxy->service() : QString(); } +QString NMAccessPoint::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network diff --git a/src/network/nm/accesspoint.hpp b/src/network/nm/accesspoint.hpp new file mode 100644 index 00000000..d52ea995 --- /dev/null +++ b/src/network/nm/accesspoint.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_accesspoint.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211ApSecurityFlags::Enum; + static DBusResult fromWire(Wire wire); +}; + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NM80211Mode::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +/// Proxy of a /org/freedesktop/NetworkManager/AccessPoint/* object. +class NMAccessPoint: public QObject { + Q_OBJECT; + +public: + explicit NMAccessPoint(const QString& path, NMWirelessCapabilities::Enum caps, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QByteArray ssid() const { return this->bSsid; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() const { return this->mCaps; }; + [[nodiscard]] quint8 signalStrength() const { return this->bSignalStrength; }; + [[nodiscard]] NM80211ApFlags::Enum flags() const { return this->bFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum wpaFlags() const { return this->bWpaFlags; }; + [[nodiscard]] NM80211ApSecurityFlags::Enum rsnFlags() const { return this->bRsnFlags; }; + [[nodiscard]] NM80211Mode::Enum mode() const { return this->bMode; }; + [[nodiscard]] NMWirelessSecurityType::Enum security() const { return this->bSecurity; }; + [[nodiscard]] bool active() const { return this->mActive; }; + void setActive(bool active) { this->mActive = active; }; + +signals: + void ssidChanged(const QByteArray& ssid); + void signalStrengthChanged(quint8 signal); + void wpaFlagsChanged(NM80211ApSecurityFlags::Enum wpaFlags); + void rsnFlagsChanged(NM80211ApSecurityFlags::Enum rsnFlags); + void flagsChanged(NM80211ApFlags::Enum flags); + void securityChanged(NMWirelessSecurityType::Enum security); + void modeChanged(NM80211Mode::Enum mode); + void ready(); + void disappeared(); + +private: + NMWirelessCapabilities::Enum mCaps; + bool mActive = false; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, QByteArray, bSsid, &NMAccessPoint::ssidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, quint8, bSignalStrength, &NMAccessPoint::signalStrengthChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApFlags::Enum, bFlags, &NMAccessPoint::flagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bWpaFlags, &NMAccessPoint::wpaFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211ApSecurityFlags::Enum, bRsnFlags, &NMAccessPoint::rsnFlagsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NM80211Mode::Enum, bMode, &NMAccessPoint::modeChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMAccessPoint, NMWirelessSecurityType::Enum, bSecurity, &NMAccessPoint::securityChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMAccessPointAdapter, accessPointProperties); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSsid, bSsid, accessPointProperties, "Ssid"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pSignalStrength, bSignalStrength, accessPointProperties, "Strength"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pFlags, bFlags, accessPointProperties, "Flags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pWpaFlags, bWpaFlags, accessPointProperties, "WpaFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pRsnFlags, bRsnFlags, accessPointProperties, "RsnFlags"); + QS_DBUS_PROPERTY_BINDING(NMAccessPoint, pMode, bMode, accessPointProperties, "Mode"); + // clang-format on + + DBusNMAccessPointProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/backend.cpp b/src/network/nm/backend.cpp new file mode 100644 index 00000000..bb539f0e --- /dev/null +++ b/src/network/nm/backend.cpp @@ -0,0 +1,228 @@ +#include "backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "device.hpp" +#include "nm/dbus_nm_backend.h" +#include "wireless.hpp" + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +const QString NM_SERVICE = "org.freedesktop.NetworkManager"; +const QString NM_PATH = "/org/freedesktop/NetworkManager"; + +NetworkManager::NetworkManager(QObject* parent): NetworkBackend(parent) { + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { + qCWarning(logNetworkManager + ) << "Could not connect to DBus. NetworkManager backend will not work."; + return; + } + + this->proxy = new DBusNetworkManagerProxy(NM_SERVICE, NM_PATH, bus, this); + + if (!this->proxy->isValid()) { + qCDebug(logNetworkManager + ) << "NetworkManager service is not currently running. This network backend will not work"; + } else { + this->init(); + } +} + +void NetworkManager::init() { + // clang-format off + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceAdded, this, &NetworkManager::onDevicePathAdded); + QObject::connect(this->proxy, &DBusNetworkManagerProxy::DeviceRemoved, this, &NetworkManager::onDevicePathRemoved); + // clang-format on + + this->dbusProperties.setInterface(this->proxy); + this->dbusProperties.updateAllViaGetAll(); + + this->registerDevices(); +} + +void NetworkManager::registerDevices() { + auto pending = this->proxy->GetAllDevices(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to get devices: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerDevice(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::registerDevice(const QString& path) { + if (this->mDeviceHash.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of device" << path; + return; + } + + // Introspect to decide the device variant. (For now, only Wireless) + auto* introspection = new QDBusInterface( + "org.freedesktop.NetworkManager", + path, + "org.freedesktop.DBus.Introspectable", + QDBusConnection::systemBus(), + this + ); + + auto pending = introspection->asyncCall("Introspect"); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this, path](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) << "Failed to introspect device: " << reply.error().message(); + } else { + QXmlStreamReader xml(reply.value()); + + while (!xml.atEnd() && !xml.hasError()) { + xml.readNext(); + + if (xml.isStartElement() && xml.name() == "interface") { + QString name = xml.attributes().value("name").toString(); + if (name.startsWith("org.freedesktop.NetworkManager.Device.Wireless")) { + this->registerWifiDevice(path); + break; + } + } + } + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +NetworkConnectionState::Enum NetworkManager::toNetworkDeviceState(NMDeviceState::Enum state) { + switch (state) { + case 0 ... 20: return NetworkConnectionState::Unknown; + case 30: return NetworkConnectionState::Disconnected; + case 40 ... 90: return NetworkConnectionState::Connecting; + case 100: return NetworkConnectionState::Connected; + case 110 ... 120: return NetworkConnectionState::Disconnecting; + } +} + +void NetworkManager::registerWifiDevice(const QString& path) { + auto* wireless = new NMWirelessDevice(path); + if (!wireless->isWirelessValid() || !wireless->isDeviceValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete wireless; + return; + } + + auto* device = new WifiDevice(this); + wireless->setParent(device); + this->mDeviceHash.insert(path, device); + + // clang-format off + QObject::connect(wireless, &NMWirelessDevice::interfaceChanged, device, &WifiDevice::setName); + QObject::connect(wireless, &NMWirelessDevice::hwAddressChanged, device, &WifiDevice::setAddress); + QObject::connect(wireless, &NMWirelessDevice::stateChanged, device, &WifiDevice::setNmState); + QObject::connect(wireless, &NMWirelessDevice::stateChanged, device, [device](NMDeviceState::Enum state) { device->setState(qs::network::NetworkManager::toNetworkDeviceState(state));}); + QObject::connect(wireless, &NMWirelessDevice::lastScanChanged, device, &WifiDevice::scanComplete); + QObject::connect(wireless, &NMWirelessDevice::addAndActivateConnection, this, &NetworkManager::addAndActivateConnection); + QObject::connect(wireless, &NMWirelessDevice::activateConnection, this, &NetworkManager::activateConnection); + QObject::connect(wireless, &NMWirelessDevice::wifiNetworkAdded, device, &WifiDevice::networkAdded); + QObject::connect(wireless, &NMWirelessDevice::wifiNetworkRemoved, device, &WifiDevice::networkRemoved); + QObject::connect(device, &WifiDevice::requestScan, wireless, &NMWirelessDevice::scan); + // clang-format on + + emit wifiDeviceAdded(device); +} + +void NetworkManager::onDevicePathAdded(const QDBusObjectPath& path) { + this->registerDevice(path.path()); +} + +void NetworkManager::onDevicePathRemoved(const QDBusObjectPath& path) { + auto iter = this->mDeviceHash.find(path.path()); + if (iter == this->mDeviceHash.end()) { + qCWarning(logNetworkManager) << "NetworkManager sent removal signal for" << path.path() + << "which is not registered."; + } else { + auto* device = iter.value(); + this->mDeviceHash.erase(iter); + if (auto* wifi = qobject_cast(device)) { + emit wifiDeviceRemoved(wifi); + }; + delete device; + } +} + +void NetworkManager::activateConnection( + const QDBusObjectPath& connPath, + const QDBusObjectPath& devPath +) { + auto pending = this->proxy->ActivateConnection(connPath, devPath, QDBusObjectPath("/")); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to request connection activation:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath +) { + auto pending = this->proxy->AddAndActivateConnection(settings, devPath, specificObjectPath); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to start add and activate connection:" << reply.error().message(); + } + delete call; + }; + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NetworkManager::setWifiEnabled(bool enabled) { + if (enabled == this->bWifiEnabled) return; + this->bWifiEnabled = enabled; + this->pWifiEnabled.write(); +} + +bool NetworkManager::isAvailable() const { return this->proxy && this->proxy->isValid(); }; + +} // namespace qs::network diff --git a/src/network/nm/backend.hpp b/src/network/nm/backend.hpp new file mode 100644 index 00000000..9d5481af --- /dev/null +++ b/src/network/nm/backend.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "../network.hpp" +#include "../wifi.hpp" +#include "device.hpp" +#include "nm/dbus_nm_backend.h" + +namespace qs::network { + +class NetworkManager: public NetworkBackend { + Q_OBJECT; + +signals: + void wifiEnabledChanged(bool enabled); + void wifiHardwareEnabledChanged(bool enabled); + void wifiDeviceAdded(WifiDevice* device); + void wifiDeviceRemoved(WifiDevice* device); + +public: + explicit NetworkManager(QObject* parent = nullptr); + [[nodiscard]] bool isAvailable() const override; + +public slots: + void setWifiEnabled(bool enabled); + +private slots: + void onDevicePathAdded(const QDBusObjectPath& path); + void onDevicePathRemoved(const QDBusObjectPath& path); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& specificObjectPath + ); + +private: + void init(); + void registerDevices(); + void registerDevice(const QString& path); + void registerWifiDevice(const QString& path); + static NetworkConnectionState::Enum toNetworkDeviceState(NMDeviceState::Enum state); + + QHash mDeviceHash; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiEnabled, &NetworkManager::wifiEnabledChanged); + Q_OBJECT_BINDABLE_PROPERTY(NetworkManager, bool, bWifiHardwareEnabled, &NetworkManager::wifiHardwareEnabledChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NetworkManager, dbusProperties); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiEnabled, bWifiEnabled, dbusProperties, "WirelessEnabled"); + QS_DBUS_PROPERTY_BINDING(NetworkManager, pWifiHardwareEnabled, bWifiHardwareEnabled, dbusProperties, "WirelessHardwareEnabled"); + // clang-format on + DBusNetworkManagerProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/connection.cpp b/src/network/nm/connection.cpp new file mode 100644 index 00000000..f93b9cef --- /dev/null +++ b/src/network/nm/connection.cpp @@ -0,0 +1,121 @@ +#include "connection.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "nm/utils.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +// NMConnectionAdapter + +NMConnectionSettings::NMConnectionSettings(const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + + this->proxy = new DBusNMConnectionSettingsProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(this->proxy, &DBusNMConnectionSettingsProxy::Updated, this, &NMConnectionSettings::updateSettings); + bSecurity.setBinding([&] { return securityFromConnectionSettings(this->bSettings); }); + // clang-format on + // + this->connectionSettingsProperties.setInterface(this->proxy); + this->connectionSettingsProperties.updateAllViaGetAll(); + + this->updateSettings(); +} + +void NMConnectionSettings::updateSettings() { + auto pending = this->proxy->GetSettings(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get" << this->path() << "settings:" << reply.error().message(); + } else { + this->bSettings = reply.value(); + } + + emit this->ready(); + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +bool NMConnectionSettings::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMConnectionSettings::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMConnectionSettings::path() const { return this->proxy ? this->proxy->path() : QString(); } + +NMActiveConnection::NMActiveConnection(const QString& path, QObject* parent): QObject(parent) { + this->proxy = new DBusNMActiveConnectionProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->proxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for connection at" << path; + return; + } + + // clang-format off + QObject::connect(&this->activeConnectionProperties, &DBusPropertyGroup::getAllFinished, this, [this]() { emit this->ready(); }, Qt::SingleShotConnection); + QObject::connect(this->proxy, &DBusNMActiveConnectionProxy::StateChanged, this, &NMActiveConnection::onStateChanged); + // clang-format on + + this->activeConnectionProperties.setInterface(this->proxy); + this->activeConnectionProperties.updateAllViaGetAll(); +} + +void NMActiveConnection::onStateChanged(quint32 /*state*/, quint32 reason) { + auto enumReason = static_cast(reason); + if (this->mStateReason != enumReason) { + this->mStateReason = enumReason; + emit this->stateReasonChanged(enumReason); + } +} + +bool NMActiveConnection::isValid() const { return this->proxy && this->proxy->isValid(); } +QString NMActiveConnection::address() const { + return this->proxy ? this->proxy->service() : QString(); +} +QString NMActiveConnection::path() const { return this->proxy ? this->proxy->path() : QString(); } + +} // namespace qs::network diff --git a/src/network/nm/connection.hpp b/src/network/nm/connection.hpp new file mode 100644 index 00000000..54997f69 --- /dev/null +++ b/src/network/nm/connection.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_types.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_active_connection.h" +#include "nm/dbus_nm_connection_settings.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMConnectionState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Settings/Connection/* object. +class NMConnectionSettings: public QObject { + Q_OBJECT; + +public: + explicit NMConnectionSettings(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] ConnectionSettingsMap settings() const { return this->bSettings; }; + [[nodiscard]] NMWirelessSecurityType::Enum security() const { return this->bSecurity; }; + +signals: + void settingsChanged(ConnectionSettingsMap settings); + void securityChanged(NMWirelessSecurityType::Enum security); + void ssidChanged(QString ssid); + void ready(); + void disappeared(); + +private: + void updateSettings(); + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, ConnectionSettingsMap, bSettings, &NMConnectionSettings::settingsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMConnectionSettings, NMWirelessSecurityType::Enum, bSecurity, &NMConnectionSettings::securityChanged); + // clang-format on + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMConnectionSettings, connectionSettingsProperties); + + DBusNMConnectionSettingsProxy* proxy = nullptr; +}; + +// Proxy of a /org/freedesktop/NetworkManager/ActiveConnection/* object. +class NMActiveConnection: public QObject { + Q_OBJECT; + +public: + explicit NMActiveConnection(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QDBusObjectPath connection() const { return this->bConnection; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->bState; }; + [[nodiscard]] NMConnectionStateReason::Enum stateReason() const { return this->mStateReason; }; + [[nodiscard]] QString uuid() const { return this->bUuid; }; + +signals: + void stateChanged(NMConnectionState::Enum state); + void stateReasonChanged(NMConnectionStateReason::Enum reason); + void connectionChanged(QDBusObjectPath path); + void uuidChanged(const QString& uuid); + void ready(); + void disappeared(); + +private slots: + void onStateChanged(quint32 state, quint32 reason); + +private: + NMConnectionStateReason::Enum mStateReason = NMConnectionStateReason::Unknown; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QDBusObjectPath, bConnection, &NMActiveConnection::connectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, QString, bUuid, &NMActiveConnection::uuidChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMActiveConnection, NMConnectionState::Enum, bState, &NMActiveConnection::stateChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMActiveConnection, activeConnectionProperties); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pConnection, bConnection, activeConnectionProperties, "Connection"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pUuid, bUuid, activeConnectionProperties, "Uuid"); + QS_DBUS_PROPERTY_BINDING(NMActiveConnection, pState, bState, activeConnectionProperties, "State"); + // clang-format on + DBusNMActiveConnectionProxy* proxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/dbus_types.hpp b/src/network/nm/dbus_types.hpp new file mode 100644 index 00000000..dadbcf38 --- /dev/null +++ b/src/network/nm/dbus_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include +#include + +using ConnectionSettingsMap = QMap; +Q_DECLARE_METATYPE(ConnectionSettingsMap); diff --git a/src/network/nm/device.cpp b/src/network/nm/device.cpp new file mode 100644 index 00000000..932e7ecf --- /dev/null +++ b/src/network/nm/device.cpp @@ -0,0 +1,123 @@ +#include "device.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMDevice::NMDevice(const QString& path, QObject* parent): QObject(parent) { + this->deviceProxy = new DBusNMDeviceProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->deviceProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for device at" << path; + return; + } + + // clang-format off + QObject::connect(this, &NMDevice::availableConnectionPathsChanged, this, &NMDevice::onAvailableConnectionPathsChanged); + QObject::connect(this, &NMDevice::activeConnectionPathChanged, this, &NMDevice::onActiveConnectionPathChanged); + QObject::connect(&this->deviceProperties, &DBusPropertyGroup::getAllFinished, this, [this]() { emit this->deviceReady(); }, Qt::SingleShotConnection); + // clang-format on + + this->deviceProperties.setInterface(this->deviceProxy); + this->deviceProperties.updateAllViaGetAll(); +} + +void NMDevice::onActiveConnectionPathChanged(const QDBusObjectPath& path) { + QString stringPath = path.path(); + if (this->mActiveConnection) { + QObject::disconnect(this->mActiveConnection, nullptr, this, nullptr); + emit this->mActiveConnection->disappeared(); + delete this->mActiveConnection; + this->mActiveConnection = nullptr; + } + + if (stringPath != "/") { + auto* active = new NMActiveConnection(stringPath, this); + this->mActiveConnection = active; + QObject::connect( + active, + &NMActiveConnection::ready, + this, + [this, active]() { emit this->activeConnectionLoaded(active); }, + Qt::SingleShotConnection + ); + } +} + +void NMDevice::onAvailableConnectionPathsChanged(const QList& paths) { + QSet newConnectionPaths; + for (const QDBusObjectPath& path: paths) { + newConnectionPaths.insert(path.path()); + } + + QSet addedConnections = newConnectionPaths - this->mConnectionPaths; + QSet removedConnections = this->mConnectionPaths - newConnectionPaths; + for (const QString& path: addedConnections) { + registerConnection(path); + } + for (const QString& path: removedConnections) { + auto* connection = this->mConnectionMap.take(path); + if (!connection) { + qCDebug(logNetworkManager) << "NetworkManager backend sent removal signal for" << path + << "which is not registered."; + } else { + emit connection->disappeared(); + delete connection; + } + this->mConnectionPaths.remove(path); + }; +} + +void NMDevice::registerConnection(const QString& path) { + auto* connection = new NMConnectionSettings(path, this); + if (!connection->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete connection; + } else { + this->mConnectionMap.insert(path, connection); + this->mConnectionPaths.insert(path); + QObject::connect( + connection, + &NMConnectionSettings::ready, + this, + [this, connection]() { emit this->connectionLoaded(connection); }, + Qt::SingleShotConnection + ); + } +} + +void NMDevice::disconnect() { this->deviceProxy->Disconnect(); } +bool NMDevice::isDeviceValid() const { return this->deviceProxy && this->deviceProxy->isValid(); } +QString NMDevice::address() const { + return this->deviceProxy ? this->deviceProxy->service() : QString(); +} +QString NMDevice::path() const { return this->deviceProxy ? this->deviceProxy->path() : QString(); } + +} // namespace qs::network diff --git a/src/network/nm/device.hpp b/src/network/nm/device.hpp new file mode 100644 index 00000000..73035fab --- /dev/null +++ b/src/network/nm/device.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "connection.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_device.h" + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMDeviceState::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus + +namespace qs::network { + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Only the members from the org.freedesktop.NetworkManager.Device interface. +// Owns the lifetime of NMAvailableConnection(s) and NMConnectionSetting(s). +class NMDevice: public QObject { + Q_OBJECT; + +public: + explicit NMDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isDeviceValid() const; + [[nodiscard]] QString path() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString interface() const { return this->bInterface; }; + [[nodiscard]] QString hwAddress() const { return this->bHwAddress; }; + [[nodiscard]] NMDeviceState::Enum state() const { return this->bState; }; + [[nodiscard]] NMActiveConnection* activeConnection() const { return this->mActiveConnection; }; + +public slots: + void disconnect(); + +signals: + void interfaceChanged(const QString& interface); + void hwAddressChanged(const QString& hwAddress); + void stateChanged(NMDeviceState::Enum state); + void connectionLoaded(NMConnectionSettings* connection); + void connectionRemoved(NMConnectionSettings* connection); + void availableConnectionPathsChanged(QList paths); + void activeConnectionPathChanged(const QDBusObjectPath& connection); + void activeConnectionLoaded(NMActiveConnection* active); + void deviceReady(); + +private slots: + void onAvailableConnectionPathsChanged(const QList& paths); + void onActiveConnectionPathChanged(const QDBusObjectPath& path); + +private: + void registerConnection(const QString& path); + + QSet mConnectionPaths; + QHash mConnectionMap; + NMActiveConnection* mActiveConnection = nullptr; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bInterface, &NMDevice::interfaceChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QString, bHwAddress, &NMDevice::hwAddressChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, NMDeviceState::Enum, bState, &NMDevice::stateChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QList, bAvailableConnections, &NMDevice::availableConnectionPathsChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMDevice, QDBusObjectPath, bActiveConnection, &NMDevice::activeConnectionPathChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMDeviceAdapter, deviceProperties); + QS_DBUS_PROPERTY_BINDING(NMDevice, pName, bInterface, deviceProperties, "Interface"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAddress, bHwAddress, deviceProperties, "HwAddress"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pState, bState, deviceProperties, "State"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pAvailableConnections, bAvailableConnections, deviceProperties, "AvailableConnections"); + QS_DBUS_PROPERTY_BINDING(NMDevice, pActiveConnection, bActiveConnection, deviceProperties, "ActiveConnection"); + // clang-format on + + DBusNMDeviceProxy* deviceProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/nm/enums.hpp b/src/network/nm/enums.hpp new file mode 100644 index 00000000..33a4f7d6 --- /dev/null +++ b/src/network/nm/enums.hpp @@ -0,0 +1,237 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +// 802.11 specific device encryption and authentication capabilities. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceWifiCapabilities. +class NMWirelessCapabilities: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + CipherWep40 = 1, + CipherWep104 = 2, + CipherTkip = 4, + CipherCcmp = 8, + Wpa = 16, + Rsn = 32, + Ap = 64, + Adhoc = 128, + FreqValid = 256, + Freq2Ghz = 512, + Freq5Ghz = 1024, + Freq6Ghz = 2048, + Mesh = 4096, + IbssRsn = 8192, + }; + Q_ENUM(Enum); +}; + +class NMWirelessSecurityType: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Wpa3SuiteB192 = 0, + Sae = 1, + Wpa2Eap = 2, + Wpa2Psk = 3, + WpaEap = 4, + WpaPsk = 5, + StaticWep = 6, + DynamicWep = 7, + Leap = 8, + Owe = 9, + None = 10, + Unknown = 11, + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMWirelessSecurityType::Enum type) { + switch (type) { + case Wpa3SuiteB192: return "WPA3 Suite B 192-bit"; + case Sae: return "WPA3"; + case Wpa2Eap: return "WPA2 Enterprise"; + case Wpa2Psk: return "WPA2"; + case WpaEap: return "WPA Enterprise"; + case WpaPsk: return "WPA"; + case StaticWep: return "WEP"; + case DynamicWep: return "Dynamic WEP"; + case Leap: return "LEAP"; + case Owe: return "OWE"; + case None: return "None"; + default: return "Unknown"; + } + } +}; + +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState. +class NMDeviceState: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IPConfig = 70, + IPCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, + }; + Q_ENUM(Enum); +}; + +// Indicates the 802.11 mode an access point is currently in. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211Mode. +class NM80211Mode: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + Unknown = 0, + Adhoc = 1, + Infra = 2, + Ap = 3, + Mesh = 3, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point flags. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint8 { + None = 0, + Privacy = 1, + Wps = 2, + WpsPbc = 4, + WpsPin = 8, + }; + Q_ENUM(Enum); +}; + +// 802.11 access point security and authentication flags. +// These flags describe the current system requirements of an access point as determined from the access point's beacon. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NM80211ApSecurityFlags. +class NM80211ApSecurityFlags: public QObject { + Q_OBJECT; + +public: + enum Enum : quint16 { + None = 0, + PairWep40 = 1, + PairWep104 = 2, + PairTkip = 4, + PairCcmp = 8, + GroupWep40 = 16, + GroupWep104 = 32, + GroupTkip = 64, + GroupCcmp = 128, + KeyMgmtPsk = 256, + KeyMgmt8021x = 512, + KeyMgmtSae = 1024, + KeyMgmtOwe = 2048, + KeyMgmtOweTm = 4096, + KeyMgmtEapSuiteB192 = 8192, + }; + Q_ENUM(Enum); +}; + +// Indicates the state of a connection to a specific network while it is starting, connected, or disconnected from that network. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState. +class NMConnectionState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + Activating = 1, + Activated = 2, + Deactivating = 3, + Deactivated = 4 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionState::Enum state) { + switch (state) { + case Unknown: return "Unknown"; + case Activating: return "Activating"; + case Activated: return "Activated"; + case Deactivating: return "Deactivating"; + case Deactivated: return "Deactivated"; + } + } +}; + +// Active connection state reasons. +// In sync with https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionStateReason. +class NMConnectionStateReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + Unknown = 0, + None = 1, + UserDisconnected = 2, + DeviceDisconnected = 3, + ServiceStopped = 4, + IpConfigInvalid = 5, + ConnectTimeout = 6, + ServiceStartTimeout = 7, + ServiceStartFailed = 8, + NoSecrets = 9, + LoginFailed = 10, + ConnectionRemoved = 11, + DependencyFailed = 12, + DeviceRealizeFailed = 13, + DeviceRemoved = 14 + }; + Q_ENUM(Enum); + Q_INVOKABLE static QString toString(NMConnectionStateReason::Enum reason) { + switch (reason) { + case Unknown: return "Unknown"; + case None: return "No reason"; + case UserDisconnected: return "User disconnection"; + case DeviceDisconnected: return "The device the connection was using was disconnected."; + case ServiceStopped: return "The service providing the VPN connection was stopped."; + case IpConfigInvalid: return "The IP config of the active connection was invalid."; + case ConnectTimeout: return "The connection attempt to the VPN service timed out."; + case ServiceStartTimeout: + return "A timeout occurred while starting the service providing the VPN connection."; + case ServiceStartFailed: return "Starting the service providing the VPN connection failed."; + case NoSecrets: return "Necessary secrets for the connection were not provided."; + case LoginFailed: return "Authentication to the server failed."; + case ConnectionRemoved: return "Necessary secrets for the connection were not provided."; + case DependencyFailed: return " Master connection of this connection failed to activate."; + case DeviceRealizeFailed: return "Could not create the software device link."; + case DeviceRemoved: return "The device this connection depended on disappeared."; + }; + }; +}; + +} // namespace qs::network diff --git a/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml new file mode 100644 index 00000000..c5e7737d --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.AccessPoint.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml new file mode 100644 index 00000000..fa0e778c --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Connection.Active.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml new file mode 100644 index 00000000..984f43dc --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.Wireless.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Device.xml b/src/network/nm/org.freedesktop.NetworkManager.Device.xml new file mode 100644 index 00000000..322635f3 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Device.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml new file mode 100644 index 00000000..22a2b0d3 --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.Settings.Connection.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/org.freedesktop.NetworkManager.xml b/src/network/nm/org.freedesktop.NetworkManager.xml new file mode 100644 index 00000000..6165af1d --- /dev/null +++ b/src/network/nm/org.freedesktop.NetworkManager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/network/nm/utils.cpp b/src/network/nm/utils.cpp new file mode 100644 index 00000000..32b54d90 --- /dev/null +++ b/src/network/nm/utils.cpp @@ -0,0 +1,341 @@ +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings) { + const QVariantMap& security = settings.value("802-11-wireless-security"); + if (security.isEmpty()) { + return NMWirelessSecurityType::Unknown; + }; + + QString keyMgmt = security["key-mgmt"].toString(); + QString authAlg = security["auth-alg"].toString(); + QList proto = security["proto"].toList(); + + if (keyMgmt == "none") { + return NMWirelessSecurityType::StaticWep; + } else if (keyMgmt == "ieee8021x") { + if (authAlg == "leap") { + return NMWirelessSecurityType::Leap; + } else { + return NMWirelessSecurityType::DynamicWep; + } + } else if (keyMgmt == "wpa-psk") { + if (proto.contains("wpa") && proto.contains("rsn")) { + return NMWirelessSecurityType::WpaPsk; + } + return NMWirelessSecurityType::Wpa2Psk; + } else if (keyMgmt == "wpa-eap") { + if (proto.contains("wpa") && proto.contains("rsn")) { + return NMWirelessSecurityType::WpaEap; + } + return NMWirelessSecurityType::Wpa2Eap; + } else if (keyMgmt == "sae") { + return NMWirelessSecurityType::Sae; + } else if (keyMgmt == "wpa-eap-suite-b-192") { + return NMWirelessSecurityType::Wpa3SuiteB192; + } + + return NMWirelessSecurityType::None; +} + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +) { + bool havePair = false; + bool haveGroup = false; + // Device needs to support at least one pairwise and one group cipher + + if (type == NMWirelessSecurityType::StaticWep) { + // Static WEP only uses group ciphers + havePair = true; + } else { + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::PairWep40) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::PairWep104) + { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::PairTkip) { + havePair = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::PairCcmp) { + havePair = true; + } + } + + if (caps & NMWirelessCapabilities::CipherWep40 && apFlags & NM80211ApSecurityFlags::GroupWep40) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherWep104 && apFlags & NM80211ApSecurityFlags::GroupWep104) + { + haveGroup = true; + } + if (type == NMWirelessSecurityType::StaticWep) { + if (caps & NMWirelessCapabilities::CipherTkip && apFlags & NM80211ApSecurityFlags::GroupTkip) { + haveGroup = true; + } + if (caps & NMWirelessCapabilities::CipherCcmp && apFlags & NM80211ApSecurityFlags::GroupCcmp) { + haveGroup = true; + } + } + + return (havePair && haveGroup); +} + +// In sync with NetworkManager/libnm-core/nm-utils.c:nm_utils_security_valid() +// Given a set of device capabilities, and a desired security type to check +// against, determines whether the combination of device, desired security type, +// and AP capabilities intersect. +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + bool good = true; + + if (!haveAp) { + if (type == NMWirelessSecurityType::None) { + return true; + } + if ((type == NMWirelessSecurityType::StaticWep) + || ((type == NMWirelessSecurityType::DynamicWep) && !adhoc) + || ((type == NMWirelessSecurityType::Leap) && !adhoc)) + { + return caps & NMWirelessCapabilities::CipherWep40 + || caps & NMWirelessCapabilities::CipherWep104; + } + } + + switch (type) { + case NMWirelessSecurityType::None: + if (apFlags & NM80211ApFlags::Privacy) { + return false; + } + if (apWpa || apRsn) { + return false; + } + break; + case NMWirelessSecurityType::Leap: + if (adhoc) { + return false; + } + case NMWirelessSecurityType::StaticWep: + if (!(apFlags & NM80211ApFlags::Privacy)) { + return false; + } + if (apWpa || apRsn) { + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::StaticWep)) { + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::StaticWep)) { + return false; + } + } + } + break; + case NMWirelessSecurityType::DynamicWep: + if (adhoc) { + return false; + } + if (apRsn || !(apFlags & NM80211ApFlags::Privacy)) { + return false; + } + if (apWpa) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::DynamicWep)) { + return false; + } + } + break; + case NMWirelessSecurityType::WpaPsk: + if (adhoc) { + return false; + } + + if (!(caps & NMWirelessCapabilities::Wpa)) { + return false; + } + if (haveAp) { + if (apWpa & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apWpa & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) { + return true; + } + if (apWpa & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } + return false; + } + break; + case NMWirelessSecurityType::Wpa2Psk: + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) { + return false; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtPsk) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) + { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) + { + return true; + } + } + } + return false; + } + break; + case NMWirelessSecurityType::WpaEap: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Wpa)) { + return false; + } + if (haveAp) { + if (!(apWpa & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + // Ensure at least one WPA cipher is supported + if (!deviceSupportsApCiphers(caps, apWpa, NMWirelessSecurityType::WpaEap)) { + return false; + } + } + break; + case NMWirelessSecurityType::Wpa2Eap: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmt8021x)) { + return false; + } + // Ensure at least one WPA cipher is supported + if (!deviceSupportsApCiphers(caps, apRsn, NMWirelessSecurityType::Wpa2Eap)) { + return false; + } + } + break; + case NMWirelessSecurityType::Sae: + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (adhoc) { + if (!(caps & NMWirelessCapabilities::IbssRsn)) { + return false; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) { + return true; + } + } else { + if (apRsn & NM80211ApSecurityFlags::KeyMgmtSae) { + if (apRsn & NM80211ApSecurityFlags::PairTkip && caps & NMWirelessCapabilities::CipherTkip) + { + return true; + } + if (apRsn & NM80211ApSecurityFlags::PairCcmp && caps & NMWirelessCapabilities::CipherCcmp) + { + return true; + } + } + } + return false; + } + break; + case NMWirelessSecurityType::Owe: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp) { + if (!(apRsn & NM80211ApSecurityFlags::KeyMgmtOwe) + && !(apRsn & NM80211ApSecurityFlags::KeyMgmtOweTm)) + { + return false; + } + } + break; + case NMWirelessSecurityType::Wpa3SuiteB192: + if (adhoc) { + return false; + } + if (!(caps & NMWirelessCapabilities::Rsn)) { + return false; + } + if (haveAp && !(apRsn & NM80211ApSecurityFlags::KeyMgmtEapSuiteB192)) { + return false; + } + break; + default: good = false; break; + } + + return good; +} + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +) { + const QList types = { + NMWirelessSecurityType::Wpa3SuiteB192, + NMWirelessSecurityType::Sae, + NMWirelessSecurityType::Wpa2Eap, + NMWirelessSecurityType::Wpa2Psk, + NMWirelessSecurityType::WpaEap, + NMWirelessSecurityType::WpaPsk, + NMWirelessSecurityType::StaticWep, + NMWirelessSecurityType::DynamicWep, + NMWirelessSecurityType::Leap, + NMWirelessSecurityType::Owe, + NMWirelessSecurityType::None + }; + + for (NMWirelessSecurityType::Enum type: types) { + if (securityIsValid(type, caps, haveAp, adHoc, apFlags, apWpa, apRsn)) { + return type; + } + } + return NMWirelessSecurityType::Unknown; +} + +} // namespace qs::network diff --git a/src/network/nm/utils.hpp b/src/network/nm/utils.hpp new file mode 100644 index 00000000..7cdcc0b8 --- /dev/null +++ b/src/network/nm/utils.hpp @@ -0,0 +1,46 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "enums.hpp" + +namespace qs::network { + +NMWirelessSecurityType::Enum securityFromConnectionSettings(const ConnectionSettingsMap& settings); + +bool deviceSupportsApCiphers( + NMWirelessCapabilities::Enum caps, + NM80211ApSecurityFlags::Enum apFlags, + NMWirelessSecurityType::Enum type +); + +bool securityIsValid( + NMWirelessSecurityType::Enum type, + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adhoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +NMWirelessSecurityType::Enum findBestWirelessSecurity( + NMWirelessCapabilities::Enum caps, + bool haveAp, + bool adHoc, + NM80211ApFlags::Enum apFlags, + NM80211ApSecurityFlags::Enum apWpa, + NM80211ApSecurityFlags::Enum apRsn +); + +} // namespace qs::network diff --git a/src/network/nm/wireless.cpp b/src/network/nm/wireless.cpp new file mode 100644 index 00000000..9f5c376d --- /dev/null +++ b/src/network/nm/wireless.cpp @@ -0,0 +1,396 @@ +#include "wireless.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_types.hpp" +#include "nm/enums.hpp" + +namespace qs::dbus { + +DBusResult +DBusDataTransform::fromWire(quint32 wire) { + return DBusResult(static_cast(wire)); +} + +} // namespace qs::dbus + +namespace qs::network { +using namespace qs::dbus; + +namespace { +Q_LOGGING_CATEGORY(logNetworkManager, "quickshell.network.networkmanager", QtWarningMsg); +} + +NMWirelessNetwork::NMWirelessNetwork(QString ssid, QObject* parent) + : QObject(parent) + , mSsid(std::move(ssid)) {} + +void NMWirelessNetwork::updateReferenceConnection() { + // If the network has no connections, the reference is nullptr. + if (this->mConnections.isEmpty()) { + this->mReferenceConn = nullptr; + }; + + // If the network has an active connection, use it as the reference. + if (this->mActiveConnection) { + auto* ref = mConnections.value(this->mActiveConnection->connection().path()); + if (ref) { + this->mReferenceConn = ref; + return; + } + } + + // Otherwise, choose the connection with the strongest security settings. + NMWirelessSecurityType::Enum selectedSecurity = NMWirelessSecurityType::Unknown; + NMConnectionSettings* selectedConn = nullptr; + for (auto* conn: this->mConnections) { + NMWirelessSecurityType::Enum security = conn->security(); + if (selectedSecurity >= security) { + selectedSecurity = security; + selectedConn = conn; + } + } + + if (selectedConn && this->mReferenceConn != selectedConn) { + this->mReferenceConn = selectedConn; + } +} + +void NMWirelessNetwork::setState(NMConnectionState::Enum state) { + if (this->mState == state) return; + this->mState = state; + emit this->stateChanged(state); +} + +void NMWirelessNetwork::setReason(NMConnectionStateReason::Enum reason) { + if (this->mReason == reason) return; + this->mReason = reason; + emit this->reasonChanged(reason); +}; + +void NMWirelessNetwork::setKnown(bool known) { + if (this->mKnown == known) return; + this->mKnown = known; + emit this->knownChanged(known); +} + +void NMWirelessNetwork::updateSignalStrength() { + quint8 selectedStrength = 0; + NMAccessPoint* selectedAp = nullptr; + + for (auto* ap: this->mAccessPoints.values()) { + if (ap->active()) { + selectedStrength = ap->signalStrength(); + selectedAp = ap; + break; + } + if (selectedStrength <= ap->signalStrength()) { + selectedStrength = ap->signalStrength(); + selectedAp = ap; + } + } + + if (selectedStrength != this->mSignalStrength) { + this->mSignalStrength = selectedStrength; + emit this->signalStrengthChanged(selectedStrength); + } + + if (selectedAp && this->mReferenceAp != selectedAp) { + this->mReferenceAp = selectedAp; + NMWirelessSecurityType::Enum selectedSecurity = selectedAp->security(); + if (this->mSecurity != selectedSecurity) { + this->mSecurity = selectedSecurity; + emit this->securityChanged(selectedSecurity); + } + } +} + +void NMWirelessNetwork::addAccessPoint(NMAccessPoint* ap) { + if (this->mAccessPoints.contains(ap->path())) return; + this->mAccessPoints.insert(ap->path(), ap); + // clang-format off + QObject::connect(ap, &NMAccessPoint::signalStrengthChanged, this, &NMWirelessNetwork::updateSignalStrength); + QObject::connect(ap, &NMAccessPoint::disappeared, this, [this, ap] { this->removeAccessPoint(ap); }); + // clang-format on + this->updateSignalStrength(); +}; + +void NMWirelessNetwork::removeAccessPoint(NMAccessPoint* ap) { + auto* found = this->mAccessPoints.take(ap->path()); + if (!found) { + qCWarning(logNetworkManager) << "Backend network" << this->ssid() << "is not in sync!"; + } else { + if (this->mAccessPoints.isEmpty()) { + emit this->disappeared(); + } else { + QObject::disconnect(ap, nullptr, this, nullptr); + this->updateSignalStrength(); + } + } +}; + +void NMWirelessNetwork::addConnectionSettings(NMConnectionSettings* conn) { + if (this->mConnections.contains(conn->path())) return; + this->mConnections.insert(conn->path(), conn); + // clang-format off + QObject::connect(conn, &NMConnectionSettings::securityChanged, this, &NMWirelessNetwork::updateReferenceConnection); + QObject::connect(conn, &NMConnectionSettings::disappeared, this, [this, conn]() { this->removeConnectionSettings(conn); }); + // clang-format on + this->updateReferenceConnection(); + this->setKnown(true); +}; + +void NMWirelessNetwork::removeConnectionSettings(NMConnectionSettings* conn) { + auto* found = this->mConnections.take(conn->path()); + if (!found) { + qCWarning(logNetworkManager) << "Backend network" << this->ssid() << "is not in sync!"; + } else { + QObject::disconnect(conn, nullptr, this, nullptr); + this->updateReferenceConnection(); + if (mConnections.isEmpty()) { + this->setKnown(false); + } + } +}; + +void NMWirelessNetwork::addActiveConnection(NMActiveConnection* active) { + if (this->mActiveConnection) { + qCWarning(logNetworkManager) << "Backend network" << this->ssid() << "is not in sync!"; + return; + } + this->setState(active->state()); + this->setReason(active->stateReason()); + this->mActiveConnection = active; + // clang-format off + QObject::connect(active, &NMActiveConnection::stateChanged, this, &NMWirelessNetwork::setState); + QObject::connect(active, &NMActiveConnection::stateReasonChanged, this, &NMWirelessNetwork::setReason); + QObject::connect(active, &NMActiveConnection::disappeared, this, [this, active] { this->removeActiveConnection(active); }); + // clang-format on +}; + +void NMWirelessNetwork::removeActiveConnection(NMActiveConnection* active) { + if (this->mActiveConnection && this->mActiveConnection == active) { + QObject::disconnect(active, nullptr, this, nullptr); + this->setState(NMConnectionState::Deactivated); + this->setReason(NMConnectionStateReason::None); + this->mActiveConnection = nullptr; + } else { + qCWarning(logNetworkManager) << "Backend network" << this->ssid() << "is not in sync!"; + } +}; + +NMWirelessDevice::NMWirelessDevice(const QString& path, QObject* parent): NMDevice(path, parent) { + this->wirelessProxy = new DBusNMWirelessProxy( + "org.freedesktop.NetworkManager", + path, + QDBusConnection::systemBus(), + this + ); + + if (!this->wirelessProxy->isValid()) { + qCWarning(logNetworkManager) << "Cannot create DBus interface for wireless device at" << path; + return; + } + + QObject::connect( + &this->wirelessProperties, + &DBusPropertyGroup::getAllFinished, + this, + &NMWirelessDevice::initWireless, + Qt::SingleShotConnection + ); + + this->wirelessProperties.setInterface(this->wirelessProxy); + this->wirelessProperties.updateAllViaGetAll(); +} + +void NMWirelessDevice::initWireless() { + // clang-format off + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointAdded, this, &NMWirelessDevice::onAccessPointPathAdded); + QObject::connect(this->wirelessProxy, &DBusNMWirelessProxy::AccessPointRemoved, this, &NMWirelessDevice::onAccessPointPathRemoved); + QObject::connect(this, &NMWirelessDevice::accessPointLoaded, this, &NMWirelessDevice::onAccessPointLoaded); + QObject::connect(this, &NMWirelessDevice::connectionLoaded, this, &NMWirelessDevice::onConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::activeConnectionLoaded, this, &NMWirelessDevice::onActiveConnectionLoaded); + QObject::connect(this, &NMWirelessDevice::activeAccessPointChanged, this, &NMWirelessDevice::onActiveAccessPointChanged); + // clang-format on + this->registerAccessPoints(); +} + +void NMWirelessDevice::onActiveAccessPointChanged(const QDBusObjectPath& path) { + // Make previous AP inactive + auto* prevAp = this->mAccessPoints.value(this->mActiveApPath); + if (prevAp) prevAp->setActive(false); + + // Make current AP active + this->mActiveApPath = path.path(); + auto* curAp = this->mAccessPoints.value(path.path()); + if (curAp) curAp->setActive(true); +} + +void NMWirelessDevice::onAccessPointPathAdded(const QDBusObjectPath& path) { + this->registerAccessPoint(path.path()); +} + +void NMWirelessDevice::onAccessPointPathRemoved(const QDBusObjectPath& path) { + auto* ap = mAccessPoints.take(path.path()); + + if (!ap) { + qCDebug(logNetworkManager) << "NetworkManager sent removal signal for" << path.path() + << "which is not registered."; + return; + } + + emit ap->disappeared(); + delete ap; +} + +void NMWirelessDevice::registerAccessPoints() { + auto pending = this->wirelessProxy->GetAllAccessPoints(); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply> reply = *call; + + if (reply.isError()) { + qCWarning(logNetworkManager) + << "Failed to get all access points: " << reply.error().message(); + } else { + for (const QDBusObjectPath& devicePath: reply.value()) { + this->registerAccessPoint(devicePath.path()); + } + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void NMWirelessDevice::registerAccessPoint(const QString& path) { + if (this->mAccessPoints.contains(path)) { + qCDebug(logNetworkManager) << "Skipping duplicate registration of access point" << path; + return; + } + + auto* ap = new NMAccessPoint(path, this->capabilities(), this); + if (ap->path() == this->mActiveApPath) ap->setActive(true); + + if (!ap->isValid()) { + qCWarning(logNetworkManager) << "Ignoring invalid registration of" << path; + delete ap; + return; + } + + this->mAccessPoints.insert(path, ap); + QObject::connect( + ap, + &NMAccessPoint::ready, + this, + [this, ap]() { emit this->accessPointLoaded(ap); }, + Qt::SingleShotConnection + ); +} + +NMWirelessNetwork* NMWirelessDevice::registerNetwork(const QString& ssid) { + auto* frontend = new WifiNetwork(ssid, this); + auto* backend = new NMWirelessNetwork(ssid, this); + + // clang-format off + frontend->setSignalStrength(backend->signalStrength()); + frontend->setState(static_cast(backend->state())); + frontend->setKnown(backend->known()); + frontend->setNmReason(backend->reason()); + frontend->setNmSecurity(backend->security()); + QObject::connect(backend, &NMWirelessNetwork::signalStrengthChanged, frontend, &WifiNetwork::setSignalStrength); + QObject::connect(backend, &NMWirelessNetwork::knownChanged, frontend, &WifiNetwork::setKnown); + QObject::connect(backend, &NMWirelessNetwork::reasonChanged, frontend, &WifiNetwork::setNmReason); + QObject::connect(backend, &NMWirelessNetwork::securityChanged, frontend, &WifiNetwork::setNmSecurity); + QObject::connect(backend, &NMWirelessNetwork::stateChanged, frontend, + [frontend](NMConnectionState::Enum state) { frontend->setState(static_cast(state)); } + ); + // clang-format on + QObject::connect(backend, &NMWirelessNetwork::disappeared, this, [this, frontend, backend]() { + QObject::disconnect(backend, nullptr, nullptr, nullptr); + emit this->wifiNetworkRemoved(frontend); + this->mBackendNetworks.remove(backend->ssid()); + delete backend; + delete frontend; + }); + QObject::connect(frontend, &WifiNetwork::requestConnect, this, [this, backend]() { + if (backend->referenceConnection()) { + emit this->activateConnection( + QDBusObjectPath(backend->referenceConnection()->path()), + QDBusObjectPath(this->path()) + ); + return; + } + if (backend->referenceAp()) { + emit this->addAndActivateConnection( + ConnectionSettingsMap(), + QDBusObjectPath(this->path()), + QDBusObjectPath(backend->referenceAp()->path()) + ); + } + }); + + this->mBackendNetworks.insert(ssid, backend); + emit this->wifiNetworkAdded(frontend); + return backend; +} + +void NMWirelessDevice::onAccessPointLoaded(NMAccessPoint* ap) { + QString ssid = ap->ssid(); + if (!ssid.isEmpty()) { + auto* net = this->mBackendNetworks.value(ssid); + if (!net) net = registerNetwork(ssid); + net->addAccessPoint(ap); + } +} + +void NMWirelessDevice::onConnectionLoaded(NMConnectionSettings* conn) { + const ConnectionSettingsMap& settings = conn->settings(); + // Early return for invalid connections + if (settings["connection"]["id"].toString().isEmpty() + || settings["connection"]["uuid"].toString().isEmpty() + || !settings.contains("802-11-wireless") + || settings["802-11-wireless"]["mode"].toString() == "ap" + || settings["802-11-wireless"]["ssid"].toString().isEmpty()) + { + return; + } + + const QString ssid = settings["802-11-wireless"]["ssid"].toString(); + auto* net = mBackendNetworks.value(ssid); + if (!net) net = registerNetwork(ssid); + + net->addConnectionSettings(conn); + auto* active = this->activeConnection(); + if (active && conn->path() == active->connection().path()) { + net->addActiveConnection(active); + } +} + +void NMWirelessDevice::onActiveConnectionLoaded(NMActiveConnection* active) { + QString connPath = active->connection().path(); + for (auto* net: this->mBackendNetworks.values()) { + for (auto* conn: net->connections()) { + if (connPath == conn->path()) { + net->addActiveConnection(active); + return; + } + } + } +} + +void NMWirelessDevice::scan() { this->wirelessProxy->RequestScan({}); } +bool NMWirelessDevice::isWirelessValid() const { + return this->wirelessProxy && this->wirelessProxy->isValid(); +} + +} // namespace qs::network diff --git a/src/network/nm/wireless.hpp b/src/network/nm/wireless.hpp new file mode 100644 index 00000000..e3ef0af1 --- /dev/null +++ b/src/network/nm/wireless.hpp @@ -0,0 +1,149 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../wifi.hpp" +#include "accesspoint.hpp" +#include "connection.hpp" +#include "device.hpp" +#include "enums.hpp" +#include "nm/dbus_nm_wireless.h" + +namespace qs::dbus { +template <> +struct DBusDataTransform { + using Wire = quint32; + using Data = qs::network::NMWirelessCapabilities::Enum; + static DBusResult fromWire(Wire wire); +}; + +} // namespace qs::dbus +namespace qs::network { + +// NMWirelessNetwork aggregates all NMActiveConnection, NMAccessPoint, +// and NMConnectionSetting objects with the same ssid. +class NMWirelessNetwork: public QObject { + Q_OBJECT; + +public: + explicit NMWirelessNetwork(QString ssid, QObject* parent = nullptr); + void addAccessPoint(NMAccessPoint* ap); + void removeAccessPoint(NMAccessPoint* ap); + void addConnectionSettings(NMConnectionSettings* conn); + void removeConnectionSettings(NMConnectionSettings* conn); + void addActiveConnection(NMActiveConnection* active); + void removeActiveConnection(NMActiveConnection* active); + void updateSignalStrength(); + void updateReferenceConnection(); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->mSignalStrength; }; + [[nodiscard]] NMConnectionState::Enum state() const { return this->mState; }; + [[nodiscard]] bool known() const { return this->mKnown; }; + [[nodiscard]] NMConnectionStateReason::Enum reason() const { return this->mReason; }; + [[nodiscard]] NMWirelessSecurityType::Enum security() const { return this->mSecurity; }; + [[nodiscard]] NMAccessPoint* referenceAp() const { return this->mReferenceAp; }; + [[nodiscard]] NMConnectionSettings* referenceConnection() const { return this->mReferenceConn; }; + [[nodiscard]] QList accessPoints() const { return this->mAccessPoints.values(); }; + [[nodiscard]] QList connections() const { return this->mConnections.values(); }; + +public slots: + void setState(NMConnectionState::Enum state); + void setReason(NMConnectionStateReason::Enum reason); + void setKnown(bool known); + +signals: + void signalStrengthChanged(quint8 signal); + void stateChanged(NMConnectionState::Enum state); + void knownChanged(bool known); + void reasonChanged(NMConnectionStateReason::Enum reason); + void securityChanged(NMWirelessSecurityType::Enum security); + void disappeared(); + +private: + QString mSsid; + QHash mAccessPoints; + QHash mConnections; + NMActiveConnection* mActiveConnection = nullptr; + NMAccessPoint* mReferenceAp = nullptr; + NMConnectionSettings* mReferenceConn = nullptr; + NMConnectionState::Enum mState = NMConnectionState::Deactivated; + NMConnectionStateReason::Enum mReason = NMConnectionStateReason::None; + NMWirelessSecurityType::Enum mSecurity = NMWirelessSecurityType::None; + quint8 mSignalStrength = 0; + bool mKnown = false; + bool mActive = false; +}; + +// Proxy of a /org/freedesktop/NetworkManager/Device/* object. +// Extends NMDevice to also include members from the org.freedesktop.NetworkManager.Device.Wireless interface +// Owns the lifetime of NMAccessPoints(s), NMWirelessNetwork(s), frontend WifiNetwork(s). +class NMWirelessDevice: public NMDevice { + Q_OBJECT; + +public: + explicit NMWirelessDevice(const QString& path, QObject* parent = nullptr); + + [[nodiscard]] bool isWirelessValid() const; + [[nodiscard]] qint64 getLastScan() { return this->bLastScan; }; + [[nodiscard]] NMWirelessCapabilities::Enum capabilities() { return this->bCapabilities; }; + +public slots: + void scan(); + +signals: + void lastScanChanged(qint64 lastScan); + void capabilitiesChanged(NMWirelessCapabilities::Enum caps); + void activeAccessPointChanged(const QDBusObjectPath& path); + void accessPointLoaded(NMAccessPoint* ap); + void accessPointRemoved(NMAccessPoint* ap); + void wifiNetworkAdded(WifiNetwork* net); + void wifiNetworkRemoved(WifiNetwork* net); + void activateConnection(const QDBusObjectPath& connPath, const QDBusObjectPath& devPath); + void addAndActivateConnection( + const ConnectionSettingsMap& settings, + const QDBusObjectPath& devPath, + const QDBusObjectPath& apPath + ); + +private slots: + void onAccessPointPathAdded(const QDBusObjectPath& path); + void onAccessPointPathRemoved(const QDBusObjectPath& path); + void onActiveAccessPointChanged(const QDBusObjectPath& path); + void onAccessPointLoaded(NMAccessPoint* ap); + void onConnectionLoaded(NMConnectionSettings* conn); + void onActiveConnectionLoaded(NMActiveConnection* active); + +private: + void registerAccessPoint(const QString& path); + void registerAccessPoints(); + void initWireless(); + NMWirelessNetwork* registerNetwork(const QString& ssid); + + QString mActiveApPath; + QHash mAccessPoints; + QHash mBackendNetworks; + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, qint64, bLastScan, &NMWirelessDevice::lastScanChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, NMWirelessCapabilities::Enum, bCapabilities, &NMWirelessDevice::capabilitiesChanged); + Q_OBJECT_BINDABLE_PROPERTY(NMWirelessDevice, QDBusObjectPath, bActiveAccessPoint, &NMWirelessDevice::activeAccessPointChanged); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(NMWireless, wirelessProperties); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pLastScan, bLastScan, wirelessProperties, "LastScan"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pCapabilities, bCapabilities, wirelessProperties, "WirelessCapabilities"); + QS_DBUS_PROPERTY_BINDING(NMWirelessDevice, pActiveAccessPoint, bActiveAccessPoint, wirelessProperties, "ActiveAccessPoint"); + // clang-format on + + DBusNMWirelessProxy* wirelessProxy = nullptr; +}; + +} // namespace qs::network diff --git a/src/network/test/network.qml b/src/network/test/network.qml new file mode 100644 index 00000000..d20e0e7d --- /dev/null +++ b/src/network/test/network.qml @@ -0,0 +1,169 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Network + +FloatingWindow { + property var sortedNetworks: { + return [...Network.wifi.defaultDevice.networks.values].sort((a, b) => { + const aIsConnected = a.state === NetworkConnectionState.Connected + const bIsConnected = b.state === NetworkConnectionState.Connected + + if (aIsConnected !== bIsConnected) { + return bIsConnected - aIsConnected + } + return b.signalStrength - a.signalStrength + }) + } + Column { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + // WiFi toggle + RowLayout { + Label { + text: "WiFi" + font.bold: true + font.pointSize: 16 + } + Switch { + checked: Network.wifi.enabled + onClicked: Network.wifi.setEnabled(!Network.wifi.enabled) + } + } + + Label { + text: "Devices" + font.bold: true + font.pointSize: 12 + } + + WrapperRectangle { + width: 175 + implicitHeight: deviceList.contentHeight + + Component { + id: deviceDelegate + Item { + id: deviceItem + implicitWidth: deviceRow.implicitWidth + 10 + height: 20 + property bool isDefault: Network.wifi.defaultDevice === modelData + MouseArea { + anchors.fill: parent + onClicked: { + Network.wifi.defaultDevice = modelData + } + } + RowLayout { + id: deviceRow + anchors.centerIn: parent + spacing: 5 + Text { + text: modelData.name; font.bold: true; + Layout.preferredWidth: 60 + Layout.alignment: Qt.AlignLeft + elide: Text.ElideRight + } + Text { + text: modelData.address + Layout.preferredWidth: 115 + Layout.alignment: Qt.AlignLeft + } + } + } + } + + ListView { + id: deviceList + model: Network.wifi.devices + interactive: false + delegate: deviceDelegate + highlight: Rectangle { color: "lightskyblue"; radius: 5; border.width: 0; border.color: "grey" } + currentIndex: model.indexOf(Network.wifi.defaultDevice) + } + } + + RowLayout { + Label { + text: "Networks" + font.bold: true + font.pointSize: 12 + } + Button { + Layout.preferredWidth: 42 + Layout.preferredHeight: 20 + text: "Scan" + font.pointSize: 10 + onClicked: Network.wifi.defaultDevice.scan() + visible: Network.wifi.defaultDevice.scanning === false; + } + } + + WrapperRectangle { + width: 375 + implicitHeight: networkList.contentHeight + + Component { + id: networkDelegate + RowLayout { + Rectangle { + id: networkItem + implicitWidth: networkRow.implicitWidth + 10 + height: 20 + color: modelData.state === NetworkConnectionState.Connected ? "lightskyblue" : "whitesmoke"; + radius: 4; border.width: 0; border.color: "grey" + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { parent.color = "silver" } + onExited: { parent.color = modelData.state === NetworkConnectionState.Connected ? "lightskyblue" : "whitesmoke" } + onClicked: modelData.connect() + visible: modelData.state !== NetworkConnectionState.Connected; + } + RowLayout { + id: networkRow + anchors.centerIn: parent + spacing: 8 + Text { + text: modelData.ssid + Layout.preferredWidth: 100 + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + Text { + text: NMWirelessSecurityType.toString(modelData.nmSecurity) + Layout.preferredWidth: 100 + horizontalAlignment: Text.AlignRight + elide: Text.ElideLeft + } + Text { + text: modelData.known ? "Known" : "" + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + } + Text { + text: modelData.signalStrength + "%" + horizontalAlignment: Text.AlignLeft + } + Text { + text: NetworkConnectionState.toString(modelData.state) + horizontalAlignment: Text.AlignLeft + } + } + } + } + } + ListView { + id: networkList + model: sortedNetworks + interactive: false + delegate: networkDelegate + spacing: 2 + } + } + } +} diff --git a/src/network/wifi.cpp b/src/network/wifi.cpp new file mode 100644 index 00000000..ce4af34e --- /dev/null +++ b/src/network/wifi.cpp @@ -0,0 +1,124 @@ +#include "wifi.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace qs::network { + +namespace { +Q_LOGGING_CATEGORY(logWifiDevice, "quickshell.wifi.device", QtWarningMsg); +Q_LOGGING_CATEGORY(logWifiNetwork, "quickshell.wifi.network", QtWarningMsg); +Q_LOGGING_CATEGORY(logWifi, "quickshell.wifi", QtWarningMsg); +} // namespace + +WifiDevice::WifiDevice(QObject* parent): NetworkDevice(parent) {}; + +void WifiDevice::scanComplete() { + if (this->mScanning) { + this->mScanning = false; + emit this->scanningChanged(); + } +} + +void WifiDevice::scan() { + if (this->mScanning) { + qCCritical(logWifiDevice) << "Wireless device" << this->name() << "is already scanning"; + return; + } + + qCDebug(logWifiDevice) << "Requesting scan on wireless device" << this->name(); + this->mScanning = true; + emit this->scanningChanged(); + this->requestScan(); +} + +void WifiDevice::networkAdded(WifiNetwork* net) { this->mNetworks.insertObject(net); } +void WifiDevice::networkRemoved(WifiNetwork* net) { this->mNetworks.removeObject(net); } + +WifiNetwork::WifiNetwork(QString ssid, QObject* parent): QObject(parent), mSsid(std::move(ssid)) {}; + +void WifiNetwork::setSignalStrength(quint8 signal) { + if (this->mSignalStrength == signal) return; + this->mSignalStrength = signal; + emit this->signalStrengthChanged(); +} + +void WifiNetwork::setState(NetworkConnectionState::Enum state) { + if (this->mState == state) return; + this->mState = state; + emit this->stateChanged(); +} + +void WifiNetwork::setKnown(bool known) { + if (this->mKnown == known) return; + this->mKnown = known; + emit this->knownChanged(); +} + +void WifiNetwork::connect() { + if (this->mState == NetworkConnectionState::Connected) { + qCCritical(logWifiNetwork) << this->ssid() << "is already connected."; + return; + } + if (this->mState == NetworkConnectionState::Connecting) { + qCCritical(logWifiNetwork) << this->ssid() << "is already connecting."; + return; + } + this->requestConnect(); +} + +void WifiNetwork::setNmReason(NMConnectionStateReason::Enum reason) { + if (this->mNmReason == reason) return; + this->mNmReason = reason; + emit this->nmReasonChanged(); +} + +void WifiNetwork::setNmSecurity(NMWirelessSecurityType::Enum security) { + if (this->mNmSecurity == security) return; + this->mNmSecurity = security; + emit this->nmSecurityChanged(); +} + +Wifi::Wifi(QObject* parent): QObject(parent) {}; + +void Wifi::onDeviceAdded(WifiDevice* dev) { + this->mDevices.insertObject(dev); + if (this->mDefaultDevice) return; + this->setDefaultDevice(dev); +} + +void Wifi::onDeviceRemoved(WifiDevice* dev) { this->mDevices.removeObject(dev); }; + +void Wifi::onHardwareEnabledSet(bool enabled) { + if (this->mHardwareEnabled == enabled) return; + this->mHardwareEnabled = enabled; + emit this->hardwareEnabledChanged(); +} + +void Wifi::setEnabled(bool enabled) { + if (this->mEnabled == enabled) { + QString state = enabled ? "enabled" : "disabled"; + qCCritical(logWifi) << "Wifi is already" << state; + } else { + emit this->requestSetEnabled(enabled); + } +} + +void Wifi::onEnabledSet(bool enabled) { + if (this->mEnabled == enabled) return; + this->mEnabled = enabled; + emit this->enabledChanged(); +} + +void Wifi::setDefaultDevice(WifiDevice* dev) { + if (this->mDefaultDevice == dev) return; + this->mDefaultDevice = dev; + emit this->defaultDeviceChanged(); +} + +} // namespace qs::network diff --git a/src/network/wifi.hpp b/src/network/wifi.hpp new file mode 100644 index 00000000..1f13fd4a --- /dev/null +++ b/src/network/wifi.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/model.hpp" +#include "device.hpp" +#include "nm/enums.hpp" + +namespace qs::network { + +///! An available wifi network. +class WifiNetwork: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiNetwork can only be acquired through WifiDevice"); + /// The SSID (service set identifier) of the wifi network. + Q_PROPERTY(QString ssid READ ssid CONSTANT); + /// The current signal strength of the network, in percent. + Q_PROPERTY(quint8 signalStrength READ signalStrength NOTIFY signalStrengthChanged); + /// The connection state of the wifi network. + Q_PROPERTY(NetworkConnectionState::Enum state READ state NOTIFY stateChanged); + /// True if the wifi network has known connection settings saved. + Q_PROPERTY(bool known READ known NOTIFY knownChanged); + /// The specific reason for the connection state when the backend is NetworkManager. + Q_PROPERTY(NMConnectionStateReason::Enum nmReason READ nmReason NOTIFY nmReasonChanged); + /// The security type of the wifi network when the backend is NetworkManager. + Q_PROPERTY(NMWirelessSecurityType::Enum nmSecurity READ nmSecurity NOTIFY nmSecurityChanged); + +signals: + void signalStrengthChanged(); + void stateChanged(); + void knownChanged(); + void nmReasonChanged(); + void nmSecurityChanged(); + void requestConnect(); + +public slots: + void setSignalStrength(quint8 signalStrength); + void setState(NetworkConnectionState::Enum state); + void setNmReason(NMConnectionStateReason::Enum reason); + void setNmSecurity(NMWirelessSecurityType::Enum security); + void setKnown(bool known); + +public: + explicit WifiNetwork(QString ssid, QObject* parent = nullptr); + + /// Attempt to connect to the wifi network. + Q_INVOKABLE void connect(); + + [[nodiscard]] QString ssid() const { return this->mSsid; }; + [[nodiscard]] quint8 signalStrength() const { return this->mSignalStrength; }; + [[nodiscard]] NetworkConnectionState::Enum state() const { return this->mState; }; + [[nodiscard]] NMConnectionStateReason::Enum nmReason() const { return this->mNmReason; }; + [[nodiscard]] NMWirelessSecurityType::Enum nmSecurity() const { return this->mNmSecurity; }; + [[nodiscard]] bool known() const { return this->mKnown; }; + +private: + QString mSsid; + quint8 mSignalStrength = 0; + bool mKnown = false; + NetworkConnectionState::Enum mState = NetworkConnectionState::Disconnected; + NMConnectionStateReason::Enum mNmReason = NMConnectionStateReason::Unknown; + NMWirelessSecurityType::Enum mNmSecurity = NMWirelessSecurityType::Unknown; +}; + +///! Wireless variant of a network device. +class WifiDevice: public NetworkDevice { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("WifiDevices can only be acquired through Wifi"); + + /// True if the wifi device is currently scanning for available wifi networks. + Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged); + /// A list of all wifi networks currently available. + Q_PROPERTY(UntypedObjectModel* networks READ networks CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*) + +signals: + void requestScan(); + void scanningChanged(); + +public slots: + void scanComplete(); + void networkAdded(WifiNetwork* net); + void networkRemoved(WifiNetwork* net); + +public: + explicit WifiDevice(QObject* parent = nullptr); + + /// Request the wireless device to scan for available WiFi networks. + /// This should be invoked everytime you want to show the user an accurate list of available networks. + Q_INVOKABLE void scan(); + + [[nodiscard]] bool scanning() const { return this->mScanning; }; + UntypedObjectModel* networks() { return &this->mNetworks; }; + +private: + ObjectModel mNetworks {this}; + bool mScanning = false; +}; + +///! A manager for all wifi state and devices. +class Wifi: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE("Wifi can only be acquired through Network"); + + // clang-format off + /// A list of all wifi devices. + Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT); + QSDOC_TYPE_OVERRIDE(ObjectModel*); + /// The default wifi device. + /// Useful for choosing the device you'd like to use for any wifi connection. + /// By default this will be the first device discovered. + Q_PROPERTY(WifiDevice* defaultDevice READ defaultDevice WRITE setDefaultDevice NOTIFY defaultDeviceChanged); + /// True when the wifi software switch is enabled. + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); + /// True when the wifi hardware switch is enabled. + Q_PROPERTY(bool hardwareEnabled READ hardwareEnabled NOTIFY hardwareEnabledChanged); + +signals: + void enabledChanged(); + void hardwareEnabledChanged(); + void defaultDeviceChanged(); + void requestSetEnabled(bool enabled); + +public slots: + void onDeviceAdded(WifiDevice* dev); + void onDeviceRemoved(WifiDevice* dev); + void onEnabledSet(bool enabled); + void onHardwareEnabledSet(bool enabled); + +public: + explicit Wifi(QObject* parent = nullptr); + + /// Set the state of the wifi software switch. + Q_INVOKABLE void setEnabled(bool enabled); + + [[nodiscard]] bool hardwareEnabled() const { return this->mHardwareEnabled; }; + [[nodiscard]] bool enabled() const { return this->mEnabled; }; + [[nodiscard]] UntypedObjectModel* devices() { return &this->mDevices; }; + [[nodiscard]] WifiDevice* defaultDevice() const { return this->mDefaultDevice; }; + void setDefaultDevice(WifiDevice* dev); + +private: + ObjectModel mDevices {this}; + WifiDevice* mDefaultDevice = nullptr; + bool mEnabled = false; + bool mHardwareEnabled = false; +}; + +}; // namespace qs::network