initial support for gemini, will be extended for webengine variant soon, gemini2html parser is rather basic

This commit is contained in:
Martin Rotter 2024-12-18 12:31:06 +01:00
parent 9cb9b7162c
commit a5eaf87d53
8 changed files with 469 additions and 180 deletions

View file

@ -255,6 +255,8 @@ set(SOURCES
network-web/adblock/adblockrequestinfo.h network-web/adblock/adblockrequestinfo.h
network-web/gemini/geminiclient.cpp network-web/gemini/geminiclient.cpp
network-web/gemini/geminiclient.h network-web/gemini/geminiclient.h
network-web/gemini/geminiparser.cpp
network-web/gemini/geminiparser.h
network-web/apiserver.cpp network-web/apiserver.cpp
network-web/apiserver.h network-web/apiserver.h
network-web/articleparse.cpp network-web/articleparse.cpp

View file

@ -30,7 +30,6 @@
#include "miscellaneous/settings.h" #include "miscellaneous/settings.h"
#include "network-web/adblock/adblockicon.h" #include "network-web/adblock/adblockicon.h"
#include "network-web/adblock/adblockmanager.h" #include "network-web/adblock/adblockmanager.h"
#include "network-web/gemini/geminiclient.h"
#include "network-web/webfactory.h" #include "network-web/webfactory.h"
#include "services/abstract/serviceroot.h" #include "services/abstract/serviceroot.h"

View file

@ -4,6 +4,7 @@
#include "miscellaneous/application.h" #include "miscellaneous/application.h"
#include "network-web/cookiejar.h" #include "network-web/cookiejar.h"
#include "network-web/gemini/geminiparser.h"
#include "network-web/networkfactory.h" #include "network-web/networkfactory.h"
#include "network-web/silentnetworkaccessmanager.h" #include "network-web/silentnetworkaccessmanager.h"
#include "network-web/webfactory.h" #include "network-web/webfactory.h"
@ -14,15 +15,20 @@
#include <QTimer> #include <QTimer>
Downloader::Downloader(QObject* parent) Downloader::Downloader(QObject* parent)
: QObject(parent), m_activeReply(nullptr), m_downloadManager(new SilentNetworkAccessManager(this)), : QObject(parent), m_geminiClient(new GeminiClient(this)), m_activeReply(nullptr),
m_timer(new QTimer(this)), m_inputData(QByteArray()), m_inputMultipartData(nullptr), m_targetProtected(false), m_downloadManager(new SilentNetworkAccessManager(this)), m_timer(new QTimer(this)), m_inputData(QByteArray()),
m_targetUsername(QString()), m_targetPassword(QString()), m_lastOutputData({}), m_inputMultipartData(nullptr), m_targetProtected(false), m_targetUsername(QString()), m_targetPassword(QString()),
m_lastOutputError(QNetworkReply::NetworkError::NoError), m_lastHttpStatusCode(0), m_lastHeaders({}) { m_lastOutputData({}), m_lastOutputError(QNetworkReply::NetworkError::NoError), m_lastHttpStatusCode(0),
m_lastHeaders({}) {
m_timer->setInterval(DOWNLOAD_TIMEOUT); m_timer->setInterval(DOWNLOAD_TIMEOUT);
m_timer->setSingleShot(true); m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &Downloader::cancel); 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()); m_downloadManager->setCookieJar(qApp->web()->cookieJar());
qApp->web()->cookieJar()->setParent(nullptr); qApp->web()->cookieJar()->setParent(nullptr);
} }
@ -31,6 +37,44 @@ Downloader::~Downloader() {
qDebugNN << LOGSEC_NETWORK << "Destroying Downloader instance."; 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, void Downloader::downloadFile(const QString& url,
int timeout, int timeout,
bool protected_contents, bool protected_contents,
@ -80,6 +124,19 @@ void Downloader::manipulateData(const QString& url,
manipulateData(url, operation, data, nullptr, timeout, protected_contents, username, password); 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, void Downloader::manipulateData(const QString& url,
QNetworkAccessManager::Operation operation, QNetworkAccessManager::Operation operation,
const QByteArray& data, const QByteArray& data,
@ -89,48 +146,57 @@ void Downloader::manipulateData(const QString& url,
const QString& username, const QString& username,
const QString& password) { const QString& password) {
QString sanitized_url = NetworkFactory::sanitizeUrl(url); QString sanitized_url = NetworkFactory::sanitizeUrl(url);
auto cookies = CookieJar::extractCookiesFromUrl(sanitized_url);
if (!cookies.isEmpty()) { if (m_geminiClient->supportsUrl(sanitized_url)) {
qApp->web()->cookieJar()->setCookiesFromUrl(cookies, sanitized_url); QUrl gemini_url = QUrl::fromUserInput(sanitized_url);
runGeminiRequest(gemini_url);
} }
else {
QNetworkRequest request; auto cookies = CookieJar::extractCookiesFromUrl(sanitized_url);
QHashIterator<QByteArray, QByteArray> i(m_customHeaders);
while (i.hasNext()) { if (!cookies.isEmpty()) {
i.next(); qApp->web()->cookieJar()->setCookiesFromUrl(cookies, sanitized_url);
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); QNetworkRequest request;
QHashIterator<QByteArray, QByteArray> 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. // Download action timed-out, too slow connection or target is not reachable.
m_activeReply->abort(); m_activeReply->abort();
} }
else {
m_geminiClient->cancelRequest();
}
} }
void Downloader::appendRawHeader(const QByteArray& name, const QByteArray& value) { void Downloader::appendRawHeader(const QByteArray& name, const QByteArray& value) {

View file

@ -4,6 +4,7 @@
#define DOWNLOADER_H #define DOWNLOADER_H
#include "definitions/definitions.h" #include "definitions/definitions.h"
#include "network-web/gemini/geminiclient.h"
#include "network-web/httpresponse.h" #include "network-web/httpresponse.h"
#include <QHttpMultiPart> #include <QHttpMultiPart>
@ -74,6 +75,9 @@ class Downloader : public QObject {
void completed(const QUrl& url, QNetworkReply::NetworkError status, int http_code, QByteArray contents = {}); void completed(const QUrl& url, QNetworkReply::NetworkError status, int http_code, QByteArray contents = {});
private slots: 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. // Called when current reply is processed.
void finished(); void finished();
@ -97,8 +101,10 @@ class Downloader : public QObject {
void runPostRequest(const QNetworkRequest& request, QHttpMultiPart* multipart_data); void runPostRequest(const QNetworkRequest& request, QHttpMultiPart* multipart_data);
void runPostRequest(const QNetworkRequest& request, const QByteArray& data); void runPostRequest(const QNetworkRequest& request, const QByteArray& data);
void runGetRequest(const QNetworkRequest& request); void runGetRequest(const QNetworkRequest& request);
void runGeminiRequest(const QUrl& url);
private: private:
GeminiClient* m_geminiClient;
QNetworkReply* m_activeReply; QNetworkReply* m_activeReply;
QScopedPointer<SilentNetworkAccessManager> m_downloadManager; QScopedPointer<SilentNetworkAccessManager> m_downloadManager;
QTimer* m_timer; QTimer* m_timer;

View file

@ -1,138 +1,170 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
//
// This file is heavily inspired by https://github.com/ikskuh/kristall.
#include "network-web/gemini/geminiclient.h" #include "network-web/gemini/geminiclient.h"
#include "definitions/definitions.h"
#include <cassert> #include <cassert>
#include <QDebug> #include <QDebug>
#include <QRegExp> #include <QRegularExpression>
#include <QSslConfiguration> #include <QSslConfiguration>
#include <QUrl> #include <QUrl>
bool CryptoIdentity::isValid() const {
return !certificate.isNull() && !private_key.isNull();
}
bool CryptoIdentity::isHostFiltered(const QUrl& url) const { bool CryptoIdentity::isHostFiltered(const QUrl& url) const {
if (this->host_filter.isEmpty()) if (host_filter.isEmpty()) {
return false; return false;
}
QString url_text = url.toString(QUrl::FullyEncoded); 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.match(url_text).hasMatch();
return not pattern.exactMatch(url_text);
} }
bool CryptoIdentity::isAutomaticallyEnabledOn(const QUrl& url) const { bool CryptoIdentity::isAutomaticallyEnabledOn(const QUrl& url) const {
if (this->host_filter.isEmpty()) if (host_filter.isEmpty()) {
return false; return false;
if (not this->auto_enable) }
if (!auto_enable) {
return false; return false;
}
QString url_text = url.toString(QUrl::FullyEncoded); 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.match(url_text).hasMatch();
return pattern.exactMatch(url_text);
} }
GeminiClient::GeminiClient(QObject* parent) : QObject(parent) { GeminiClient::GeminiClient(QObject* parent) : QObject(parent) {
connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted); connect(&m_socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted);
connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead); connect(&m_socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead);
connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected); connect(&m_socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected);
connect(&m_socket, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors);
// connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) { // connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) {
// qDebug() << "Socket state changed to " << 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)) #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 #else
connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GeminiClient::socketError); connect(&m_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GeminiClient::socketError);
#endif #endif
// States // States
connect(&socket, &QAbstractSocket::hostFound, this, [this]() { connect(&m_socket, &QAbstractSocket::hostFound, this, [this]() {
emit this->requestStateChange(RequestState::HostFound); emit this->requestStateChange(RequestState::HostFound);
}); });
connect(&socket, &QAbstractSocket::connected, this, [this]() {
connect(&m_socket, &QAbstractSocket::connected, this, [this]() {
emit this->requestStateChange(RequestState::Connected); 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 this->requestStateChange(RequestState::None);
emit requestStateChange(RequestState::None);
} }
GeminiClient::~GeminiClient() { GeminiClient::~GeminiClient() {
is_receiving_body = false; m_isReceivingBody = false;
} }
bool GeminiClient::supportsScheme(const QString& scheme) const { bool GeminiClient::supportsUrl(const QString& url) const {
return (scheme == "gemini"); 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) { bool GeminiClient::startRequest(const QUrl& url, RequestOptions options) {
if (url.scheme() != "gemini") if (!supportsUrl(url)) {
return false; return false;
}
// qDebug() << "start request" << url; // qDebug() << "start request" << url;
if (socket.state() != QTcpSocket::UnconnectedState) { if (m_socket.state() != QTcpSocket::UnconnectedState) {
socket.disconnectFromHost(); m_socket.disconnectFromHost();
socket.close(); m_socket.close();
if (not socket.waitForDisconnected(1500))
if (!m_socket.waitForDisconnected(1500)) {
return false; 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 = m_socket.sslConfiguration();
QSslConfiguration ssl_config = socket.sslConfiguration();
ssl_config.setProtocol(QSsl::TlsV1_2OrLater); ssl_config.setProtocol(QSsl::TlsV1_2OrLater);
/* /*
if (not kristall::globals().trust.gemini.enable_ca) if (not kristall::globals().trust.gemini.enable_ca)
ssl_config.setCaCertificates(QList<QSslCertificate>{}); ssl_config.setCaCertificates(QList<QSslCertificate>{});
else else
*/ */
ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates()); 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(); if (!m_socket.isOpen()) {
this->body.clear();
this->is_receiving_body = false;
this->suppress_socket_tls_error = true;
if (not socket.isOpen())
return false; return false;
}
target_url = url; m_targetUrl = url;
mime_type = "<invalid>"; m_mimeType = "<invalid>";
return true; return true;
} }
bool GeminiClient::isInProgress() const { bool GeminiClient::inProgress() const {
return (socket.state() != QTcpSocket::UnconnectedState); return m_socket.state() != QTcpSocket::UnconnectedState;
} }
bool GeminiClient::cancelRequest() { bool GeminiClient::cancelRequest() {
// qDebug() << "cancel request" << isInProgress(); // qDebug() << "cancel request" << isInProgress();
if (isInProgress()) { if (inProgress()) {
this->is_receiving_body = false; m_isReceivingBody = false;
this->socket.disconnectFromHost(); m_socket.disconnectFromHost();
this->buffer.clear(); m_buffer.clear();
this->body.clear(); m_body.clear();
if (socket.state() != QTcpSocket::UnconnectedState) {
socket.disconnectFromHost(); if (m_socket.state() != QTcpSocket::UnconnectedState) {
m_socket.disconnectFromHost();
} }
this->socket.waitForDisconnected(500);
this->socket.close(); m_socket.waitForDisconnected(500);
bool success = not isInProgress(); m_socket.close();
bool success = !inProgress();
// qDebug() << "cancel success" << success; // qDebug() << "cancel success" << success;
return success; return success;
} }
else { else {
@ -141,14 +173,15 @@ bool GeminiClient::cancelRequest() {
} }
bool GeminiClient::enableClientCertificate(const CryptoIdentity& ident) { bool GeminiClient::enableClientCertificate(const CryptoIdentity& ident) {
this->socket.setLocalCertificate(ident.certificate); m_socket.setLocalCertificate(ident.certificate);
this->socket.setPrivateKey(ident.private_key); m_socket.setPrivateKey(ident.private_key);
return true; return true;
} }
void GeminiClient::disableClientCertificate() { void GeminiClient::disableClientCertificate() {
this->socket.setLocalCertificate(QSslCertificate{}); m_socket.setLocalCertificate(QSslCertificate{});
this->socket.setPrivateKey(QSslKey{}); m_socket.setPrivateKey(QSslKey{});
} }
void GeminiClient::emitNetworkError(QAbstractSocket::SocketError error_code, const QString& textual_description) { 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: case QAbstractSocket::ConnectionRefusedError:
network_error = ConnectionRefused; network_error = ConnectionRefused;
break; break;
case QAbstractSocket::HostNotFoundError: case QAbstractSocket::HostNotFoundError:
network_error = HostNotFound; network_error = HostNotFound;
break; break;
case QAbstractSocket::SocketTimeoutError: case QAbstractSocket::SocketTimeoutError:
network_error = Timeout; network_error = Timeout;
break; break;
case QAbstractSocket::SslHandshakeFailedError: case QAbstractSocket::SslHandshakeFailedError:
network_error = TlsFailure; network_error = TlsFailure;
break; break;
case QAbstractSocket::SslInternalError: case QAbstractSocket::SslInternalError:
network_error = TlsFailure; network_error = TlsFailure;
break; break;
case QAbstractSocket::SslInvalidUserDataError: case QAbstractSocket::SslInvalidUserDataError:
network_error = TlsFailure; network_error = TlsFailure;
break; break;
default: default:
qDebug() << "unhandled network error:" << error_code; qDebug() << "unhandled network error:" << error_code;
break; break;
} }
emit this->networkError(network_error, textual_description); emit networkError(network_error, textual_description);
} }
void GeminiClient::socketEncrypted() { 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(); QByteArray request_bytes = request.toUtf8();
qint64 offset = 0; qint64 offset = 0;
while (offset < request_bytes.size()) { 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) { if (len <= 0) {
socket.close(); m_socket.close();
return; return;
} }
offset += len; offset += len;
} }
} }
void GeminiClient::socketReadyRead() { void GeminiClient::socketReadyRead() {
if (this->is_error_state) // don't do any further if (m_inErrorState) {
return; return;
QByteArray response = socket.readAll(); }
if (is_receiving_body) { QByteArray response = m_socket.readAll();
body.append(response);
emit this->requestProgress(body.size()); if (m_isReceivingBody) {
m_body.append(response);
emit requestProgress(m_body.size());
} }
else { else {
for (int i = 0; i < response.size(); i++) { for (int i = 0; i < response.size(); i++) {
if (response[i] == '\n') { if (response[i] == '\n') {
buffer.append(response.data(), i); m_buffer.append(response.data(), i);
body.append(response.data() + i + 1, response.size() - i - 1); m_body.append(response.data() + i + 1, response.size() - i - 1);
// "XY " <META> <CR> <LF> // "XY " <META> <CR> <LF>
if (buffer.size() < 4) { // we allow an empty <META> if (m_buffer.size() < 4) { // we allow an empty <META>
socket.close(); m_socket.close();
qDebug() << buffer;
emit networkError(ProtocolViolation, QObject::tr("Line is too short for valid protocol")); qDebug() << m_buffer;
emit networkError(ProtocolViolation, tr("Line is too short for valid protocol"));
return; return;
} }
if (buffer.size() >= 1200) { if (m_buffer.size() >= 1200) {
emit networkError(ProtocolViolation, QObject::tr("response too large!")); emit networkError(ProtocolViolation, tr("response too large!"));
socket.close();
m_socket.close();
} }
if (buffer[buffer.size() - 1] != '\r') { if (m_buffer[m_buffer.size() - 1] != '\r') {
socket.close(); m_socket.close();
qDebug() << buffer;
emit networkError(ProtocolViolation, QObject::tr("Line does not end with <CR> <LF>")); qDebug() << m_buffer;
emit networkError(ProtocolViolation, tr("Line does not end with <CR> <LF>"));
return; return;
} }
if (not isdigit(buffer[0])) { if (!isdigit(m_buffer[0])) {
socket.close(); m_socket.close();
qDebug() << buffer;
emit networkError(ProtocolViolation, QObject::tr("First character is not a digit.")); qDebug() << m_buffer;
emit networkError(ProtocolViolation, tr("First character is not a digit."));
return; return;
} }
if (not isdigit(buffer[1])) { if (not isdigit(m_buffer[1])) {
socket.close(); m_socket.close();
qDebug() << buffer;
emit networkError(ProtocolViolation, QObject::tr("Second character is not a digit.")); qDebug() << m_buffer;
emit networkError(ProtocolViolation, tr("Second character is not a digit."));
return; return;
} }
// TODO: Implement stricter version // TODO: Implement stricter version
// if(buffer[2] != ' ') { // if(buffer[2] != ' ') {
if (not isspace(buffer[2])) { if (!isspace(m_buffer[2])) {
socket.close(); m_socket.close();
qDebug() << buffer;
emit networkError(ProtocolViolation, QObject::tr("Third character is not a space.")); qDebug() << m_buffer;
emit networkError(ProtocolViolation, tr("Third character is not a space."));
return; 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 primary_code = m_buffer[0] - '0';
int secondary_code = buffer[1] - '0'; int secondary_code = m_buffer[1] - '0';
qDebug() << primary_code << secondary_code << meta; qDebug() << primary_code << secondary_code << meta;
// We don't need to receive any data after that. // We don't need to receive any data after that.
if (primary_code != 2) if (primary_code != 2) {
socket.close(); m_socket.close();
}
switch (primary_code) { switch (primary_code) {
case 1: // requesting input case 1: // requesting input
@ -269,69 +332,87 @@ void GeminiClient::socketReadyRead() {
case 1: case 1:
emit inputRequired(meta, true); emit inputRequired(meta, true);
break; break;
case 0: case 0:
default: default:
emit inputRequired(meta, false); emit inputRequired(meta, false);
} }
return; return;
case 2: // success case 2: // success
is_receiving_body = true; m_isReceivingBody = true;
mime_type = meta; m_mimeType = meta;
return; return;
case 3: { // redirect case 3: { // redirect
QUrl new_url(meta); QUrl new_url(meta);
if (new_url.isValid()) { if (new_url.isValid()) {
if (new_url.isRelative()) if (new_url.isRelative()) {
new_url = target_url.resolved(new_url); new_url = m_targetUrl.resolved(new_url);
}
assert(not new_url.isRelative()); assert(not new_url.isRelative());
emit redirected(new_url, (secondary_code == 1)); emit redirected(new_url, (secondary_code == 1));
} }
else { else {
emit networkError(ProtocolViolation, QObject::tr("Invalid URL for redirection!")); emit networkError(ProtocolViolation, tr("Invalid URL for redirection!"));
} }
return; return;
} }
case 4: { // temporary failure case 4: { // temporary failure
NetworkError type = UnknownError; NetworkError type = UnknownError;
switch (secondary_code) { switch (secondary_code) {
case 1: case 1:
type = InternalServerError; type = InternalServerError;
break; break;
case 2: case 2:
type = InternalServerError; type = InternalServerError;
break; break;
case 3: case 3:
type = InternalServerError; type = InternalServerError;
break; break;
case 4: case 4:
type = UnknownError; type = UnknownError;
break; break;
} }
emit networkError(type, meta); emit networkError(type, meta);
return; return;
} }
case 5: { // permanent failure case 5: { // permanent failure
NetworkError type = UnknownError; NetworkError type = UnknownError;
switch (secondary_code) { switch (secondary_code) {
case 1: case 1:
type = ResourceNotFound; type = ResourceNotFound;
break; break;
case 2: case 2:
type = ResourceNotFound; type = ResourceNotFound;
break; break;
case 3: case 3:
type = ProxyRequest; type = ProxyRequest;
break; break;
case 9: case 9:
type = BadRequest; type = BadRequest;
break; break;
} }
emit networkError(type, meta); emit networkError(type, meta);
return; return;
} }
@ -350,36 +431,40 @@ void GeminiClient::socketReadyRead() {
emit networkError(InvalidClientCertificate, meta); emit networkError(InvalidClientCertificate, meta);
return; return;
} }
return; return;
default: default:
emit networkError(ProtocolViolation, QObject::tr("Unspecified status code used!")); emit networkError(ProtocolViolation, tr("Unspecified status code used!"));
return; return;
} }
assert(false and "unreachable"); assert(false && "unreachable");
} }
} }
if ((buffer.size() + response.size()) >= 1200) { if ((m_buffer.size() + response.size()) >= 1200) {
emit networkError(ProtocolViolation, QObject::tr("META too large!")); emit networkError(ProtocolViolation, tr("META too large!"));
socket.close();
m_socket.close();
} }
buffer.append(response);
m_buffer.append(response);
} }
} }
void GeminiClient::socketDisconnected() { void GeminiClient::socketDisconnected() {
if (this->is_receiving_body and not this->is_error_state) { if (m_isReceivingBody && !m_inErrorState) {
body.append(socket.readAll()); m_body.append(m_socket.readAll());
emit requestComplete(body, mime_type); emit requestComplete(m_body, m_mimeType);
} }
} }
void GeminiClient::sslErrors(const QList<QSslError>& errors) { void GeminiClient::sslErrors(const QList<QSslError>& errors) {
emit this->hostCertificateLoaded(this->socket.peerCertificate()); emit hostCertificateLoaded(m_socket.peerCertificate());
if (options & IgnoreTlsErrors) { if (m_options & IgnoreTlsErrors) {
socket.ignoreSslErrors(errors); m_socket.ignoreSslErrors(errors);
return; return;
} }
@ -428,7 +513,7 @@ void GeminiClient::sslErrors(const QList<QSslError>& errors) {
} }
} }
socket.ignoreSslErrors(ignored_errors); m_socket.ignoreSslErrors(ignored_errors);
qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size(); qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size();
@ -437,7 +522,7 @@ void GeminiClient::sslErrors(const QList<QSslError>& errors) {
} }
if (remaining_errors.size() > 0) { 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 // This is more sane then erroring out here as it's a perfectly legal
// state and we know the TLS connection has ended. // state and we know the TLS connection has ended.
if (socketError == QAbstractSocket::RemoteHostClosedError) { if (socketError == QAbstractSocket::RemoteHostClosedError) {
socket.close(); m_socket.close();
return; return;
} }
this->is_error_state = true; m_inErrorState = true;
if (not this->suppress_socket_tls_error) {
this->emitNetworkError(socketError, socket.errorString()); if (!m_suppressSocketTlsErrors) {
emitNetworkError(socketError, m_socket.errorString());
} }
} }
QUrl GeminiClient::targetUrl() const {
return m_targetUrl;
}

