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