aboutsummaryrefslogtreecommitdiff
path: root/src/protocols
diff options
context:
space:
mode:
authorFelix (xq) Queißner <git@mq32.de>2020-06-22 21:10:04 +0200
committerFelix (xq) Queißner <git@mq32.de>2020-06-22 21:10:04 +0200
commit75ec461eeaa851cb5c53f4cfffc434e3e529ed1d (patch)
tree3944737340718ca3675381aa06636045d397e780 /src/protocols
parent8dbfb0890560fd1cd698d06fa05ac868c4db8576 (diff)
Restructures the project source and cleans up a bit
Diffstat (limited to 'src/protocols')
-rw-r--r--src/protocols/abouthandler.cpp61
-rw-r--r--src/protocols/abouthandler.hpp23
-rw-r--r--src/protocols/filehandler.cpp45
-rw-r--r--src/protocols/filehandler.hpp23
-rw-r--r--src/protocols/fingerclient.cpp83
-rw-r--r--src/protocols/fingerclient.hpp39
-rw-r--r--src/protocols/geminiclient.cpp375
-rw-r--r--src/protocols/geminiclient.hpp55
-rw-r--r--src/protocols/gopherclient.cpp109
-rw-r--r--src/protocols/gopherclient.hpp42
-rw-r--r--src/protocols/webclient.cpp229
-rw-r--r--src/protocols/webclient.hpp48
12 files changed, 1132 insertions, 0 deletions
diff --git a/src/protocols/abouthandler.cpp b/src/protocols/abouthandler.cpp
new file mode 100644
index 0000000..01eefff
--- /dev/null
+++ b/src/protocols/abouthandler.cpp
@@ -0,0 +1,61 @@
+#include "abouthandler.hpp"
+#include "kristall.hpp"
+
+#include <QUrl>
+#include <QFile>
+
+AboutHandler::AboutHandler()
+{
+
+}
+
+bool AboutHandler::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "about");
+}
+
+bool AboutHandler::startRequest(const QUrl &url, ProtocolHandler::RequestOptions options)
+{
+ Q_UNUSED(options)
+ if (url.path() == "blank")
+ {
+ emit this->requestComplete("", "text/gemini");
+ }
+ else if (url.path() == "favourites")
+ {
+ QByteArray document;
+
+ document.append("# Favourites\n");
+ document.append("\n");
+
+ for (auto const &fav : global_favourites.getAll())
+ {
+ document.append("=> " + fav.toString().toUtf8() + "\n");
+ }
+
+ this->requestComplete(document, "text/gemini");
+ }
+ else
+ {
+ QFile file(QString(":/about/%1.gemini").arg(url.path()));
+ if (file.open(QFile::ReadOnly))
+ {
+ emit this->requestComplete(file.readAll(), "text/gemini");
+ }
+ else
+ {
+ emit this->networkError(ResourceNotFound, "The requested resource does not exist.");
+ }
+ }
+ return true;
+}
+
+bool AboutHandler::isInProgress() const
+{
+ return false;
+}
+
+bool AboutHandler::cancelRequest()
+{
+ return true;
+}
diff --git a/src/protocols/abouthandler.hpp b/src/protocols/abouthandler.hpp
new file mode 100644
index 0000000..86b9180
--- /dev/null
+++ b/src/protocols/abouthandler.hpp
@@ -0,0 +1,23 @@
+#ifndef ABOUTHANDLER_HPP
+#define ABOUTHANDLER_HPP
+
+#include <QObject>
+
+#include "protocolhandler.hpp"
+
+class AboutHandler : public ProtocolHandler
+{
+ Q_OBJECT
+public:
+ AboutHandler();
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, ProtocolHandler::RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+};
+
+#endif // ABOUTHANDLER_HPP
diff --git a/src/protocols/filehandler.cpp b/src/protocols/filehandler.cpp
new file mode 100644
index 0000000..d26cd57
--- /dev/null
+++ b/src/protocols/filehandler.cpp
@@ -0,0 +1,45 @@
+#include "filehandler.hpp"
+
+#include <QMimeDatabase>
+#include <QUrl>
+#include <QFile>
+
+FileHandler::FileHandler()
+{
+
+}
+
+bool FileHandler::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "file");
+}
+
+bool FileHandler::startRequest(const QUrl &url, RequestOptions options)
+{
+ Q_UNUSED(options)
+
+ QFile file { url.path() };
+
+ if (file.open(QFile::ReadOnly))
+ {
+ QMimeDatabase db;
+ auto mime = db.mimeTypeForUrl(url).name();
+ auto data = file.readAll();
+ emit this->requestComplete(data, mime);
+ }
+ else
+ {
+ emit this->networkError(ResourceNotFound, "The requested file does not exist!");
+ }
+ return true;
+}
+
+bool FileHandler::isInProgress() const
+{
+ return false;
+}
+
+bool FileHandler::cancelRequest()
+{
+ return true;
+}
diff --git a/src/protocols/filehandler.hpp b/src/protocols/filehandler.hpp
new file mode 100644
index 0000000..55ac248
--- /dev/null
+++ b/src/protocols/filehandler.hpp
@@ -0,0 +1,23 @@
+#ifndef FILEHANDLER_HPP
+#define FILEHANDLER_HPP
+
+#include <QObject>
+
+#include "protocolhandler.hpp"
+
+class FileHandler : public ProtocolHandler
+{
+ Q_OBJECT
+public:
+ FileHandler();
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+};
+
+#endif // FILEHANDLER_HPP
diff --git a/src/protocols/fingerclient.cpp b/src/protocols/fingerclient.cpp
new file mode 100644
index 0000000..1c9a6a3
--- /dev/null
+++ b/src/protocols/fingerclient.cpp
@@ -0,0 +1,83 @@
+#include "fingerclient.hpp"
+#include "ioutil.hpp"
+
+FingerClient::FingerClient() : ProtocolHandler(nullptr)
+{
+ connect(&socket, &QTcpSocket::connected, this, &FingerClient::on_connected);
+ connect(&socket, &QTcpSocket::readyRead, this, &FingerClient::on_readRead);
+ connect(&socket, &QTcpSocket::disconnected, this, &FingerClient::on_finished);
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
+ connect(&socket, &QTcpSocket::errorOccurred, this, &FingerClient::on_socketError);
+#else
+ connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &FingerClient::on_socketError);
+#endif
+}
+
+FingerClient::~FingerClient()
+{
+
+}
+
+bool FingerClient::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "finger");
+}
+
+bool FingerClient::startRequest(const QUrl &url, RequestOptions options)
+{
+ Q_UNUSED(options)
+
+ if(isInProgress())
+ return false;
+
+ if(url.scheme() != "finger")
+ return false;
+
+ this->requested_user = url.userName();
+ this->was_cancelled = false;
+ socket.connectToHost(url.host(), url.port(79));
+
+ return true;
+}
+
+bool FingerClient::isInProgress() const
+{
+ return socket.isOpen();
+}
+
+bool FingerClient::cancelRequest()
+{
+ was_cancelled = true;
+ socket.close();
+ body.clear();
+ return true;
+}
+
+void FingerClient::on_connected()
+{
+ auto blob = (requested_user + "\r\n").toUtf8();
+
+ IoUtil::writeAll(socket, blob);
+}
+
+void FingerClient::on_readRead()
+{
+ body.append(socket.readAll());
+ emit this->requestProgress(body.size());
+}
+
+void FingerClient::on_finished()
+{
+ if(not was_cancelled)
+ {
+ emit this->requestComplete(this->body, "text/finger");
+ was_cancelled = true;
+ }
+ body.clear();
+}
+
+void FingerClient::on_socketError(QAbstractSocket::SocketError error_code)
+{
+ this->emitNetworkError(error_code, socket.errorString());
+}
diff --git a/src/protocols/fingerclient.hpp b/src/protocols/fingerclient.hpp
new file mode 100644
index 0000000..63d04fd
--- /dev/null
+++ b/src/protocols/fingerclient.hpp
@@ -0,0 +1,39 @@
+#ifndef FINGERCLIENT_HPP
+#define FINGERCLIENT_HPP
+
+#include <QObject>
+#include <QTcpSocket>
+#include <QUrl>
+
+#include "protocolhandler.hpp"
+
+class FingerClient : public ProtocolHandler
+{
+ Q_OBJECT
+public:
+ explicit FingerClient();
+
+ ~FingerClient() override;
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+
+private slots:
+ void on_connected();
+ void on_readRead();
+ void on_finished();
+ void on_socketError(QTcpSocket::SocketError error_code);
+
+private:
+ QTcpSocket socket;
+ QByteArray body;
+ bool was_cancelled;
+ QString requested_user;
+};
+
+#endif // FINGERCLIENT_HPP
diff --git a/src/protocols/geminiclient.cpp b/src/protocols/geminiclient.cpp
new file mode 100644
index 0000000..6986250
--- /dev/null
+++ b/src/protocols/geminiclient.cpp
@@ -0,0 +1,375 @@
+#include "geminiclient.hpp"
+#include <cassert>
+#include <QDebug>
+#include <QSslConfiguration>
+#include "kristall.hpp"
+
+GeminiClient::GeminiClient() : ProtocolHandler(nullptr)
+{
+ connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted);
+ connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead);
+ connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected);
+// connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) {
+// qDebug() << "Socket state changed to " << state;
+// });
+ connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors);
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
+ connect(&socket, &QTcpSocket::errorOccurred, this, &GeminiClient::socketError);
+#else
+ connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GeminiClient::socketError);
+#endif
+}
+
+GeminiClient::~GeminiClient()
+{
+ is_receiving_body = false;
+}
+
+bool GeminiClient::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "gemini");
+}
+
+bool GeminiClient::startRequest(const QUrl &url, RequestOptions options)
+{
+ if(url.scheme() != "gemini")
+ return false;
+
+ // qDebug() << "start request" << url;
+
+ if(socket.state() != QTcpSocket::UnconnectedState) {
+ socket.disconnectFromHost();
+ socket.close();
+ if(not socket.waitForDisconnected(1500))
+ return false;
+ }
+
+ this->is_error_state = false;
+
+ this->options = options;
+
+ QSslConfiguration ssl_config = socket.sslConfiguration();
+ ssl_config.setProtocol(QSsl::TlsV1_2);
+ if(not global_gemini_trust.enable_ca)
+ ssl_config.setCaCertificates(QList<QSslCertificate> { });
+ else
+ ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates());
+ socket.setSslConfiguration(ssl_config);
+
+ socket.connectToHostEncrypted(url.host(), url.port(1965));
+
+ this->buffer.clear();
+ this->body.clear();
+ this->is_receiving_body = false;
+ this->suppress_socket_tls_error = true;
+
+ if(not socket.isOpen())
+ return false;
+
+ target_url = url;
+ mime_type = "<invalid>";
+
+ return true;
+}
+
+bool GeminiClient::isInProgress() const
+{
+ return (socket.state() != QTcpSocket::UnconnectedState);
+}
+
+bool GeminiClient::cancelRequest()
+{
+ // qDebug() << "cancel request" << isInProgress();
+ if(isInProgress())
+ {
+ this->is_receiving_body = false;
+ this->socket.disconnectFromHost();
+ this->buffer.clear();
+ this->body.clear();
+ this->socket.waitForDisconnected(500);
+ this->socket.close();
+ bool success = not isInProgress();
+ // qDebug() << "cancel success" << success;
+ return success;
+ }
+ else
+ {
+ return true;
+ }
+}
+
+bool GeminiClient::enableClientCertificate(const CryptoIdentity &ident)
+{
+ this->socket.setLocalCertificate(ident.certificate);
+ this->socket.setPrivateKey(ident.private_key);
+ return true;
+}
+
+void GeminiClient::disableClientCertificate()
+{
+ this->socket.setLocalCertificate(QSslCertificate{});
+ this->socket.setPrivateKey(QSslKey { });
+}
+
+void GeminiClient::socketEncrypted()
+{
+ emit this->hostCertificateLoaded(this->socket.peerCertificate());
+
+ QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n";
+
+ QByteArray request_bytes = request.toUtf8();
+
+ qint64 offset = 0;
+ while(offset < request_bytes.size()) {
+ auto const len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset);
+ if(len <= 0)
+ {
+ socket.close();
+ return;
+ }
+ offset += len;
+ }
+}
+
+void GeminiClient::socketReadyRead()
+{
+ if(this->is_error_state) // don't do any further
+ return;
+ QByteArray response = socket.readAll();
+
+ if(is_receiving_body)
+ {
+ body.append(response);
+ emit this->requestProgress(body.size());
+ }
+ else
+ {
+ for(int i = 0; i < response.size(); i++)
+ {
+ if(response[i] == '\n') {
+ buffer.append(response.data(), i);
+ body.append(response.data() + i + 1, response.size() - i - 1);
+
+ // "XY " <META> <CR> <LF>
+ if(buffer.size() <= 5) {
+ socket.close();
+ qDebug() << buffer;
+ emit networkError(ProtocolViolation, "Line is too short for valid protocol");
+ return;
+ }
+ if(buffer.size() >= 1200)
+ {
+ emit networkError(ProtocolViolation, "response too large!");
+ socket.close();
+ }
+ if(buffer[buffer.size() - 1] != '\r') {
+ socket.close();
+ qDebug() << buffer;
+ emit networkError(ProtocolViolation, "Line does not end with <CR> <LF>");
+ return;
+ }
+ if(not isdigit(buffer[0])) {
+ socket.close();
+ qDebug() << buffer;
+ emit networkError(ProtocolViolation, "First character is not a digit.");
+ return;
+ }
+ if(not isdigit(buffer[1])) {
+ socket.close();
+ qDebug() << buffer;
+ emit networkError(ProtocolViolation, "Second character is not a digit.");
+ return;
+ }
+ // TODO: Implement stricter version
+ // if(buffer[2] != ' ') {
+ if(not isspace(buffer[2])) {
+ socket.close();
+ qDebug() << buffer;
+ emit networkError(ProtocolViolation, "Third character is not a space.");
+ return;
+ }
+
+ QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4);
+
+ int primary_code = buffer[0] - '0';
+ int secondary_code = buffer[1] - '0';
+
+ qDebug() << primary_code << secondary_code << meta;
+
+ // We don't need to receive any data after that.
+ if(primary_code != 2)
+ socket.close();
+
+ switch(primary_code)
+ {
+ case 1: // requesting input
+ emit inputRequired(meta);
+ return;
+
+ case 2: // success
+ is_receiving_body = true;
+ mime_type = meta;
+ return;
+
+ case 3: { // redirect
+ QUrl new_url(meta);
+ if(new_url.isValid()) {
+ if(new_url.isRelative())
+ new_url = target_url.resolved(new_url);
+ assert(not new_url.isRelative());
+
+ emit redirected(new_url, (secondary_code == 1));
+ }
+ else {
+ emit networkError(ProtocolViolation, "Invalid URL for redirection!");
+ }
+ return;
+ }
+
+ case 4: { // temporary failure
+ NetworkError type = UnknownError;
+ switch(secondary_code)
+ {
+ case 1: type = InternalServerError; break;
+ case 2: type = InternalServerError; break;
+ case 3: type = InternalServerError; break;
+ case 4: type = UnknownError; break;
+ }
+ emit networkError(type, meta);
+ return;
+ }
+
+ case 5: { // permanent failure
+ NetworkError type = UnknownError;
+ switch(secondary_code)
+ {
+ case 1: type = ResourceNotFound; break;
+ case 2: type = ResourceNotFound; break;
+ case 3: type = ProxyRequest; break;
+ case 9: type = BadRequest; break;
+ }
+ emit networkError(type, meta);
+ return;
+ }
+
+ case 6: // client certificate required
+ switch(secondary_code)
+ {
+ case 0:
+ emit certificateRequired(meta);
+ return;
+
+ case 1:
+ emit networkError(Unauthorized, meta);
+ return;
+
+ default:
+ case 2:
+ emit networkError(InvalidClientCertificate, meta);
+ return;
+ }
+ return;
+
+ default:
+ emit networkError(ProtocolViolation, "Unspecified status code used!");
+ return;
+ }
+
+ assert(false and "unreachable");
+ }
+ }
+ if((buffer.size() + response.size()) >= 1200)
+ {
+ emit networkError(ProtocolViolation, "META too large!");
+ socket.close();
+ }
+ buffer.append(response);
+ }
+}
+
+void GeminiClient::socketDisconnected()
+{
+ if(this->is_receiving_body and not this->is_error_state) {
+ body.append(socket.readAll());
+ emit requestComplete(body, mime_type);
+ }
+}
+
+void GeminiClient::sslErrors(QList<QSslError> const & errors)
+{
+ emit this->hostCertificateLoaded(this->socket.peerCertificate());
+
+ if(options & IgnoreTlsErrors) {
+ socket.ignoreSslErrors(errors);
+ return;
+ }
+
+ 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(SslTrust::isTrustRelated(err.error()))
+ {
+ switch(global_gemini_trust.getTrust(target_url, socket.peerCertificate()))
+ {
+ case SslTrust::Trusted:
+ ignore = true;
+ break;
+ case SslTrust::Untrusted:
+ this->is_error_state = true;
+ this->suppress_socket_tls_error = true;
+ emit this->networkError(UntrustedHost, toFingerprintString(socket.peerCertificate()));
+ return;
+ case SslTrust::Mistrusted:
+ this->is_error_state = true;
+ this->suppress_socket_tls_error = true;
+ emit this->networkError(MistrustedHost, toFingerprintString(socket.peerCertificate()));
+ return;
+ }
+ }
+ 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();
+ }
+
+ if(remaining_errors.size() > 0) {
+ emit this->networkError(TlsFailure, remaining_errors.first().errorString());
+ }
+}
+
+void GeminiClient::socketError(QAbstractSocket::SocketError socketError)
+{
+ // When remote host closes TLS session, the client closes the socket.
+ // This is more sane then erroring out here as it's a perfectly legal
+ // state and we know the TLS connection has ended.
+ if(socketError == QAbstractSocket::RemoteHostClosedError) {
+ socket.close();
+ } else {
+ this->is_error_state = true;
+ if(not this->suppress_socket_tls_error) {
+ this->emitNetworkError(socketError, socket.errorString());
+ }
+ }
+}
diff --git a/src/protocols/geminiclient.hpp b/src/protocols/geminiclient.hpp
new file mode 100644
index 0000000..85c4f76
--- /dev/null
+++ b/src/protocols/geminiclient.hpp
@@ -0,0 +1,55 @@
+#ifndef GEMINICLIENT_HPP
+#define GEMINICLIENT_HPP
+
+#include <QObject>
+#include <QMimeType>
+#include <QSslSocket>
+#include <QUrl>
+
+#include "protocolhandler.hpp"
+
+class GeminiClient : public ProtocolHandler
+{
+private:
+ Q_OBJECT
+public:
+ explicit GeminiClient();
+
+ ~GeminiClient() override;
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+
+ bool enableClientCertificate(CryptoIdentity const & ident) override;
+ void disableClientCertificate() override;
+
+private slots:
+ void socketEncrypted();
+
+ void socketReadyRead();
+
+ void socketDisconnected();
+
+ void sslErrors(const QList<QSslError> &errors);
+
+ void socketError(QAbstractSocket::SocketError socketError);
+
+private:
+ bool is_receiving_body;
+ bool suppress_socket_tls_error;
+ bool is_error_state;
+
+ QUrl target_url;
+ QSslSocket socket;
+ QByteArray buffer;
+ QByteArray body;
+ QString mime_type;
+ RequestOptions options;
+};
+
+#endif // GEMINICLIENT_HPP
diff --git a/src/protocols/gopherclient.cpp b/src/protocols/gopherclient.cpp
new file mode 100644
index 0000000..63c35ca
--- /dev/null
+++ b/src/protocols/gopherclient.cpp
@@ -0,0 +1,109 @@
+#include "gopherclient.hpp"
+#include "ioutil.hpp"
+
+GopherClient::GopherClient(QObject *parent) : ProtocolHandler(parent)
+{
+ connect(&socket, &QTcpSocket::connected, this, &GopherClient::on_connected);
+ connect(&socket, &QTcpSocket::readyRead, this, &GopherClient::on_readRead);
+ connect(&socket, &QTcpSocket::disconnected, this, &GopherClient::on_finished);
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
+ connect(&socket, &QTcpSocket::errorOccurred, this, &GopherClient::on_socketError);
+#else
+ connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GopherClient::on_socketError);
+#endif
+}
+
+GopherClient::~GopherClient()
+{
+
+}
+
+bool GopherClient::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "gopher");
+}
+
+bool GopherClient::startRequest(const QUrl &url, RequestOptions options)
+{
+ Q_UNUSED(options)
+
+ if(isInProgress())
+ return false;
+
+ if(url.scheme() != "gopher")
+ return false;
+
+ // Second char on the URL path denotes the Gopher type
+ // See https://tools.ietf.org/html/rfc4266
+ QString type = url.path().mid(1, 1);
+
+ mime = "application/octet-stream";
+ if(type == "") mime = "text/gophermap";
+ else if(type == "0") mime = "text/plain";
+ else if(type == "1") mime = "text/gophermap";
+ else if(type == "g") mime = "image/gif";
+ else if(type == "I") mime = "image/unknown";
+ else if(type == "h") mime = "text/html";
+ else if(type == "s") mime = "audio/unknown";
+
+ is_processing_binary = (type == "5") or (type == "9") or (type == "I") or (type == "g");
+
+ this->requested_url = url;
+ this->was_cancelled = false;
+ socket.connectToHost(url.host(), url.port(70));
+
+ return true;
+}
+
+bool GopherClient::isInProgress() const
+{
+ return socket.isOpen();
+}
+
+bool GopherClient::cancelRequest()
+{
+ was_cancelled = true;
+ socket.close();
+ body.clear();
+ return true;
+}
+
+void GopherClient::on_connected()
+{
+ auto blob = (requested_url.path().mid(2) + "\r\n").toUtf8();
+
+ IoUtil::writeAll(socket, blob);
+}
+
+void GopherClient::on_readRead()
+{
+ body.append(socket.readAll());
+
+ if(not is_processing_binary) {
+ // Strip the "lone dot" from gopher data
+ if(int index = body.indexOf("\r\n.\r\n"); index >= 0) {
+ body.resize(index + 2);
+ socket.close();
+ }
+ }
+
+ if(not was_cancelled) {
+ emit this->requestProgress(body.size());
+ }
+}
+
+void GopherClient::on_finished()
+{
+ if(not was_cancelled)
+ {
+ emit this->requestComplete(this->body, mime);
+ was_cancelled = true;
+ }
+ body.clear();
+}
+
+void GopherClient::on_socketError(QAbstractSocket::SocketError error_code)
+{
+ this->emitNetworkError(error_code, socket.errorString());
+}
diff --git a/src/protocols/gopherclient.hpp b/src/protocols/gopherclient.hpp
new file mode 100644
index 0000000..888ebd8
--- /dev/null
+++ b/src/protocols/gopherclient.hpp
@@ -0,0 +1,42 @@
+#ifndef GOPHERCLIENT_HPP
+#define GOPHERCLIENT_HPP
+
+#include <QObject>
+#include <QTcpSocket>
+#include <QUrl>
+
+#include "protocolhandler.hpp"
+
+class GopherClient : public ProtocolHandler
+{
+ Q_OBJECT
+public:
+ explicit GopherClient(QObject *parent = nullptr);
+
+ ~GopherClient() override;
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+
+private: // slots
+ void on_connected();
+ void on_readRead();
+ void on_finished();
+ void on_socketError(QAbstractSocket::SocketError errorCode);
+
+
+private:
+ QTcpSocket socket;
+ QByteArray body;
+ QUrl requested_url;
+ bool was_cancelled;
+ QString mime;
+ bool is_processing_binary;
+};
+
+#endif // GOPHERCLIENT_HPP
diff --git a/src/protocols/webclient.cpp b/src/protocols/webclient.cpp
new file mode 100644
index 0000000..ed87694
--- /dev/null
+++ b/src/protocols/webclient.cpp
@@ -0,0 +1,229 @@
+#include "webclient.hpp"
+#include "kristall.hpp"
+
+#include <QNetworkRequest>
+#include <QNetworkReply>
+
+WebClient::WebClient() :
+ ProtocolHandler(nullptr),
+ current_reply(nullptr)
+{
+ manager.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
+}
+
+WebClient::~WebClient()
+{
+
+}
+
+bool WebClient::supportsScheme(const QString &scheme) const
+{
+ return (scheme == "https") or (scheme == "http");
+}
+
+bool WebClient::startRequest(const QUrl &url, RequestOptions options)
+{
+ if(url.scheme() != "http" and url.scheme() != "https")
+ return false;
+
+ if(this->current_reply != nullptr)
+ return true;
+
+ this->options = options;
+ this->body.clear();
+
+ QNetworkRequest request(url);
+
+ auto ssl_config = request.sslConfiguration();
+ // ssl_config.setProtocol(QSsl::TlsV1_2);
+ if(global_https_trust.enable_ca)
+ ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates());
+ else
+ ssl_config.setCaCertificates(QList<QSslCertificate> { });
+
+ if(this->current_identity.isValid()) {
+ ssl_config.setLocalCertificate(this->current_identity.certificate);
+ ssl_config.setPrivateKey(this->current_identity.private_key);
+ }
+
+ // request.setMaximumRedirectsAllowed(5);
+ request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, false);
+ request.setSslConfiguration(ssl_config);
+
+ this->manager.clearAccessCache();
+ this->manager.clearConnectionCache();
+ this->current_reply = manager.get(request);
+ if(this->current_reply == nullptr)
+ return false;
+
+ this->suppress_socket_tls_error = true;
+
+ 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::sslErrors, this, &WebClient::on_sslErrors);
+ connect(this->current_reply, &QNetworkReply::redirected, this, &WebClient::on_redirected);
+
+ return true;
+}
+
+bool WebClient::isInProgress() const
+{
+ return (this->current_reply != nullptr);
+}
+
+bool WebClient::cancelRequest()
+{
+ if(this->current_reply != nullptr)
+ {
+ this->current_reply->abort();
+ this->current_reply = nullptr;
+ }
+ this->body.clear();
+ return true;
+}
+
+bool WebClient::enableClientCertificate(const CryptoIdentity &ident)
+{
+ current_identity = ident;
+ return true;
+}
+
+void WebClient::disableClientCertificate()
+{
+ current_identity = CryptoIdentity();
+}
+
+void WebClient::on_data()
+{
+ this->body.append(this->current_reply->readAll());
+ emit this->requestProgress(this->body.size());
+}
+
+void WebClient::on_finished()
+{
+ emit this->hostCertificateLoaded(this->current_reply->sslConfiguration().peerCertificate());
+
+ auto * const reply = this->current_reply;
+ this->current_reply = nullptr;
+
+ reply->deleteLater();
+
+ if(reply->error() != QNetworkReply::NoError)
+ {
+ NetworkError error = UnknownError;
+ switch(reply->error())
+ {
+ case QNetworkReply::ConnectionRefusedError: error = ConnectionRefused; break;
+ case QNetworkReply::RemoteHostClosedError: error = ProtocolViolation; break;
+ case QNetworkReply::HostNotFoundError: error = HostNotFound; break;
+ case QNetworkReply::TimeoutError: error = Timeout; break;
+ case QNetworkReply::SslHandshakeFailedError: error = TlsFailure; break;
+
+ case QNetworkReply::ContentAccessDenied: error = Unauthorized; break;
+ case QNetworkReply::ContentOperationNotPermittedError: error = BadRequest; break;
+ case QNetworkReply::ContentNotFoundError: error = ResourceNotFound; break;
+ case QNetworkReply::AuthenticationRequiredError: error = Unauthorized; break;
+ case QNetworkReply::ContentGoneError: error = ResourceNotFound; break;
+
+ case QNetworkReply::InternalServerError: error = InternalServerError; break;
+ case QNetworkReply::OperationNotImplementedError: error = InternalServerError; break;
+ case QNetworkReply::ServiceUnavailableError: error = InternalServerError; break;
+ default:
+ qDebug() << "Unhandled server error:" << reply->error();
+ break;
+ }
+
+ qDebug() << "web network error" << reply->errorString();
+ qDebug() << this->body;
+
+ if(not this->suppress_socket_tls_error) {
+ emit this->networkError(error, reply->errorString());
+ }
+ }
+ else
+ {
+ int statusCode =reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ if(statusCode >= 200 and statusCode < 300) {
+ auto mime = reply->header(QNetworkRequest::ContentTypeHeader).toString();
+ emit this->requestComplete(this->body, mime);
+ }
+ else if(statusCode >= 300 and statusCode < 400) {
+ auto url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+
+ emit this->redirected(url, (statusCode == 301) or (statusCode == 308));
+ }
+ else {
+ emit networkError(UnknownError, QString("Unhandled HTTP status code %1").arg(statusCode));
+ }
+
+ this->body.clear();
+ }
+}
+
+void WebClient::on_sslErrors(const QList<QSslError> &errors)
+{
+ emit this->hostCertificateLoaded(this->current_reply->sslConfiguration().peerCertificate());
+
+ if(options & IgnoreTlsErrors) {
+ this->current_reply->ignoreSslErrors(errors);
+ return;
+ }
+
+ 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(SslTrust::isTrustRelated(err.error()))
+ {
+ auto cert = this->current_reply->sslConfiguration().peerCertificate();
+ switch(global_https_trust.getTrust(this->current_reply->url(), cert))
+ {
+ case SslTrust::Trusted:
+ ignore = true;
+ break;
+ case SslTrust::Untrusted:
+ this->suppress_socket_tls_error = true;
+ emit this->networkError(UntrustedHost, toFingerprintString(cert));
+ return;
+ case SslTrust::Mistrusted:
+ this->suppress_socket_tls_error = true;
+ emit this->networkError(MistrustedHost, toFingerprintString(cert));
+ return;
+ }
+ }
+ else if(err.error() == QSslError::UnableToVerifyFirstCertificate)
+ {
+ ignore = true;
+ }
+
+ if(ignore) {
+ ignored_errors.append(err);
+ remaining_errors.removeAt(0);
+ } else {
+ i += 1;
+ }
+ }
+
+ current_reply->ignoreSslErrors(ignored_errors);
+
+ qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size();
+
+ for(auto const & error : remaining_errors) {
+ qWarning() << int(error.error()) << error.errorString();
+ }
+
+ if(remaining_errors.size() > 0) {
+ emit this->networkError(TlsFailure, remaining_errors.first().errorString());
+ }
+}
+
+void WebClient::on_redirected(const QUrl &url)
+{
+ qDebug() << "redirected to" << url;
+}
diff --git a/src/protocols/webclient.hpp b/src/protocols/webclient.hpp
new file mode 100644
index 0000000..58ae029
--- /dev/null
+++ b/src/protocols/webclient.hpp
@@ -0,0 +1,48 @@
+#ifndef WEBCLIENT_HPP
+#define WEBCLIENT_HPP
+
+#include <QObject>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include "protocolhandler.hpp"
+
+class WebClient: public ProtocolHandler
+{
+private:
+ Q_OBJECT
+public:
+ explicit WebClient();
+
+ ~WebClient() override;
+
+ bool supportsScheme(QString const & scheme) const override;
+
+ bool startRequest(QUrl const & url, RequestOptions options) override;
+
+ bool isInProgress() const override;
+
+ bool cancelRequest() override;
+
+ bool enableClientCertificate(CryptoIdentity const & ident) override;
+ void disableClientCertificate() override;
+
+private slots:
+ void on_data();
+ void on_finished();
+ void on_sslErrors(const QList<QSslError> &errors);
+ void on_redirected(const QUrl &url);
+
+private:
+ QNetworkAccessManager manager;
+ QNetworkReply * current_reply;
+
+ QByteArray body;
+ RequestOptions options;
+
+ CryptoIdentity current_identity;
+
+ bool suppress_socket_tls_error;
+};
+
+#endif // WEBCLIENT_HPP