initial gemini sources

This commit is contained in:
Martin Rotter 2024-12-18 08:55:54 +01:00
parent 9326418478
commit 9cb9b7162c
9 changed files with 633 additions and 29 deletions

1
.gitignore vendored
View File

@ -20,4 +20,5 @@ resources/skins/*/*.map
aqtinstall.log
.sass-cache
build-dir/
build/
docs/build

View File

@ -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",

View File

@ -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

View File

@ -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.";

View File

@ -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();

View File

@ -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);

View File

@ -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]"));

View 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());
}
}

View 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