settingsvpnmodel.cpp 32.8 KB
Newer Older
1
/*
2 3
 * Copyright (c) 2016 - 2019 Jolla Ltd.
 * Copyright (c) 2019 Open Mobile Platform LLC.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
 *
 * You may use this file under the terms of the BSD license as follows:
 *
 * "Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *   * Neither the name of Nemo Mobile nor the names of its contributors
 *     may be used to endorse or promote products derived from this
 *     software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
 */

33
#include <QRegularExpression>
34
#include <QStandardPaths>
35
#include <QDataStream>
36
#include <QCryptographicHash>
37 38 39 40
#include <QQmlEngine>
#include <QDir>
#include "logging_p.h"
#include "vpnmanager.h"
41

42
#include "settingsvpnmodel.h"
43

44 45
namespace {

46 47
const auto defaultDomain = QStringLiteral("sailfishos.org");
const auto legacyDefaultDomain(QStringLiteral("merproject.org"));
48

49
int numericValue(VpnConnection::ConnectionState state)
50
{
51
    return (state == VpnConnection::Ready ? 3 :
52 53 54 55 56 57 58 59 60 61
                (state == VpnConnection::Configuration ? 2 : 0));
}

VpnConnection::ConnectionState getMaxState(VpnConnection::ConnectionState newState, VpnConnection::ConnectionState oldState)
{
    if (numericValue(newState) > numericValue(oldState)) {
        return newState;
    }

    return oldState;
62
}
63

64 65
} // end anonymous namespace

66 67
SettingsVpnModel::SettingsVpnModel(QObject* parent)
    : VpnModel(parent)
