mirror of
https://github.com/martinrotter/rssguard.git
synced 2025-02-02 10:27:15 +01:00
initial gemini sources
This commit is contained in:
parent
9326418478
commit
9cb9b7162c
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,4 +20,5 @@ resources/skins/*/*.map
|
||||
aqtinstall.log
|
||||
.sass-cache
|
||||
build-dir/
|
||||
build/
|
||||
docs/build
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
"title": "GNU GPL v3.0",
|
||||
"file": "COPYING_GNU_GPL",
|
||||
"components": "RSS Guard, mimesis, Numix"
|
||||
"components": "RSS Guard, mimesis, Numix, Kristall/geminiclient"
|
||||
},
|
||||
{
|
||||
"title": "GNU LGPL v3.0",
|
||||
|
@ -253,6 +253,8 @@ set(SOURCES
|
||||
network-web/adblock/adblockmanager.h
|
||||
network-web/adblock/adblockrequestinfo.cpp
|
||||
network-web/adblock/adblockrequestinfo.h
|
||||
network-web/gemini/geminiclient.cpp
|
||||
network-web/gemini/geminiclient.h
|
||||
network-web/apiserver.cpp
|
||||
network-web/apiserver.h
|
||||
network-web/articleparse.cpp
|
||||
|
@ -267,6 +267,7 @@ void FeedDownloader::updateOneFeed(ServiceRoot* acc,
|
||||
<< " microseconds.";
|
||||
|
||||
QList<Message> read_msgs, important_msgs;
|
||||
QHash<int, bool> loaded_filters;
|
||||
|
||||
for (int i = 0; i < msgs.size(); i++) {
|
||||
Message msg_original(msgs[i]);
|
||||
@ -295,7 +296,10 @@ void FeedDownloader::updateOneFeed(ServiceRoot* acc,
|
||||
tmr.restart();
|
||||
|
||||
try {
|
||||
MessageObject::FilteringAction decision = msg_filter->filterMessage(&filter_engine);
|
||||
MessageObject::FilteringAction decision =
|
||||
msg_filter->filterMessage(&filter_engine, !loaded_filters.contains(msg_filter->id()));
|
||||
|
||||
loaded_filters.insert(msg_filter->id(), true);
|
||||
|
||||
qDebugNN << LOGSEC_FEEDDOWNLOADER << "Running filter script, it took " << tmr.nsecsElapsed() / 1000
|
||||
<< " microseconds.";
|
||||
|
@ -8,17 +8,21 @@
|
||||
|
||||
MessageFilter::MessageFilter(int id, QObject* parent) : QObject(parent), m_id(id) {}
|
||||
|
||||
MessageObject::FilteringAction MessageFilter::filterMessage(QJSEngine* engine) {
|
||||
QJSValue filter_func = engine->evaluate(qApp->replaceUserDataFolderPlaceholder(m_script));
|
||||
MessageObject::FilteringAction MessageFilter::filterMessage(QJSEngine* engine, bool evaluate_filter) {
|
||||
if (evaluate_filter) {
|
||||
QJSValue filter_func =
|
||||
engine->evaluate(qApp->replaceUserDataFolderPlaceholder(m_script).replace(QSL("filterMessage()"),
|
||||
QSL("filterMessage%1()").arg(m_id)));
|
||||
|
||||
if (filter_func.isError()) {
|
||||
QJSValue::ErrorType error = filter_func.errorType();
|
||||
QString message = filter_func.toString();
|
||||
if (filter_func.isError()) {
|
||||
QJSValue::ErrorType error = filter_func.errorType();
|
||||
QString message = filter_func.toString();
|
||||
|
||||
throw FilteringException(error, message);
|
||||
throw FilteringException(error, message);
|
||||
}
|
||||
}
|
||||
|
||||
auto filter_output = engine->evaluate(QSL("filterMessage()"));
|
||||
auto filter_output = engine->evaluate(QSL("filterMessage%1()").arg(m_id));
|
||||
|
||||
if (filter_output.isError()) {
|
||||
QJSValue::ErrorType error = filter_output.errorType();
|
||||
|
@ -16,7 +16,7 @@ class RSSGUARD_DLLSPEC MessageFilter : public QObject {
|
||||
public:
|
||||
explicit MessageFilter(int id = -1, QObject* parent = nullptr);
|
||||
|
||||
MessageObject::FilteringAction filterMessage(QJSEngine* engine);
|
||||
MessageObject::FilteringAction filterMessage(QJSEngine* engine, bool evaluate_filter = true);
|
||||
|
||||
int id() const;
|
||||
void setId(int id);
|
||||
|
@ -30,6 +30,7 @@
|
||||
#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"
|
||||
|
||||
@ -97,20 +98,6 @@ Application::Application(const QString& id, int& argc, char** argv, const QStrin
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
QString aa = "Fri, 12 Apr 2024 5:23:57 GMT";
|
||||
QDateTimeParser par(QMetaType::QDateTime, QDateTimeParser::FromString, QCalendar());
|
||||
|
||||
par.setDefaultLocale(QLocale::c());
|
||||
|
||||
QString st = "ddd, dd MMM yyyy H:m:s";
|
||||
bool parsed = par.parseFormat(st);
|
||||
QDateTime dt;
|
||||
par.fromString(aa, &dt);
|
||||
|
||||
// QDateTime tim = QDateTime::fromString(aa, form);
|
||||
QString check = dt.toString();
|
||||
*/
|
||||
QString custom_ua;
|
||||
|
||||
parseCmdArgumentsFromMyInstance(raw_cli_args, custom_ua);
|
||||
@ -1390,13 +1377,20 @@ void Application::fillCmdArgumentsParser(QCommandLineParser& parser) {
|
||||
.arg(MAX_THREADPOOL_THREADS),
|
||||
QSL("count"));
|
||||
|
||||
parser.addOptions({
|
||||
help, version, log_file, custom_data_folder, disable_singleinstance, disable_only_debug, disable_debug,
|
||||
parser.addOptions({help,
|
||||
version,
|
||||
log_file,
|
||||
custom_data_folder,
|
||||
disable_singleinstance,
|
||||
disable_only_debug,
|
||||
disable_debug,
|
||||
#if defined(NO_LITE)
|
||||
force_lite,
|
||||
force_lite,
|
||||
#endif
|
||||
forced_style, adblock_port, custom_ua, custom_threads
|
||||
});
|
||||
forced_style,
|
||||
adblock_port,
|
||||
custom_ua,
|
||||
custom_threads});
|
||||
parser.addPositionalArgument(QSL("urls"),
|
||||
QSL("List of URL addresses pointing to individual online feeds which should be added."),
|
||||
QSL("[url-1 ... url-n]"));
|
||||
|
457
src/librssguard/network-web/gemini/geminiclient.cpp
Normal file
457
src/librssguard/network-web/gemini/geminiclient.cpp
Normal file
@ -0,0 +1,457 @@
|
||||
#include "network-web/gemini/geminiclient.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QRegExp>
|
||||
#include <QSslConfiguration>
|
||||
#include <QUrl>
|
||||
|
||||
bool CryptoIdentity::isHostFiltered(const QUrl& url) const {
|
||||
if (this->host_filter.isEmpty())
|
||||
return false;
|
||||
|
||||
QString url_text = url.toString(QUrl::FullyEncoded);
|
||||
|
||||
QRegExp pattern{this->host_filter, Qt::CaseInsensitive, QRegExp::Wildcard};
|
||||
|
||||
return not pattern.exactMatch(url_text);
|
||||
}
|
||||
|
||||
bool CryptoIdentity::isAutomaticallyEnabledOn(const QUrl& url) const {
|
||||
if (this->host_filter.isEmpty())
|
||||
return false;
|
||||
if (not this->auto_enable)
|
||||
return false;
|
||||
|
||||
QString url_text = url.toString(QUrl::FullyEncoded);
|
||||
|
||||
QRegExp pattern{this->host_filter, Qt::CaseInsensitive, QRegExp::Wildcard};
|
||||
|
||||
return pattern.exactMatch(url_text);
|
||||
}
|
||||
|
||||
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(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) {
|
||||
// qDebug() << "Socket state changed to " << state;
|
||||
// });
|
||||
connect(&socket, QOverload<const QList<QSslError>&>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors);
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
|
||||
connect(&socket, &QTcpSocket::errorOccurred, this, &GeminiClient::socketError);
|
||||
#else
|
||||
connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GeminiClient::socketError);
|
||||
#endif
|
||||
|
||||
// States
|
||||
connect(&socket, &QAbstractSocket::hostFound, this, [this]() {
|
||||
emit this->requestStateChange(RequestState::HostFound);
|
||||
});
|
||||
connect(&socket, &QAbstractSocket::connected, this, [this]() {
|
||||
emit this->requestStateChange(RequestState::Connected);
|
||||
});
|
||||
connect(&socket, &QAbstractSocket::disconnected, this, [this]() {
|
||||
emit this->requestStateChange(RequestState::None);
|
||||
});
|
||||
emit this->requestStateChange(RequestState::None);
|
||||
}
|
||||
|
||||
GeminiClient::~GeminiClient() {
|
||||
is_receiving_body = false;
|
||||
}
|
||||
|
||||
bool GeminiClient::supportsScheme(const QString& scheme) const {
|
||||
return (scheme == "gemini");
|
||||
}
|
||||
|
||||
bool GeminiClient::startRequest(const QUrl& url, RequestOptions options) {
|
||||
if (url.scheme() != "gemini")
|
||||
return false;
|
||||
|
||||
// qDebug() << "start request" << url;
|
||||
|
||||
if (socket.state() != QTcpSocket::UnconnectedState) {
|
||||
socket.disconnectFromHost();
|
||||
socket.close();
|
||||
if (not socket.waitForDisconnected(1500))
|
||||
return false;
|
||||
}
|
||||
|
||||
emit this->requestStateChange(RequestState::Started);
|
||||
|
||||
this->is_error_state = false;
|
||||
|
||||
this->options = options;
|
||||
|
||||
QSslConfiguration ssl_config = socket.sslConfiguration();
|
||||
ssl_config.setProtocol(QSsl::TlsV1_2OrLater);
|
||||
/*
|
||||
if (not kristall::globals().trust.gemini.enable_ca)
|
||||
ssl_config.setCaCertificates(QList<QSslCertificate>{});
|
||||
else
|
||||
*/
|
||||
ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates());
|
||||
/*
|
||||
*/
|
||||
|
||||
socket.setSslConfiguration(ssl_config);
|
||||
|
||||
socket.connectToHostEncrypted(url.host(), url.port(1965));
|
||||
|
||||
this->buffer.clear();
|
||||
this->body.clear();
|
||||
this->is_receiving_body = false;
|
||||
this->suppress_socket_tls_error = true;
|
||||
|
||||
if (not socket.isOpen())
|
||||
return false;
|
||||
|
||||
target_url = url;
|
||||
mime_type = "<invalid>";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GeminiClient::isInProgress() const {
|
||||
return (socket.state() != QTcpSocket::UnconnectedState);
|
||||
}
|
||||
|
||||
bool GeminiClient::cancelRequest() {
|
||||
// qDebug() << "cancel request" << isInProgress();
|
||||
if (isInProgress()) {
|
||||
this->is_receiving_body = false;
|
||||
this->socket.disconnectFromHost();
|
||||
this->buffer.clear();
|
||||
this->body.clear();
|
||||
if (socket.state() != QTcpSocket::UnconnectedState) {
|
||||
socket.disconnectFromHost();
|
||||
}
|
||||
this->socket.waitForDisconnected(500);
|
||||
this->socket.close();
|
||||
bool success = not isInProgress();
|
||||
// qDebug() << "cancel success" << success;
|
||||
return success;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool GeminiClient::enableClientCertificate(const CryptoIdentity& ident) {
|
||||
this->socket.setLocalCertificate(ident.certificate);
|
||||
this->socket.setPrivateKey(ident.private_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GeminiClient::disableClientCertificate() {
|
||||
this->socket.setLocalCertificate(QSslCertificate{});
|
||||
this->socket.setPrivateKey(QSslKey{});
|
||||
}
|
||||
|
||||
void GeminiClient::emitNetworkError(QAbstractSocket::SocketError error_code, const QString& textual_description) {
|
||||
NetworkError network_error = UnknownError;
|
||||
|
||||
switch (error_code) {
|
||||
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);
|
||||
}
|
||||
|
||||
void GeminiClient::socketEncrypted() {
|
||||
emit this->hostCertificateLoaded(this->socket.peerCertificate());
|
||||
|
||||
QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n";
|
||||
|
||||
QByteArray request_bytes = request.toUtf8();
|
||||
|
||||
qint64 offset = 0;
|
||||
while (offset < request_bytes.size()) {
|
||||
const auto len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset);
|
||||
if (len <= 0) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
offset += len;
|
||||
}
|
||||
}
|
||||
|
||||
void GeminiClient::socketReadyRead() {
|
||||
if (this->is_error_state) // don't do any further
|
||||
return;
|
||||
QByteArray response = socket.readAll();
|
||||
|
||||
if (is_receiving_body) {
|
||||
body.append(response);
|
||||
emit this->requestProgress(body.size());
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < response.size(); i++) {
|
||||
if (response[i] == '\n') {
|
||||
buffer.append(response.data(), i);
|
||||
body.append(response.data() + i + 1, response.size() - i - 1);
|
||||
|
||||
// "XY " <META> <CR> <LF>
|
||||
if (buffer.size() < 4) { // we allow an empty <META>
|
||||
socket.close();
|
||||
qDebug() << buffer;
|
||||
emit networkError(ProtocolViolation, QObject::tr("Line is too short for valid protocol"));
|
||||
return;
|
||||
}
|
||||
if (buffer.size() >= 1200) {
|
||||
emit networkError(ProtocolViolation, QObject::tr("response too large!"));
|
||||
socket.close();
|
||||
}
|
||||
if (buffer[buffer.size() - 1] != '\r') {
|
||||
socket.close();
|
||||
qDebug() << buffer;
|
||||
emit networkError(ProtocolViolation, QObject::tr("Line does not end with <CR> <LF>"));
|
||||
return;
|
||||
}
|
||||
if (not isdigit(buffer[0])) {
|
||||
socket.close();
|
||||
qDebug() << buffer;
|
||||
emit networkError(ProtocolViolation, QObject::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."));
|
||||
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."));
|
||||
return;
|
||||
}
|
||||
|
||||
QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4);
|
||||
|
||||
int primary_code = buffer[0] - '0';
|
||||
int secondary_code = buffer[1] - '0';
|
||||
|
||||
qDebug() << primary_code << secondary_code << meta;
|
||||
|
||||
// We don't need to receive any data after that.
|
||||
if (primary_code != 2)
|
||||
socket.close();
|
||||
|
||||
switch (primary_code) {
|
||||
case 1: // requesting input
|
||||
switch (secondary_code) {
|
||||
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;
|
||||
return;
|
||||
|
||||
case 3: { // redirect
|
||||
QUrl new_url(meta);
|
||||
if (new_url.isValid()) {
|
||||
if (new_url.isRelative())
|
||||
new_url = target_url.resolved(new_url);
|
||||
assert(not new_url.isRelative());
|
||||
|
||||
emit redirected(new_url, (secondary_code == 1));
|
||||
}
|
||||
else {
|
||||
emit networkError(ProtocolViolation, QObject::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;
|
||||
}
|
||||
|
||||
case 6: // client certificate required
|
||||
switch (secondary_code) {
|
||||
case 0:
|
||||
emit certificateRequired(meta);
|
||||
return;
|
||||
|
||||
case 1:
|
||||
emit networkError(Unauthorized, meta);
|
||||
return;
|
||||
|
||||
default:
|
||||
case 2:
|
||||
emit networkError(InvalidClientCertificate, meta);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
emit networkError(ProtocolViolation, QObject::tr("Unspecified status code used!"));
|
||||
return;
|
||||
}
|
||||
|
||||
assert(false and "unreachable");
|
||||
}
|
||||
}
|
||||
if ((buffer.size() + response.size()) >= 1200) {
|
||||
emit networkError(ProtocolViolation, QObject::tr("META too large!"));
|
||||
socket.close();
|
||||
}
|
||||
buffer.append(response);
|
||||
}
|
||||
}
|
||||
|
||||
void GeminiClient::socketDisconnected() {
|
||||
if (this->is_receiving_body and not this->is_error_state) {
|
||||
body.append(socket.readAll());
|
||||
emit requestComplete(body, mime_type);
|
||||
}
|
||||
}
|
||||
|
||||
void GeminiClient::sslErrors(const QList<QSslError>& errors) {
|
||||
emit this->hostCertificateLoaded(this->socket.peerCertificate());
|
||||
|
||||
if (options & IgnoreTlsErrors) {
|
||||
socket.ignoreSslErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QSslError> remaining_errors = errors;
|
||||
QList<QSslError> ignored_errors;
|
||||
|
||||
int i = 0;
|
||||
while (i < remaining_errors.size()) {
|
||||
const auto& err = remaining_errors.at(i);
|
||||
|
||||
bool ignore = false;
|
||||
|
||||
/*
|
||||
*/
|
||||
ignore = true;
|
||||
/*
|
||||
if (SslTrust::isTrustRelated(err.error())) {
|
||||
switch (kristall::globals().trust.gemini.getTrust(target_url, socket.peerCertificate())) {
|
||||
case SslTrust::Trusted:
|
||||
ignore = true;
|
||||
break;
|
||||
case SslTrust::Untrusted:
|
||||
this->is_error_state = true;
|
||||
this->suppress_socket_tls_error = true;
|
||||
emit this->networkError(UntrustedHost, toFingerprintString(socket.peerCertificate()));
|
||||
return;
|
||||
case SslTrust::Mistrusted:
|
||||
this->is_error_state = true;
|
||||
this->suppress_socket_tls_error = true;
|
||||
emit this->networkError(MistrustedHost, toFingerprintString(socket.peerCertificate()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
else */
|
||||
if (err.error() == QSslError::UnableToVerifyFirstCertificate) {
|
||||
ignore = true;
|
||||
}
|
||||
|
||||
if (ignore) {
|
||||
ignored_errors.append(err);
|
||||
remaining_errors.removeAt(0);
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
socket.ignoreSslErrors(ignored_errors);
|
||||
|
||||
qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size();
|
||||
|
||||
for (const auto& error : remaining_errors) {
|
||||
qWarning() << int(error.error()) << error.errorString();
|
||||
}
|
||||
|
||||
if (remaining_errors.size() > 0) {
|
||||
emit this->networkError(TlsFailure, remaining_errors.first().errorString());
|
||||
}
|
||||
}
|
||||
|
||||
void GeminiClient::socketError(QAbstractSocket::SocketError socketError) {
|
||||
// When remote host closes TLS session, the client closes the socket.
|
||||
// This is more sane then erroring out here as it's a perfectly legal
|
||||
// state and we know the TLS connection has ended.
|
||||
if (socketError == QAbstractSocket::RemoteHostClosedError) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this->is_error_state = true;
|
||||
if (not this->suppress_socket_tls_error) {
|
||||
this->emitNetworkError(socketError, socket.errorString());
|
||||
}
|
||||
}
|
142
src/librssguard/network-web/gemini/geminiclient.h
Normal file
142
src/librssguard/network-web/gemini/geminiclient.h
Normal file
@ -0,0 +1,142 @@
|
||||
#ifndef GEMINICLIENT_HPP
|
||||
#define GEMINICLIENT_HPP
|
||||
|
||||
#include <QMimeType>
|
||||
#include <QObject>
|
||||
#include <QSslCertificate>
|
||||
#include <QSslKey>
|
||||
#include <QSslSocket>
|
||||
#include <QUrl>
|
||||
|
||||
//! Cryptographic user identitiy consisting
|
||||
//! of a key-certificate pair and some user information.
|
||||
struct CryptoIdentity {
|
||||
//! The certificate that is used for cryptography
|
||||
QSslCertificate certificate;
|
||||
|
||||
//! The actual private key that is used for cryptography
|
||||
QSslKey private_key;
|
||||
|
||||
//! The title with which the identity is presented to the user.
|
||||
QString display_name;
|
||||
|
||||
//! Notes that the user can have per identity for improved identity management
|
||||
QString user_notes;
|
||||
|
||||
//! True for long-lived identities
|
||||
bool is_persistent = false;
|
||||
|
||||
//! If not empty, Kristall will check
|
||||
QString host_filter = "";
|
||||
|
||||
//! When this is set to true and the host_filter is not empty,
|
||||
//! 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());
|
||||
}
|
||||
|
||||
//! returns true if a host does not match the filter criterion
|
||||
bool isHostFiltered(const QUrl& url) const;
|
||||
|
||||
//! returns true when the identity should be enabled on url
|
||||
bool isAutomaticallyEnabledOn(const QUrl& url) const;
|
||||
};
|
||||
|
||||
class GeminiClient : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class RequestState {
|
||||
None = 0,
|
||||
Started = 1,
|
||||
HostFound = 2,
|
||||
Connected = 3,
|
||||
|
||||
StartedWeb = 255,
|
||||
};
|
||||
|
||||
enum NetworkError {
|
||||
UnknownError, //!< There was an unhandled network error
|
||||
ProtocolViolation, //!< The server responded with something unexpected and violated the protocol
|
||||
HostNotFound, //!< The host was not found by the client
|
||||
ConnectionRefused, //!< The host refused connection on that port
|
||||
ResourceNotFound, //!< The requested resource was not found on the server
|
||||
BadRequest, //!< Our client misbehaved and did a request the server cannot understand
|
||||
ProxyRequest, //!< We requested a proxy operation, but the server does not allow that
|
||||
InternalServerError,
|
||||
InvalidClientCertificate,
|
||||
UntrustedHost, //!< We don't know the host, and we don't trust it
|
||||
MistrustedHost, //!< We know the host and it's not the server identity we've seen before
|
||||
Unauthorized, //!< The requested resource could not be accessed.
|
||||
TlsFailure, //!< Unspecified TLS failure
|
||||
Timeout, //!< The network connection timed out.
|
||||
};
|
||||
|
||||
enum RequestOptions {
|
||||
Default = 0,
|
||||
IgnoreTlsErrors = 1,
|
||||
};
|
||||
|
||||
explicit GeminiClient(QObject* parent = nullptr);
|
||||
virtual ~GeminiClient();
|
||||
|
||||
bool supportsScheme(const QString& scheme) const;
|
||||
|
||||
bool startRequest(const QUrl& url, RequestOptions options);
|
||||
bool isInProgress() const;
|
||||
bool cancelRequest();
|
||||
|
||||
bool enableClientCertificate(const CryptoIdentity& ident);
|
||||
void disableClientCertificate();
|
||||
|
||||
signals:
|
||||
//! We successfully transferred some bytes from the server
|
||||
void requestProgress(qint64 transferred);
|
||||
|
||||
//! The request completed with the given data and mime type
|
||||
void requestComplete(const QByteArray& data, const QString& mime);
|
||||
|
||||
//! The state of the request has changed
|
||||
void requestStateChange(RequestState state);
|
||||
|
||||
//! Server redirected us to another URL
|
||||
void redirected(const QUrl& uri, bool is_permanent);
|
||||
|
||||
//! The server needs some information from the user to process this query.
|
||||
void inputRequired(const QString& user_query, bool is_sensitive);
|
||||
|
||||
//! There was an error while processing the request
|
||||
void networkError(NetworkError error, const QString& reason);
|
||||
|
||||
//! The server wants us to use a client certificate
|
||||
void certificateRequired(const QString& info);
|
||||
|
||||
//! The server uses TLS and has a certificate.
|
||||
void hostCertificateLoaded(const QSslCertificate& cert);
|
||||
|
||||
protected:
|
||||
void emitNetworkError(QAbstractSocket::SocketError error_code, const QString& textual_description);
|
||||
|
||||
private slots:
|
||||
void socketEncrypted();
|
||||
void socketReadyRead();
|
||||
void socketDisconnected();
|
||||
void sslErrors(const QList<QSslError>& errors);
|
||||
void socketError(QAbstractSocket::SocketError socketError);
|
||||
|
||||
private:
|
||||
bool is_receiving_body;
|
||||
bool suppress_socket_tls_error;
|
||||
bool is_error_state;
|
||||
|
||||
QUrl target_url;
|
||||
QSslSocket socket;
|
||||
QByteArray buffer;
|
||||
QByteArray body;
|
||||
QString mime_type;
|
||||
RequestOptions options;
|
||||
};
|
||||
|
||||
#endif // GEMINICLIENT_HPP
|
Loading…
x
Reference in New Issue
Block a user