initial support for gemini, will be extended for webengine variant soon, gemini2html parser is rather basic
This commit is contained in:
parent
9cb9b7162c
commit
a5eaf87d53
8 changed files with 469 additions and 180 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
96
src/librssguard/network-web/gemini/geminiparser.cpp
Normal file
96
src/librssguard/network-web/gemini/geminiparser.cpp
Normal 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);
|
||||||
|
}
|
22
src/librssguard/network-web/gemini/geminiparser.h
Normal file
22
src/librssguard/network-web/gemini/geminiparser.h
Normal 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
|
Loading…
Add table
Reference in a new issue