68 69 70 71 72
    , credentials_(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/system/privileged/vpn-data"))
    , bestState_(VpnConnection::Idle)
    , autoConnect_(false)
    , orderByConnected_(true)
    , provisioningOutputPath_(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/system/privileged/vpn-provisioning"))
73
    , roles(VpnModel::roleNames())
74
{
75
    VpnManager *manager = vpnManager();
76

77
    roles.insert(ConnectedRole, "connected");
78

79 80 81
    connect(manager, &VpnManager::connectionAdded, this, &SettingsVpnModel::connectionAdded, Qt::UniqueConnection);
    connect(manager, &VpnManager::connectionRemoved, this, &SettingsVpnModel::connectionRemoved, Qt::UniqueConnection);
    connect(manager, &VpnManager::connectionsRefreshed, this, &SettingsVpnModel::connectionsRefreshed, Qt::UniqueConnection);
82 83
}

84
SettingsVpnModel::~SettingsVpnModel()
85
{
86
    VpnManager *manager = vpnManager();
87

88
    disconnect(manager, 0, this, 0);
89 90
}

91
void SettingsVpnModel::createConnection(const QVariantMap &createProperties)
92
{
93 94 95 96 97 98 99
    QVariantMap properties(createProperties);
    const QString domain(properties.value(QString("domain")).toString());
    if (domain.isEmpty()) {
        properties.insert(QString("domain"), QVariant::fromValue(createDefaultDomain()));
    }

    vpnManager()->createConnection(properties);
100 101
}

102
QHash<int, QByteArray> SettingsVpnModel::roleNames() const
103
{
104
    return roles;
105 106
}

107
QVariant SettingsVpnModel::data(const QModelIndex &index, int role) const
108
{
109 110 111 112 113
    if (index.isValid() && index.row() >= 0 && index.row() < connections().count()) {
        switch (role) {
        case ConnectedRole:
            return QVariant::fromValue((bool)connections().at(index.row())->connected());
        default:
114
            return VpnModel::data(index, role);
115 116 117
        }
    }

118
    return QVariant();
119 120
}

121
VpnConnection::ConnectionState SettingsVpnModel::bestState() const
122
{
123
    return bestState_;
124 125
}

126
bool SettingsVpnModel::autoConnect() const
127
{
128 129
    return autoConnect_;
}
130

131
bool SettingsVpnModel::orderByConnected() const
132 133 134
{
    return orderByConnected_;
}
135

136
void SettingsVpnModel::setOrderByConnected(bool orderByConnected)
137 138 139
{
    if (orderByConnected != orderByConnected_) {
        orderByConnected_ = orderByConnected;
140
        VpnModel::connectionsChanged();
141 142
        emit orderByConnectedChanged();
    }
143 144
}

145
void SettingsVpnModel::modifyConnection(const QString &path, const QVariantMap &properties)
146
{
147 148 149 150
    VpnConnection *conn = vpnManager()->connection(path);
    if (conn) {
        QVariantMap updatedProperties(properties);
        const QString domain(updatedProperties.value(QString("domain")).toString());
151

152 153 154 155 156 157 158 159 160
        if (domain.isEmpty()) {
            if (isDefaultDomain(conn->domain())) {
                // The connection already has a default domain, no need to change it
                updatedProperties.remove("domain");
            }
            else {
                updatedProperties.insert(QString("domain"), QVariant::fromValue(createDefaultDomain()));
            }
        }
161

162 163 164
        const QString location(CredentialsRepository::locationForObjectPath(path));
        const bool couldStoreCredentials(credentials_.credentialsExist(location));
        const bool canStoreCredentials(properties.value(QString("storeCredentials")).toBool());
165

166
        vpnManager()->modifyConnection(path, updatedProperties);
167

168 169 170 171 172 173
        if (canStoreCredentials != couldStoreCredentials) {
            if (canStoreCredentials) {
                credentials_.storeCredentials(location, QVariantMap());
            } else {
                credentials_.removeCredentials(location);
            }
174 175
        }
    }
176 177
    else {
        qCWarning(lcVpnLog) << "VPN connection modification failed: connection doesn't exist";
178 179 180
    }
}

181
void SettingsVpnModel::deleteConnection(const QString &path)
182
{
183 184 185 186 187 188
    if (VpnConnection *conn = vpnManager()->connection(path)) {
        // Remove cached credentials
        const QString location(CredentialsRepository::locationForObjectPath(path));
        if (credentials_.credentialsExist(location)) {
            credentials_.removeCredentials(location);
        }
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        // Remove provisioned files
        if (conn->type() == QStringLiteral("openvpn")) {
            QVariantMap providerProperties = conn->providerProperties();
            QStringList fileProperties;
            fileProperties << QStringLiteral("OpenVPN.Cert") << QStringLiteral("OpenVPN.Key") << QStringLiteral("OpenVPN.CACert") << QStringLiteral("OpenVPN.ConfigFile");
            for (const QString property : fileProperties) {
                const QString filename = providerProperties.value(property).toString();

                // Check if the file has been provisioned
                if (filename.contains(provisioningOutputPath_)) {
                    int timesUsed = 0;

                    // Check the same file is not used by other connections
                    for (VpnConnection *c : connections()) {
                        if (filename == c->providerProperties().value(property).toString()) {
                            timesUsed++;
                            if (timesUsed > 1) {
                                break;
                            }
                        }
                    }
211

212 213 214 215
                    if (timesUsed > 1) {
                        qCInfo(lcVpnLog) << "VPN provisioning file kept, used by" << timesUsed << "connections.";
                        continue;
                    }
mvogt's avatar
mvogt committed
216

217 218 219 220 221 222 223 224 225
                    qCInfo(lcVpnLog) << "VPN provisioning file removed: " << filename;
                    if (!QFile::remove(filename)) {
                        qCWarning(lcVpnLog) << "VPN provisioning file could not be removed: " << filename;
                    }
                }
            }
        }

        vpnManager()->deleteConnection(path);
mvogt's avatar
mvogt committed
226 227 228 229
    }

}

230
void SettingsVpnModel::activateConnection(const QString &path)
mvogt's avatar
mvogt committed
231
{
232
    vpnManager()->activateConnection(path);
mvogt's avatar
mvogt committed
233 234
}

235
void SettingsVpnModel::deactivateConnection(const QString &path)
mvogt's avatar
mvogt committed
236
{
237
    vpnManager()->deactivateConnection(path);
mvogt's avatar
mvogt committed
238 239
}

240
VpnConnection *SettingsVpnModel::get(int index) const
mvogt's avatar
mvogt committed
241
{
242 243 244 245
    if (index >= 0 && index < connections().size()) {
        VpnConnection *item(connections().at(index));
        QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership);
        return item;
mvogt's avatar
mvogt committed
246 247
    }

248
    return 0;
mvogt's avatar
mvogt committed
249 250 251

}

252 253 254
// ==========================================================================
// QAbstractListModel Ordering
// ==========================================================================
mvogt's avatar
mvogt committed
255

256
bool SettingsVpnModel::compareConnections(const VpnConnection *i, const VpnConnection *j)
mvogt's avatar
mvogt committed
257
{
258 259 260
    return ((orderByConnected_ && (i->connected() > j->connected()))
            || ((!orderByConnected_ || (i->connected() == j->connected()))
                && (i->name().localeAwareCompare(j->name()) <= 0)));
mvogt's avatar
mvogt committed
261 262
}

263
void SettingsVpnModel::orderConnections(QVector<VpnConnection*> &connections)
264
{
265 266 267
    std::sort(connections.begin(), connections.end(), [this](const VpnConnection *i, const VpnConnection *j) -> bool {
        // Return true if i should appear before j in the list
        return compareConnections(i, j);
268
    });
269
}
270

271
void SettingsVpnModel::reorderConnection(VpnConnection * conn)
272 273
{
    const int itemCount(connections().size());
274

275 276 277 278 279 280 281 282 283
    if (itemCount > 1) {
        int index = 0;
        for ( ; index < itemCount; ++index) {
            const VpnConnection *existing = connections().at(index);
            // Scenario 1 orderByConnected == true: order first by connected, second by name
            // Scenario 2 orderByConnected == false: order only by name
            if (!compareConnections(existing, conn)) {
                break;
            }
284
        }
285 286 287
        const int currentIndex = connections().indexOf(conn);
        if (index != currentIndex && (index - 1) != currentIndex) {
            moveItem(currentIndex, (currentIndex < index ? (index - 1) : index));
288
        }
289
    }
290 291
}

292
void SettingsVpnModel::updatedConnectionPosition()
293
{
294 295
    VpnConnection *conn = qobject_cast<VpnConnection *>(sender());
    reorderConnection(conn);
296 297
}

298
void SettingsVpnModel::connectedChanged()
299
{
300
    VpnConnection *conn = qobject_cast<VpnConnection *>(sender());
301

302 303 304 305 306 307
    int row = connections().indexOf(conn);
    if (row >= 0) {
        QModelIndex index = createIndex(row, 0);;
        emit dataChanged(index, index);
    }
    reorderConnection(conn);
308 309
}

310
void SettingsVpnModel::connectionAdded(const QString &path)
311
{
312 313 314 315
    qCDebug(lcVpnLog) << "VPN connection added";
    if (VpnConnection *conn = vpnManager()->connection(path)) {
        bool credentialsExist = credentials_.credentialsExist(CredentialsRepository::locationForObjectPath(path));
        conn->setStoreCredentials(credentialsExist);
316

317 318 319
        connect(conn, &VpnConnection::nameChanged, this, &SettingsVpnModel::updatedConnectionPosition, Qt::UniqueConnection);
        connect(conn, &VpnConnection::connectedChanged, this, &SettingsVpnModel::connectedChanged, Qt::UniqueConnection);
        connect(conn, &VpnConnection::stateChanged, this, &SettingsVpnModel::stateChanged, Qt::UniqueConnection);
320 321 322
    }
}

323
void SettingsVpnModel::connectionRemoved(const QString &path)
324
{
325 326 327
    qCDebug(lcVpnLog) << "VPN connection removed";
    if (VpnConnection *conn = vpnManager()->connection(path)) {
        disconnect(conn, 0, this, 0);
328 329 330
    }
}

331
void SettingsVpnModel::connectionsRefreshed()
332 333 334
{
    qCDebug(lcVpnLog) << "VPN connections refreshed";
    QVector<VpnConnection*> connections = vpnManager()->connections();
335 336 337

    // Check to see if the best state has changed
    VpnConnection::ConnectionState maxState = VpnConnection::Idle;
338
    for (VpnConnection *conn : connections) {
339 340 341
        connect(conn, &VpnConnection::nameChanged, this, &SettingsVpnModel::updatedConnectionPosition, Qt::UniqueConnection);
        connect(conn, &VpnConnection::connectedChanged, this, &SettingsVpnModel::connectedChanged, Qt::UniqueConnection);
        connect(conn, &VpnConnection::stateChanged, this, &SettingsVpnModel::stateChanged, Qt::UniqueConnection);
342 343

        maxState = getMaxState(conn->state(), maxState);
344
    }
345 346

    updateBestState(maxState);
347 348
}

349
void SettingsVpnModel::stateChanged()
350
{
351 352
    // Emit the state changed signal needed for the VPN EnableSwitch
    VpnConnection *conn = qobject_cast<VpnConnection *>(sender());
353
    emit connectionStateChanged(conn->path(), conn->state());
354

355
    // Check to see if the best state has changed
356 357
    VpnConnection::ConnectionState maxState = getMaxState(conn->state(), VpnConnection::Idle);
    updateBestState(maxState);
358 359
}

360 361 362 363
// ==========================================================================
// Automatic domain allocation
// ==========================================================================

364
bool SettingsVpnModel::domainInUse(const QString &domain) const
365
{
366 367 368 369 370
    const int itemCount(count());
    for (int index = 0; index < itemCount; ++index) {
        const VpnConnection *connection = connections().at(index);
        if (connection->domain() == domain) {
            return true;
371 372
        }
    }
373 374
    return false;
}
375

376
QString SettingsVpnModel::createDefaultDomain() const
377 378 379 380 381 382
{
    QString newDomain = defaultDomain;
    int index = 1;
    while (domainInUse(newDomain)) {
        newDomain = defaultDomain + QString(".%1").arg(index);
        ++index;
383
    }
384
    return newDomain;
385 386
}

387
bool SettingsVpnModel::isDefaultDomain(const QString &domain)
388
{
389 390 391 392 393
    if (domain == legacyDefaultDomain)
        return true;

    static const QRegularExpression domainPattern(QStringLiteral("^%1(\\.\\d+)?$").arg(defaultDomain));
    return domainPattern.match(domain).hasMatch();
394 395
}

396 397 398 399
// ==========================================================================
// Credential storage
// ==========================================================================

400
QVariantMap SettingsVpnModel::connectionCredentials(const QString &path)
mvogt's avatar
mvogt committed
401 402 403
{
    QVariantMap rv;

404
    if (VpnConnection *conn = vpnManager()->connection(path)) {
mvogt's avatar
mvogt committed
405 406 407 408 409 410
        const QString location(CredentialsRepository::locationForObjectPath(path));
        const bool enabled(credentials_.credentialsExist(location));

        if (enabled) {
            rv = credentials_.credentials(location);
        } else {
411
            qWarning() << "VPN does not permit credentials storage:" << path;
mvogt's avatar
mvogt committed
412 413
        }

414
        conn->setStoreCredentials(enabled);
mvogt's avatar
mvogt committed
415
    } else {
416
        qWarning() << "Unable to return credentials for unknown VPN connection:" << path;
mvogt's avatar
mvogt committed
417 418 419 420 421
    }

    return rv;
}

422
void SettingsVpnModel::setConnectionCredentials(const QString &path, const QVariantMap &credentials)
mvogt's avatar
mvogt committed
423
{
424
    if (VpnConnection *conn = vpnManager()->connection(path)) {
mvogt's avatar
mvogt committed
425 426
        credentials_.storeCredentials(CredentialsRepository::locationForObjectPath(path), credentials);

427
        conn->setStoreCredentials(true);
mvogt's avatar
mvogt committed
428
    } else {
429
        qWarning() << "Unable to set credentials for unknown VPN connection:" << path;
mvogt's avatar
mvogt committed
430 431 432
    }
}

433
bool SettingsVpnModel::connectionCredentialsEnabled(const QString &path)
mvogt's avatar
mvogt committed
434
{
435
    if (VpnConnection *conn = vpnManager()->connection(path)) {
mvogt's avatar
mvogt committed
436 437 438
        const QString location(CredentialsRepository::locationForObjectPath(path));
        const bool enabled(credentials_.credentialsExist(location));

439
        conn->setStoreCredentials(enabled);
mvogt's avatar
mvogt committed
440 441
        return enabled;
    } else {
442
        qWarning() << "Unable to test credentials storage for unknown VPN connection:" << path;
mvogt's avatar
mvogt committed
443 444 445 446 447
    }

    return false;
}

448
void SettingsVpnModel::disableConnectionCredentials(const QString &path)
mvogt's avatar
mvogt committed
449
{
450
    if (VpnConnection *conn = vpnManager()->connection(path)) {
mvogt's avatar
mvogt committed
451 452 453 454 455
        const QString location(CredentialsRepository::locationForObjectPath(path));
        if (credentials_.credentialsExist(location)) {
            credentials_.removeCredentials(location);
        }

456
        conn->setStoreCredentials(false);
mvogt's avatar
mvogt committed
457
    } else {
458
        qWarning() << "Unable to set automatic connection for unknown VPN connection:" << path;
mvogt's avatar
mvogt committed
459 460 461
    }
}

462
QVariantMap SettingsVpnModel::connectionSettings(const QString &path)
463
{
464 465
    QVariantMap properties;
    if (VpnConnection *conn = vpnManager()->connection(path)) {
mvogt's avatar
mvogt committed
466 467
        // Check if the credentials storage has been changed
        const QString location(CredentialsRepository::locationForObjectPath(path));
468
        conn->setStoreCredentials(credentials_.credentialsExist(location));
mvogt's avatar
mvogt committed
469

470
        properties = VpnModel::connectionSettings(path);
471
    }
472
    return properties;
473 474
}

475 476 477
// ==========================================================================
// CredentialsRepository
// ==========================================================================
478

479
SettingsVpnModel::CredentialsRepository::CredentialsRepository(const QString &path)
480 481 482 483
    : baseDir_(path)
{
    if (!baseDir_.exists() && !baseDir_.mkpath(path)) {
        qWarning() << "Unable to create base directory for VPN credentials:" << path;
484 485 486
    }
}

487
QString SettingsVpnModel::CredentialsRepository::locationForObjectPath(const QString &path)
488
{
489 490 491 492
    int index = path.lastIndexOf(QChar('/'));
    if (index != -1) {
        return path.mid(index + 1);
    }
493

494 495
    return QString();
}
496

497
bool SettingsVpnModel::CredentialsRepository::credentialsExist(const QString &location) const
498 499 500
{
    // Test the FS, as another process may store/remove the credentials
    return baseDir_.exists(location);
501 502
}

503
bool SettingsVpnModel::CredentialsRepository::storeCredentials(const QString &location, const QVariantMap &credentials)
504
{
505 506 507 508 509 510 511 512
    QFile credentialsFile(baseDir_.absoluteFilePath(location));
    if (!credentialsFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
        qWarning() << "Unable to write credentials file:" << credentialsFile.fileName();
        return false;
    } else {
        credentialsFile.write(encodeCredentials(credentials));
        credentialsFile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ReadOther | QFileDevice::WriteOther);
        credentialsFile.close();
513 514
    }

515
    return true;
516 517
}

