diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-22 21:10:04 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-22 21:10:04 +0200 |
| commit | 75ec461eeaa851cb5c53f4cfffc434e3e529ed1d (patch) | |
| tree | 3944737340718ca3675381aa06636045d397e780 /src/protocols | |
| parent | 8dbfb0890560fd1cd698d06fa05ac868c4db8576 (diff) | |
Restructures the project source and cleans up a bit
Diffstat (limited to 'src/protocols')
| -rw-r--r-- | src/protocols/abouthandler.cpp | 61 | ||||
| -rw-r--r-- | src/protocols/abouthandler.hpp | 23 | ||||
| -rw-r--r-- | src/protocols/filehandler.cpp | 45 | ||||
| -rw-r--r-- | src/protocols/filehandler.hpp | 23 | ||||
| -rw-r--r-- | src/protocols/fingerclient.cpp | 83 | ||||
| -rw-r--r-- | src/protocols/fingerclient.hpp | 39 | ||||
| -rw-r--r-- | src/protocols/geminiclient.cpp | 375 | ||||
| -rw-r--r-- | src/protocols/geminiclient.hpp | 55 | ||||
| -rw-r--r-- | src/protocols/gopherclient.cpp | 109 | ||||
| -rw-r--r-- | src/protocols/gopherclient.hpp | 42 | ||||
| -rw-r--r-- | src/protocols/webclient.cpp | 229 | ||||
| -rw-r--r-- | src/protocols/webclient.hpp | 48 |
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 |
