diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-16 00:41:57 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-16 00:41:57 +0200 |
| commit | 33c91102a58e2fbcf9d7a66e33b41a65fa3f0e0c (patch) | |
| tree | a724f0c3dcc48c8ce1f78c2665fe8ef170acb379 /src | |
| parent | 5bb3f3f92e62a0af02fe475943759b8c25cd4592 (diff) | |
| download | kristall-33c91102a58e2fbcf9d7a66e33b41a65fa3f0e0c.tar.gz | |
Adds improved client certificate management, adds server certificate management.
Diffstat (limited to 'src')
29 files changed, 867 insertions, 37 deletions
diff --git a/src/about/updates.gemini b/src/about/updates.gemini index 416cfee..ad5ae6c 100644 --- a/src/about/updates.gemini +++ b/src/about/updates.gemini @@ -6,6 +6,8 @@ * Added this changelog to the software itself * Fixed bug: Status bar label now does elide links that are too long instead of resizing the window. * Fixed bug: Gopher end-of-file marker is now better detected. +* Adds support for server certificate handling for gemini:// +* ## 0.2 * Implement Ctrl+D/*Add to favourites* menu item diff --git a/src/browsertab.cpp b/src/browsertab.cpp index d566da6..d3cdea4 100644 --- a/src/browsertab.cpp +++ b/src/browsertab.cpp @@ -52,6 +52,7 @@ BrowserTab::BrowserTab(MainWindow * mainWindow) : connect(&gemini_client, &GeminiClient::transientCertificateRequested, this, &BrowserTab::on_transientCertificateRequested); connect(&gemini_client, &GeminiClient::authorisedCertificateRequested, this, &BrowserTab::on_authorisedCertificateRequested); connect(&gemini_client, &GeminiClient::certificateRejected, this, &BrowserTab::on_certificateRejected); + connect(&gemini_client, &GeminiClient::networkError, this, &BrowserTab::on_networkError); connect(&gopher_client, &GopherClient::requestComplete, this, &BrowserTab::on_requestComplete); connect(&gopher_client, &GopherClient::requestFailed, this, &BrowserTab::on_requestFailed); @@ -268,6 +269,11 @@ void BrowserTab::on_requestFailed(const QString &reason) this->setErrorMessage(QString("Request failed:\n%1").arg(reason)); } +void BrowserTab::on_networkError(const QString &reason) +{ + this->setErrorMessage(QString("Network error:\n%1").arg(reason)); +} + void BrowserTab::on_requestComplete(const QByteArray &data, const QString &mime) { qDebug() << "Loaded" << data.length() << "bytes of type" << mime; diff --git a/src/browsertab.hpp b/src/browsertab.hpp index 04e6c0d..22a07bb 100644 --- a/src/browsertab.hpp +++ b/src/browsertab.hpp @@ -71,6 +71,8 @@ private slots: void on_requestFailed(QString const & reason); + void on_networkError(QString const & reason); + void on_protocolViolation(QString const & reason); void on_inputRequired(QString const & query); diff --git a/src/certificatemanagementdialog.cpp b/src/certificatemanagementdialog.cpp index defa3cc..f1f1453 100644 --- a/src/certificatemanagementdialog.cpp +++ b/src/certificatemanagementdialog.cpp @@ -3,7 +3,10 @@ #include "kristall.hpp" +#include "newidentitiydialog.hpp" + #include <QCryptographicHash> +#include <QMessageBox> CertificateManagementDialog::CertificateManagementDialog(QWidget *parent) : QDialog(parent), @@ -15,7 +18,13 @@ CertificateManagementDialog::CertificateManagementDialog(QWidget *parent) : this->ui->certificates->setModel(&global_identities); this->ui->certificates->expandAll(); - on_certificates_clicked(QModelIndex { }); + connect( + this->ui->certificates->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &CertificateManagementDialog::on_certificates_selected + ); + on_certificates_selected(QModelIndex { }, QModelIndex { }); } CertificateManagementDialog::~CertificateManagementDialog() @@ -23,11 +32,12 @@ CertificateManagementDialog::~CertificateManagementDialog() delete ui; } -void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &index) +void CertificateManagementDialog::on_certificates_selected(QModelIndex const& index, QModelIndex const & previous) { + Q_UNUSED(previous); + selected_identity = global_identities.getMutableIdentity(index); - this->ui->delete_cert_button->setEnabled(selected_identity != nullptr); this->ui->export_cert_button->setEnabled(selected_identity != nullptr); if(selected_identity != nullptr) @@ -43,6 +53,7 @@ void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &ind ); this->ui->cert_notes->setPlainText(cert.user_notes); + this->ui->delete_cert_button->setEnabled(true); } else { @@ -52,6 +63,12 @@ void CertificateManagementDialog::on_certificates_clicked(const QModelIndex &ind this->ui->cert_expiration_date->setDateTime(QDateTime { }); this->ui->cert_livetime->setText(""); this->ui->cert_fingerprint->setPlainText(""); + + if(auto group_name = global_identities.group(index); not group_name.isEmpty()) { + this->ui->delete_cert_button->setEnabled(global_identities.canDeleteGroup(group_name)); + } else { + this->ui->delete_cert_button->setEnabled(false); + } } } @@ -68,3 +85,67 @@ void CertificateManagementDialog::on_cert_display_name_textChanged(const QString this->selected_identity->display_name = this->ui->cert_display_name->text(); } } + +void CertificateManagementDialog::on_delete_cert_button_clicked() +{ + auto index = this->ui->certificates->currentIndex(); + + if(global_identities.getMutableIdentity(index) != nullptr) + { + auto answer = QMessageBox::question( + this, + "Kristall", + "Do you really want to delete this certificate?\r\n\r\nYou will not be able to restore the identity after this!", + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + ); + if(answer != QMessageBox::Yes) + return; + if(not global_identities.destroyIdentity(index)) { + QMessageBox::warning(this, "Kristall", "Could not destroy identity!"); + } + } + else if(auto group_name = global_identities.group(index); not group_name.isEmpty()) { + + auto answer = QMessageBox::question( + this, + "Kristall", + QString("Do you want to delete the group '%1'").arg(group_name) + ); + if(answer != QMessageBox::Yes) + return; + + if(not global_identities.deleteGroup(group_name)) { + QMessageBox::warning(this, "Kristall", "Could not delete group!"); + } + } +} + +void CertificateManagementDialog::on_export_cert_button_clicked() +{ + +} + +void CertificateManagementDialog::on_import_cert_button_clicked() +{ + +} + +void CertificateManagementDialog::on_create_cert_button_clicked() +{ + NewIdentitiyDialog dialog { this }; + + dialog.setGroupName(global_identities.group(this->ui->certificates->currentIndex())); + + if(dialog.exec() != QDialog::Accepted) + return; + + auto id = dialog.createIdentity(); + if(not id.isValid()) + return; + id.is_persistent = true; + + global_identities.addCertificate( + dialog.groupName(), + id); +} diff --git a/src/certificatemanagementdialog.hpp b/src/certificatemanagementdialog.hpp index 7b43053..b66b9cc 100644 --- a/src/certificatemanagementdialog.hpp +++ b/src/certificatemanagementdialog.hpp @@ -18,12 +18,19 @@ public: ~CertificateManagementDialog(); private slots: - void on_certificates_clicked(const QModelIndex &index); - void on_cert_notes_textChanged(); void on_cert_display_name_textChanged(const QString &arg1); + void on_delete_cert_button_clicked(); + + void on_export_cert_button_clicked(); + + void on_import_cert_button_clicked(); + + void on_create_cert_button_clicked(); +private: + void on_certificates_selected(const QModelIndex &index, QModelIndex const & previous); private: Ui::CertificateManagementDialog *ui; diff --git a/src/certificatemanagementdialog.ui b/src/certificatemanagementdialog.ui index 2c3acc6..b4282f9 100644 --- a/src/certificatemanagementdialog.ui +++ b/src/certificatemanagementdialog.ui @@ -103,6 +103,10 @@ <property name="text"> <string>...</string> </property> + <property name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/delete-alert.svg</normaloff>:/icons/delete-alert.svg</iconset> + </property> </widget> </item> </layout> diff --git a/src/geminiclient.cpp b/src/geminiclient.cpp index e3036ce..bf29ed5 100644 --- a/src/geminiclient.cpp +++ b/src/geminiclient.cpp @@ -2,6 +2,7 @@ #include <cassert> #include <QDebug> #include <QSslConfiguration> +#include "kristall.hpp" GeminiClient::GeminiClient(QObject *parent) : QObject(parent) { @@ -11,13 +12,10 @@ GeminiClient::GeminiClient(QObject *parent) : QObject(parent) connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::error), this, &GeminiClient::socketError); - QSslConfiguration ssl_config; ssl_config.setProtocol(QSsl::TlsV1_2); - // ssl_config.setLocalCertificate(QSslCertificate::1 - + ssl_config.setCaCertificates(QList<QSslCertificate> { }); socket.setSslConfiguration(ssl_config); - } GeminiClient::~GeminiClient() @@ -76,6 +74,8 @@ void GeminiClient::disableClientCertificate() void GeminiClient::socketEncrypted() { + qDebug() << "Pub key =" << socket.peerCertificate().publicKey().toPem(); + QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n"; QByteArray request_bytes = request.toUtf8(); @@ -255,13 +255,59 @@ void GeminiClient::socketDisconnected() } } -void GeminiClient::sslErrors(const QList<QSslError> &errors) +static bool isTrustRelated(QSslError::SslError err) +{ + switch(err) + { + case QSslError::CertificateUntrusted: return true; + case QSslError::SelfSignedCertificate: return true; + case QSslError::UnableToGetLocalIssuerCertificate: return true; + default: return false; + } +} + +void GeminiClient::sslErrors(QList<QSslError> const & errors) { - for(auto const & error : errors) { - qWarning() << error.errorString() ; + QList<QSslError> remaining_errors = errors; + QList<QSslError> ignored_errors; + + int i = 0; + while(i < remaining_errors.size()) + { + auto const & err = remaining_errors.at(i); + + bool ignore = false; + if(isTrustRelated(err.error())) + { + if(global_trust.isTrusted(target_url, socket.peerCertificate())) + { + ignore = true; + } + } + else if(err.error() == QSslError::UnableToVerifyFirstCertificate) + { + ignore = true; + } + + if(ignore) { + ignored_errors.append(err); + remaining_errors.removeAt(0); + } else { + i += 1; + } + } + + socket.ignoreSslErrors(ignored_errors); + + qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size(); + + for(auto const & error : remaining_errors) { + qWarning() << int(error.error()) << error.errorString(); } - socket.ignoreSslErrors(errors); + if(remaining_errors.size() > 0) { + emit this->networkError(remaining_errors.first().errorString()); + } } void GeminiClient::socketError(QAbstractSocket::SocketError socketError) @@ -272,6 +318,7 @@ void GeminiClient::socketError(QAbstractSocket::SocketError socketError) if(socketError == QAbstractSocket::RemoteHostClosedError) { socket.close(); } else { - qWarning() << socketError << socket.errorString(); + // qWarning() << socketError << socket.errorString(); + emit this->networkError(socket.errorString()); } } diff --git a/src/geminiclient.hpp b/src/geminiclient.hpp index 67e767b..deac50b 100644 --- a/src/geminiclient.hpp +++ b/src/geminiclient.hpp @@ -70,6 +70,8 @@ signals: void certificateRejected(CertificateRejection reason, QString const & info); + void networkError(QString const & reason); + private slots: void socketEncrypted(); diff --git a/src/icons.qrc b/src/icons.qrc index 7987c86..2bd76e6 100644 --- a/src/icons.qrc +++ b/src/icons.qrc @@ -38,5 +38,6 @@ <file>icons/shield-outline.svg</file> <file>icons/shield-lock.svg</file> <file>icons/certificate.svg</file> + <file>icons/delete-alert.svg</file> </qresource> </RCC> diff --git a/src/icons/delete-alert.svg b/src/icons/delete-alert.svg new file mode 100644 index 0000000..3773f1c --- /dev/null +++ b/src/icons/delete-alert.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17 4V6H3V4H6.5L7.5 3H12.5L13.5 4H17M4 19V7H16V19C16 20.1 15.1 21 14 21H6C4.9 21 4 20.1 4 19M19 16H21V18H19V16M19 9H21V14H19V9Z" /></svg>
\ No newline at end of file diff --git a/src/identitycollection.cpp b/src/identitycollection.cpp index 989dc9f..1cb55e8 100644 --- a/src/identitycollection.cpp +++ b/src/identitycollection.cpp @@ -7,6 +7,7 @@ IdentityCollection::IdentityCollection(QObject *parent) : QAbstractItemModel(parent) { + } void IdentityCollection::load(QSettings &settings) @@ -95,28 +96,24 @@ void IdentityCollection::save(QSettings &settings) const settings.endArray(); } +bool IdentityCollection::addGroup(const QString &group_name) +{ + GroupNode * group; + return internalAddGroup(group_name, group); +} + bool IdentityCollection::addCertificate(const QString &group_name, const CryptoIdentity &crypto_id) { // Don't allow saving transient certificates if(not crypto_id.is_persistent) return false; - this->beginResetModel(); + GroupNode * group; + internalAddGroup(group_name, group); - GroupNode * group = nullptr; - for(auto const & grp : root.children) - { - auto * g = static_cast<GroupNode*>(grp.get()); - if(g->title == group_name) { - group = g; - break; - } - } - if(group == nullptr) { - group = new GroupNode(); - group->title = group_name; - this->root.children.emplace_back(group); - } + QModelIndex parent_index = createIndex(group->index, 0, group); + + beginInsertRows(parent_index, group->children.size(), group->children.size() + 1); auto id = std::make_unique<IdentityNode>(); id->identity = crypto_id; @@ -124,7 +121,7 @@ bool IdentityCollection::addCertificate(const QString &group_name, const CryptoI this->relayout(); - this->endResetModel(); + this->endInsertRows(); return true; } @@ -171,6 +168,77 @@ QStringList IdentityCollection::groups() const return result; } +QString IdentityCollection::group(const QModelIndex &index) const +{ + if (!index.isValid()) + return QString { }; + + Node const *item = static_cast<Node const*>(index.internalPointer()); + + switch(item->type) { + case Node::Root: return QString { }; + case Node::Group: return static_cast<GroupNode const *>(item)->title; + case Node::Identity: return static_cast<IdentityNode const *>(item)->parent->as<GroupNode>().title; + default: return QString { }; + } +} + +bool IdentityCollection::destroyIdentity(const QModelIndex &index) +{ + if (!index.isValid()) + return false; + + Node * childItem = static_cast<Node *>(index.internalPointer()); + Node * parent = childItem->parent; + + if (parent == &root) + return false; + + beginRemoveRows(this->parent(index), index.row(), index.row() + 1); + + parent->children.erase(parent->children.begin() + childItem->index); + + endRemoveRows(); + + return true; +} + +bool IdentityCollection::canDeleteGroup(const QString &group_name) +{ + for(auto const & group_node : root.children) + { + auto & group = group_node->as<GroupNode>(); + if((group.children.size() == 0) and (group.title == group_name)) + return true; + + } + return false; +} + +bool IdentityCollection::deleteGroup(const QString &group_name) +{ + size_t index = 0; + for(auto it = root.children.begin(); it != root.children.end(); it++, index++) + { + auto & group = it->get()->as<GroupNode>(); + if(group.title == group_name) { + if(group.children.size() > 0) { + qDebug() << "cannot delete non-empty group" << group_name; + return false; + } + + beginRemoveRows(QModelIndex { }, index, index + 1); + + root.children.erase(it); + + endRemoveRows(); + + return true; + } + } + return false; +} + QModelIndex IdentityCollection::index(int row, int column, const QModelIndex &parent) const { if (not hasIndex(row, column, parent)) @@ -265,7 +333,7 @@ void IdentityCollection::relayout() group.parent = &root; group.index = i; - qDebug() << "group[" << group.index << "]" << group.as<GroupNode>().title; + // qDebug() << "group[" << group.index << "]" << group.as<GroupNode>().title; for(size_t j = 0; j < group.children.size(); j++) { @@ -274,7 +342,33 @@ void IdentityCollection::relayout() id.index = j; assert(id.children.size() == 0); - qDebug() << "id[" << id.index << "]" << id.as<IdentityNode>().identity.display_name; + // qDebug() << "id[" << id.index << "]" << id.as<IdentityNode>().identity.display_name; } } } + +bool IdentityCollection::internalAddGroup(const QString &group_name, GroupNode * & group) +{ + for(auto const & grp : root.children) + { + auto * g = static_cast<GroupNode*>(grp.get()); + if(g->title == group_name) { + group = g; + return false; + } + } + + auto parent = QModelIndex { }; + + beginInsertRows(parent, this->root.children.size(), this->root.children.size() + 1); + + group = new GroupNode(); + group->title = group_name; + this->root.children.emplace_back(group); + + this->relayout(); + + endInsertRows(); + + return true; +} diff --git a/src/identitycollection.hpp b/src/identitycollection.hpp index 3290688..eecf3a4 100644 --- a/src/identitycollection.hpp +++ b/src/identitycollection.hpp @@ -48,6 +48,8 @@ public: void save(QSettings & settings) const; + bool addGroup(QString const & group); + bool addCertificate(QString const & group, CryptoIdentity const & id); CryptoIdentity getIdentity(QModelIndex const & index) const; @@ -56,6 +58,14 @@ public: QStringList groups() const; + //! Returns the group name of the index. + QString group(QModelIndex const & index) const; + + bool destroyIdentity(QModelIndex const & index); + + bool canDeleteGroup(QString const & group_name); + bool deleteGroup(QString const & group_name); + public: // Header: // QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; @@ -73,6 +83,8 @@ public: private: void relayout(); + bool internalAddGroup(QString const & group_name, GroupNode * & out_group); + private: RootNode root; }; diff --git a/src/kristall.hpp b/src/kristall.hpp index a0a4b49..00faff5 100644 --- a/src/kristall.hpp +++ b/src/kristall.hpp @@ -5,9 +5,11 @@ #include <QClipboard> #include "identitycollection.hpp" +#include "ssltrust.hpp" extern QSettings global_settings; extern IdentityCollection global_identities; extern QClipboard * global_clipboard; +extern SslTrust global_trust; #endif // KRISTALL_HPP diff --git a/src/kristall.pro b/src/kristall.pro index 413bc6b..e46b735 100644 --- a/src/kristall.pro +++ b/src/kristall.pro @@ -1,4 +1,4 @@ -QT += core gui +QT += core gui svg greaterThan(QT_MAJOR_VERSION, 4): QT += widgets network multimedia multimediawidgets @@ -30,6 +30,13 @@ win32-msvc { INCLUDEPATH += "C:\Program Files\OpenSSL\include" } +android: include(/home/felix/projects/android-hass/android-sdk/android_openssl/openssl.pri) + +# android { +# INCLUDEPATH += /home/felix/projects/android-hass/android-sdk/android_openssl/static/include +# LIBS += -L /home/felix/projects/android-hass/android-sdk/android_openssl/static/lib/arm/ +# } + INCLUDEPATH += $$PWD/../lib/luis-l-gist/ DEPENDPATH += $$PWD/../lib/luis-l-gist/ @@ -58,7 +65,10 @@ SOURCES += \ plaintextrenderer.cpp \ protocolsetup.cpp \ settingsdialog.cpp \ + ssltrust.cpp \ tabbrowsinghistory.cpp \ + trustedhost.cpp \ + trustedhostcollection.cpp \ webclient.cpp HEADERS += \ @@ -86,7 +96,10 @@ HEADERS += \ plaintextrenderer.hpp \ protocolsetup.hpp \ settingsdialog.hpp \ + ssltrust.hpp \ tabbrowsinghistory.hpp \ + trustedhost.hpp \ + trustedhostcollection.hpp \ webclient.hpp FORMS += \ diff --git a/src/main.cpp b/src/main.cpp index c280425..81ace41 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ IdentityCollection global_identities; QSettings global_settings { "xqTechnologies", "Kristall" }; QClipboard * global_clipboard; +SslTrust global_trust; int main(int argc, char *argv[]) { @@ -28,6 +29,10 @@ int main(int argc, char *argv[]) global_identities.load(global_settings); global_settings.endGroup(); + global_settings.beginGroup("Trusted Servers"); + global_trust.load(global_settings); + global_settings.endGroup(); + MainWindow w(&app); auto urls = cli_parser.positionalArguments(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1f37d5d..e7dcfeb 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -149,6 +149,10 @@ void MainWindow::saveSettings() global_identities.save(global_settings); global_settings.endGroup(); + global_settings.beginGroup("Trusted Servers"); + global_trust.save(global_settings); + global_settings.endGroup(); + global_settings.beginGroup("Theme"); this->current_style.save(global_settings); global_settings.endGroup(); @@ -245,6 +249,7 @@ void MainWindow::on_actionSettings_triggered() dialog.setStartPage(global_settings.value("start_page").toString()); dialog.setProtocols(this->protocols); dialog.setUiTheme(global_settings.value("theme").toString()); + dialog.setSslTrust(global_trust); if(dialog.exec() != QDialog::Accepted) return; @@ -253,6 +258,7 @@ void MainWindow::on_actionSettings_triggered() global_settings.setValue("start_page", url.toString()); } + global_trust = dialog.sslTrust(); global_settings.setValue("theme", dialog.uiTheme()); this->protocols = dialog.protocols(); diff --git a/src/newidentitiydialog.cpp b/src/newidentitiydialog.cpp index 5419d74..af2a97e 100644 --- a/src/newidentitiydialog.cpp +++ b/src/newidentitiydialog.cpp @@ -45,6 +45,11 @@ QString NewIdentitiyDialog::groupName() const return this->ui->group->currentText(); } +void NewIdentitiyDialog::setGroupName(const QString &name) +{ + this->ui->group->setCurrentText(name); +} + void NewIdentitiyDialog::updateUI() { bool is_ok = true; @@ -59,16 +64,18 @@ void NewIdentitiyDialog::updateUI() void NewIdentitiyDialog::on_group_editTextChanged(const QString &arg1) { - qDebug() << arg1; + Q_UNUSED(arg1); this->updateUI(); } void NewIdentitiyDialog::on_display_name_textChanged(const QString &arg1) { + Q_UNUSED(arg1); this->updateUI(); } void NewIdentitiyDialog::on_common_name_textChanged(const QString &arg1) { + Q_UNUSED(arg1); this->updateUI(); } diff --git a/src/newidentitiydialog.hpp b/src/newidentitiydialog.hpp index bc8f90e..83d740b 100644 --- a/src/newidentitiydialog.hpp +++ b/src/newidentitiydialog.hpp @@ -22,6 +22,7 @@ public: CryptoIdentity createIdentity() const; QString groupName() const; + void setGroupName(QString const & name); private slots: void on_group_editTextChanged(const QString &arg1); diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 3581f03..7ef05fb 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -7,6 +7,7 @@ #include <QInputDialog> #include <QFileDialog> #include <QMessageBox> +#include <QDebug> #include "kristall.hpp" @@ -85,6 +86,22 @@ SettingsDialog::SettingsDialog(QWidget *parent) : this->on_presets_currentIndexChanged(-1); } + this->ui->trust_level->clear(); + this->ui->trust_level->addItem("Trust on first encounter", QVariant::fromValue<int>(SslTrust::TrustOnFirstUse)); + this->ui->trust_level->addItem("Trust everything", QVariant::fromValue<int>(SslTrust::TrustEverything)); + this->ui->trust_level->addItem("Manually verify fingerprints", QVariant::fromValue<int>(SslTrust::TrustNoOne)); + + this->ui->trusted_hosts->setModel(&this->current_trust.trusted_hosts); + + this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + this->ui->trusted_hosts->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + + connect( + this->ui->trusted_hosts->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &SettingsDialog::on_trusted_server_selection); } SettingsDialog::~SettingsDialog() @@ -201,6 +218,27 @@ void SettingsDialog::setUiTheme(const QString &theme) } +SslTrust SettingsDialog::sslTrust() const +{ + return this->current_trust; +} + +void SettingsDialog::setSslTrust(const SslTrust &trust) +{ + this->current_trust = trust; + + this->ui->trust_level->setCurrentIndex( + this->ui->trust_level->findData(QVariant::fromValue<int>(trust.trust_level)) + ); + + if(trust.enable_ca) + this->ui->trust_enable_ca->setChecked(true); + else + this->ui->trust_disable__ca->setChecked(true); + + this->ui->trusted_hosts->resizeColumnsToContents(); +} + void SettingsDialog::reloadStylePreview() { auto const document = R"gemini(# H1 Header @@ -295,6 +333,15 @@ void SettingsDialog::updateColor(QColor &input) } } +void SettingsDialog::on_trusted_server_selection(const QModelIndex ¤t, const QModelIndex &previous) +{ + if(auto host = this->current_trust.trusted_hosts.get(current); host) { + this->ui->trust_revoke_selected->setEnabled(true); + } else { + this->ui->trust_revoke_selected->setEnabled(false); + } +} + void SettingsDialog::on_std_change_color_clicked() { updateColor(current_style.standard_color); @@ -531,3 +578,23 @@ void SettingsDialog::on_preset_export_clicked() this->predefined_styles.value(name).save(export_settings); export_settings.sync(); } + +void SettingsDialog::on_trust_enable_ca_clicked() +{ + this->current_trust.enable_ca = true; +} + +void SettingsDialog::on_trust_disable__ca_clicked() +{ + this->current_trust.enable_ca = false; +} + +void SettingsDialog::on_trust_level_currentIndexChanged(int index) +{ + this->current_trust.trust_level = SslTrust::TrustLevel(this->ui->trust_level->itemData(index).toInt()); +} + +void SettingsDialog::on_trust_revoke_selected_clicked() +{ + this->current_trust.trusted_hosts.remove(this->ui->trusted_hosts->currentIndex()); +} diff --git a/src/settingsdialog.hpp b/src/settingsdialog.hpp index 475d5b0..0b96a55 100644 --- a/src/settingsdialog.hpp +++ b/src/settingsdialog.hpp @@ -6,6 +6,7 @@ #include "geminirenderer.hpp" #include "protocolsetup.hpp" #include "documentstyle.hpp" +#include "ssltrust.hpp" namespace Ui { class SettingsDialog; @@ -34,6 +35,9 @@ public: QString uiTheme() const; void setUiTheme(QString const & theme); + SslTrust sslTrust() const; + void setSslTrust(SslTrust const & trust); + private slots: void on_std_change_font_clicked(); @@ -89,6 +93,14 @@ private slots: void on_preset_export_clicked(); + void on_trust_enable_ca_clicked(); + + void on_trust_disable__ca_clicked(); + + void on_trust_level_currentIndexChanged(int index); + + void on_trust_revoke_selected_clicked(); + private: void reloadStylePreview(); @@ -96,6 +108,8 @@ private: void updateColor(QColor & input); + void on_trusted_server_selection(QModelIndex const & current, QModelIndex const & previous); + private: Ui::SettingsDialog *ui; @@ -103,6 +117,8 @@ private: std::unique_ptr<QTextDocument> preview_document; QMap<QString, DocumentStyle> predefined_styles; + + SslTrust current_trust; }; #endif // SETTINGSDIALOG_HPP diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 67767bf..5671001 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -24,6 +24,10 @@ <number>0</number> </property> <widget class="QWidget" name="generic"> + <attribute name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/settings.svg</normaloff>:/icons/settings.svg</iconset> + </attribute> <attribute name="title"> <string>Generic</string> </attribute> @@ -240,6 +244,10 @@ </layout> </widget> <widget class="QWidget" name="style_tab"> + <attribute name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/palette.svg</normaloff>:/icons/palette.svg</iconset> + </attribute> <attribute name="title"> <string>Style</string> </attribute> @@ -794,6 +802,103 @@ </item> </layout> </widget> + <widget class="QWidget" name="gem_trust_page"> + <attribute name="icon"> + <iconset resource="icons.qrc"> + <normaloff>:/icons/certificate.svg</normaloff>:/icons/certificate.svg</iconset> + </attribute> + <attribute name="title"> + <string>Gemini TLS</string> + </attribute> + <layout class="QFormLayout" name="formLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_23"> + <property name="text"> + <string>Trust Level</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="trust_level"/> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_24"> + <property name="text"> + <string>Certificate Authorities</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QRadioButton" name="trust_enable_ca"> + <property name="text"> + <string>Use local certificate authorities</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_2</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="trust_disable__ca"> + <property name="text"> + <string>Don't use local certificate authorities</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">buttonGroup_2</string> + </attribute> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_25"> + <property name="text"> + <string>Trusted Hosts</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTableView" name="trusted_hosts"> + <property name="cornerButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_9"> + <item> + <widget class="QToolButton" name="trust_revoke_selected"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Revoke trust</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> </widget> </item> <item> @@ -846,9 +951,10 @@ </connection> </connections> <buttongroups> - <buttongroup name="buttonGroup"/> - <buttongroup name="gophermapBtnGroup"/> <buttongroup name="textHighlightsBtnGroup"/> + <buttongroup name="gophermapBtnGroup"/> <buttongroup name="textRenderingBtnGroup"/> + <buttongroup name="buttonGroup_2"/> + <buttongroup name="buttonGroup"/> </buttongroups> </ui> diff --git a/src/ssltrust.cpp b/src/ssltrust.cpp new file mode 100644 index 0000000..92d913c --- /dev/null +++ b/src/ssltrust.cpp @@ -0,0 +1,76 @@ +#include "ssltrust.hpp" + +#include <QDebug> + +void SslTrust::load(QSettings &settings) +{ + trust_level = TrustLevel(settings.value("trust_level", int(TrustOnFirstUse)).toInt()); + enable_ca = settings.value("enable_ca", QVariant::fromValue(false)).toBool(); + + trusted_hosts.clear(); + + int size = settings.beginReadArray("trusted_hosts"); + for(int i = 0; i < size; i++) + { + settings.setArrayIndex(i); + + auto key_type = QSsl::KeyAlgorithm(settings.value("key_type").toInt()); + auto key_value = settings.value("key_bits").toByteArray(); + + TrustedHost host; + host.host_name = settings.value("host_name").toString(); + host.trusted_at = settings.value("trusted_at").toDateTime(); + host.public_key = QSslKey(key_value, key_type, QSsl::Der, QSsl::PublicKey); + + trusted_hosts.insert(host); + } + settings.endArray(); +} + +void SslTrust::save(QSettings &settings) const +{ + settings.setValue("trust_level", int(trust_level)); + settings.setValue("enable_ca", enable_ca); + + auto all = trusted_hosts.getAll(); + settings.beginWriteArray("trusted_hosts", all.size()); + for(int i = 0; i < all.size(); i++) + { + settings.setArrayIndex(i); + + settings.setValue("host_name", all.at(i).host_name); + settings.setValue("trusted_at", all.at(i).trusted_at); + settings.setValue("key_type", int(all.at(i).public_key.algorithm())); + settings.setValue("key_bits", all.at(i).public_key.toDer()); + } + settings.endArray(); +} + +bool SslTrust::isTrusted(QUrl const & url, const QSslCertificate &certificate) +{ + if(trust_level == TrustEverything) + return true; + + if(auto host_or_none = trusted_hosts.get(url.host()); host_or_none) + { + if(host_or_none->public_key == certificate.publicKey()) + return true; + qDebug() << "certificate mismatch for" << url; + return false; + } + else + { + if(trust_level == TrustOnFirstUse) + { + TrustedHost host; + host.host_name = url.host(); + host.trusted_at = QDateTime::currentDateTime(); + host.public_key = certificate.publicKey(); + + bool ok = trusted_hosts.insert(host); + assert(ok); + return true; + } + return false; + } +} diff --git a/src/ssltrust.hpp b/src/ssltrust.hpp new file mode 100644 index 0000000..62d4985 --- /dev/null +++ b/src/ssltrust.hpp @@ -0,0 +1,37 @@ +#ifndef SSLTRUST_HPP +#define SSLTRUST_HPP + +#include <QSslCertificate> +#include <QSslKey> +#include <QSettings> + +#include "trustedhostcollection.hpp" + +struct SslTrust +{ + enum TrustLevel { + TrustOnFirstUse = 0, // default + TrustEverything = 1, // not recommended + TrustNoOne = 2, // approve every fingerprint by hand + }; + + SslTrust() = default; + SslTrust(SslTrust const &) = default; + SslTrust(SslTrust &&) = default; + + SslTrust & operator=(SslTrust const &) = default; + SslTrust & operator=(SslTrust &&) = default; + + TrustLevel trust_level = TrustOnFirstUse; + + TrustedHostCollection trusted_hosts; + + bool enable_ca = false; + + void load(QSettings & settings); + void save(QSettings & settings) const; + + bool isTrusted(QUrl const & url, QSslCertificate const & certificate); +}; + +#endif // SSLTRUST_HPP diff --git a/src/trustedhost.cpp b/src/trustedhost.cpp new file mode 100644 index 0000000..94a17b9 --- /dev/null +++ b/src/trustedhost.cpp @@ -0,0 +1,2 @@ +#include "trustedhost.hpp" + diff --git a/src/trustedhost.hpp b/src/trustedhost.hpp new file mode 100644 index 0000000..6cba5ab --- /dev/null +++ b/src/trustedhost.hpp @@ -0,0 +1,15 @@ +#ifndef TRUSTEDHOST_HPP +#define TRUSTEDHOST_HPP + +#include <QSslKey> +#include <QUrl> +#include <QDateTime> + +struct TrustedHost +{ + QString host_name; + QSslKey public_key; + QDateTime trusted_at; +}; + +#endif // TRUSTEDHOST_HPP diff --git a/src/trustedhostcollection.cpp b/src/trustedhostcollection.cpp new file mode 100644 index 0000000..30a6eff --- /dev/null +++ b/src/trustedhostcollection.cpp @@ -0,0 +1,151 @@ +#include "trustedhostcollection.hpp" + +TrustedHostCollection::TrustedHostCollection(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +TrustedHostCollection::TrustedHostCollection(const TrustedHostCollection & other) : + items(other.items) +{ + assert(other.parent() == nullptr); + +} + +TrustedHostCollection::TrustedHostCollection(TrustedHostCollection &&other) : + items(std::move(other.items)) +{ + assert(other.parent() == nullptr); +} + +QVariant TrustedHostCollection::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Vertical) + return QVariant { }; + if(role == Qt::DisplayRole) + { + switch(section) + { + case 0: return "Host Name"; + case 1: return "First Seen"; + case 2: return "Key Type"; + } + } + return QVariant { }; +} + +int TrustedHostCollection::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return items.size(); +} + +int TrustedHostCollection::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return 3; +} + +QVariant TrustedHostCollection::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant { }; + if(index.row() < 0 or index.row() >= items.size()) + return QVariant { }; + + auto const & host = items.at(index.row()); + + if(role == Qt::DisplayRole) + { + switch(index.column()) + { + case 0: return host.host_name; + case 1: return host.trusted_at.toString(); + case 2: switch(host.public_key.algorithm()) + { + case QSsl::Rsa: return "RSA"; + case QSsl::Ec: return "EC"; + case QSsl::Dh: return "DH"; + case QSsl::Dsa: return "DSA"; + case QSsl::Opaque: return "Opaque"; + default: return "Unforseen"; + } + } + } + return QVariant { }; +} + +TrustedHostCollection &TrustedHostCollection::operator=(const TrustedHostCollection & other) +{ + beginResetModel(); + this->items = other.items; + endResetModel(); + return *this; +} + +TrustedHostCollection &TrustedHostCollection::operator=(TrustedHostCollection && other) +{ + beginResetModel(); + this->items = std::move(other.items); + endResetModel(); + return *this; +} + +void TrustedHostCollection::clear() +{ + beginResetModel(); + this->items.clear(); + endResetModel(); +} + +bool TrustedHostCollection::insert(const TrustedHost &host) +{ + for(auto const & item : items) + { + if(item.host_name == host.host_name) + return false; + } + + beginInsertRows(QModelIndex { }, items.size(), items.size() + 1); + items.append(host); + endInsertRows(); + + return true; +} + +std::optional<TrustedHost> TrustedHostCollection::get(QString const & host_name) const +{ + for(auto const & item : items) + { + if(item.host_name == host_name) + return item; + } + return std::nullopt; +} + +std::optional<TrustedHost> TrustedHostCollection::get(const QModelIndex &index) const +{ + if(not index.isValid()) + return std::nullopt; + if(index.row() < 0 or index.row() >= items.size()) + return std::nullopt; + return items.at(index.row()); +} + +void TrustedHostCollection::remove(const QModelIndex &index) +{ + if(not index.isValid()) + return; + if(index.row() < 0 or index.row() >= items.size()) + return; + beginRemoveRows(QModelIndex{}, index.row(), index.row() + 1); + items.removeAt(index.row()); + endRemoveRows(); +} + +QVector<TrustedHost> TrustedHostCollection::getAll() const +{ + return items; +} diff --git a/src/trustedhostcollection.hpp b/src/trustedhostcollection.hpp new file mode 100644 index 0000000..6974a10 --- /dev/null +++ b/src/trustedhostcollection.hpp @@ -0,0 +1,48 @@ +#ifndef TRUSTEDHOSTCOLLECTION_HPP +#define TRUSTEDHOSTCOLLECTION_HPP + +#include <QAbstractTableModel> + +#include "trustedhost.hpp" +#include <optional> + +class TrustedHostCollection : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit TrustedHostCollection(QObject *parent = nullptr); + + TrustedHostCollection(TrustedHostCollection const &); + TrustedHostCollection(TrustedHostCollection &&); + + // Header: + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + // Basic functionality: + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +public: + TrustedHostCollection & operator=(TrustedHostCollection const &); + TrustedHostCollection & operator=(TrustedHostCollection &&); + + void clear(); + + bool insert(TrustedHost const & host); + + std::optional<TrustedHost> get(QString const & host_name) const; + + std::optional<TrustedHost> get(QModelIndex const & index) const; + + void remove(QModelIndex const & index); + + QVector<TrustedHost> getAll() const; + +private: + QVector<TrustedHost> items; +}; + +#endif // TRUSTEDHOSTCOLLECTION_HPP diff --git a/src/webclient.cpp b/src/webclient.cpp index d8e1da5..d73842b 100644 --- a/src/webclient.cpp +++ b/src/webclient.cpp @@ -35,6 +35,8 @@ bool WebClient::startRequest(const QUrl &url) connect(this->current_reply, &QNetworkReply::readyRead, this, &WebClient::on_data); connect(this->current_reply, &QNetworkReply::finished, this, &WebClient::on_finished); + connect(this->current_reply, &QNetworkReply::errorOccurred, this, &WebClient::on_networkError); + connect(this->current_reply, &QNetworkReply::sslErrors, this, &WebClient::on_sslErrors); return true; } @@ -71,7 +73,7 @@ void WebClient::on_finished() { auto mime = this->current_reply->header(QNetworkRequest::ContentTypeHeader).toString(); - qDebug() << this->current_reply->url() << mime; + // qDebug() << this->current_reply->url() << mime; emit this->requestComplete(this->body, mime); @@ -80,3 +82,15 @@ void WebClient::on_finished() this->current_reply->deleteLater(); this->current_reply = nullptr; } + +void WebClient::on_networkError(QNetworkReply::NetworkError code) +{ + qDebug() << code << this->current_reply->errorString(); +} + +void WebClient::on_sslErrors(const QList<QSslError> &errors) +{ + for(auto const & err : errors) + qDebug() << err; + this->current_reply->ignoreSslErrors(); +} diff --git a/src/webclient.hpp b/src/webclient.hpp index 8cbf3ab..4b0b083 100644 --- a/src/webclient.hpp +++ b/src/webclient.hpp @@ -3,6 +3,7 @@ #include <QObject> #include <QNetworkAccessManager> +#include <QNetworkReply> class WebClient: public QObject { @@ -26,9 +27,13 @@ signals: void requestFailed(QString const & message); + void networkError(QString const & message); + private slots: void on_data(); void on_finished(); + void on_networkError(QNetworkReply::NetworkError code); + void on_sslErrors(const QList<QSslError> &errors); private: QNetworkAccessManager manager; |