518
bool SettingsVpnModel::CredentialsRepository::removeCredentials(const QString &location)
519
{
520 521 522 523
    if (baseDir_.exists(location)) {
        if (!baseDir_.remove(location)) {
            qWarning() << "Unable to delete credentials file:" << location;
            return false;
524
        }
525
    }
526

527 528
    return true;
}
529

530
QVariantMap SettingsVpnModel::CredentialsRepository::credentials(const QString &location) const
531 532
{
    QVariantMap rv;
533

534 535 536 537 538 539
    QFile credentialsFile(baseDir_.absoluteFilePath(location));
    if (!credentialsFile.open(QIODevice::ReadOnly)) {
        qWarning() << "Unable to read credentials file:" << credentialsFile.fileName();
    } else {
        const QByteArray encoded = credentialsFile.readAll();
        credentialsFile.close();
540

541 542
        rv = decodeCredentials(encoded);
    }
543

544
    return rv;
545 546
}

547
QByteArray SettingsVpnModel::CredentialsRepository::encodeCredentials(const QVariantMap &credentials)
548
{
549 550
    // We can't store these values securely, but we may as well encode them to protect from grep, at least...
    QByteArray encoded;
551

552 553
    QDataStream os(&encoded, QIODevice::WriteOnly);
    os.setVersion(QDataStream::Qt_5_6);
554

555 556
    const quint32 version = 1u;
    os << version;
557

558 559
    const quint32 items = credentials.size();
    os << items;
560

561 562 563
    for (auto it = credentials.cbegin(), end = credentials.cend(); it != end; ++it) {
        os << it.key();
        os << it.value().toString();
564 565
    }

566 567
    return encoded.toBase64();
}
568

