You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
651 lines
28 KiB
C++
651 lines
28 KiB
C++
/*
|
|
Copyright 2013-2014 Jan Grulich <jgrulich@redhat.com>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) version 3, or any
|
|
later version accepted by the membership of KDE e.V. (or its
|
|
successor approved by the membership of KDE e.V.), which shall
|
|
act as a proxy defined in Section 6 of version 3 of the license.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "handler.h"
|
|
#include "configuration.h"
|
|
#include "networkclient.h"
|
|
#include "vpnutils.h"
|
|
#include "wifiutils.h"
|
|
|
|
#include <QDBusConnection>
|
|
#include <QDBusPendingCallWatcher>
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QProcess>
|
|
#include <QRegularExpression>
|
|
#include <QTimer>
|
|
#include <QUuid>
|
|
|
|
|
|
using namespace NetworkDBus;
|
|
|
|
namespace {
|
|
|
|
// The shell's secret agent, which asks for a new network's password up front.
|
|
const QString promptService = QStringLiteral("com.cutefish.SecretAgent");
|
|
const QString promptPath = QStringLiteral("/SecretAgent");
|
|
const QString promptInterface = QStringLiteral("com.cutefish.SecretAgent");
|
|
|
|
const QString wirelessSecurity = QStringLiteral("802-11-wireless-security");
|
|
|
|
// Activations and profile changes may wait on an administrator password.
|
|
constexpr int kAuthorizedCallTimeoutMs = 2 * 60 * 1000;
|
|
constexpr int kForgetWatchTimeoutMs = 2 * 60 * 1000;
|
|
|
|
QVariant objectPath(const QString &path)
|
|
{
|
|
return QVariant::fromValue(QDBusObjectPath(path.isEmpty() ? QStringLiteral("/") : path));
|
|
}
|
|
|
|
// At most 32 bytes, cut on a character boundary.
|
|
QByteArray ssidBytes(const QString &name)
|
|
{
|
|
QByteArray bytes = name.toUtf8();
|
|
if (bytes.size() <= 32)
|
|
return bytes;
|
|
int size = 32;
|
|
while (size > 0 && (static_cast<uchar>(bytes.at(size)) & 0xc0) == 0x80)
|
|
--size;
|
|
return bytes.left(size);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Handler::Handler(QObject *parent)
|
|
: QObject(parent)
|
|
, m_client(NetworkClient::instance())
|
|
{
|
|
connect(m_client, &NetworkClient::changed, this, &Handler::update);
|
|
connect(m_client, &NetworkClient::activeStateChanged, this,
|
|
[this](const QString &active, uint state, uint) { activeStateChanged(active, state); });
|
|
connect(m_client, &NetworkClient::objectRemoved, this, [this](const QString &path) {
|
|
// Gone without a final state: the activation never made it.
|
|
if (m_forget.contains(path) || path == m_hotspotActivation)
|
|
activeStateChanged(path, ActiveDeactivated);
|
|
});
|
|
connect(m_client, &NetworkClient::readyChanged, this, [this] {
|
|
if (!m_client->ready()) {
|
|
m_forget.clear();
|
|
return;
|
|
}
|
|
if (m_scanRequested) {
|
|
m_scanRequested = false;
|
|
requestScan(m_scanInterface);
|
|
}
|
|
});
|
|
update();
|
|
}
|
|
|
|
void Handler::activateConnection(const QString &connection, const QString &device,
|
|
const QString &specificObject)
|
|
{
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("ActivateConnection"),
|
|
{objectPath(connection), objectPath(device), objectPath(specificObject)}, this,
|
|
{}, kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::addAndActivateConnection(const QString &device, const QString &specificObject,
|
|
const QString &password)
|
|
{
|
|
addAndActivateWireless(device, specificObject, password, true);
|
|
}
|
|
|
|
void Handler::addAndActivateWireless(const QString &device, const QString &specificObject,
|
|
const QString &password, bool askForPassword)
|
|
{
|
|
if (!m_client->has(specificObject, accessPointInterface))
|
|
return;
|
|
|
|
const auto ap = [&](const char *name) {
|
|
return m_client->property(specificObject, accessPointInterface, QLatin1String(name));
|
|
};
|
|
const QByteArray ssid = ap("Ssid").toByteArray();
|
|
const QString name = WifiUtils::ssidLabel(ssid);
|
|
const Enums::SecurityType security = WifiUtils::security(
|
|
m_client->property(device, wirelessInterface, QStringLiteral("WirelessCapabilities")).toUInt(),
|
|
ap("Flags").toUInt(), ap("WpaFlags").toUInt(), ap("RsnFlags").toUInt(), ap("Mode").toUInt());
|
|
|
|
// Enterprise networks need an identity and certificates, which Settings asks for.
|
|
if (WifiUtils::enterprise(security))
|
|
return;
|
|
|
|
// Asked for before anything is added: activating first would save the network
|
|
// and show it connecting while the user is still typing, or has cancelled.
|
|
if (askForPassword && password.isEmpty() && WifiUtils::needsPassword(security)) {
|
|
auto message = QDBusMessage::createMethodCall(promptService, promptPath, promptInterface,
|
|
QStringLiteral("ConnectWireless"));
|
|
message << device << specificObject << name << (security == Enums::StaticWep);
|
|
|
|
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
|
|
connect(watcher, &QDBusPendingCallWatcher::finished, this,
|
|
[this, device, specificObject](QDBusPendingCallWatcher *watcher) {
|
|
watcher->deleteLater();
|
|
// Without the shell there is no one to ask first; NetworkManager
|
|
// asks whichever agent there is once activation needs the secret.
|
|
if (watcher->isError())
|
|
addAndActivateWireless(device, specificObject, QString(), false);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// NetworkManager completes the rest from the access point: the SSID's own bytes, the
|
|
// mode, and the key management, which for a WPA2/WPA3 network is wpa-psk so drivers
|
|
// without SAE still connect.
|
|
QVariantMap connection {{QStringLiteral("id"), name}};
|
|
// A shared profile keeps its password in NetworkManager for every user; a private
|
|
// one keeps it in the user's keyring through the secret agent.
|
|
const bool shared = m_client->sharesNewConnections();
|
|
if (!shared)
|
|
connection.insert(QStringLiteral("permissions"), QStringList {NetworkClient::userPermission()});
|
|
|
|
NMVariantMapMap settings;
|
|
settings.insert(QStringLiteral("connection"), connection);
|
|
if (WifiUtils::needsPassword(security)) {
|
|
const uint secretFlags = shared ? SecretSystemOwned : SecretAgentOwned;
|
|
QVariantMap secrets;
|
|
if (security == Enums::StaticWep) {
|
|
secrets.insert(QStringLiteral("wep-key-flags"), secretFlags);
|
|
if (!password.isEmpty()) {
|
|
secrets.insert(QStringLiteral("wep-key0"), password);
|
|
// A 5 or 13 character, or 10 or 26 digit key; not a passphrase.
|
|
secrets.insert(QStringLiteral("wep-key-type"), 1u);
|
|
}
|
|
} else {
|
|
secrets.insert(QStringLiteral("psk-flags"), secretFlags);
|
|
if (!password.isEmpty())
|
|
secrets.insert(QStringLiteral("psk"), password);
|
|
}
|
|
settings.insert(wirelessSecurity, secrets);
|
|
}
|
|
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("AddAndActivateConnection"),
|
|
{QVariant::fromValue(settings), objectPath(device), objectPath(specificObject)}, this,
|
|
[this](const QDBusMessage &reply) {
|
|
if (reply.type() != QDBusMessage::ReplyMessage) {
|
|
qWarning() << "Cannot join the network:" << reply.errorMessage();
|
|
return;
|
|
}
|
|
forgetUnlessActivated(NetworkClient::path(reply.arguments().value(0)),
|
|
NetworkClient::path(reply.arguments().value(1)));
|
|
},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::forgetUnlessActivated(const QString &connection, const QString &active)
|
|
{
|
|
// A wrong password the user then gave up on would otherwise stay behind as a known
|
|
// network. The active connection arrived before this reply did.
|
|
if (connection.isEmpty())
|
|
return;
|
|
if (active.isEmpty() || !m_client->has(active, activeInterface)) {
|
|
removeConnection(connection);
|
|
return;
|
|
}
|
|
|
|
m_forget.insert(active, connection);
|
|
activeStateChanged(active, m_client->property(active, activeInterface, QStringLiteral("State")).toUInt());
|
|
// An activation NetworkManager drops without a final state is let go of.
|
|
QTimer::singleShot(kForgetWatchTimeoutMs, this, [this, active] { m_forget.remove(active); });
|
|
}
|
|
|
|
void Handler::activeStateChanged(const QString &active, uint state)
|
|
{
|
|
if (!m_hotspotActivation.isEmpty() && active == m_hotspotActivation) {
|
|
if (state == ActiveDeactivated) {
|
|
m_hotspotActivation.clear();
|
|
Q_EMIT hotspotFailed(tr("The hotspot could not be started. The Wi-Fi adapter may not "
|
|
"support it on this channel or with this password."));
|
|
} else if (state == ActiveActivated) {
|
|
m_hotspotActivation.clear();
|
|
}
|
|
}
|
|
|
|
const auto it = m_forget.constFind(active);
|
|
if (it == m_forget.cend())
|
|
return;
|
|
|
|
if (state == ActiveDeactivated) {
|
|
const QString connection = it.value();
|
|
m_forget.erase(it);
|
|
removeConnection(connection);
|
|
} else if (state == ActiveActivated) {
|
|
m_forget.erase(it);
|
|
}
|
|
}
|
|
|
|
void Handler::deactivateConnection(const QString &connection, const QString &device)
|
|
{
|
|
for (const QString &active : m_client->activeConnections()) {
|
|
const QString profile = NetworkClient::path(
|
|
m_client->property(active, activeInterface, QStringLiteral("Connection")));
|
|
const QStringList devices = m_client->property(active, activeInterface, QStringLiteral("Devices")).toStringList();
|
|
const bool vpn = m_client->property(active, activeInterface, QStringLiteral("Vpn")).toBool();
|
|
|
|
const bool matches = connection.isEmpty() ? devices.contains(device) : profile == connection;
|
|
if (!matches)
|
|
continue;
|
|
|
|
// A VPN, or a profile named without a device (WireGuard brings its own), goes down
|
|
// alone; disconnecting a device also keeps it from connecting again by itself.
|
|
if (vpn || device.isEmpty()) {
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("DeactivateConnection"),
|
|
{objectPath(active)}, this, {}, kAuthorizedCallTimeoutMs);
|
|
} else if (devices.contains(device)) {
|
|
m_client->call(device, deviceInterface, QStringLiteral("Disconnect"), {}, this, {},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Handler::removeConnection(const QString &connection)
|
|
{
|
|
const QString uuid = m_client->setting(connection, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
|
|
|
|
// Port profiles go with the profile they belong to.
|
|
if (!uuid.isEmpty()) {
|
|
for (const QString &port : m_client->connections()) {
|
|
const QString controller =
|
|
m_client->setting(port, QStringLiteral("connection"), QStringLiteral("controller")).toString();
|
|
if (controller == uuid
|
|
|| m_client->setting(port, QStringLiteral("connection"), QStringLiteral("master")).toString() == uuid)
|
|
m_client->call(port, connectionInterface, QStringLiteral("Delete"), {}, this, {},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
}
|
|
|
|
m_client->call(connection, connectionInterface, QStringLiteral("Delete"), {}, this, {},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::requestScan(const QString &interface)
|
|
{
|
|
if (!m_client->ready()) {
|
|
m_scanRequested = true;
|
|
m_scanInterface = interface;
|
|
return;
|
|
}
|
|
|
|
if (!m_client->manager(QStringLiteral("WirelessEnabled")).toBool())
|
|
return;
|
|
|
|
// NetworkManager rate-limits and defers scans itself; it only refuses devices that
|
|
// cannot scan, and one in AP mode would drop the hotspot's clients.
|
|
for (const QString &device : m_client->devices()) {
|
|
if (m_client->deviceType(device) != DeviceWifi
|
|
|| m_client->deviceState(device) < DeviceDisconnected
|
|
|| m_client->property(device, wirelessInterface, QStringLiteral("Mode")).toUInt() == ModeAp)
|
|
continue;
|
|
if (!interface.isEmpty()
|
|
&& m_client->property(device, deviceInterface, QStringLiteral("Interface")).toString() != interface)
|
|
continue;
|
|
|
|
m_client->call(device, wirelessInterface, QStringLiteral("RequestScan"), {QVariantMap()}, this);
|
|
}
|
|
}
|
|
|
|
void Handler::enableNetworking(bool enable)
|
|
{
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("Enable"), {enable}, this, {},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::enableWireless(bool enable)
|
|
{
|
|
m_client->setProperty(managerPath, managerInterface, QStringLiteral("WirelessEnabled"), enable, this);
|
|
}
|
|
|
|
void Handler::enableWired(bool enable)
|
|
{
|
|
for (const QString &device : m_client->devices()) {
|
|
if (m_client->deviceType(device) != DeviceEthernet
|
|
|| !m_client->property(device, deviceInterface, QStringLiteral("Managed")).toBool())
|
|
continue;
|
|
|
|
// Disconnecting clears the device's autoconnect flag and activating sets it again, so the
|
|
// flag is written only when neither applies; writing it next to either races the other.
|
|
const uint state = m_client->deviceState(device);
|
|
if (!enable && state >= DevicePrepare && state <= DeviceActivated) {
|
|
m_client->call(device, deviceInterface, QStringLiteral("Disconnect"), {}, this, {},
|
|
kAuthorizedCallTimeoutMs);
|
|
} else if (enable && state == DeviceDisconnected) {
|
|
// "/" lets NetworkManager pick the device's best connection, even one that doesn't autoconnect.
|
|
activateConnection(QString(), device, QString());
|
|
} else {
|
|
m_client->setProperty(device, deviceInterface, QStringLiteral("Autoconnect"), enable, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Handler::enableWwan(bool enable)
|
|
{
|
|
m_client->setProperty(managerPath, managerInterface, QStringLiteral("WwanEnabled"), enable, this);
|
|
}
|
|
|
|
void Handler::createHotspot()
|
|
{
|
|
uint capabilities = 0;
|
|
const QString device = hotspotDevice(&capabilities);
|
|
if (device.isEmpty()) {
|
|
Q_EMIT hotspotFailed(tr("No Wi-Fi adapter can run a hotspot."));
|
|
return;
|
|
}
|
|
|
|
const bool apMode = capabilities & CapAp;
|
|
const Configuration configuration;
|
|
const QString name = configuration.hotspotName();
|
|
const QString password = configuration.hotspotPassword();
|
|
const QString existing = m_client->hotspotConnection();
|
|
const QString uuid = existing.isEmpty()
|
|
? QUuid::createUuid().toString(QUuid::WithoutBraces)
|
|
: m_client->setting(existing, QStringLiteral("connection"), QStringLiteral("uuid")).toString();
|
|
|
|
NMVariantMapMap settings;
|
|
settings.insert(QStringLiteral("connection"),
|
|
{{QStringLiteral("id"), name},
|
|
{QStringLiteral("uuid"), uuid},
|
|
{QStringLiteral("type"), QStringLiteral("802-11-wireless")},
|
|
{QStringLiteral("autoconnect"), false}});
|
|
settings.insert(QStringLiteral("802-11-wireless"),
|
|
{{QStringLiteral("ssid"), ssidBytes(name)},
|
|
{QStringLiteral("mode"), apMode ? QStringLiteral("ap") : QStringLiteral("adhoc")}});
|
|
// NetworkManager picks a channel that is valid where the machine is.
|
|
if (!password.isEmpty()) {
|
|
QVariantMap security;
|
|
if (apMode) {
|
|
security.insert(QStringLiteral("key-mgmt"), QStringLiteral("wpa-psk"));
|
|
security.insert(QStringLiteral("psk"), password);
|
|
if (capabilities & CapRsn) {
|
|
security.insert(QStringLiteral("proto"), QStringList {QStringLiteral("rsn")});
|
|
security.insert(QStringLiteral("pairwise"), QStringList {QStringLiteral("ccmp")});
|
|
security.insert(QStringLiteral("group"), QStringList {QStringLiteral("ccmp")});
|
|
}
|
|
} else {
|
|
security.insert(QStringLiteral("key-mgmt"), QStringLiteral("none"));
|
|
security.insert(QStringLiteral("wep-key-type"), 2u);
|
|
security.insert(QStringLiteral("wep-key0"), password);
|
|
}
|
|
settings.insert(wirelessSecurity, security);
|
|
}
|
|
settings.insert(QStringLiteral("ipv4"), {{QStringLiteral("method"), QStringLiteral("shared")}});
|
|
settings.insert(QStringLiteral("ipv6"), {{QStringLiteral("method"), QStringLiteral("ignore")}});
|
|
|
|
// One profile, kept and updated, rather than a new one each time.
|
|
if (existing.isEmpty()) {
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("AddAndActivateConnection"),
|
|
{QVariant::fromValue(settings), objectPath(device), objectPath(QString())}, this,
|
|
[this](const QDBusMessage &reply) { hotspotRequestFinished(reply, 1); },
|
|
kAuthorizedCallTimeoutMs);
|
|
return;
|
|
}
|
|
|
|
m_client->call(existing, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(settings)},
|
|
this, [this, existing, device](const QDBusMessage &reply) {
|
|
if (reply.type() != QDBusMessage::ReplyMessage) {
|
|
hotspotRequestFinished(reply, 0);
|
|
return;
|
|
}
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("ActivateConnection"),
|
|
{objectPath(existing), objectPath(device), objectPath(QString())}, this,
|
|
[this](const QDBusMessage &reply) { hotspotRequestFinished(reply, 0); },
|
|
kAuthorizedCallTimeoutMs);
|
|
},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::hotspotRequestFinished(const QDBusMessage &reply, int activePathArgument)
|
|
{
|
|
if (reply.type() != QDBusMessage::ReplyMessage) {
|
|
// Cancelling the administrator password prompt is no failure to report.
|
|
if (reply.errorName() != QLatin1String("org.freedesktop.NetworkManager.PermissionDenied"))
|
|
Q_EMIT hotspotFailed(reply.errorMessage());
|
|
return;
|
|
}
|
|
|
|
const QString active = reply.arguments().value(activePathArgument).value<QDBusObjectPath>().path();
|
|
const uint state = m_client->property(active, activeInterface, QStringLiteral("State")).toUInt();
|
|
if (state == ActiveActivated)
|
|
return;
|
|
m_hotspotActivation = active;
|
|
// Its state may have changed before this reply arrived.
|
|
if (state == ActiveDeactivated || state == ActiveDeactivating)
|
|
activeStateChanged(active, ActiveDeactivated);
|
|
}
|
|
|
|
void Handler::stopHotspot()
|
|
{
|
|
m_hotspotActivation.clear();
|
|
const QString active = hotspotActiveConnection();
|
|
if (active.isEmpty())
|
|
return;
|
|
m_client->call(managerPath, managerInterface, QStringLiteral("DeactivateConnection"),
|
|
{objectPath(active)}, this, {}, kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::importVpn(const QUrl &file)
|
|
{
|
|
const QString path = file.isLocalFile() ? file.toLocalFile() : file.toString();
|
|
|
|
QFile config(path);
|
|
if (config.open(QIODevice::ReadOnly | QIODevice::Text) && config.size() < 1024 * 1024) {
|
|
const QByteArray content = config.readAll();
|
|
if (VpnUtils::isWireGuardConfig(content)) {
|
|
QString error;
|
|
const NMVariantMapMap profile = VpnUtils::wireGuardProfile(content, QFileInfo(path).completeBaseName(), &error);
|
|
if (profile.isEmpty())
|
|
Q_EMIT vpnAddFailed(error);
|
|
else
|
|
addPrivateVpn(profile);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// NetworkManager's own readers, the ones nmcli and both big desktops use; whichever
|
|
// reads the file first wins.
|
|
QStringList plugins = VpnUtils::installedPlugins();
|
|
const QString suffix = QFileInfo(path).suffix().toLower();
|
|
const QString likely = suffix == QLatin1String("ovpn") || suffix == QLatin1String("conf") ? QStringLiteral("openvpn")
|
|
: suffix == QLatin1String("pcf") ? QStringLiteral("vpnc")
|
|
: QString();
|
|
if (plugins.removeAll(likely))
|
|
plugins.prepend(likely);
|
|
|
|
if (plugins.isEmpty()) {
|
|
Q_EMIT vpnAddFailed(tr("No VPN plugin can read this file. Install the one for your VPN, "
|
|
"for example network-manager-openvpn for OpenVPN."));
|
|
return;
|
|
}
|
|
importVpnAs(path, plugins, QString());
|
|
}
|
|
|
|
void Handler::importVpnAs(const QString &file, QStringList types, const QString &error)
|
|
{
|
|
if (types.isEmpty()) {
|
|
Q_EMIT vpnAddFailed(error.isEmpty() ? tr("This file isn't a VPN configuration this system can read.")
|
|
: error);
|
|
return;
|
|
}
|
|
|
|
const QString type = types.takeFirst();
|
|
|
|
// A plugin's reader runs in nmcli: NetworkManager offers no way to call it over D-Bus.
|
|
auto *process = new QProcess(this);
|
|
process->setProgram(QStringLiteral("nmcli"));
|
|
process->setArguments({QStringLiteral("connection"), QStringLiteral("import"), QStringLiteral("type"), type,
|
|
QStringLiteral("file"), file});
|
|
connect(process, &QProcess::finished, this,
|
|
[this, process, file, types, error](int code, QProcess::ExitStatus status) {
|
|
process->deleteLater();
|
|
const QString output = QString::fromUtf8(process->readAllStandardOutput());
|
|
const QString message = QString::fromUtf8(process->readAllStandardError()).trimmed();
|
|
if (status == QProcess::NormalExit && code == 0) {
|
|
const auto uuid = QRegularExpression(QStringLiteral("[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}"))
|
|
.match(output);
|
|
Q_EMIT vpnAdded(uuid.captured());
|
|
return;
|
|
}
|
|
importVpnAs(file, types, message.isEmpty() ? error : message);
|
|
});
|
|
connect(process, &QProcess::errorOccurred, this, [this, process](QProcess::ProcessError failure) {
|
|
if (failure != QProcess::FailedToStart)
|
|
return;
|
|
process->deleteLater();
|
|
Q_EMIT vpnAddFailed(tr("nmcli, which reads VPN files, is not installed."));
|
|
});
|
|
process->start();
|
|
}
|
|
|
|
void Handler::setVpnUserName(const QString &connection, const QString &userName)
|
|
{
|
|
const QString service = m_client->setting(connection, QStringLiteral("vpn"), QStringLiteral("service-type")).toString();
|
|
const QVariantMap current = m_client->setting(connection, QStringLiteral("vpn"), QStringLiteral("data")).toMap();
|
|
QMap<QString, QString> data;
|
|
for (auto it = current.cbegin(); it != current.cend(); ++it)
|
|
data.insert(it.key(), it.value().toString());
|
|
|
|
const QString key = VpnUtils::userNameKey(service, data);
|
|
if (key.isEmpty() || data.value(key) == userName)
|
|
return;
|
|
if (userName.isEmpty())
|
|
data.remove(key);
|
|
else
|
|
data.insert(key, userName);
|
|
|
|
// vpn.data is a{ss}; without secrets in it, the update keeps the stored ones.
|
|
NMVariantMapMap settings = m_client->settings(connection);
|
|
settings[QStringLiteral("vpn")].insert(QStringLiteral("data"), QVariant::fromValue(data));
|
|
m_client->call(connection, connectionInterface, QStringLiteral("Update"), {QVariant::fromValue(settings)}, this,
|
|
[](const QDBusMessage &reply) {
|
|
if (reply.type() == QDBusMessage::ErrorMessage)
|
|
qWarning() << "Cannot save the VPN user name:" << reply.errorMessage();
|
|
},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
void Handler::addVpn(const QVariantMap &form)
|
|
{
|
|
QString error;
|
|
const NMVariantMapMap profile = VpnUtils::manualProfile(form, &error);
|
|
if (profile.isEmpty())
|
|
Q_EMIT vpnAddFailed(error);
|
|
else
|
|
addPrivateVpn(profile);
|
|
}
|
|
|
|
QVariantList Handler::vpnKinds() const
|
|
{
|
|
return VpnUtils::manualKinds();
|
|
}
|
|
|
|
void Handler::addPrivateVpn(NMVariantMapMap profile)
|
|
{
|
|
// As GNOME does: the VPN is this user's, which needs no administrator password, and
|
|
// comes up only when asked to. A WireGuard profile would otherwise connect the moment
|
|
// it is added, its interface being its own.
|
|
profile[QStringLiteral("connection")].insert(QStringLiteral("autoconnect"), false);
|
|
profile[QStringLiteral("connection")].insert(QStringLiteral("permissions"),
|
|
QStringList {NetworkClient::userPermission()});
|
|
|
|
const QString uuid = profile.value(QStringLiteral("connection")).value(QStringLiteral("uuid")).toString();
|
|
m_client->call(settingsPath, settingsInterface, QStringLiteral("AddConnection"), {QVariant::fromValue(profile)},
|
|
this,
|
|
[this, uuid](const QDBusMessage &reply) {
|
|
if (reply.type() == QDBusMessage::ReplyMessage)
|
|
Q_EMIT vpnAdded(uuid);
|
|
else
|
|
Q_EMIT vpnAddFailed(reply.errorMessage());
|
|
},
|
|
kAuthorizedCallTimeoutMs);
|
|
}
|
|
|
|
QString Handler::hotspotActiveConnection() const
|
|
{
|
|
const QString hotspot = m_client->hotspotConnection();
|
|
if (hotspot.isEmpty())
|
|
return {};
|
|
for (const QString &active : m_client->activeConnections()) {
|
|
const uint state = m_client->property(active, activeInterface, QStringLiteral("State")).toUInt();
|
|
if (NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection"))) == hotspot
|
|
&& (state == ActiveActivating || state == ActiveActivated))
|
|
return active;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QString Handler::hotspotDevice(uint *capabilities) const
|
|
{
|
|
// An idle adapter that can run an access point is best; one in use gets disconnected.
|
|
QString device;
|
|
uint deviceCapabilities = 0;
|
|
for (const QString &candidate : m_client->devices()) {
|
|
if (m_client->deviceType(candidate) != DeviceWifi
|
|
|| !m_client->property(candidate, deviceInterface, QStringLiteral("Managed")).toBool())
|
|
continue;
|
|
const uint caps = m_client->property(candidate, wirelessInterface,
|
|
QStringLiteral("WirelessCapabilities")).toUInt();
|
|
if (!(caps & (CapAp | CapAdhoc)))
|
|
continue;
|
|
const bool idle = m_client->activeConnection(candidate).isEmpty();
|
|
const bool better = device.isEmpty() || ((caps & CapAp) && !(deviceCapabilities & CapAp))
|
|
|| (bool(caps & CapAp) == bool(deviceCapabilities & CapAp) && idle
|
|
&& !m_client->activeConnection(device).isEmpty());
|
|
if (better) {
|
|
device = candidate;
|
|
deviceCapabilities = caps;
|
|
}
|
|
}
|
|
if (capabilities)
|
|
*capabilities = deviceCapabilities;
|
|
return device;
|
|
}
|
|
|
|
void Handler::update()
|
|
{
|
|
const QString device = hotspotDevice();
|
|
const bool capable = !device.isEmpty();
|
|
|
|
// Polkit answers "auth" when an administrator password would do; an unread answer
|
|
// doesn't block either.
|
|
const QString permission = m_client->permission(QStringLiteral("org.freedesktop.NetworkManager.wifi.share.protected"));
|
|
const bool allowed = m_client->manager(QStringLiteral("WirelessEnabled")).toBool()
|
|
&& permission != QLatin1String("no");
|
|
|
|
// The network this adapter is on goes when the hotspot takes it over.
|
|
QString replaces;
|
|
const QString active = capable ? m_client->activeConnection(device) : QString();
|
|
if (!active.isEmpty()
|
|
&& NetworkClient::path(m_client->property(active, activeInterface, QStringLiteral("Connection")))
|
|
!= m_client->hotspotConnection())
|
|
replaces = m_client->property(active, activeInterface, QStringLiteral("Id")).toString();
|
|
|
|
if (capable != m_hotspotCapable || allowed != m_hotspotAllowed || replaces != m_hotspotReplaces) {
|
|
m_hotspotCapable = capable;
|
|
m_hotspotAllowed = allowed;
|
|
m_hotspotReplaces = replaces;
|
|
Q_EMIT hotspotSupportedChanged();
|
|
}
|
|
|
|
const bool hotspot = !hotspotActiveConnection().isEmpty();
|
|
if (hotspot != m_hotspotActive) {
|
|
m_hotspotActive = hotspot;
|
|
Q_EMIT hotspotActiveChanged();
|
|
}
|
|
}
|