View file

@ -1,5 +1,9 @@
#ifndef GEMINICLIENT_HPP // For license of this file, see <project-root-folder>/LICENSE.md.
#define GEMINICLIENT_HPP //
// This file is heavily inspired by https://github.com/ikskuh/kristall.
#ifndef GEMINICLIENT_H
#define GEMINICLIENT_H
#include <QMimeType> #include <QMimeType>
#include <QObject> #include <QObject>
@ -33,9 +37,7 @@ struct CryptoIdentity {
//! the certificate will be automatically enabled for hosts matching the filter. //! the certificate will be automatically enabled for hosts matching the filter.
bool auto_enable = false; bool auto_enable = false;
bool isValid() const { bool isValid() const;
return (not this->certificate.isNull()) and (not this->private_key.isNull());
}
//! returns true if a host does not match the filter criterion //! returns true if a host does not match the filter criterion
bool isHostFiltered(const QUrl& url) const; bool isHostFiltered(const QUrl& url) const;
@ -82,15 +84,18 @@ class GeminiClient : public QObject {
explicit GeminiClient(QObject* parent = nullptr); explicit GeminiClient(QObject* parent = nullptr);
virtual ~GeminiClient(); 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 startRequest(const QUrl& url, RequestOptions options);
bool isInProgress() const; bool inProgress() const;
bool cancelRequest(); bool cancelRequest();
bool enableClientCertificate(const CryptoIdentity& ident); bool enableClientCertificate(const CryptoIdentity& ident);
void disableClientCertificate(); void disableClientCertificate();
QUrl targetUrl() const;
signals: signals:
//! We successfully transferred some bytes from the server //! We successfully transferred some bytes from the server
void requestProgress(qint64 transferred); void requestProgress(qint64 transferred);
@ -127,16 +132,16 @@ class GeminiClient : public QObject {
void socketError(QAbstractSocket::SocketError socketError); void socketError(QAbstractSocket::SocketError socketError);
private: private:
bool is_receiving_body; bool m_isReceivingBody;
bool suppress_socket_tls_error; bool m_suppressSocketTlsErrors;
bool is_error_state; bool m_inErrorState;
QUrl target_url; QUrl m_targetUrl;
QSslSocket socket; QSslSocket m_socket;
QByteArray buffer; QByteArray m_buffer;
QByteArray body; QByteArray m_body;
QString mime_type; QString m_mimeType;
RequestOptions options; RequestOptions m_options;
}; };
#endif // GEMINICLIENT_HPP #endif // GEMINICLIENT_H

View file

@ -0,0 +1,96 @@
// For license of this file, see <project-root-folder>/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("<p>🔗 <a href=\"%1\">%2</a></p>\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("<h%1>%2</h%1>\n").arg(QString::number(level), header);
}
QString GeminiParser::parseQuote(const QRegularExpressionMatch &mtch) const {
QString text = mtch.captured(1);
return QSL("<p align=\"center\" style=\""
"background-color: #E1E5EE;"
"font-style: italic;"
"margin-left: 20px;"
"margin-right: 20px;"
"\">%1</p>\n").arg(text.isEmpty() ? QString() : QSL("“%1”").arg(text));
}
QString GeminiParser::parseList(const QRegularExpressionMatch &mtch) const {
QString text = mtch.captured(1);
return QSL("<p style=\""
"margin-left: 20px;"
"\">• %1</p>\n").arg(text);
}
QString GeminiParser::parseTextInNormalMode(const QString &line) const{
return QSL("<p>%1</p>\n").arg(line);
}
QString GeminiParser::parseInPreMode(const QString& line) const {
return QSL("<pre>%1</pre>\n").arg(line);
}

View file

@ -0,0 +1,22 @@
// For license of this file, see <project-root-folder>/LICENSE.md.
#ifndef GEMINIPARSER_H
#define GEMINIPARSER_H
#include <QString>
#include <QRegularExpressionMatch>
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