569
QVariantMap SettingsVpnModel::CredentialsRepository::decodeCredentials(const QByteArray &encoded)
570 571
{
    QVariantMap rv;
572

573
    QByteArray decoded(QByteArray::fromBase64(encoded));
574

575 576
    QDataStream is(decoded);
    is.setVersion(QDataStream::Qt_5_6);
577

578 579
    quint32 version;
    is >> version;
580

581 582 583 584 585
    if (version != 1u) {
        qWarning() << "Invalid version for stored credentials:" << version;
    } else {
        quint32 items;
        is >> items;
586

587 588 589 590 591 592
        for (quint32 i = 0; i < items; ++i) {
            QString key, value;
            is >> key;
            is >> value;
            rv.insert(key, QVariant::fromValue(value));
        }
593 594
    }

595
    return rv;
596 597
}

598 599 600 601
// ==========================================================================
// Provisioning files
// ==========================================================================

602
QVariantMap SettingsVpnModel::processProvisioningFile(const QString &path, const QString &type)
603
{
604
    QVariantMap rv;
605

606 607 608 609 610 611
    QFile provisioningFile(path);
    if (provisioningFile.open(QIODevice::ReadOnly)) {
        if (type == QString("openvpn")) {
            rv = processOpenVpnProvisioningFile(provisioningFile);
        } else {
            qWarning() << "Provisioning not currently supported for VPN type:" << type;
612
        }
613 614
    } else {
        qWarning() << "Unable to open provisioning file:" << path;
615
    }
616 617

    return rv;
618 619
}

