forked from sqlitebrowser/sqlitebrowser
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemoteDatabase.cpp
More file actions
559 lines (484 loc) · 21.1 KB
/
RemoteDatabase.cpp
File metadata and controls
559 lines (484 loc) · 21.1 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
557
558
559
#include <QApplication>
#include <QNetworkAccessManager>
#include <QMessageBox>
#include <QNetworkReply>
#include <QFile>
#include <QSslKey>
#include <QProgressDialog>
#include <QInputDialog>
#include <QDir>
#include <QStandardPaths>
#include <QUrlQuery>
#include "RemoteDatabase.h"
#include "version.h"
#include "Settings.h"
#include "sqlite.h"
RemoteDatabase::RemoteDatabase() :
m_manager(new QNetworkAccessManager),
m_progress(nullptr),
m_currentReply(nullptr),
m_dbLocal(nullptr)
{
// Set up SSL configuration
m_sslConfiguration = QSslConfiguration::defaultConfiguration();
m_sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyPeer);
// Load CA certs from resource file
QDir dirCaCerts(":/certs");
QStringList caCertsList = dirCaCerts.entryList();
QList<QSslCertificate> caCerts;
foreach(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::finished, this, &RemoteDatabase::gotReply);
connect(m_manager, &QNetworkAccessManager::encrypted, this, &RemoteDatabase::gotEncrypted);
connect(m_manager, &QNetworkAccessManager::sslErrors, this, &RemoteDatabase::gotError);
}
RemoteDatabase::~RemoteDatabase()
{
delete m_manager;
delete m_progress;
// Close local storage db - but only if it was created/opened in the meantime
if(m_dbLocal)
sqlite3_close(m_dbLocal);
}
void RemoteDatabase::reloadSettings()
{
// Load all configured client certificates
m_clientCertFiles.clear();
auto client_certs = Settings::getValue("remote", "client_certificates").toStringList();
foreach(const QString& path, client_certs)
{
QFile file(path);
file.open(QFile::ReadOnly);
QSslCertificate cert(&file);
file.close();
m_clientCertFiles.insert(path, cert);
}
// TODO Add support for proxies here
}
void RemoteDatabase::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 RemoteDatabase::gotReply(QNetworkReply* reply)
{
// Check if request was successful
if(reply->error() != QNetworkReply::NoError)
{
QMessageBox::warning(0, qApp->applicationName(),
tr("Error when connecting to %1.\n%2").arg(reply->url().toString()).arg(reply->errorString()));
reply->deleteLater();
return;
}
// Check for redirect
QString redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
if(!redirectUrl.isEmpty())
{
// Avoid redirect loop
if(reply->url() == redirectUrl)
{
reply->deleteLater();
return;
}
fetch(redirectUrl, static_cast<RequestType>(reply->property("type").toInt()), reply->property("certfile").toString(), reply->property("userdata"));
reply->deleteLater();
return;
}
// 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.
// Generate a unique file name to save the file under
QString saveFileAs = Settings::getValue("remote", "clonedirectory").toString() +
QString("/%2_%1.remotedb").arg(QDateTime::currentMSecsSinceEpoch()).arg(reply->url().fileName());
// Add cloned database to list of local databases
localAdd(saveFileAs, reply->property("certfile").toString(), reply->url());
// Save the downloaded data under the generated file name
QFile file(saveFileAs);
file.open(QIODevice::WriteOnly);
file.write(reply->readAll());
file.close();
// Tell the application to open this file
emit openFile(saveFileAs);
}
break;
case RequestTypeDirectory:
emit gotDirList(reply->readAll(), reply->property("userdata"));
break;
case RequestTypeNewVersionCheck:
{
QString version = reply->readLine().trimmed();
QString url = reply->readLine().trimmed();
emit gotCurrentVersion(version, url);
break;
}
default:
break;
}
// Delete reply later, i.e. after returning from this slot function
m_currentReply = nullptr;
reply->deleteLater();
}
void RemoteDatabase::gotError(QNetworkReply* reply, const QList<QSslError>& errors)
{
// Are there any errors in here that aren't about self-signed certificates and non-matching hostnames?
// TODO What about the hostname mismatch? Can we remove that from the list of ignored errors later?
bool serious_errors = false;
foreach(const QSslError& error, errors)
{
if(error.error() != QSslError::SelfSignedCertificate && error.error() != QSslError::HostNameMismatch)
{
serious_errors = true;
break;
}
}
// 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()).arg(errors.at(0).errorString());
QMessageBox::warning(0, qApp->applicationName(), message);
// Delete reply later, i.e. after returning from this slot function
m_progress->reset();
reply->deleteLater();
}
void RemoteDatabase::updateProgress(qint64 bytesTransmitted, qint64 bytesTotal)
{
// 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
m_progress->setMinimum(0);
m_progress->setMaximum(bytesTotal);
m_progress->setValue(bytesTransmitted);
}
// Check if the Cancel button has been pressed
qApp->processEvents();
if(m_currentReply && m_progress->wasCanceled())
{
m_currentReply->abort();
m_progress->reset();
}
}
const QList<QSslCertificate>& RemoteDatabase::caCertificates() const
{
static QList<QSslCertificate> certs = m_sslConfiguration.caCertificates();
return certs;
}
bool RemoteDatabase::prepareSsl(QNetworkRequest* request, const QString& clientCert)
{
// Check if client cert exists
const QSslCertificate& cert = m_clientCertFiles[clientCert];
if(cert.isNull())
{
QMessageBox::warning(0, 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(0, 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 RemoteDatabase::prepareProgressDialog(bool upload, const QString& url)
{
// Instantiate progress dialog and apply some basic settings
if(!m_progress)
m_progress = new QProgressDialog();
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setCancelButtonText(tr("Cancel"));
// Set dialog text
if(upload)
m_progress->setLabelText(tr("Uploading remote database to\n%1.").arg(url));
else
m_progress->setLabelText(tr("Downloading remote database from\n%1.").arg(url));
// Show dialog
m_progress->show();
qApp->processEvents();
// Make sure the dialog is updated
if(upload)
connect(m_currentReply, &QNetworkReply::uploadProgress, this, &RemoteDatabase::updateProgress);
else
connect(m_currentReply, &QNetworkReply::downloadProgress, this, &RemoteDatabase::updateProgress);
}
void RemoteDatabase::fetch(const QString& url, RequestType type, const QString& clientCert, QVariant userdata)
{
// Check if network is accessible. If not, abort right here
if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible)
{
QMessageBox::warning(0, qApp->applicationName(), tr("Error: The network is not accessible."));
return;
}
// If this is a request for a database there is a chance that we've already cloned that database. So check for that first
if(type == RequestTypeDatabase)
{
QString exists = localExists(url, clientCert);
if(!exists.isEmpty())
{
// Database has already been cloned! So open the local file instead of fetching the one from the
// server again.
emit openFile(exists);
return;
}
}
// Build network request
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName()).arg(APP_VERSION).toUtf8());
// 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 = QUrl(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;
}
// Fetch database and save pending reply. Note that we're only supporting one active download here at the moment.
m_currentReply = m_manager->get(request);
m_currentReply->setProperty("type", type);
m_currentReply->setProperty("certfile", clientCert);
m_currentReply->setProperty("userdata", userdata);
// Initialise the progress dialog for this request, but only if this is a database file. Directory listing are small enough to be loaded
// without progress dialog.
if(type == RequestTypeDatabase)
prepareProgressDialog(false, url);
}
void RemoteDatabase::push(const QString& filename, const QString& url, const QString& clientCert)
{
// Check if network is accessible. If not, abort right here
if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible)
{
QMessageBox::warning(0, qApp->applicationName(), tr("Error: The network is not accessible."));
return;
}
// Open the file to send and check if it exists
QFile file(filename);
if(!file.open(QFile::ReadOnly))
{
QMessageBox::warning(0, 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()).arg(APP_VERSION).toUtf8());
// Set SSL configuration when trying to access a file via the HTTPS protocol
bool https = QUrl(url).scheme().compare("https", Qt::CaseInsensitive) == 0;
if(https)
{
// If configuring the SSL connection fails, abort the request here
if(!prepareSsl(&request, clientCert))
return;
}
// Get file data
// TODO: Don't read the entire file here but directly pass the file handle to the put() call below in order
// to read larger files chunk by chunk.
QByteArray file_data = file.readAll();
file.close();
// Fetch database and save pending reply. Note that we're only supporting one active download here at the moment.
m_currentReply = m_manager->put(request, file_data);
m_currentReply->setProperty("type", RequestTypePush);
// Initialise the progress dialog for this request
prepareProgressDialog(true, url);
}
void RemoteDatabase::localAssureOpened()
{
// This function should be called first in each RemoteDatabase::local* function. It assures the database for storing
// the local database information is opened and ready. If the database file doesn't exist yet it is created by this
// function. If the database file is already created and opened this function does nothing. The reason to open the
// database on first use instead of doing that in the constructor of this class is that this way no database file is
// going to be created and no database handle is held when it's not actually needed. For people not interested in
// the dbhub.io functionality this means no unnecessary files being created.
// Check if database is already opened and return if it is
if(m_dbLocal)
return;
// Open file
QString database_file = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/remotedbs.db";
if(sqlite3_open_v2(database_file.toUtf8(), &m_dbLocal, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK)
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error opening local databases list.\n%1").arg(QString::fromUtf8(sqlite3_errmsg(m_dbLocal))));
return;
}
// Create local local table if it doesn't exists yet
char* errmsg;
QString statement = QString("CREATE TABLE IF NOT EXISTS \"local\"("
"\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
"\"identity\" TEXT NOT NULL,"
"\"name\" TEXT NOT NULL,"
"\"url\" TEXT NOT NULL,"
"\"version\" INTEGER NOT NULL,"
"\"file\" INTEGER,"
"\"modified\" INTEGER DEFAULT 0"
")");
if(sqlite3_exec(m_dbLocal, statement.toUtf8(), NULL, NULL, &errmsg) != SQLITE_OK)
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error creating local databases list.\n%1").arg(QString::fromUtf8(errmsg)));
sqlite3_free(errmsg);
sqlite3_close(m_dbLocal);
m_dbLocal = nullptr;
return;
}
}
void RemoteDatabase::localAdd(QString filename, QString identity, const QUrl& url)
{
// This function adds a new local database clone to our internal list. It does so by adding a single
// new record to the remote dbs database. All the fields are extracted from the filename, the identity
// and (most importantly) the url parameters. Note that for the version field to be correctly filled we
// require the version to be part of the url parameter. Also note that this function doesn't check if the
// database has already been added to the list before. This needs to be done before calling this function,
// ideally even before sending out a request to the network.
localAssureOpened();
// Insert database into local database list
QString sql = QString("INSERT INTO local(identity, name, url, version, file) VALUES(?, ?, ?, ?, ?)");
sqlite3_stmt* stmt;
if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, 0) != SQLITE_OK)
return;
QFileInfo f(identity); // Remove the path
identity = f.fileName();
if(sqlite3_bind_text(stmt, 1, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return;
}
if(sqlite3_bind_text(stmt, 2, url.fileName().toUtf8(), url.fileName().toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return;
}
QUrl url_without_query = url; // Remove the '?version=x' bit from the URL
url_without_query.setQuery(QString());
if(sqlite3_bind_text(stmt, 3, url_without_query.toString().toUtf8(), url_without_query.toString().toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return;
}
if(sqlite3_bind_int(stmt, 4, QUrlQuery(url).queryItemValue("version").toInt()))
{
sqlite3_finalize(stmt);
return;
}
f = QFileInfo(filename); // Remove the path
filename = f.fileName();
if(sqlite3_bind_text(stmt, 5, filename.toUtf8(), filename.toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return;
}
if(sqlite3_step(stmt) != SQLITE_DONE)
{
sqlite3_finalize(stmt);
return;
}
sqlite3_finalize(stmt);
}
QString RemoteDatabase::localExists(const QUrl& url, QString identity)
{
// This function checks if there already is a clone for the given combination of url and identity. It returns the filename
// of this clone if there is or a null string if there isn't a clone yet. The identity needs to be part of this check because
// with the url alone there could be corner cases where different versions or whatever may not be accessible for all users.
localAssureOpened();
// Extract version from url and remove query part afterwards
int url_version = QUrlQuery(url).queryItemValue("version").toInt();
QUrl url_without_query = url;
url_without_query.setQuery(QString());
// Query version and filename for the given combination of url and identity
QString sql = QString("SELECT id, version, file FROM local WHERE url=? AND identity=?");
sqlite3_stmt* stmt;
if(sqlite3_prepare_v2(m_dbLocal, sql.toUtf8(), -1, &stmt, 0) != SQLITE_OK)
return QString();
if(sqlite3_bind_text(stmt, 1, url_without_query.toString().toUtf8(), url_without_query.toString().toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return QString();
}
QFileInfo f(identity); // Remove the path
identity = f.fileName();
if(sqlite3_bind_text(stmt, 2, identity.toUtf8(), identity.toUtf8().length(), SQLITE_TRANSIENT))
{
sqlite3_finalize(stmt);
return QString();
}
if(sqlite3_step(stmt) != SQLITE_ROW)
{
// If there was either an error or no record was found for this combination of url and
// identity, stop here.
sqlite3_finalize(stmt);
return QString();
}
// Having come here we can assume that at least some local clone for the given combination of
// url and identity exists. So extract all the information we have on it.
//int local_id = sqlite3_column_int(stmt, 0);
int local_version = sqlite3_column_int(stmt, 1);
QString local_file = QString::fromUtf8(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)));
sqlite3_finalize(stmt);
// There are three possibilities now: either the requested version is the same as the local version, or the requested version
// is newer, or the local version is newer.
if(local_version == url_version)
{
// Both versions are the same. That's the perfect match, so just return the path to the local file
return Settings::getValue("remote", "clonedirectory").toString() + "/" + local_file;
} else {
// In all the other cases just treat the remote database as a completely new database for now.
// TODO Add some way to update the local clone here. Maybe ask the user what to do because I don't really know what the
// most sensible way to go is in the two remaining cases. We can use the local_id variable (see above) to update the
// record afterwards.
return QString();
}
}