1 #include "geminiclient.hpp"
2 #include <cassert>
3 #include <QDebug>
4 #include <QSslConfiguration>
5 #include "kristall.hpp"
6 
GeminiClient()7 GeminiClient::GeminiClient() : ProtocolHandler(nullptr)
8 {
9     connect(&socket, &QSslSocket::encrypted, this, &GeminiClient::socketEncrypted);
10     connect(&socket, &QSslSocket::readyRead, this, &GeminiClient::socketReadyRead);
11     connect(&socket, &QSslSocket::disconnected, this, &GeminiClient::socketDisconnected);
12 //    connect(&socket, &QSslSocket::stateChanged, [](QSslSocket::SocketState state) {
13 //        qDebug() << "Socket state changed to " << state;
14 //    });
15     connect(&socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), this, &GeminiClient::sslErrors);
16 
17 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
18     connect(&socket, &QTcpSocket::errorOccurred, this, &GeminiClient::socketError);
19 #else
20     connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &GeminiClient::socketError);
21 #endif
22 
23     // States
24     connect(&socket, &QAbstractSocket::hostFound, this, [this]() {
25         emit this->requestStateChange(RequestState::HostFound);
26     });
27     connect(&socket, &QAbstractSocket::connected, this, [this]() {
28         emit this->requestStateChange(RequestState::Connected);
29     });
30     connect(&socket, &QAbstractSocket::disconnected, this, [this]() {
31         emit this->requestStateChange(RequestState::None);
32     });
33     emit this->requestStateChange(RequestState::None);
34 }
35 
~GeminiClient()36 GeminiClient::~GeminiClient()
37 {
38     is_receiving_body = false;
39 }
40 
supportsScheme(const QString & scheme) const41 bool GeminiClient::supportsScheme(const QString &scheme) const
42 {
43     return (scheme == "gemini");
44 }
45 
startRequest(const QUrl & url,RequestOptions options)46 bool GeminiClient::startRequest(const QUrl &url, RequestOptions options)
47 {
48     if(url.scheme() != "gemini")
49         return false;
50 
51     // qDebug() << "start request" << url;
52 
53     if(socket.state() != QTcpSocket::UnconnectedState) {
54         socket.disconnectFromHost();
55         socket.close();
56         if(not socket.waitForDisconnected(1500))
57             return false;
58     }
59 
60     emit this->requestStateChange(RequestState::Started);
61 
62     this->is_error_state = false;
63 
64     this->options = options;
65 
66     QSslConfiguration ssl_config = socket.sslConfiguration();
67     ssl_config.setProtocol(QSsl::TlsV1_2OrLater);
68     if(not kristall::globals().trust.gemini.enable_ca)
69         ssl_config.setCaCertificates(QList<QSslCertificate> { });
70     else
71         ssl_config.setCaCertificates(QSslConfiguration::systemCaCertificates());
72     socket.setSslConfiguration(ssl_config);
73 
74     socket.connectToHostEncrypted(url.host(), url.port(1965));
75 
76     this->buffer.clear();
77     this->body.clear();
78     this->is_receiving_body = false;
79     this->suppress_socket_tls_error = true;
80 
81     if(not socket.isOpen())
82         return false;
83 
84     target_url = url;
85     mime_type = "<invalid>";
86 
87     return true;
88 }
89 
isInProgress() const90 bool GeminiClient::isInProgress() const
91 {
92     return (socket.state() != QTcpSocket::UnconnectedState);
93 }
94 
cancelRequest()95 bool GeminiClient::cancelRequest()
96 {
97     // qDebug() << "cancel request" << isInProgress();
98     if(isInProgress())
99     {
100         this->is_receiving_body = false;
101         this->socket.disconnectFromHost();
102         this->buffer.clear();
103         this->body.clear();
104         if (socket.state() != QTcpSocket::UnconnectedState)
105         {
106             socket.disconnectFromHost();
107         }
108         this->socket.waitForDisconnected(500);
109         this->socket.close();
110         bool success = not isInProgress();
111         // qDebug() << "cancel success" << success;
112         return success;
113     }
114     else
115     {
116         return true;
117     }
118 }
119 
enableClientCertificate(const CryptoIdentity & ident)120 bool GeminiClient::enableClientCertificate(const CryptoIdentity &ident)
121 {
122     this->socket.setLocalCertificate(ident.certificate);
123     this->socket.setPrivateKey(ident.private_key);
124     return true;
125 }
126 
disableClientCertificate()127 void GeminiClient::disableClientCertificate()
128 {
129     this->socket.setLocalCertificate(QSslCertificate{});
130     this->socket.setPrivateKey(QSslKey { });
131 }
132 
socketEncrypted()133 void GeminiClient::socketEncrypted()
134 {
135     emit this->hostCertificateLoaded(this->socket.peerCertificate());
136 
137     QString request = target_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n";
138 
139     QByteArray request_bytes = request.toUtf8();
140 
141     qint64 offset = 0;
142     while(offset < request_bytes.size()) {
143         auto const len = socket.write(request_bytes.constData() + offset, request_bytes.size() - offset);
144         if(len <= 0)
145         {
146             socket.close();
147             return;
148         }
149         offset += len;
150     }
151 }
152 
socketReadyRead()153 void GeminiClient::socketReadyRead()
154 {
155     if(this->is_error_state) // don't do any further
156         return;
157     QByteArray response = socket.readAll();
158 
159     if(is_receiving_body)
160     {
161         body.append(response);
162         emit this->requestProgress(body.size());
163     }
164     else
165     {
166         for(int i = 0; i < response.size(); i++)
167         {
168             if(response[i] == '\n') {
169                 buffer.append(response.data(), i);
170                 body.append(response.data() + i + 1, response.size() - i - 1);
171 
172                 // "XY " <META> <CR> <LF>
173                 if(buffer.size() < 4) { // we allow an empty <META>
174                     socket.close();
175                     qDebug() << buffer;
176                     emit networkError(ProtocolViolation, QObject::tr("Line is too short for valid protocol"));
177                     return;
178                 }
179                 if(buffer.size() >= 1200)
180                 {
181                     emit networkError(ProtocolViolation, QObject::tr("response too large!"));
182                     socket.close();
183                 }
184                 if(buffer[buffer.size() - 1] != '\r') {
185                     socket.close();
186                     qDebug() << buffer;
187                     emit networkError(ProtocolViolation, QObject::tr("Line does not end with <CR> <LF>"));
188                     return;
189                 }
190                 if(not isdigit(buffer[0])) {
191                     socket.close();
192                     qDebug() << buffer;
193                     emit networkError(ProtocolViolation, QObject::tr("First character is not a digit."));
194                     return;
195                 }
196                 if(not isdigit(buffer[1])) {
197                     socket.close();
198                     qDebug() << buffer;
199                     emit networkError(ProtocolViolation, QObject::tr("Second character is not a digit."));
200                     return;
201                 }
202                 // TODO: Implement stricter version
203                 // if(buffer[2] != ' ') {
204                 if(not isspace(buffer[2])) {
205                     socket.close();
206                     qDebug() << buffer;
207                     emit networkError(ProtocolViolation, QObject::tr("Third character is not a space."));
208                     return;
209                 }
210 
211                 QString meta = QString::fromUtf8(buffer.data() + 3, buffer.size() - 4);
212 
213                 int primary_code = buffer[0] - '0';
214                 int secondary_code = buffer[1] - '0';
215 
216                 qDebug() << primary_code << secondary_code << meta;
217 
218                 // We don't need to receive any data after that.
219                 if(primary_code != 2)
220                     socket.close();
221 
222                 switch(primary_code)
223                 {
224                 case 1: // requesting input
225                     switch (secondary_code) {
226                         case 1:
227                         emit inputRequired(meta, true);
228                         break;
229                         case 0:
230                         default:
231                         emit inputRequired(meta, false);
232                     }
233                     return;
234 
235                 case 2: // success
236                     is_receiving_body = true;
237                     mime_type = meta;
238                     return;
239 
240                 case 3: { // redirect
241                     QUrl new_url(meta);
242                     if(new_url.isValid()) {
243                         if(new_url.isRelative())
244                             new_url =  target_url.resolved(new_url);
245                         assert(not new_url.isRelative());
246 
247                         emit redirected(new_url, (secondary_code == 1));
248                     }
249                     else {
250                         emit networkError(ProtocolViolation, QObject::tr("Invalid URL for redirection!"));
251                     }
252                     return;
253                 }
254 
255                 case 4: { // temporary failure
256                     NetworkError type = UnknownError;
257                     switch(secondary_code)
258                     {
259                     case 1: type = InternalServerError; break;
260                     case 2: type = InternalServerError; break;
261                     case 3: type = InternalServerError; break;
262                     case 4: type = UnknownError; break;
263                     }
264                     emit networkError(type, meta);
265                     return;
266                 }
267 
268                 case 5: { // permanent failure
269                     NetworkError type = UnknownError;
270                     switch(secondary_code)
271                     {
272                     case 1: type = ResourceNotFound; break;
273                     case 2: type = ResourceNotFound; break;
274                     case 3: type = ProxyRequest; break;
275                     case 9: type = BadRequest; break;
276                     }
277                     emit networkError(type, meta);
278                     return;
279                 }
280 
281                 case 6: // client certificate required
282                     switch(secondary_code)
283                     {
284                     case 0:
285                         emit certificateRequired(meta);
286                         return;
287 
288                     case 1:
289                         emit networkError(Unauthorized, meta);
290                         return;
291 
292                     default:
293                     case 2:
294                         emit networkError(InvalidClientCertificate, meta);
295                         return;
296                     }
297                     return;
298 
299                 default:
300                     emit networkError(ProtocolViolation, QObject::tr("Unspecified status code used!"));
301                     return;
302                 }
303 
304                 assert(false and "unreachable");
305             }
306         }
307         if((buffer.size() + response.size()) >= 1200)
308         {
309             emit networkError(ProtocolViolation, QObject::tr("META too large!"));
310             socket.close();
311         }
312         buffer.append(response);
313     }
314 }
315 
socketDisconnected()316 void GeminiClient::socketDisconnected()
317 {
318     if(this->is_receiving_body and not this->is_error_state) {
319         body.append(socket.readAll());
320         emit requestComplete(body, mime_type);
321     }
322 }
323 
sslErrors(QList<QSslError> const & errors)324 void GeminiClient::sslErrors(QList<QSslError> const & errors)
325 {
326     emit this->hostCertificateLoaded(this->socket.peerCertificate());
327 
328     if(options & IgnoreTlsErrors) {
329         socket.ignoreSslErrors(errors);
330         return;
331     }
332 
333     QList<QSslError> remaining_errors = errors;
334     QList<QSslError> ignored_errors;
335 
336     int i = 0;
337     while(i < remaining_errors.size())
338     {
339         auto const & err = remaining_errors.at(i);
340 
341         bool ignore = false;
342         if(SslTrust::isTrustRelated(err.error()))
343         {
344             switch(kristall::globals().trust.gemini.getTrust(target_url, socket.peerCertificate()))
345             {
346             case SslTrust::Trusted:
347                 ignore = true;
348                 break;
349             case SslTrust::Untrusted:
350                 this->is_error_state = true;
351                 this->suppress_socket_tls_error = true;
352                 emit this->networkError(UntrustedHost, toFingerprintString(socket.peerCertificate()));
353                 return;
354             case SslTrust::Mistrusted:
355                 this->is_error_state = true;
356                 this->suppress_socket_tls_error = true;
357                 emit this->networkError(MistrustedHost, toFingerprintString(socket.peerCertificate()));
358                 return;
359             }
360         }
361         else if(err.error() == QSslError::UnableToVerifyFirstCertificate)
362         {
363             ignore = true;
364         }
365 
366         if(ignore) {
367             ignored_errors.append(err);
368             remaining_errors.removeAt(0);
369         } else {
370             i += 1;
371         }
372     }
373 
374     socket.ignoreSslErrors(ignored_errors);
375 
376     qDebug() << "ignoring" << ignored_errors.size() << "out of" << errors.size();
377 
378     for(auto const & error : remaining_errors) {
379         qWarning() << int(error.error()) << error.errorString();
380     }
381 
382     if(remaining_errors.size() > 0) {
383         emit this->networkError(TlsFailure, remaining_errors.first().errorString());
384     }
385 }
386 
socketError(QAbstractSocket::SocketError socketError)387 void GeminiClient::socketError(QAbstractSocket::SocketError socketError)
388 {
389     // When remote host closes TLS session, the client closes the socket.
390     // This is more sane then erroring out here as it's a perfectly legal
391     // state and we know the TLS connection has ended.
392     if(socketError == QAbstractSocket::RemoteHostClosedError) {
393         socket.close();
394         return;
395     }
396 
397     this->is_error_state = true;
398     if(not this->suppress_socket_tls_error) {
399         this->emitNetworkError(socketError, socket.errorString());
400     }
401 }
402