forked from sqlitebrowser/sqlitebrowser
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemoteNetwork.cpp
More file actions
556 lines (480 loc) · 21.2 KB
/
RemoteNetwork.cpp
File metadata and controls
556 lines (480 loc) · 21.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
#include <QApplication>
#include <QtNetwork/QNetworkAccessManager>
#include <QMessageBox>
#include <QtNetwork/QNetworkReply>
#include <QFile>
#include <QtNetwork/QSslKey>
#include <QProgressDialog>
#include <QInputDialog>
#include <QDir>
#include <QUrlQuery>
#include <QtNetwork/QHttpMultiPart>
#include <QtNetwork/QNetworkProxyFactory>
#include <QTimeZone>
#include <QtNetwork/QNetworkProxy>
#include <json.hpp>
#include <QRegularExpression>
#include <iterator>
#include "FileDialog.h"
#include "RemoteNetwork.h"
#include "Settings.h"
#include "sqlite.h"
#include "version.h"
using json = nlohmann::json;
RemoteNetwork::RemoteNetwork() :
m_manager(new QNetworkAccessManager),
m_progress(nullptr),
m_sslConfiguration(QSslConfiguration::defaultConfiguration())
{
// Set up SSL configuration
m_sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyPeer);
// Load CA certs from resource file
QDir dirCaCerts(":/certs");
const QStringList caCertsList = dirCaCerts.entryList();
QList<QSslCertificate> caCerts;
for(const QString& caCertName : caCertsList)
caCerts += QSslCertificate::fromPath(":/certs/" + caCertName);
m_sslConfiguration.setCaCertificates(caCerts);
// Load settings and set up some more stuff while doing so
reloadSettings();
// Set up signals
connect(m_manager, &QNetworkAccessManager::encrypted, this, &RemoteNetwork::gotEncrypted);
connect(m_manager, &QNetworkAccessManager::sslErrors, this, &RemoteNetwork::gotError);
}
RemoteNetwork::~RemoteNetwork()
{
delete m_manager;
delete m_progress;
}
void RemoteNetwork::reloadSettings()
{
// Load all configured client certificates
m_clientCertFiles.clear();
const auto client_certs = Settings::getValue("remote", "client_certificates").toStringList();
for(const QString& path : client_certs)
{
QFile file(path);
file.open(QFile::ReadOnly);
QSslCertificate cert(&file);
file.close();
m_clientCertFiles.insert({path, cert});
}
// Always add the default certificate for anonymous access to dbhub.io
{
QFile file(":/user_certs/public.cert.pem");
file.open(QFile::ReadOnly);
QSslCertificate cert(&file);
file.close();
m_clientCertFiles.insert({":/user_certs/public.cert.pem", cert});
}
// Configure proxy to use
{
QString type = Settings::getValue("proxy", "type").toString();
QNetworkProxy proxy;
if(type == "system")
{
// For system settings we have to get the system-wide proxy and use that
// Get list of proxies for accessing dbhub.io via HTTPS and use the first one
auto list = QNetworkProxyFactory::systemProxyForQuery(QNetworkProxyQuery(QUrl("https://db4s.dbhub.io/")));
proxy = list.front();
} else {
// For any other type we have to set up our own proxy configuration
// Retrieve the required settings
QString host = Settings::getValue("proxy", "host").toString();
unsigned short port = static_cast<unsigned short>(Settings::getValue("proxy", "port").toUInt());
bool authentication = Settings::getValue("proxy", "authentication").toBool();
if(type == "http")
proxy.setType(QNetworkProxy::HttpProxy);
else if(type == "socks5")
proxy.setType(QNetworkProxy::Socks5Proxy);
else
proxy.setType(QNetworkProxy::NoProxy);
proxy.setHostName(host);
proxy.setPort(port);
// Only set authentication details when authentication is required
if(authentication)
{
QString user = Settings::getValue("proxy", "user").toString();
QString password = Settings::getValue("proxy", "password").toString();
proxy.setUser(user);
proxy.setPassword(password);
}
}
// Start using the new proxy configuration
QNetworkProxy::setApplicationProxy(proxy);
}
}
void RemoteNetwork::gotEncrypted(QNetworkReply* reply)
{
#ifdef Q_OS_MAC
// Temporary workaround for now, as Qt 5.8 and below doesn't support
// verifying certificates on OSX: https://bugreports.qt.io/browse/QTBUG-56973
// Hopefully this is fixed in Qt 5.9
return;
#else
// Verify the server's certificate using our CA certs
auto verificationErrors = reply->sslConfiguration().peerCertificate().verify(m_sslConfiguration.caCertificates());
bool good = false;
if(verificationErrors.size() == 0)
{
good = true;
} else if(verificationErrors.size() == 1) {
// Ignore any self signed certificate errors
if(verificationErrors.at(0).error() == QSslError::SelfSignedCertificate || verificationErrors.at(0).error() == QSslError::SelfSignedCertificateInChain)
good = true;
}
// If the server certificate didn't turn out to be good, abort the reply here
if(!good)
reply->abort();
#endif
}
void RemoteNetwork::gotReply(QNetworkReply* reply)
{
// What type of data is this?
RequestType type = static_cast<RequestType>(reply->property("type").toInt());
// Hide progress dialog before opening a file dialog to make sure the progress dialog doesn't interfer with the file dialog
if(type == RequestTypeDatabase || type == RequestTypePush)
m_progress->reset();
// Handle the reply data
switch(type)
{
case RequestTypeDatabase:
{
// It's a database file.
// Get last modified date as provided by the server
QDateTime last_modified;
QString content_disposition = reply->rawHeader("Content-Disposition");
const static QRegularExpression regex("^.*modification-date=\"(.+)\";.*$", QRegularExpression::InvertedGreedinessOption);
const QRegularExpressionMatch match = regex.match(content_disposition);
if(match.hasMatch())
last_modified = QDateTime::fromString(match.captured(1), Qt::ISODate);
// Extract all other information from reply and send it to slots
emit fetchFinished(reply->url().fileName(),
reply->property("certfile").toString(),
reply->url(),
QUrlQuery(reply->url()).queryItemValue("commit").toStdString(),
QUrlQuery(reply->url()).queryItemValue("branch").toStdString(),
last_modified,
reply);
}
break;
case RequestTypePush:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Extract all information from reply and send it to slots
emit pushFinished(reply->url().fileName(),
reply->property("certfile").toString(),
QString::fromStdString(obj["url"]),
obj["commit_id"],
QUrlQuery(QUrl(QString::fromStdString(obj["url"]))).queryItemValue("branch").toStdString(),
reply->property("source_file").toString());
break;
}
case RequestTypeDownload:
{
// It's a download
// Where should we save it?
QString path = FileDialog::getSaveFileName(FileDialogTypes::CreateDatabaseFile,
nullptr,
tr("Choose a location to save the file"),
QString(),
reply->url().fileName() + "_" + QUrlQuery(reply->url()).queryItemValue("commit") + ".db");
if(path.isEmpty())
break;
// Save the downloaded data in that file
QFile file(path);
file.open(QIODevice::WriteOnly);
file.write(reply->readAll());
file.close();
}
break;
case RequestTypeCustom:
break;
}
// Delete reply later, i.e. after returning from this slot function
reply->deleteLater();
}
void RemoteNetwork::gotError(QNetworkReply* reply, const QList<QSslError>& errors)
{
// Are there any errors in here that aren't about self-signed certificates and non-matching hostnames?
bool serious_errors = std::any_of(errors.begin(), errors.end(), [](const QSslError& error) { return error.error() != QSslError::SelfSignedCertificate; });
// Just stop the error checking here and accept the reply if there were no 'serious' errors
if(!serious_errors)
{
reply->ignoreSslErrors(errors);
return;
}
// Build an error message and short it to the user
QString message = tr("Error opening remote file at %1.\n%2").arg(reply->url().toString(), errors.at(0).errorString());
QMessageBox::warning(nullptr, qApp->applicationName(), message);
// Delete reply later, i.e. after returning from this slot function
if(m_progress)
m_progress->reset();
reply->deleteLater();
}
void RemoteNetwork::updateProgress(qint64 bytesTransmitted, qint64 bytesTotal)
{
// Find out to which pending reply this progress update belongs
QNetworkReply* reply = qobject_cast<QNetworkReply*>(QObject::sender());
// Update progress dialog
if(bytesTotal == -1)
{
// We don't know anything about the current progress, but it's still downloading
m_progress->setMinimum(0);
m_progress->setMaximum(0);
m_progress->setValue(0);
} else if(bytesTransmitted == bytesTotal) {
// The download has finished
m_progress->reset();
} else {
// It's still downloading and we know the current progress
// Were using a range 0 to 10000 here, the progress dialog will calculate 0% to 100% values from that. The reason we're not using
// the byte counts as-is is that they're 64bit wide while the progress dialog takes only 32bit values, so for large files the values
// would lose precision. The reason why we're not using a range 0 to 100 is that our range increases the precision a bit and this way
// we're prepared if the progress dialog will show decimal numbers one day on one platform.
m_progress->setMinimum(0);
m_progress->setMaximum(10000);
m_progress->setValue(static_cast<int>((static_cast<float>(bytesTransmitted) / static_cast<float>(bytesTotal)) * 10000.0f));
}
// Check if the Cancel button has been pressed
if(reply && m_progress->wasCanceled())
{
reply->abort();
m_progress->reset();
}
}
const QList<QSslCertificate>& RemoteNetwork::caCertificates() const
{
static QList<QSslCertificate> certs = m_sslConfiguration.caCertificates();
return certs;
}
QString RemoteNetwork::getInfoFromClientCert(const QString& cert, CertInfo info) const
{
// Get the common name of the certificate and split it into user name and server address
QString cn = m_clientCertFiles.at(cert).subjectInfo(QSslCertificate::CommonName).at(0);
QStringList cn_parts = cn.split("@");
if(cn_parts.size() < 2)
return QString();
// Return requested part of the CN
if(info == CertInfoUser)
{
return cn_parts.first();
} else if(info == CertInfoServer) {
// Assemble the full URL from the host name. We use port 443 by default but for
// local development purposes we use 5550 instead.
QString host = cn_parts.last();
host = QString("https://%1%2/").arg(host, host.contains("docker-dev") ? ":5550" : "");
return host;
}
return QString();
}
bool RemoteNetwork::prepareSsl(QNetworkRequest* request, const QString& clientCert)
{
// Check if client cert exists
const QSslCertificate& cert = m_clientCertFiles[clientCert];
if(cert.isNull())
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: Invalid client certificate specified."));
return false;
}
// Load private key for the client certificate
QFile fileClientCert(clientCert);
fileClientCert.open(QFile::ReadOnly);
QSslKey clientKey(&fileClientCert, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
while(clientKey.isNull())
{
// If the private key couldn't be read, we assume it's password protected. So ask the user for the correct password and try reading it
// again. If the user cancels the password dialog, abort the whole process.
QString password = QInputDialog::getText(nullptr, qApp->applicationName(), tr("Please enter the passphrase for this client certificate in order to authenticate."));
if(password.isEmpty())
return false;
clientKey = QSslKey(&fileClientCert, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, password.toUtf8());
}
fileClientCert.close();
// Set client certificate (from the cache) and private key (just loaded)
m_sslConfiguration.setLocalCertificate(cert);
m_sslConfiguration.setPrivateKey(clientKey);
// Apply SSL configuration
request->setSslConfiguration(m_sslConfiguration);
return true;
}
void RemoteNetwork::prepareProgressDialog(QNetworkReply* reply, bool upload, const QUrl& url)
{
// Instantiate progress dialog and apply some basic settings
if(!m_progress)
m_progress = new QProgressDialog();
m_progress->reset();
// Disable context help button on Windows
m_progress->setWindowFlags(m_progress->windowFlags()
& ~Qt::WindowContextHelpButtonHint);
m_progress->setWindowModality(Qt::NonModal);
m_progress->setCancelButtonText(tr("Cancel"));
// Set dialog text
QString url_for_display = url.toString(QUrl::PrettyDecoded | QUrl::RemoveQuery);
if(upload)
m_progress->setLabelText(tr("Uploading remote database to\n%1").arg(url_for_display));
else
m_progress->setLabelText(tr("Downloading remote database from\n%1").arg(url_for_display));
// Show dialog
m_progress->show();
// Make sure the dialog is updated
if(upload)
connect(reply, &QNetworkReply::uploadProgress, this, &RemoteNetwork::updateProgress);
else
connect(reply, &QNetworkReply::downloadProgress, this, &RemoteNetwork::updateProgress);
}
void RemoteNetwork::fetch(const QUrl& url, RequestType type, const QString& clientCert,
std::function<void(QByteArray)> when_finished, bool synchronous, bool ignore_errors)
{
// Build network request
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName(), APP_VERSION).toUtf8());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#elif QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
// Set SSL configuration when trying to access a file via the HTTPS protocol.
// Skip this step when no client certificate was specified. In this case the default HTTPS configuration is used.
bool https = url.scheme().compare("https", Qt::CaseInsensitive) == 0;
if(https && !clientCert.isNull())
{
// If configuring the SSL connection fails, abort the request here
if(!prepareSsl(&request, clientCert))
return;
}
// Clear access cache if necessary
clearAccessCache(clientCert);
// Fetch database and prepare pending reply for future processing
QNetworkReply* reply = m_manager->get(request);
reply->setProperty("type", type);
reply->setProperty("certfile", clientCert);
reply->setProperty("ignore_errors", ignore_errors);
// Hook up custom handler when there is one and global handler otherwise
if(when_finished)
{
connect(reply, &QNetworkReply::finished, reply, [this, when_finished, reply]() {
if(handleReply(reply))
when_finished(reply->readAll());
});
} else {
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if(handleReply(reply))
gotReply(reply);
});
}
// When the synchrounous flag is set we wait for the request to finish before continuing
if(synchronous)
{
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
}
// Initialise the progress dialog for this request, but only if this is a database file or a download.
// Directory listing and similar are small enough to be loaded without progress dialog.
if(type == RequestTypeDatabase || type == RequestTypeDownload)
prepareProgressDialog(reply, false, url);
}
void RemoteNetwork::push(const QString& filename, const QUrl& url, const QString& clientCert, const QString& remotename,
const QString& commitMessage, const QString& licence, bool isPublic, const QString& branch,
bool forcePush, const QString& last_commit)
{
// Open the file to send and check if it exists
QFile* file = new QFile(filename);
if(!file->open(QFile::ReadOnly))
{
delete file;
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: Cannot open the file for sending."));
return;
}
// Build network request
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName(), APP_VERSION).toUtf8());
// Get the last modified date of the file and prepare it for conversion into the ISO date format
QDateTime last_modified = QFileInfo(filename).lastModified().toOffsetFromUtc(0);
// Prepare HTTP multi part data containing all the information about the commit we're about to push
QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addPart(multipart, "file", file, remotename);
addPart(multipart, "commitmsg", commitMessage);
addPart(multipart, "licence", licence);
addPart(multipart, "public", isPublic ? "true" : "false");
addPart(multipart, "branch", branch);
addPart(multipart, "force", forcePush ? "true" : "false");
addPart(multipart, "lastmodified", last_modified.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"));
// Only add commit id if one was provided
if(!last_commit.isEmpty())
addPart(multipart, "commit", last_commit);
// Set SSL configuration when trying to access a file via the HTTPS protocol
bool https = url.scheme().compare("https", Qt::CaseInsensitive) == 0;
if(https)
{
// If configuring the SSL connection fails, abort the request here
if(!prepareSsl(&request, clientCert))
{
delete file;
return;
}
}
// Clear access cache if necessary
clearAccessCache(clientCert);
// Put database to remote server and save pending reply for future processing
QNetworkReply* reply = m_manager->post(request, multipart);
reply->setProperty("type", RequestTypePush);
reply->setProperty("certfile", clientCert);
reply->setProperty("source_file", filename);
multipart->setParent(reply); // Delete the multi-part object along with the reply
// Connect reply handler
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if(handleReply(reply))
gotReply(reply);
});
// Initialise the progress dialog for this request
prepareProgressDialog(reply, true, url);
}
void RemoteNetwork::addPart(QHttpMultiPart* multipart, const QString& name, const QString& value) const
{
QHttpPart part;
part.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"").arg(name));
part.setBody(value.toUtf8());
multipart->append(part);
}
void RemoteNetwork::addPart(QHttpMultiPart* multipart, const QString& name, QFile* file, const QString& filename) const
{
QHttpPart part;
part.setHeader(QNetworkRequest::ContentDispositionHeader, QString("form-data; name=\"%1\"; filename=\"%2\"").arg(name, filename));
part.setBodyDevice(file);
file->setParent(multipart); // Close the file and delete the file object as soon as the multi-part object is destroyed
multipart->append(part);
}
void RemoteNetwork::clearAccessCache(const QString& clientCert)
{
// When the client certificate is different from the one before, clear the access and authentication cache.
// Otherwise Qt might use the old certificate again.
static QString lastClientCert;
if(lastClientCert != clientCert)
{
lastClientCert = clientCert;
m_manager->clearAccessCache();
}
}
bool RemoteNetwork::handleReply(QNetworkReply* reply)
{
// Check if request was successful
if(reply->error() != QNetworkReply::NoError)
{
// Do not show error message when operation was cancelled on purpose
if(reply->error() != QNetworkReply::OperationCanceledError && !reply->property("ignore_errors").toBool())
{
QMessageBox::warning(nullptr, qApp->applicationName(),
reply->errorString() + "\n" + reply->readAll());
}
reply->deleteLater();
return false;
}
return true;
}