mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-02-09 21:28:30 -06:00
551 lines
21 KiB
C++
551 lines
21 KiB
C++
#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 <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");
|
|
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();
|
|
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");
|
|
QRegExp regex("^.*modification-date=\"(.+)\";.*$");
|
|
regex.setMinimal(true); // Set to non-greedy matching
|
|
if(regex.indexIn(content_disposition) != -1)
|
|
last_modified = QDateTime::fromString(regex.cap(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).arg(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();
|
|
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(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;
|
|
}
|