620
QVariantMap SettingsVpnModel::processOpenVpnProvisioningFile(QFile &provisioningFile)
621 622 623 624 625 626 627 628 629 630 631 632
{
    QVariantMap rv;

    QString embeddedMarker;
    QString embeddedContent;
    QStringList extraOptions;

    const QRegularExpression commentLeader(QStringLiteral("^\\s*(?:\\#|\\;)"));
    const QRegularExpression embeddedLeader(QStringLiteral("^\\s*<([^\\/>]+)>"));
    const QRegularExpression embeddedTrailer(QStringLiteral("^\\s*<\\/([^\\/>]+)>"));
    const QRegularExpression whitespace(QStringLiteral("\\s"));

633 634 635 636 637 638 639 640
    auto normaliseProtocol = [](const QString &proto) {
        if (proto == QStringLiteral("tcp")) {
            // 'tcp' is an undocumented option, which is interpreted by openvpn as 'tcp-client'
            return QStringLiteral("tcp-client");
        }
        return proto;
    };

641 642 643 644 645 646 647 648 649 650
    QTextStream is(&provisioningFile);
    while (!is.atEnd()) {
        QString line(is.readLine());

        QRegularExpressionMatch match;
        if (line.contains(commentLeader)) {
            // Skip
        } else if (line.contains(embeddedLeader, &match)) {
            embeddedMarker = match.captured(1);
            if (embeddedMarker.isEmpty()) {
651
                qWarning() << "Invalid embedded content";
652 653 654 655
            }
        } else if (line.contains(embeddedTrailer, &match)) {
            const QString marker = match.captured(1);
            if (marker != embeddedMarker) {
656
                qWarning() << "Invalid embedded content:" << marker << "!=" << embeddedMarker;
657 658
            } else {
                if (embeddedContent.isEmpty()) {
659
                    qWarning() << "Ignoring empty embedded content:" << embeddedMarker;
660 661 662 663 664 665
                } else {
                    if (embeddedMarker == QStringLiteral("connection")) {
                        // Special case: not embedded content, but a <connection> structure - pass through as an extra option
                        extraOptions.append(QStringLiteral("<connection>\n") + embeddedContent + QStringLiteral("</connection>"));
                    } else {
                        // Embedded content
666 667
                        QDir outputDir(provisioningOutputPath_);
                        if (!outputDir.exists() && !outputDir.mkpath(provisioningOutputPath_)) {
668
                            qWarning() << "Unable to create base directory for VPN provisioning content:" << provisioningOutputPath_;
669 670 671 672 673 674 675 676
                        } else {
                            // Name the file according to content
                            QCryptographicHash hash(QCryptographicHash::Sha1);
                            hash.addData(embeddedContent.toUtf8());

                            const QString outputFileName(QString(hash.result().toHex()) + QChar('.') + embeddedMarker);
                            QFile outputFile(outputDir.absoluteFilePath(outputFileName));
                            if (!outputFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
677
                                qWarning() << "Unable to write VPN provisioning content file:" << outputFile.fileName();
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
                            } else {
                                QTextStream os(&outputFile);
                                os << embeddedContent;

                                // Add the file to the configuration
                                if (embeddedMarker == QStringLiteral("ca")) {
                                    rv.insert(QStringLiteral("OpenVPN.CACert"), outputFile.fileName());
                                } else if (embeddedMarker == QStringLiteral("cert")) {
                                    rv.insert(QStringLiteral("OpenVPN.Cert"), outputFile.fileName());
                                } else if (embeddedMarker == QStringLiteral("key")) {
                                    rv.insert(QStringLiteral("OpenVPN.Key"), outputFile.fileName());
                                } else {
                                    // Assume that the marker corresponds to the openvpn option, (such as 'tls-auth')
                                    extraOptions.append(embeddedMarker + QChar(' ') + outputFile.fileName());
                                }
                            }
                        }
                    }
                }
            }
            embeddedMarker.clear();
            embeddedContent.clear();
        } else if (!embeddedMarker.isEmpty()) {
            embeddedContent.append(line + QStringLiteral("\n"));
        } else {
            QStringList tokens(line.split(whitespace, QString::SkipEmptyParts));
            if (!tokens.isEmpty()) {
                // Find directives that become part of the connman configuration
                const QString& directive(tokens.front());
                const QStringList arguments(tokens.count() > 1 ? tokens.mid(1) : QStringList());

                if (directive == QStringLiteral("remote")) {
                    // Connman supports a single remote host - if we get further instances, pass them through the config file
                    if (!rv.contains(QStringLiteral("Host"))) {
                        if (arguments.count() > 0) {
                            rv.insert(QStringLiteral("Host"), arguments.at(0));
                        }
                        if (arguments.count() > 1) {
                            rv.insert(QStringLiteral("OpenVPN.Port"), arguments.at(1));
                        }
                        if (arguments.count() > 2) {
719
                            rv.insert(QStringLiteral("OpenVPN.Proto"), normaliseProtocol(arguments.at(2)));
720 721 722 723 724 725 726 727 728 729
                        }
                    } else {
                        extraOptions.append(line);
                    }
                } else if (directive == QStringLiteral("ca") ||
                           directive == QStringLiteral("cert") ||
                           directive == QStringLiteral("key") ||
                           directive == QStringLiteral("auth-user-pass")) {
                    if (!arguments.isEmpty()) {
                        // If these file paths are not absolute, assume they are in the same directory as the provisioning file
730
                        QString file(arguments.at(0));
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
                        if (!file.startsWith(QChar('/'))) {
                            const QFileInfo info(provisioningFile.fileName());
                            file = info.dir().absoluteFilePath(file);
                        }
                        if (directive == QStringLiteral("ca")) {
                            rv.insert(QStringLiteral("OpenVPN.CACert"), file);
                        } else if (directive == QStringLiteral("cert")) {
                            rv.insert(QStringLiteral("OpenVPN.Cert"), file);
                        } else if (directive == QStringLiteral("key")) {
                            rv.insert(QStringLiteral("OpenVPN.Key"), file);
                        } else if (directive == QStringLiteral("auth-user-pass")) {
                            rv.insert(QStringLiteral("OpenVPN.AuthUserPass"), file);
                        }
                    } else if (directive == QStringLiteral("auth-user-pass")) {
                        // Preserve this option to mean ask for credentials
                        rv.insert(QStringLiteral("OpenVPN.AuthUserPass"), QStringLiteral("-"));
                    }
                } else if (directive == QStringLiteral("mtu") ||
                           directive == QStringLiteral("tun-mtu")) {
                    // Connman appears to use a long obsolete form of this option...
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.MTU"), arguments.join(QChar(' ')));
                    }
                } else if (directive == QStringLiteral("ns-cert-type")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.NSCertType"), arguments.join(QChar(' ')));
                    }
                } else if (directive == QStringLiteral("proto")) {
                    if (!arguments.isEmpty()) {
                        // All values from a 'remote' directive to take precedence
                        if (!rv.contains(QStringLiteral("OpenVPN.Proto"))) {
762
                            rv.insert(QStringLiteral("OpenVPN.Proto"), normaliseProtocol(arguments.join(QChar(' '))));
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
                        }
                    }
                } else if (directive == QStringLiteral("port")) {
                    // All values from a 'remote' directive to take precedence
                    if (!rv.contains(QStringLiteral("OpenVPN.Port"))) {
                        if (!arguments.isEmpty()) {
                            rv.insert(QStringLiteral("OpenVPN.Port"), arguments.join(QChar(' ')));
                        }
                    }
                } else if (directive == QStringLiteral("askpass")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.AskPass"), arguments.join(QChar(' ')));
                    } else {
                        rv.insert(QStringLiteral("OpenVPN.AskPass"), QString());
                    }
                } else if (directive == QStringLiteral("auth-nocache")) {
                    rv.insert(QStringLiteral("OpenVPN.AuthNoCache"), QStringLiteral("true"));
                } else if (directive == QStringLiteral("tls-remote")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.TLSRemote"), arguments.join(QChar(' ')));
                    }
                } else if (directive == QStringLiteral("cipher")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.Cipher"), arguments.join(QChar(' ')));
                    }
                } else if (directive == QStringLiteral("auth")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.Auth"), arguments.join(QChar(' ')));
                    }
                } else if (directive == QStringLiteral("comp-lzo")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.CompLZO"), arguments.join(QChar(' ')));
                    } else {
                        rv.insert(QStringLiteral("OpenVPN.CompLZO"), QStringLiteral("adaptive"));
                    }
                } else if (directive == QStringLiteral("remote-cert-tls")) {
                    if (!arguments.isEmpty()) {
                        rv.insert(QStringLiteral("OpenVPN.RemoteCertTls"), arguments.join(QChar(' ')));
                    }
                } else {
                    // A directive that connman does not care about - pass through to the config file
                    extraOptions.append(line);
                }
            }
        }
    }

    if (!extraOptions.isEmpty()) {
        // Write a config file to contain the extra options
812 813
        QDir outputDir(provisioningOutputPath_);
        if (!outputDir.exists() && !outputDir.mkpath(provisioningOutputPath_)) {
814
            qWarning() << "Unable to create base directory for VPN provisioning content:" << provisioningOutputPath_;
815 816 817 818 819 820 821 822 823 824
        } else {
            // Name the file according to content
            QCryptographicHash hash(QCryptographicHash::Sha1);
            foreach (const QString &line, extraOptions) {
                hash.addData(line.toUtf8());
            }

            const QString outputFileName(QString(hash.result().toHex()) + QStringLiteral(".conf"));
            QFile outputFile(outputDir.absoluteFilePath(outputFileName));
            if (!outputFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
825
                qWarning() << "Unable to write VPN provisioning configuration file:" << outputFile.fileName();
826 827 828 829 830 831 832 833 834 835 836 837 838
            } else {
                QTextStream os(&outputFile);
                foreach (const QString &line, extraOptions) {
                    os << line << endl;
                }

                rv.insert(QStringLiteral("OpenVPN.ConfigFile"), outputFile.fileName());
            }
        }
    }

    return rv;
}
839 840 841 842 843 844 845 846

void SettingsVpnModel::updateBestState(VpnConnection::ConnectionState maxState)
{
    if (bestState_ != maxState) {
        bestState_ = maxState;
        emit bestStateChanged();
    }
}