diff --git a/src/librssguard/CMakeLists.txt b/src/librssguard/CMakeLists.txt index 7b00cf089..b00200153 100644 --- a/src/librssguard/CMakeLists.txt +++ b/src/librssguard/CMakeLists.txt @@ -255,6 +255,8 @@ set(SOURCES network-web/adblock/adblockrequestinfo.h network-web/gemini/geminiclient.cpp network-web/gemini/geminiclient.h + network-web/gemini/geminiparser.cpp + network-web/gemini/geminiparser.h network-web/apiserver.cpp network-web/apiserver.h network-web/articleparse.cpp diff --git a/src/librssguard/miscellaneous/application.cpp b/src/librssguard/miscellaneous/application.cpp index b8bf580dc..922fe6d72 100644 --- a/src/librssguard/miscellaneous/application.cpp +++ b/src/librssguard/miscellaneous/application.cpp @@ -30,7 +30,6 @@ #include "miscellaneous/settings.h" #include "network-web/adblock/adblockicon.h" #include "network-web/adblock/adblockmanager.h" -#include "network-web/gemini/geminiclient.h" #include "network-web/webfactory.h" #include "services/abstract/serviceroot.h" diff --git a/src/librssguard/network-web/downloader.cpp b/src/librssguard/network-web/downloader.cpp index 415f3f4e6..4796fca22 100644 --- a/src/librssguard/network-web/downloader.cpp +++ b/src/librssguard/network-web/downloader.cpp @@ -4,6 +4,7 @@ #include "miscellaneous/application.h" #include "network-web/cookiejar.h" +#include "network-web/gemini/geminiparser.h" #include "network-web/networkfactory.h" #include "network-web/silentnetworkaccessmanager.h" #include "network-web/webfactory.h" @@ -14,15 +15,20 @@ #include Downloader::Downloader(QObject* parent) - : QObject(parent), m_activeReply(nullptr), m_downloadManager(new SilentNetworkAccessManager(this)), - m_timer(new QTimer(this)), m_inputData(QByteArray()), m_inputMultipartData(nullptr), m_targetProtected(false), - m_targetUsername(QString()), m_targetPassword(QString()), m_lastOutputData({}), - m_lastOutputError(QNetworkReply::NetworkError::NoError), m_lastHttpStatusCode(0), m_lastHeaders({}) { + : QObject(parent), m_geminiClient(new GeminiClient(this)), m_activeReply(nullptr), + m_downloadManager(new SilentNetworkAccessManager(this)), m_timer(new QTimer(this)), m_inputData(QByteArray()), + m_inputMultipartData(nullptr), m_targetProtected(false), m_targetUsername(QString()), m_targetPassword(QString()), + m_lastOutputData({}), m_lastOutputError(QNetworkReply::NetworkError::NoError), m_lastHttpStatusCode(0), + m_lastHeaders({}) { m_timer->setInterval(DOWNLOAD_TIMEOUT); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &Downloader::cancel); + connect(m_geminiClient, &GeminiClient::redirected, this, &Downloader::geminiRedirect); + connect(m_geminiClient, &GeminiClient::requestComplete, this, &Downloader::geminiFinished); + connect(m_geminiClient, &GeminiClient::networkError, this, &Downloader::geminiError); + m_downloadManager->setCookieJar(qApp->web()->cookieJar()); qApp->web()->cookieJar()->setParent(nullptr); } @@ -31,6 +37,44 @@ Downloader::~Downloader() { qDebugNN << LOGSEC_NETWORK << "Destroying Downloader instance."; } +void Downloader::geminiFinished(const QByteArray& data, const QString& mime) { + m_timer->stop(); + m_activeReply = nullptr; + + m_lastContentType = mime; + m_lastUrl = m_geminiClient->targetUrl(); + m_lastCookies = {}; + m_lastHeaders = {}; + m_lastHttpStatusCode = 0; + m_lastOutputError = QNetworkReply::NetworkError::NoError; + m_lastOutputMultipartData = {}; + + if (mime.startsWith(QSL("text/gemini"))) { + m_lastOutputData = GeminiParser().geminiToHtml(data).toUtf8(); + } + else { + m_lastOutputData = data; + } + + emit completed(m_lastUrl, m_lastOutputError, m_lastHttpStatusCode, m_lastOutputData); +} + +void Downloader::geminiError(GeminiClient::NetworkError error, const QString& reason) { + m_timer->stop(); + m_activeReply = nullptr; + + m_lastContentType = {}; + m_lastUrl = m_geminiClient->targetUrl(); + m_lastCookies = {}; + m_lastHeaders = {}; + m_lastHttpStatusCode = 404; + m_lastOutputData = {}; + m_lastOutputError = QNetworkReply::NetworkError::UnknownNetworkError; + m_lastOutputMultipartData = {}; + + emit completed(m_lastUrl, m_lastOutputError, m_lastHttpStatusCode); +} + void Downloader::downloadFile(const QString& url, int timeout, bool protected_contents, @@ -80,6 +124,19 @@ void Downloader::manipulateData(const QString& url, manipulateData(url, operation, data, nullptr, timeout, protected_contents, username, password); } +void Downloader::geminiRedirect(const QUrl& uri, bool is_permanent) { + m_timer->stop(); + + QUrl new_url = m_geminiClient->targetUrl().resolved(uri); + + runGeminiRequest(new_url); +} + +void Downloader::runGeminiRequest(const QUrl& url) { + m_timer->start(); + m_geminiClient->startRequest(url, GeminiClient::RequestOptions::IgnoreTlsErrors); +} + void Downloader::manipulateData(const QString& url, QNetworkAccessManager::Operation operation, const QByteArray& data, @@ -89,48 +146,57 @@ void Downloader::manipulateData(const QString& url, const QString& username, const QString& password) { QString sanitized_url = NetworkFactory::sanitizeUrl(url); - auto cookies = CookieJar::extractCookiesFromUrl(sanitized_url); - if (!cookies.isEmpty()) { - qApp->web()->cookieJar()->setCookiesFromUrl(cookies, sanitized_url); + if (m_geminiClient->supportsUrl(sanitized_url)) { + QUrl gemini_url = QUrl::fromUserInput(sanitized_url); + + runGeminiRequest(gemini_url); } + else { - QNetworkRequest request; - QHashIterator i(m_customHeaders); + auto cookies = CookieJar::extractCookiesFromUrl(sanitized_url); - while (i.hasNext()) { - i.next(); - request.setRawHeader(i.key(), i.value()); - } - - m_inputData = data; - m_inputMultipartData = multipart_data; - - // Set url for this request and fire it up. - m_timer->setInterval(timeout); - - request.setUrl(qApp->web()->processFeedUriScheme(sanitized_url)); - - m_targetProtected = protected_contents; - m_targetUsername = username; - m_targetPassword = password; - - if (operation == QNetworkAccessManager::Operation::PostOperation) { - if (m_inputMultipartData == nullptr) { - runPostRequest(request, m_inputData); + if (!cookies.isEmpty()) { + qApp->web()->cookieJar()->setCookiesFromUrl(cookies, sanitized_url); } - else { - runPostRequest(request, m_inputMultipartData); + + QNetworkRequest request; + QHashIterator i(m_customHeaders); + + while (i.hasNext()) { + i.next(); + request.setRawHeader(i.key(), i.value()); + } + + m_inputData = data; + m_inputMultipartData = multipart_data; + + // Set url for this request and fire it up. + m_timer->setInterval(timeout); + + request.setUrl(qApp->web()->processFeedUriScheme(sanitized_url)); + + m_targetProtected = protected_contents; + m_targetUsername = username; + m_targetPassword = password; + + if (operation == QNetworkAccessManager::Operation::PostOperation) { + if (m_inputMultipartData == nullptr) { + runPostRequest(request, m_inputData); + } + else { + runPostRequest(request, m_inputMultipartData); + } + } + else if (operation == QNetworkAccessManager::GetOperation) { + runGetRequest(request); + } + else if (operation == QNetworkAccessManager::PutOperation) { + runPutRequest(request, m_inputData); + } + else if (operation == QNetworkAccessManager::DeleteOperation) { + runDeleteRequest(request); } - } - else if (operation == QNetworkAccessManager::GetOperation) { - runGetRequest(request); - } - else if (operation == QNetworkAccessManager::PutOperation) { - runPutRequest(request, m_inputData); - } - else if (operation == QNetworkAccessManager::DeleteOperation) { - runDeleteRequest(request); } } @@ -405,6 +471,9 @@ void Downloader::cancel() { // Download action timed-out, too slow connection or target is not reachable. m_activeReply->abort(); } + else { + m_geminiClient->cancelRequest(); + } } void Downloader::appendRawHeader(const QByteArray& name, const QByteArray& value) { diff --git a/src/librssguard/network-web/downloader.h b/src/librssguard/network-web/downloader.h index 9af9ac37b..80629fbd4 100644 --- a/src/librssguard/network-web/downloader.h +++ b/src/librssguard/network-web/downloader.h @@ -4,6 +4,7 @@ #define DOWNLOADER_H #include "definitions/definitions.h" +#include "network-web/gemini/geminiclient.h" #include "network-web/httpresponse.h" #include @@ -74,6 +75,9 @@ class Downloader : public QObject { void completed(const QUrl& url, QNetworkReply::NetworkError status, int http_code, QByteArray contents = {}); private slots: + void geminiRedirect(const QUrl& uri, bool is_permanent); + void geminiFinished(const QByteArray& data, const QString& mime); + void geminiError(GeminiClient::NetworkError error, const QString& reason); // Called when current reply is processed. void finished(); @@ -97,8 +101,10 @@ class Downloader : public QObject { void runPostRequest(const QNetworkRequest& request, QHttpMultiPart* multipart_data); void runPostRequest(const QNetworkRequest& request, const QByteArray& data); void runGetRequest(const QNetworkRequest& request); + void runGeminiRequest(const QUrl& url); private: + GeminiClient* m_geminiClient; QNetworkReply* m_activeReply; QScopedPointer m_downloadManager; QTimer* m_timer; diff --git a/src/librssguard/network-web/gemini/geminiclient.cpp b/src/librssguard/network-web/gemini/geminiclient.cpp index ebedf5e84..0ff82192b 100644 --- a/src/librssguard/network-web/gemini/geminiclient.cpp +++ b/src/librssguard/network-web/gemini/geminiclient.cpp @@ -1,138 +1,170 @@ +// For license of this file, see /LICENSE.md. +// +// This file is heavily inspired by https://github.com/ikskuh/kristall. + #include "network-web/gemini/geminiclient.h" +#include "definitions/definitions.h" + #include #include -#include +#include #include #include +bool CryptoIdentity::isValid() const { + return !certificate.isNull() && !private_key.isNull(); +} + bool CryptoIdentity::isHostFiltered(const QUrl& url) const { - if (this->host_filter.isEmpty()) + if (host_filter.isEmpty()) { return false; + } QString url_text = url.toString(QUrl::FullyEncoded); + QRegularExpression pattern(QRegularExpression::wildcardToRegularExpression(host_filter), + QRegularExpression::PatternOption::CaseInsensitiveOption); - QRegExp pattern{this->host_filter, Qt::CaseInsensitive, QRegExp::Wildcard}; - - return not pattern.exactMatch(url_text); + return !pattern.match(url_text).hasMatch(); } bool CryptoIdentity::isAutomaticallyEnabledOn(const QUrl& url) const { - if (this->host_filter.isEmpty()) + if (host_filter.isEmpty()) { return false; - if (not this->auto_enable) + } + + if (!auto_enable) { return false; + } QString url_text = url.toString(QUrl::FullyEncoded); + QRegularExpression pattern(QRegularExpression::wildcardToRegularExpression(host_filter), + QRegularExpression::PatternOption::CaseInsensitiveOption); - QRegExp pattern{this->host_filter, Qt::CaseInsensitive, QRegExp::Wildcard}; - - return pattern.exactMatch(url_text); + return pattern.match(url_text).hasMatch(); } GeminiClient::GeminiClient(QObject* parent) : QObject(parent) { - connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); - connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); - connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); + connect(&m_socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); + connect(&m_socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); + connect(&m_socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); + connect(&m_socket, QOverload&>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); + // connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) { // qDebug() << "Socket state changed to " << state; // }); - connect(&socket, QOverload&>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - connect(&socket, &QTcpSocket::errorOccurred, this, &GeminiClient::socketError); + connect(&m_socket, &QTcpSocket::errorOccurred, this, &GeminiClient::socketError); #else - connect(&socket, QOverload::of(&QTcpSocket::error), this, &GeminiClient::socketError); + connect(&m_socket, QOverload::of(&QTcpSocket::error), this, &GeminiClient::socketError); #endif // States - connect(&socket, &QAbstractSocket::hostFound, this, [this]() { + connect(&m_socket, &QAbstractSocket::hostFound, this, [this]() { emit this->requestStateChange(RequestState::HostFound); }); - connect(&socket, &QAbstractSocket::connected, this, [this]() { + + connect(&m_socket, &QAbstractSocket::connected, this, [this]() { emit this->requestStateChange(RequestState::Connected); }); - connect(&socket, &QAbstractSocket::disconnected, this, [this]() { + + connect(&m_socket, &QAbstractSocket::disconnected, this, [this]() { emit this->requestStateChange(RequestState::None); }); - emit this->requestStateChange(RequestState::None); + + emit requestStateChange(RequestState::None); } GeminiClient::~GeminiClient() { - is_receiving_body = false; + m_isReceivingBody = false; } -bool GeminiClient::supportsScheme(const QString& scheme) const { - return (scheme == "gemini"); +bool GeminiClient::supportsUrl(const QString& url) const { + return url.startsWith(QL1S("gemini://")); +} + +bool GeminiClient::supportsUrl(const QUrl& url) const { + return url.scheme() == QL1S("gemini"); } bool GeminiClient::startRequest(const QUrl& url, RequestOptions options) { - if (url.scheme() != "gemini") + if (!supportsUrl(url)) { return false; + } // qDebug() << "start request" << url; - if (socket.state() != QTcpSocket::UnconnectedState) { - socket.disconnectFromHost(); - socket.close(); - if (not socket.waitForDisconnected(1500)) + if (m_socket.state() != QTcpSocket::UnconnectedState) { + m_socket.disconnectFromHost(); + m_socket.close(); + + if (!m_socket.waitForDisconnected(1500)) { return false; + } } - emit this->requestStateChange(RequestState::Started); + emit requestStateChange(RequestState::Started); - this->is_error_state = false; + m_inErrorState = false; + options = options; - this->options = options; - - QSslConfiguration ssl_config = socket.sslConfiguration(); + QSslConfiguration ssl_config = m_socket.sslConfiguration(); ssl_config.setProtocol(QSsl::TlsV1_2OrLater); + /* if (not kristall::globals().trust.gemini.enable_ca) ssl_config.setCaCertificates(QList{}); else */ + ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates()); + /* */ - socket.setSslConfiguration(ssl_config); + m_socket.setSslConfiguration(ssl_config); + m_socket.connectToHostEncrypted(url.host(), url.port(1965)); - socket.connectToHostEncrypted(url.host(), url.port(1965)); + m_buffer.clear(); + m_body.clear(); + m_isReceivingBody = false; + m_suppressSocketTlsErrors = true; - this->buffer.clear(); - this->body.clear(); - this->is_receiving_body = false; - this->suppress_socket_tls_error = true; - - if (not socket.isOpen()) + if (!m_socket.isOpen()) { return false; + } - target_url = url; - mime_type = ""; + m_targetUrl = url; + m_mimeType = ""; return true; } -bool GeminiClient::isInProgress() const { - return (socket.state() != QTcpSocket::UnconnectedState); +bool GeminiClient::inProgress() const { + return m_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(); - if (socket.state() != QTcpSocket::UnconnectedState) { - socket.disconnectFromHost(); + if (inProgress()) { + m_isReceivingBody = false; + m_socket.disconnectFromHost(); + m_buffer.clear(); + m_body.clear(); + + if (m_socket.state() != QTcpSocket::UnconnectedState) { + m_socket.disconnectFromHost(); } - this->socket.waitForDisconnected(500); - this->socket.close(); - bool success = not isInProgress(); + + m_socket.waitForDisconnected(500); + m_socket.close(); + + bool success = !inProgress(); + // qDebug() << "cancel success" << success; + return success; } else { @@ -141,14 +173,15 @@ bool GeminiClient::cancelRequest() { } bool GeminiClient::enableClientCertificate(const CryptoIdentity& ident) { - this->socket.setLocalCertificate(ident.certificate); - this->socket.setPrivateKey(ident.private_key); + m_socket.setLocalCertificate(ident.certificate); + m_socket.setPrivateKey(ident.private_key); + return true; } void GeminiClient::disableClientCertificate() { - this->socket.setLocalCertificate(QSslCertificate{}); - this->socket.setPrivateKey(QSslKey{}); + m_socket.setLocalCertificate(QSslCertificate{}); + m_socket.setPrivateKey(QSslKey{}); } void GeminiClient::emitNetworkError(QAbstractSocket::SocketError error_code, const QString& textual_description) { @@ -158,110 +191,140 @@ void GeminiClient::emitNetworkError(QAbstractSocket::SocketError error_code, con case QAbstractSocket::ConnectionRefusedError: network_error = ConnectionRefused; break; + case QAbstractSocket::HostNotFoundError: network_error = HostNotFound; break; + case QAbstractSocket::SocketTimeoutError: network_error = Timeout; break; + case QAbstractSocket::SslHandshakeFailedError: network_error = TlsFailure; break; + case QAbstractSocket::SslInternalError: network_error = TlsFailure; break; + case QAbstractSocket::SslInvalidUserDataError: network_error = TlsFailure; break; + default: qDebug() << "unhandled network error:" << error_code; break; } - emit this->networkError(network_error, textual_description); + emit networkError(network_error, textual_description); } void GeminiClient::socketEncrypted() { - emit this->hostCertificateLoaded(this->socket.peerCertificate()); + emit hostCertificateLoaded(m_socket.peerCertificate()); - QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n"; + QString request = m_targetUrl.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + QSL("\r\n"); QByteArray request_bytes = request.toUtf8(); qint64 offset = 0; + while (offset < request_bytes.size()) { - const auto len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset); + const auto len = m_socket.write(request_bytes.constData() + offset, request_bytes.size() - offset); + if (len <= 0) { - socket.close(); + m_socket.close(); return; } + offset += len; } } void GeminiClient::socketReadyRead() { - if (this->is_error_state) // don't do any further + if (m_inErrorState) { return; - QByteArray response = socket.readAll(); + } - if (is_receiving_body) { - body.append(response); - emit this->requestProgress(body.size()); + QByteArray response = m_socket.readAll(); + + if (m_isReceivingBody) { + m_body.append(response); + + emit requestProgress(m_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); + m_buffer.append(response.data(), i); + m_body.append(response.data() + i + 1, response.size() - i - 1); // "XY " - if (buffer.size() < 4) { // we allow an empty - socket.close(); - qDebug() << buffer; - emit networkError(ProtocolViolation, QObject::tr("Line is too short for valid protocol")); + if (m_buffer.size() < 4) { // we allow an empty + m_socket.close(); + + qDebug() << m_buffer; + + emit networkError(ProtocolViolation, tr("Line is too short for valid protocol")); + return; } - if (buffer.size() >= 1200) { - emit networkError(ProtocolViolation, QObject::tr("response too large!")); - socket.close(); + if (m_buffer.size() >= 1200) { + emit networkError(ProtocolViolation, tr("response too large!")); + + m_socket.close(); } - if (buffer[buffer.size() - 1] != '\r') { - socket.close(); - qDebug() << buffer; - emit networkError(ProtocolViolation, QObject::tr("Line does not end with ")); + if (m_buffer[m_buffer.size() - 1] != '\r') { + m_socket.close(); + + qDebug() << m_buffer; + + emit networkError(ProtocolViolation, tr("Line does not end with ")); + return; } - if (not isdigit(buffer[0])) { - socket.close(); - qDebug() << buffer; - emit networkError(ProtocolViolation, QObject::tr("First character is not a digit.")); + if (!isdigit(m_buffer[0])) { + m_socket.close(); + + qDebug() << m_buffer; + + emit networkError(ProtocolViolation, tr("First character is not a digit.")); + return; } - if (not isdigit(buffer[1])) { - socket.close(); - qDebug() << buffer; - emit networkError(ProtocolViolation, QObject::tr("Second character is not a digit.")); + if (not isdigit(m_buffer[1])) { + m_socket.close(); + + qDebug() << m_buffer; + + emit networkError(ProtocolViolation, tr("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, QObject::tr("Third character is not a space.")); + if (!isspace(m_buffer[2])) { + m_socket.close(); + + qDebug() << m_buffer; + + emit networkError(ProtocolViolation, tr("Third character is not a space.")); + return; } - QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4); + QString meta = QString::fromUtf8(m_buffer.data() + 3, m_buffer.size() - 4); - int primary_code = buffer[0] - '0'; - int secondary_code = buffer[1] - '0'; + int primary_code = m_buffer[0] - '0'; + int secondary_code = m_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(); + if (primary_code != 2) { + m_socket.close(); + } switch (primary_code) { case 1: // requesting input @@ -269,69 +332,87 @@ void GeminiClient::socketReadyRead() { case 1: emit inputRequired(meta, true); break; + case 0: default: emit inputRequired(meta, false); } + return; case 2: // success - is_receiving_body = true; - mime_type = meta; + m_isReceivingBody = true; + m_mimeType = meta; + return; case 3: { // redirect QUrl new_url(meta); if (new_url.isValid()) { - if (new_url.isRelative()) - new_url = target_url.resolved(new_url); + if (new_url.isRelative()) { + new_url = m_targetUrl.resolved(new_url); + } + assert(not new_url.isRelative()); emit redirected(new_url, (secondary_code == 1)); } else { - emit networkError(ProtocolViolation, QObject::tr("Invalid URL for redirection!")); + emit networkError(ProtocolViolation, tr("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; } @@ -350,36 +431,40 @@ void GeminiClient::socketReadyRead() { emit networkError(InvalidClientCertificate, meta); return; } + return; default: - emit networkError(ProtocolViolation, QObject::tr("Unspecified status code used!")); + emit networkError(ProtocolViolation, tr("Unspecified status code used!")); + return; } - assert(false and "unreachable"); + assert(false && "unreachable"); } } - if ((buffer.size() + response.size()) >= 1200) { - emit networkError(ProtocolViolation, QObject::tr("META too large!")); - socket.close(); + if ((m_buffer.size() + response.size()) >= 1200) { + emit networkError(ProtocolViolation, tr("META too large!")); + + m_socket.close(); } - buffer.append(response); + + m_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); + if (m_isReceivingBody && !m_inErrorState) { + m_body.append(m_socket.readAll()); + emit requestComplete(m_body, m_mimeType); } } void GeminiClient::sslErrors(const QList& errors) { - emit this->hostCertificateLoaded(this->socket.peerCertificate()); + emit hostCertificateLoaded(m_socket.peerCertificate()); - if (options & IgnoreTlsErrors) { - socket.ignoreSslErrors(errors); + if (m_options & IgnoreTlsErrors) { + m_socket.ignoreSslErrors(errors); return; } @@ -428,7 +513,7 @@ void GeminiClient::sslErrors(const QList& errors) { } } - socket.ignoreSslErrors(ignored_errors); + m_socket.ignoreSslErrors(ignored_errors); qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size(); @@ -437,7 +522,7 @@ void GeminiClient::sslErrors(const QList& errors) { } if (remaining_errors.size() > 0) { - emit this->networkError(TlsFailure, remaining_errors.first().errorString()); + emit networkError(TlsFailure, remaining_errors.first().errorString()); } } @@ -446,12 +531,17 @@ void GeminiClient::socketError(QAbstractSocket::SocketError socketError) { // 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(); + m_socket.close(); return; } - this->is_error_state = true; - if (not this->suppress_socket_tls_error) { - this->emitNetworkError(socketError, socket.errorString()); + m_inErrorState = true; + + if (!m_suppressSocketTlsErrors) { + emitNetworkError(socketError, m_socket.errorString()); } } + +QUrl GeminiClient::targetUrl() const { + return m_targetUrl; +} diff --git a/src/librssguard/network-web/gemini/geminiclient.h b/src/librssguard/network-web/gemini/geminiclient.h index 24627948f..4aa1dddfb 100644 --- a/src/librssguard/network-web/gemini/geminiclient.h +++ b/src/librssguard/network-web/gemini/geminiclient.h @@ -1,5 +1,9 @@ -#ifndef GEMINICLIENT_HPP -#define GEMINICLIENT_HPP +// For license of this file, see /LICENSE.md. +// +// This file is heavily inspired by https://github.com/ikskuh/kristall. + +#ifndef GEMINICLIENT_H +#define GEMINICLIENT_H #include #include @@ -33,9 +37,7 @@ struct CryptoIdentity { //! the certificate will be automatically enabled for hosts matching the filter. bool auto_enable = false; - bool isValid() const { - return (not this->certificate.isNull()) and (not this->private_key.isNull()); - } + bool isValid() const; //! returns true if a host does not match the filter criterion bool isHostFiltered(const QUrl& url) const; @@ -82,15 +84,18 @@ class GeminiClient : public QObject { explicit GeminiClient(QObject* parent = nullptr); virtual ~GeminiClient(); - bool supportsScheme(const QString& scheme) const; + bool supportsUrl(const QString& url) const; + bool supportsUrl(const QUrl& url) const; bool startRequest(const QUrl& url, RequestOptions options); - bool isInProgress() const; + bool inProgress() const; bool cancelRequest(); bool enableClientCertificate(const CryptoIdentity& ident); void disableClientCertificate(); + QUrl targetUrl() const; + signals: //! We successfully transferred some bytes from the server void requestProgress(qint64 transferred); @@ -127,16 +132,16 @@ class GeminiClient : public QObject { void socketError(QAbstractSocket::SocketError socketError); private: - bool is_receiving_body; - bool suppress_socket_tls_error; - bool is_error_state; + bool m_isReceivingBody; + bool m_suppressSocketTlsErrors; + bool m_inErrorState; - QUrl target_url; - QSslSocket socket; - QByteArray buffer; - QByteArray body; - QString mime_type; - RequestOptions options; + QUrl m_targetUrl; + QSslSocket m_socket; + QByteArray m_buffer; + QByteArray m_body; + QString m_mimeType; + RequestOptions m_options; }; -#endif // GEMINICLIENT_HPP +#endif // GEMINICLIENT_H diff --git a/src/librssguard/network-web/gemini/geminiparser.cpp b/src/librssguard/network-web/gemini/geminiparser.cpp new file mode 100644 index 000000000..b00efc3a8 --- /dev/null +++ b/src/librssguard/network-web/gemini/geminiparser.cpp @@ -0,0 +1,96 @@ +// For license of this file, see /LICENSE.md. + +#include "network-web/gemini/geminiparser.h" + +#include "definitions/definitions.h" +#include "miscellaneous/iofactory.h" + +QString GeminiParser::geminiToHtml(const QByteArray& gemini_data) { + QString html; + QString gemini_hypertext = + QString::fromUtf8(gemini_data).replace(QSL("\r\n"), QSL("\n")).replace(QSL("\r"), QSL("\n")); + QStringList lines = gemini_hypertext.split(QL1C('\n')); + bool normal_mode = true; + + static QRegularExpression exp_link(R"(^=>\s+([^\s]+)(?:\s+(\w.+))?$)"); + static QRegularExpression exp_heading(R"(^(#{1,6})\s+(.+)$)"); + static QRegularExpression exp_list(R"(^\*\s(.+)$)"); + static QRegularExpression exp_quote(R"((?:^>$|^>\s?(.+)$))"); + static QRegularExpression exp_pre(R"(^```.*$)"); + static QRegularExpression exp_text(R"()"); + + QRegularExpressionMatch mtch; + + for (const QString& line : lines) { + if ((mtch = exp_pre.match(line)).hasMatch()) { + normal_mode = !normal_mode; + continue; + } + + if (normal_mode) { + if ((mtch = exp_link.match(line)).hasMatch()) { + html += parseLink(mtch); + } + else if ((mtch = exp_heading.match(line)).hasMatch()) { + html += parseHeading(mtch); + } + else if ((mtch = exp_list.match(line)).hasMatch()) { + html += parseList(mtch); + } + else if ((mtch = exp_quote.match(line)).hasMatch()) { + html += parseQuote(mtch); + } + else { + html += parseTextInNormalMode(line); + } + } + else { + html += parseInPreMode(line); + } + } + + IOFactory::writeFile("a.gmi", html.toUtf8()); + + return html; +} + +QString GeminiParser::parseLink(const QRegularExpressionMatch& mtch) const { + QString link = mtch.captured(1); + QString name = mtch.captured(2); + + return QSL("

🔗 %2

\n").arg(link, name.isEmpty() ? link : name); +} + +QString GeminiParser::parseHeading(const QRegularExpressionMatch& mtch) const { + int level = mtch.captured(1).size(); + QString header = mtch.captured(2); + + return QSL("%2\n").arg(QString::number(level), header); +} + +QString GeminiParser::parseQuote(const QRegularExpressionMatch &mtch) const { + QString text = mtch.captured(1); + + return QSL("

%1

\n").arg(text.isEmpty() ? QString() : QSL("“%1”").arg(text)); +} + +QString GeminiParser::parseList(const QRegularExpressionMatch &mtch) const { + QString text = mtch.captured(1); + + return QSL("

• %1

\n").arg(text); +} + +QString GeminiParser::parseTextInNormalMode(const QString &line) const{ + return QSL("

%1

\n").arg(line); +} + +QString GeminiParser::parseInPreMode(const QString& line) const { + return QSL("
%1
\n").arg(line); +} diff --git a/src/librssguard/network-web/gemini/geminiparser.h b/src/librssguard/network-web/gemini/geminiparser.h new file mode 100644 index 000000000..af423076e --- /dev/null +++ b/src/librssguard/network-web/gemini/geminiparser.h @@ -0,0 +1,22 @@ +// For license of this file, see /LICENSE.md. + +#ifndef GEMINIPARSER_H +#define GEMINIPARSER_H + +#include +#include + +class GeminiParser { + public: + QString geminiToHtml(const QByteArray& gemini_data); + + private: + QString parseLink(const QRegularExpressionMatch& mtch) const; + QString parseHeading(const QRegularExpressionMatch& mtch) const; + QString parseQuote(const QRegularExpressionMatch& mtch) const; + QString parseList(const QRegularExpressionMatch& mtch) const; + QString parseTextInNormalMode(const QString& line) const; + QString parseInPreMode(const QString& line) const; +}; + +#endif // GEMINIPARSER_H