aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFelix (xq) Queißner <git@mq32.de>2020-06-16 00:41:57 +0200
committerFelix (xq) Queißner <git@mq32.de>2020-06-16 00:41:57 +0200
commit33c91102a58e2fbcf9d7a66e33b41a65fa3f0e0c (patch)
treea724f0c3dcc48c8ce1f78c2665fe8ef170acb379 /src
parent5bb3f3f92e62a0af02fe475943759b8c25cd4592 (diff)
downloadkristall-33c91102a58e2fbcf9d7a66e33b41a65fa3f0e0c.tar.gz
Adds improved client certificate management, adds server certificate management.
Diffstat (limited to 'src')
-rw-r--r--src/about/updates.gemini2
-rw-r--r--src/browsertab.cpp6
-rw-r--r--src/browsertab.hpp2
-rw-r--r--src/certificatemanagementdialog.cpp87
-rw-r--r--src/certificatemanagementdialog.hpp11
-rw-r--r--src/certificatemanagementdialog.ui4
-rw-r--r--src/geminiclient.cpp65
-rw-r--r--src/geminiclient.hpp2
-rw-r--r--src/icons.qrc1
-rw-r--r--src/icons/delete-alert.svg1
-rw-r--r--src/identitycollection.cpp130
-rw-r--r--src/identitycollection.hpp12
-rw-r--r--src/kristall.hpp2
-rw-r--r--src/kristall.pro15
-rw-r--r--src/main.cpp5
-rw-r--r--src/mainwindow.cpp6
-rw-r--r--src/newidentitiydialog.cpp9
-rw-r--r--src/newidentitiydialog.hpp1
-rw-r--r--src/settingsdialog.cpp67
-rw-r--r--src/settingsdialog.hpp16
-rw-r--r--src/settingsdialog.ui110
-rw-r--r--src/ssltrust.cpp76
-rw-r--r--src/ssltrust.hpp37
-rw-r--r--src/trustedhost.cpp2
-rw-r--r--src/trustedhost.hpp15
-rw-r--r--src/trustedhostcollection.cpp151
-rw-r--r--src/trustedhostcollection.hpp48
-rw-r--r--src/webclient.cpp16
-rw-r--r--src/webclient.hpp5
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 &current, 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;