dbhub: Refactor networking code

This splits up the RemoteDatabase class into two classes, RemoteDatabase
and RemoteNetwork. The first is for managing the directory of cloned
databases while the second is for network handling only. Moving the
network code into a separate class requires some rewriting but should
make the code easier to maintain and extend.
This commit is contained in:
Martin Kleusberg
2020-07-23 19:14:45 +02:00
parent ab5b633709
commit ddf5117331
17 changed files with 952 additions and 882 deletions

View File

@@ -9,6 +9,7 @@
#include "Application.h"
#include "MainWindow.h"
#include "RemoteNetwork.h"
#include "Settings.h"
#include "version.h"
@@ -215,6 +216,10 @@ QString Application::versionString()
void Application::reloadSettings()
{
// Network settings
RemoteNetwork::get().reloadSettings();
// Font settings
QFont f = font();
f.setPointSize(Settings::getValue("General", "fontsize").toInt());
setFont(f);

View File

@@ -22,7 +22,6 @@
#include "FileDialog.h"
#include "FilterTableHeader.h"
#include "RemoteDock.h"
#include "RemoteDatabase.h"
#include "FindReplaceDialog.h"
#include "RunSql.h"
#include "ExtendedTableWidget.h"
@@ -62,7 +61,6 @@ MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent),
ui(new Ui::MainWindow),
db(),
m_remoteDb(new RemoteDatabase),
editDock(new EditDialog(this)),
plotDock(new PlotDock(this)),
remoteDock(new RemoteDock(this)),
@@ -79,7 +77,6 @@ MainWindow::MainWindow(QWidget* parent)
MainWindow::~MainWindow()
{
delete m_remoteDb;
delete ui;
}
@@ -99,10 +96,10 @@ void MainWindow::init()
// Automatic update check
#ifdef CHECKNEWVERSION
connect(m_remoteDb, &RemoteDatabase::networkReady, [this]() {
connect(&RemoteNetwork::get(), &RemoteNetwork::networkReady, []() {
// Check for a new version if automatic update check aren't disabled in the settings dialog
if(Settings::getValue("checkversion", "enabled").toBool())
m_remoteDb->fetch(QUrl("https://download.sqlitebrowser.org/currentrelease"), RemoteDatabase::RequestTypeNewVersionCheck);
RemoteNetwork::get().fetch(QUrl("https://download.sqlitebrowser.org/currentrelease"), RemoteNetwork::RequestTypeNewVersionCheck);
});
#endif
@@ -389,7 +386,7 @@ void MainWindow::init()
connect(ui->dbTreeWidget->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::changeTreeSelection);
connect(ui->dockEdit, &QDockWidget::visibilityChanged, this, &MainWindow::toggleEditDock);
connect(remoteDock, SIGNAL(openFile(QString)), this, SLOT(fileOpen(QString)));
connect(m_remoteDb, &RemoteDatabase::gotCurrentVersion, this, &MainWindow::checkNewVersion);
connect(&RemoteNetwork::get(), &RemoteNetwork::gotCurrentVersion, this, &MainWindow::checkNewVersion);
connect(ui->actionDropQualifiedCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropQualifiedNames);
connect(ui->actionEnquoteNamesCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropEnquotedNames);
connect(&db, &DBBrowserDB::databaseInUseChanged, this, &MainWindow::updateDatabaseBusyStatus);
@@ -2138,9 +2135,6 @@ void MainWindow::reloadSettings()
if(!showRemoteActions)
ui->dockRemote->setHidden(true);
// Update the remote database connection settings
m_remoteDb->reloadSettings();
// Reload remote dock settings
remoteDock->reloadSettings();

View File

@@ -12,7 +12,6 @@ class EditDialog;
class ExtendedTableWidget;
class FindReplaceDialog;
class PlotDock;
class RemoteDatabase;
class RemoteDock;
class RunSql;
class SqliteTableModel;
@@ -36,7 +35,6 @@ public:
~MainWindow() override;
DBBrowserDB& getDb() { return db; }
RemoteDatabase& getRemote() { return *m_remoteDb; }
private:
struct PragmaValues
@@ -86,8 +84,6 @@ private:
QAction *recentFileActs[MaxRecentFiles];
QAction *recentSeparatorAct;
RemoteDatabase* m_remoteDb;
EditDialog* editDock;
PlotDock* plotDock;
RemoteDock* remoteDock;

View File

@@ -4,7 +4,7 @@
#include "Settings.h"
#include "Application.h"
#include "MainWindow.h"
#include "RemoteDatabase.h"
#include "RemoteNetwork.h"
#include "FileExtensionManager.h"
#include "ProxyDialog.h"
@@ -139,7 +139,7 @@ void PreferencesDialog::loadSettings()
// Remote settings
ui->checkUseRemotes->setChecked(Settings::getValue("remote", "active").toBool());
{
auto ca_certs = static_cast<Application*>(qApp)->mainWindow()->getRemote().caCertificates();
auto ca_certs = RemoteNetwork::get().caCertificates();
ui->tableCaCerts->setRowCount(ca_certs.size());
for(int i=0;i<ca_certs.size();i++)
{

View File

@@ -1,20 +1,10 @@
#include <QApplication>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkConfigurationManager>
#include <QMessageBox>
#include <QtNetwork/QNetworkReply>
#include <QFile>
#include <QtNetwork/QSslKey>
#include <QProgressDialog>
#include <QInputDialog>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QStandardPaths>
#include <QUrlQuery>
#include <QtNetwork/QHttpMultiPart>
#include <QtNetwork/QNetworkProxyFactory>
#include <QTimeZone>
#include <QtNetwork/QNetworkProxy>
#include <json.hpp>
#include <iterator>
@@ -24,664 +14,18 @@
#include "sqlite.h"
#include "version.h"
using json = nlohmann::json;
RemoteDatabase::RemoteDatabase() :
m_manager(new QNetworkAccessManager),
m_configurationManager(new QNetworkConfigurationManager),
m_progress(nullptr),
m_dbLocal(nullptr)
{
// Update network configurations
connect(m_configurationManager, &QNetworkConfigurationManager::updateCompleted, [this]() {
m_manager->setConfiguration(m_configurationManager->defaultConfiguration());
emit networkReady();
});
// 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;
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::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();
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 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)
{
// Do not show error message when operation was cancelled on purpose
if(reply->error() != QNetworkReply::OperationCanceledError)
{
QMessageBox::warning(nullptr, qApp->applicationName(),
reply->errorString() + "\n" + reply->readAll());
}
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.
// Add cloned database to list of local databases
QString saveFileAs = localAdd(reply->url().fileName(),
reply->property("certfile").toString(),
reply->url(),
QUrlQuery(reply->url()).queryItemValue("commit").toStdString(),
QUrlQuery(reply->url()).queryItemValue("branch").toStdString());
// Save the downloaded data under the generated file name
QFile file(saveFileAs);
file.open(QIODevice::WriteOnly);
file.write(reply->readAll());
// Set last modified data of the new file to the one provided by the server
// Before version 5.10, Qt didn't offer any option to set this attribute, so we're not setting it at the moment
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QString last_modified = reply->rawHeader("Content-Disposition");
QRegExp regex("^.*modification-date=\"(.+)\";.*$");
regex.setMinimal(true); // Set to non-greedy matching
if(regex.indexIn(last_modified) != -1)
{
last_modified = regex.cap(1);
bool success = file.setFileTime(QDateTime::fromString(last_modified, Qt::ISODate), QFileDevice::FileModificationTime);
if(!success)
qWarning() << file.errorString();
}
#endif
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;
}
case RequestTypeLicenceList:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Parse data and build ordered licence map: order -> (short name, long name)
std::map<int, std::pair<std::string, std::string>> licences;
for(auto it=obj.cbegin();it!=obj.cend();++it)
licences.insert({it.value()["order"], {it.key(), it.value()["full_name"]}});
// Convert the map into an ordered vector and send it to anyone who's interested
std::vector<std::pair<std::string, std::string>> licence_list;
std::transform(licences.begin(), licences.end(), std::back_inserter(licence_list), [](const std::pair<int, std::pair<std::string, std::string>>& it) {
return it.second;
});
emit gotLicenceList(licence_list);
break;
}
case RequestTypeBranchList:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
json obj_branches = obj["branches"];
// Parse data and assemble branch list
std::vector<std::string> branches;
for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it)
branches.push_back(it.key());
// Get default branch
std::string default_branch = (obj.contains("default_branch") && !obj["default_branch"].empty()) ? obj["default_branch"] : "master";
// Send branch list to anyone who is interested
emit gotBranchList(branches, default_branch);
break;
}
case RequestTypePush:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Create or update the record in our local checkout database
QString saveFileAs = localAdd(reply->url().fileName(),
reply->property("certfile").toString(),
QString::fromStdString(obj["url"]),
obj["commit_id"],
QUrlQuery(QUrl(QString::fromStdString(obj["url"]))).queryItemValue("branch").toStdString());
// If the name of the source file and the name we're saving as differ, we're doing an initial push. In this case, copy the source file to
// the destination path to avoid redownloading it when it's first used.
if(saveFileAs != reply->property("source_file").toString())
QFile::copy(reply->property("source_file").toString(), saveFileAs);
emit uploadFinished(obj["url"]);
break;
}
case RequestTypeMetadata:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Extract and convert data
json obj_branches = obj["branches"];
json obj_commits = obj["commits"];
json obj_releases = obj["releases"];
json obj_tags = obj["tags"];
std::string default_branch = (obj.contains("default_branch") && !obj["default_branch"].empty()) ? obj["default_branch"] : "master";
std::vector<RemoteMetadataBranchInfo> branches;
for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it)
branches.emplace_back(it.key(), it.value()["commit"], it.value()["description"], it.value()["commit_count"]);
std::vector<RemoteMetadataReleaseInfo> releases;
for(auto it=obj_releases.cbegin();it!=obj_releases.cend();++it)
{
releases.emplace_back(it.key(), it.value()["commit"], it.value()["date"],
it.value()["description"], it.value()["email"],
it.value()["name"], it.value()["size"]);
}
std::vector<RemoteMetadataReleaseInfo> tags;
for(auto it=obj_tags.cbegin();it!=obj_tags.cend();++it)
{
tags.emplace_back(it.key(), it.value()["commit"], it.value()["date"],
it.value()["description"], it.value()["email"],
it.value()["name"], 0);
}
// Send data list to anyone who is interested
emit gotMetadata(branches, obj_commits.dump(), releases, tags, default_branch, obj["web_page"]);
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;
}
// Delete reply later, i.e. after returning from this slot function
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?
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 RemoteDatabase::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>& RemoteDatabase::caCertificates() const
{
static QList<QSslCertificate> certs = m_sslConfiguration.caCertificates();
return certs;
}
QString RemoteDatabase::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 RemoteDatabase::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 RemoteDatabase::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, &RemoteDatabase::updateProgress);
else
connect(reply, &QNetworkReply::downloadProgress, this, &RemoteDatabase::updateProgress);
}
void RemoteDatabase::fetch(const QUrl& 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(nullptr, 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, QUrlQuery(url).queryItemValue("branch").toStdString());
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;
}
// Check if we already have a clone of this database branch. If so, show a warning because there might
// be unpushed changes. For this we don't care about the currently checked out commit id because for
// any commit local changes could be lost.
// TODO Detect local changes and don't warn when no changes were made
QUrl url_without_commit_id(url);
QUrlQuery url_without_commit_id_query(url_without_commit_id);
url_without_commit_id_query.removeQueryItem("commit");
url_without_commit_id.setQuery(url_without_commit_id_query);
if(!localExists(url_without_commit_id, clientCert, QUrlQuery(url).queryItemValue("branch").toStdString()).isEmpty())
{
if(QMessageBox::warning(nullptr,
QApplication::applicationName(),
tr("Fetching this commit might override local changes when you have not pushed them yet.\n"
"Are you sure you want to fetch it?"),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel) == QMessageBox::Cancel)
{
return;
}
}
}
// Build network request
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName(), 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 = 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("userdata", userdata);
// 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 RemoteDatabase::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)
{
// Check if network is accessible. If not, abort right here
if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible)
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: The network is not accessible."));
return;
}
// 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 the pushed file is actually a cloned file
if(filename.startsWith(Settings::getValue("remote", "clonedirectory").toString()))
addPart(multipart, "commit", QString::fromStdString(localLastCommitId(clientCert, url, branch.toStdString())));
// 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
// Initialise the progress dialog for this request
prepareProgressDialog(reply, true, url);
}
void RemoteDatabase::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 RemoteDatabase::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 RemoteDatabase::localAssureOpened()
{
// This function should be called first in each RemoteDatabase::local* function. It assures the database for storing
@@ -737,14 +81,6 @@ void RemoteDatabase::localAssureOpened()
QString RemoteDatabase::localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch)
{
// 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 commit id field to be correctly filled we
// require the commit id to be part of the url parameter. Also note that this function doesn't care if the
// database has already been added to the list or not. If you need this information you need to check it before
// calling this function, ideally even before sending out a request to the network. The function returns the full
// path of the newly created/updated file.
localAssureOpened();
// Remove the path
@@ -950,8 +286,6 @@ QString RemoteDatabase::localCheckFile(const QString& local_file)
std::string RemoteDatabase::localLastCommitId(QString identity, const QUrl& url, const std::string& branch)
{
// This function takes a file name and checks with which commit id we had checked out this file or last pushed it.
localAssureOpened();
// Query commit id for that file name
@@ -995,18 +329,6 @@ std::string RemoteDatabase::localLastCommitId(QString identity, const QUrl& url,
return local_commit_id;
}
void RemoteDatabase::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();
}
}
std::vector<RemoteDatabase::LocalFileInfo> RemoteDatabase::localGetLocalFiles(QString identity)
{
localAssureOpened();

View File

@@ -2,65 +2,12 @@
#define REMOTEDATABASE_H
#include <QObject>
#include <QtNetwork/QSslConfiguration>
#include <QVariant>
#include <map>
#include <string>
class QNetworkAccessManager;
class QNetworkConfigurationManager;
class QNetworkReply;
class QProgressDialog;
class QNetworkRequest;
class QHttpMultiPart;
class QFile;
struct sqlite3;
class RemoteMetadataBranchInfo
{
public:
RemoteMetadataBranchInfo(const std::string& _name, const std::string& _commit_id, const std::string& _description, unsigned int _commit_count) :
name(_name),
commit_id(_commit_id),
description(_description),
commit_count(_commit_count)
{}
RemoteMetadataBranchInfo() :
commit_count(0)
{}
std::string name;
std::string commit_id;
std::string description;
unsigned int commit_count;
};
class RemoteMetadataReleaseInfo
{
public:
RemoteMetadataReleaseInfo(const std::string& _name, const std::string& _commit_id, const std::string& _date,
const std::string& _description, const std::string& _email,
const std::string& _user_name, unsigned int _size) :
name(_name),
commit_id(_commit_id),
date(_date),
description(_description),
email(_email),
user_name(_user_name),
size(_size)
{}
RemoteMetadataReleaseInfo() :
size(0)
{}
std::string name;
std::string commit_id;
std::string date;
std::string description;
std::string email;
std::string user_name;
unsigned long size;
};
class RemoteDatabase : public QObject
{
Q_OBJECT
@@ -69,35 +16,6 @@ public:
RemoteDatabase();
~RemoteDatabase() override;
void reloadSettings();
enum CertInfo
{
CertInfoUser,
CertInfoServer,
};
const QList<QSslCertificate>& caCertificates() const;
const std::map<QString, QSslCertificate>& clientCertificates() const { return m_clientCertFiles; }
QString getInfoFromClientCert(const QString& cert, CertInfo info) const;
enum RequestType
{
RequestTypeDatabase,
RequestTypeDirectory,
RequestTypeNewVersionCheck,
RequestTypePush,
RequestTypeLicenceList,
RequestTypeBranchList,
RequestTypeMetadata,
RequestTypeDownload,
};
void fetch(const QUrl& url, RequestType type, const QString& clientCert = QString(), QVariant userdata = QVariant());
void push(const QString& filename, const QUrl& url, const QString& clientCert, const QString& remotename,
const QString& commitMessage = QString(), const QString& licence = QString(), bool isPublic = false,
const QString& branch = QString("master"), bool forcePush = false);
// This class compiles all the information on a lcao database file
class LocalFileInfo
{
@@ -145,55 +63,23 @@ public:
// If the URL contains a commit id (optional), this commit id is part of the check.
QString localExists(const QUrl& url, QString identity, const std::string& branch);
signals:
// As soon as you can safely open a network connection, this signal is emitted. This can be used to delay early network requests
// which might otherwise fail.
void networkReady();
// The openFile signal is emitted whenever a remote database file shall be opened in the main window. This happens when the
// fetch() call for a database is finished, either by actually downloading the database or opening the local clone.
void openFile(QString path);
// These signals are emitted when the fetch() calls are finished that are not requesting a remote database but other data, like
// a directory listing or the licence list.
void gotDirList(QString json, QVariant userdata);
void gotCurrentVersion(QString version, QString url);
void gotLicenceList(std::vector<std::pair<std::string, std::string>> licences);
void gotBranchList(std::vector<std::string> branches, std::string default_branch);
void gotMetadata(std::vector<RemoteMetadataBranchInfo> branches, std::string commits,
std::vector<RemoteMetadataReleaseInfo> releases, std::vector<RemoteMetadataReleaseInfo> tags,
std::string default_branch, std::string web_page);
// The uploadFinished() signal is emitted when a push() call is finished, i.e. a database upload has completed.
void uploadFinished(std::string url);
private:
void gotEncrypted(QNetworkReply* reply);
void gotReply(QNetworkReply* reply);
void gotError(QNetworkReply* reply, const QList<QSslError>& errors);
void updateProgress(qint64 bytesTransmitted, qint64 bytesTotal);
bool prepareSsl(QNetworkRequest* request, const QString& clientCert);
void prepareProgressDialog(QNetworkReply* reply, bool upload, const QUrl& url);
// Helper functions for managing the list of locally available databases
void localAssureOpened();
QString localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch);
QString localCheckFile(const QString& local_file);
// This function takes a file name and checks with which commit id we had checked out this file or last pushed it.
std::string localLastCommitId(QString clientCert, const QUrl& url, const std::string& branch);
// Helper functions for building multi-part HTTP requests
void addPart(QHttpMultiPart* multipart, const QString& name, const QString& value) const;
void addPart(QHttpMultiPart* multipart, const QString& name, QFile* file, const QString& filename) const;
// 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 commit id field to be correctly filled we
// require the commit id to be part of the url parameter. Also note that this function doesn't care if the
// database has already been added to the list or not. If you need this information you need to check it before
// calling this function, ideally even before sending out a request to the network. The function returns the full
// path of the newly created/updated file.
QString localAdd(QString filename, QString identity, const QUrl& url, const std::string& new_commit_id, const std::string& branch);
// Before using a new client certificate we need to clear the access and authentication cache of the network manager
// object. Otherwise Qt might reuse the old certificate if the requested URL has been used before.
void clearAccessCache(const QString& clientCert);
private:
// Helper functions for managing the list of locally available databases
void localAssureOpened();
QString localCheckFile(const QString& local_file);
QNetworkAccessManager* m_manager;
QNetworkConfigurationManager* m_configurationManager;
QProgressDialog* m_progress;
QSslConfiguration m_sslConfiguration;
std::map<QString, QSslCertificate> m_clientCertFiles;
sqlite3* m_dbLocal;
};

View File

@@ -21,9 +21,8 @@ RemoteDock::RemoteDock(MainWindow* parent)
: QDialog(parent),
ui(new Ui::RemoteDock),
mainWindow(parent),
remoteDatabase(parent->getRemote()),
remoteModel(new RemoteModel(this, parent->getRemote())),
remoteLocalFilesModel(new RemoteLocalFilesModel(this, parent->getRemote())),
remoteModel(new RemoteModel(this)),
remoteLocalFilesModel(new RemoteLocalFilesModel(this, remoteDatabase)),
remoteCommitsModel(new RemoteCommitsModel(this))
{
ui->setupUi(this);
@@ -40,18 +39,15 @@ RemoteDock::RemoteDock(MainWindow* parent)
ui->treeLocal->setColumnWidth(RemoteLocalFilesModel::ColumnSize, 80); // Make size column narrower
ui->treeLocal->setColumnHidden(RemoteLocalFilesModel::ColumnFile, true); // Hide local file name
// When a database has been downloaded and must be opened, notify users of this class
connect(&remoteDatabase, &RemoteDatabase::openFile, this, &RemoteDock::openFile);
// Reload the directory tree and the list of local checkouts when a database upload has finished
connect(&remoteDatabase, &RemoteDatabase::uploadFinished, this, &RemoteDock::refresh);
connect(&remoteDatabase, &RemoteDatabase::openFile, this, &RemoteDock::refreshLocalFileList);
// Handle finished uploads and downloads of databases
connect(&RemoteNetwork::get(), &RemoteNetwork::fetchFinished, this, &RemoteDock::fetchFinished);
connect(&RemoteNetwork::get(), &RemoteNetwork::pushFinished, this, &RemoteDock::pushFinished);
// Whenever a new directory listing has been parsed, check if it was a new root dir and, if so, open the user's directory
connect(remoteModel, &RemoteModel::directoryListingParsed, this, &RemoteDock::newDirectoryNode);
// Show metadata for a database when we get it
connect(&remoteDatabase, &RemoteDatabase::gotMetadata, this, &RemoteDock::showMetadata);
connect(&RemoteNetwork::get(), &RemoteNetwork::gotMetadata, this, &RemoteDock::showMetadata);
// When the Preferences link is clicked in the no-certificates-label, open the preferences dialog. For other links than the ones we know,
// just open them in a web browser
@@ -129,7 +125,7 @@ RemoteDock::RemoteDock(MainWindow* parent)
fetchCommit(ui->treeDatabaseCommits->currentIndex());
});
connect(ui->actionDownloadCommit, &QAction::triggered, [this]() {
fetchCommit(ui->treeDatabaseCommits->currentIndex(), RemoteDatabase::RequestTypeDownload);
fetchCommit(ui->treeDatabaseCommits->currentIndex(), RemoteNetwork::RequestTypeDownload);
});
ui->treeDatabaseCommits->addAction(ui->actionFetchCommit);
ui->treeDatabaseCommits->addAction(ui->actionDownloadCommit);
@@ -183,7 +179,7 @@ void RemoteDock::setNewIdentity(const QString& identity)
return;
// Open root directory. Get host name from client cert
remoteModel->setNewRootDir(remoteDatabase.getInfoFromClientCert(cert, RemoteDatabase::CertInfoServer), cert);
remoteModel->setNewRootDir(RemoteNetwork::get().getInfoFromClientCert(cert, RemoteNetwork::CertInfoServer), cert);
// Reset list of local checkouts
remoteLocalFilesModel->setIdentity(cert);
@@ -206,7 +202,7 @@ void RemoteDock::fetchDatabase(const QModelIndex& idx)
fetchDatabase(item->value(RemoteModelColumnUrl).toString());
}
void RemoteDock::fetchDatabase(QString url_string, RemoteDatabase::RequestType request_type)
void RemoteDock::fetchDatabase(QString url_string, RemoteNetwork::RequestType request_type)
{
// If no URL was provided ask the user. Default to the current clipboard contents
if(url_string.isEmpty())
@@ -226,7 +222,7 @@ void RemoteDock::fetchDatabase(QString url_string, RemoteDatabase::RequestType r
// Check the URL
QUrl url(url_string);
if(url.authority() != QUrl(remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoServer)).authority())
if(url.authority() != QUrl(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer)).authority())
{
QMessageBox::warning(this, qApp->applicationName(), tr("Invalid URL: The host name does not match the host name of the current identity."));
return;
@@ -242,11 +238,43 @@ void RemoteDock::fetchDatabase(QString url_string, RemoteDatabase::RequestType r
return;
}
// There is a chance that we've already cloned that database. So check for that first
QString exists = remoteDatabase.localExists(url, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString());
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;
}
// Check if we already have a clone of this database branch. If so, show a warning because there might
// be unpushed changes. For this we don't care about the currently checked out commit id because for
// any commit local changes could be lost.
// TODO Detect local changes and don't warn when no changes were made
QUrl url_without_commit_id(url);
QUrlQuery url_without_commit_id_query(url_without_commit_id);
url_without_commit_id_query.removeQueryItem("commit");
url_without_commit_id.setQuery(url_without_commit_id_query);
if(!remoteDatabase.localExists(url_without_commit_id, remoteModel->currentClientCertificate(), QUrlQuery(url).queryItemValue("branch").toStdString()).isEmpty())
{
if(QMessageBox::warning(nullptr,
QApplication::applicationName(),
tr("Fetching this commit might override local changes when you have not pushed them yet.\n"
"Are you sure you want to fetch it?"),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel) == QMessageBox::Cancel)
{
return;
}
}
// Clone the database
remoteDatabase.fetch(url.toString(), request_type, remoteModel->currentClientCertificate());
RemoteNetwork::get().fetch(url.toString(), request_type, remoteModel->currentClientCertificate());
}
void RemoteDock::fetchCommit(const QModelIndex& idx, RemoteDatabase::RequestType request_type)
void RemoteDock::fetchCommit(const QModelIndex& idx, RemoteNetwork::RequestType request_type)
{
// Fetch selected commit
QUrl url(QString::fromStdString(currently_opened_file_info.url));
@@ -296,20 +324,26 @@ void RemoteDock::pushDatabase()
name = name.remove(QRegExp("_[0-9]+.remotedb$"));
// Show the user a dialog for setting all the commit details
QString host = remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoServer);
RemotePushDialog pushDialog(this, remoteDatabase, host, remoteModel->currentClientCertificate(), name, QString::fromStdString(currently_opened_file_info.branch));
QString host = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer);
RemotePushDialog pushDialog(this, host, remoteModel->currentClientCertificate(), name, QString::fromStdString(currently_opened_file_info.branch));
if(pushDialog.exec() != QDialog::Accepted)
return;
// Build push URL
QString url = host;
url.append(remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoUser));
url.append(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser));
url.append("/");
url.append(pushDialog.name());
// Check if we are pushing a cloned database. Only in this case we provide the last known commit id
QString commit_id;
if(mainWindow->getDb().currentFile().startsWith(Settings::getValue("remote", "clonedirectory").toString()))
commit_id = QString::fromStdString(remoteDatabase.localLastCommitId(remoteModel->currentClientCertificate(), url, pushDialog.branch().toStdString()));
// Push database
remoteDatabase.push(mainWindow->getDb().currentFile(), url, remoteModel->currentClientCertificate(), pushDialog.name(),
pushDialog.commitMessage(), pushDialog.licence(), pushDialog.isPublic(), pushDialog.branch(), pushDialog.forcePush());
RemoteNetwork::get().push(mainWindow->getDb().currentFile(), url, remoteModel->currentClientCertificate(), pushDialog.name(),
pushDialog.commitMessage(), pushDialog.licence(), pushDialog.isPublic(), pushDialog.branch(),
pushDialog.forcePush(), commit_id);
}
void RemoteDock::newDirectoryNode(const QModelIndex& parent)
@@ -320,7 +354,7 @@ void RemoteDock::newDirectoryNode(const QModelIndex& parent)
// Then check if there is a directory with the current user name
// Get current user name
QString user = remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoUser);
QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser);
for(int i=0;i<remoteModel->rowCount();i++)
{
@@ -351,7 +385,7 @@ void RemoteDock::refreshLocalFileList()
remoteLocalFilesModel->refresh();
// Expand node for current user
QString user = remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoUser);
QString user = RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoUser);
for(int i=0;i<remoteLocalFilesModel->rowCount();i++)
{
QModelIndex child = remoteLocalFilesModel->index(i, RemoteLocalFilesModel::ColumnName);
@@ -414,13 +448,13 @@ void RemoteDock::fileOpened(const QString& filename)
void RemoteDock::refreshMetadata(const QString& username, const QString& dbname)
{
QUrl url(remoteDatabase.getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteDatabase::CertInfoServer) + "/metadata/get");
QUrl url(RemoteNetwork::get().getInfoFromClientCert(remoteModel->currentClientCertificate(), RemoteNetwork::CertInfoServer) + "/metadata/get");
QUrlQuery query;
query.addQueryItem("username", username);
query.addQueryItem("folder", "/");
query.addQueryItem("dbname", dbname);
url.setQuery(query);
remoteDatabase.fetch(url.toString(), RemoteDatabase::RequestTypeMetadata, remoteModel->currentClientCertificate());
RemoteNetwork::get().fetch(url.toString(), RemoteNetwork::RequestTypeMetadata, remoteModel->currentClientCertificate());
}
void RemoteDock::showMetadata(const std::vector<RemoteMetadataBranchInfo>& branches, const std::string& commits,
@@ -486,3 +520,51 @@ void RemoteDock::refresh()
if(!currently_opened_file_info.file.empty())
refreshMetadata(currently_opened_file_info.user_name(), QString::fromStdString(currently_opened_file_info.name));
}
void RemoteDock::pushFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QString& source_file)
{
// Create or update the record in our local checkout database
QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch);
// If the name of the source file and the name we're saving as differ, we're doing an initial push. In this case, copy the source file to
// the destination path to avoid redownloading it when it's first used.
if(saveFileAs != source_file)
QFile::copy(source_file, saveFileAs);
// Update info on currently opened file
currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs);
// Refresh view
refresh();
}
void RemoteDock::fetchFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QDateTime& last_modified, QIODevice* device)
{
// Add cloned database to list of local databases
QString saveFileAs = remoteDatabase.localAdd(filename, identity, url, new_commit_id, branch);
// Save the downloaded data under the generated file name
QFile file(saveFileAs);
file.open(QIODevice::WriteOnly);
file.write(device->readAll());
// Set last modified data of the new file to the one provided by the server
// Before version 5.10, Qt didn't offer any option to set this attribute, so we're not setting it at the moment
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
file.setFileTime(last_modified, QFileDevice::FileModificationTime);
#endif
// Close file
file.close();
// Update info on currently opened file
currently_opened_file_info = remoteDatabase.localGetLocalFileInfo(saveFileAs);
// Refresh data
refreshLocalFileList();
// Tell the application to open this file
emit openFile(saveFileAs);
}

View File

@@ -4,6 +4,7 @@
#include <QDialog>
#include "RemoteDatabase.h"
#include "RemoteNetwork.h"
class RemoteCommitsModel;
class RemoteLocalFilesModel;
@@ -37,8 +38,8 @@ public slots:
private slots:
void setNewIdentity(const QString& identity);
void fetchDatabase(const QModelIndex& idx);
void fetchDatabase(QString url = QString(), RemoteDatabase::RequestType request_type = RemoteDatabase::RequestTypeDatabase);
void fetchCommit(const QModelIndex& idx, RemoteDatabase::RequestType request_type = RemoteDatabase::RequestTypeDatabase);
void fetchDatabase(QString url = QString(), RemoteNetwork::RequestType request_type = RemoteNetwork::RequestTypeDatabase);
void fetchCommit(const QModelIndex& idx, RemoteNetwork::RequestType request_type = RemoteNetwork::RequestTypeDatabase);
void pushDatabase();
void newDirectoryNode(const QModelIndex& parent);
void switchToMainView();
@@ -49,6 +50,10 @@ private slots:
void deleteLocalDatabase(const QModelIndex& index);
void openCurrentDatabaseInBrowser() const;
void refresh();
void pushFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QString& source_file);
void fetchFinished(const QString& filename, const QString& identity, const QUrl& url, const std::string& new_commit_id,
const std::string& branch, const QDateTime& last_modified, QIODevice* device);
signals:
void openFile(QString file);
@@ -58,7 +63,7 @@ private:
MainWindow* mainWindow;
RemoteDatabase& remoteDatabase;
RemoteDatabase remoteDatabase;
RemoteModel* remoteModel;
RemoteLocalFilesModel* remoteLocalFilesModel;
RemoteCommitsModel* remoteCommitsModel;

View File

@@ -5,8 +5,9 @@
#include <QUrl>
#include "Data.h"
#include "RemoteLocalFilesModel.h"
#include "RemoteDatabase.h"
#include "RemoteLocalFilesModel.h"
#include "RemoteNetwork.h"
#include "Settings.h"
using json = nlohmann::json;
@@ -28,7 +29,7 @@ RemoteLocalFilesModel::~RemoteLocalFilesModel()
void RemoteLocalFilesModel::setIdentity(const QString& cert_filename)
{
current_cert_filename = cert_filename;
current_user_name = remoteDatabase.getInfoFromClientCert(cert_filename, RemoteDatabase::CertInfoUser);
current_user_name = RemoteNetwork::get().getInfoFromClientCert(cert_filename, RemoteNetwork::CertInfoUser);
refresh();
}

View File

@@ -2,7 +2,7 @@
#include "Data.h"
#include "RemoteModel.h"
#include "RemoteDatabase.h"
#include "RemoteNetwork.h"
using json = nlohmann::json;
@@ -98,14 +98,13 @@ std::vector<RemoteModelItem*> RemoteModelItem::loadArray(const json& array, Remo
return items;
}
RemoteModel::RemoteModel(QObject* parent, RemoteDatabase& remote) :
RemoteModel::RemoteModel(QObject* parent) :
QAbstractItemModel(parent),
headerList({tr("Name"), tr("Last modified"), tr("Size"), tr("Commit")}),
rootItem(new RemoteModelItem()),
remoteDatabase(remote)
rootItem(new RemoteModelItem())
{
// Set up signals
connect(&remoteDatabase, &RemoteDatabase::gotDirList, this, &RemoteModel::parseDirectoryListing);
connect(&RemoteNetwork::get(), &RemoteNetwork::gotDirList, this, &RemoteModel::parseDirectoryListing);
}
RemoteModel::~RemoteModel()
@@ -116,7 +115,7 @@ RemoteModel::~RemoteModel()
void RemoteModel::setNewRootDir(const QString& url, const QString& cert)
{
// Get user name from client cert
currentUserName = remoteDatabase.getInfoFromClientCert(cert, RemoteDatabase::CertInfoUser);
currentUserName = RemoteNetwork::get().getInfoFromClientCert(cert, RemoteNetwork::CertInfoUser);
// Save settings
currentRootDirectory = url;
@@ -129,7 +128,7 @@ void RemoteModel::setNewRootDir(const QString& url, const QString& cert)
void RemoteModel::refresh()
{
// Fetch root directory and put the reply data under the root item
remoteDatabase.fetch(currentRootDirectory, RemoteDatabase::RequestTypeDirectory, currentClientCert, QModelIndex());
RemoteNetwork::get().fetch(currentRootDirectory, RemoteNetwork::RequestTypeDirectory, currentClientCert, QModelIndex());
}
void RemoteModel::parseDirectoryListing(const QString& text, const QVariant& userdata)
@@ -307,7 +306,7 @@ void RemoteModel::fetchMore(const QModelIndex& parent)
// Fetch item URL
item->setFetchedDirectoryList(true);
remoteDatabase.fetch(item->value(RemoteModelColumnUrl).toUrl(), RemoteDatabase::RequestTypeDirectory, currentClientCert, parent);
RemoteNetwork::get().fetch(item->value(RemoteModelColumnUrl).toUrl(), RemoteNetwork::RequestTypeDirectory, currentClientCert, parent);
}
const QString& RemoteModel::currentClientCertificate() const

View File

@@ -6,8 +6,6 @@
#include <json.hpp>
class RemoteDatabase;
// List of fields stored in the JSON data
enum RemoteModelColumns
{
@@ -62,7 +60,7 @@ class RemoteModel : public QAbstractItemModel
Q_OBJECT
public:
explicit RemoteModel(QObject* parent, RemoteDatabase& remote);
explicit RemoteModel(QObject* parent);
~RemoteModel() override;
void setNewRootDir(const QString& url, const QString& cert);
@@ -105,9 +103,6 @@ private:
// Pointer to the root item. This contains all the actual item data.
RemoteModelItem* rootItem;
// Reference to the remote database object which is stored somewhere in the main window.
RemoteDatabase& remoteDatabase;
// This stores the currently used network identity so it can be used for further requests, e.g. for
// lazy population.
QUrl currentRootDirectory;

635
src/RemoteNetwork.cpp Normal file
View File

@@ -0,0 +1,635 @@
#include <QApplication>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkConfigurationManager>
#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_configurationManager(new QNetworkConfigurationManager),
m_progress(nullptr)
{
// Update network configurations
connect(m_configurationManager, &QNetworkConfigurationManager::updateCompleted, [this]() {
m_manager->setConfiguration(m_configurationManager->defaultConfiguration());
emit networkReady();
});
// 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;
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::finished, this, &RemoteNetwork::gotReply);
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)
{
// 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)
{
QMessageBox::warning(nullptr, qApp->applicationName(),
reply->errorString() + "\n" + reply->readAll());
}
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.
// 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 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;
}
case RequestTypeLicenceList:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Parse data and build ordered licence map: order -> (short name, long name)
std::map<int, std::pair<std::string, std::string>> licences;
for(auto it=obj.cbegin();it!=obj.cend();++it)
licences.insert({it.value()["order"], {it.key(), it.value()["full_name"]}});
// Convert the map into an ordered vector and send it to anyone who's interested
std::vector<std::pair<std::string, std::string>> licence_list;
std::transform(licences.begin(), licences.end(), std::back_inserter(licence_list), [](const std::pair<int, std::pair<std::string, std::string>>& it) {
return it.second;
});
emit gotLicenceList(licence_list);
break;
}
case RequestTypeBranchList:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
json obj_branches = obj["branches"];
// Parse data and assemble branch list
std::vector<std::string> branches;
for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it)
branches.push_back(it.key());
// Get default branch
std::string default_branch = (obj.contains("default_branch") && !obj["default_branch"].empty()) ? obj["default_branch"] : "master";
// Send branch list to anyone who is interested
emit gotBranchList(branches, default_branch);
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 RequestTypeMetadata:
{
// Read and check results
json obj = json::parse(reply->readAll(), nullptr, false);
if(obj.is_discarded() || !obj.is_object())
break;
// Extract and convert data
json obj_branches = obj["branches"];
json obj_commits = obj["commits"];
json obj_releases = obj["releases"];
json obj_tags = obj["tags"];
std::string default_branch = (obj.contains("default_branch") && !obj["default_branch"].empty()) ? obj["default_branch"] : "master";
std::vector<RemoteMetadataBranchInfo> branches;
for(auto it=obj_branches.cbegin();it!=obj_branches.cend();++it)
branches.emplace_back(it.key(), it.value()["commit"], it.value()["description"], it.value()["commit_count"]);
std::vector<RemoteMetadataReleaseInfo> releases;
for(auto it=obj_releases.cbegin();it!=obj_releases.cend();++it)
{
releases.emplace_back(it.key(), it.value()["commit"], it.value()["date"],
it.value()["description"], it.value()["email"],
it.value()["name"], it.value()["size"]);
}
std::vector<RemoteMetadataReleaseInfo> tags;
for(auto it=obj_tags.cbegin();it!=obj_tags.cend();++it)
{
tags.emplace_back(it.key(), it.value()["commit"], it.value()["date"],
it.value()["description"], it.value()["email"],
it.value()["name"], 0);
}
// Send data list to anyone who is interested
emit gotMetadata(branches, obj_commits.dump(), releases, tags, default_branch, obj["web_page"]);
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;
}
// 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, QVariant userdata)
{
// Check if network is accessible. If not, abort right here
if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible)
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: The network is not accessible."));
return;
}
// Build network request
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("User-Agent", QString("%1 %2").arg(qApp->organizationName(), 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 = 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("userdata", userdata);
// 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)
{
// Check if network is accessible. If not, abort right here
if(m_manager->networkAccessible() == QNetworkAccessManager::NotAccessible)
{
QMessageBox::warning(nullptr, qApp->applicationName(), tr("Error: The network is not accessible."));
return;
}
// 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
// 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();
}
}

151
src/RemoteNetwork.h Normal file
View File

@@ -0,0 +1,151 @@
#ifndef REMOTENETWORK_H
#define REMOTENETWORK_H
#include <QObject>
#include <QtNetwork/QSslConfiguration>
#include <map>
class QNetworkAccessManager;
class QNetworkConfigurationManager;
class QNetworkReply;
class QProgressDialog;
class QNetworkRequest;
class QHttpMultiPart;
class QFile;
class RemoteMetadataBranchInfo
{
public:
RemoteMetadataBranchInfo(const std::string& _name, const std::string& _commit_id, const std::string& _description, unsigned int _commit_count) :
name(_name),
commit_id(_commit_id),
description(_description),
commit_count(_commit_count)
{}
RemoteMetadataBranchInfo() :
commit_count(0)
{}
std::string name;
std::string commit_id;
std::string description;
unsigned int commit_count;
};
class RemoteMetadataReleaseInfo
{
public:
RemoteMetadataReleaseInfo(const std::string& _name, const std::string& _commit_id, const std::string& _date,
const std::string& _description, const std::string& _email,
const std::string& _user_name, unsigned int _size) :
name(_name),
commit_id(_commit_id),
date(_date),
description(_description),
email(_email),
user_name(_user_name),
size(_size)
{}
RemoteMetadataReleaseInfo() :
size(0)
{}
std::string name;
std::string commit_id;
std::string date;
std::string description;
std::string email;
std::string user_name;
unsigned long size;
};
class RemoteNetwork : public QObject
{
Q_OBJECT
public:
static RemoteNetwork& get()
{
static RemoteNetwork instance;
return instance;
}
void reloadSettings();
enum CertInfo
{
CertInfoUser,
CertInfoServer,
};
const QList<QSslCertificate>& caCertificates() const;
const std::map<QString, QSslCertificate>& clientCertificates() const { return m_clientCertFiles; }
QString getInfoFromClientCert(const QString& cert, CertInfo info) const;
enum RequestType
{
RequestTypeDatabase,
RequestTypeDirectory,
RequestTypeNewVersionCheck,
RequestTypePush,
RequestTypeLicenceList,
RequestTypeBranchList,
RequestTypeMetadata,
RequestTypeDownload,
};
void fetch(const QUrl& url, RequestType type, const QString& clientCert = QString(), QVariant userdata = QVariant());
void push(const QString& filename, const QUrl& url, const QString& clientCert, const QString& remotename,
const QString& commitMessage = QString(), const QString& licence = QString(), bool isPublic = false,
const QString& branch = QString("master"), bool forcePush = false, const QString& last_commit = QString());
signals:
// As soon as you can safely open a network connection, this signal is emitted. This can be used to delay early network requests
// which might otherwise fail.
void networkReady();
// These signals are emitted when the fetch() calls are finished that are not requesting a remote database but other data, like
// a directory listing or the licence list.
void gotDirList(QString json, QVariant userdata);
void gotCurrentVersion(QString version, QString url);
void gotLicenceList(std::vector<std::pair<std::string, std::string>> licences);
void gotBranchList(std::vector<std::string> branches, std::string default_branch);
void gotMetadata(std::vector<RemoteMetadataBranchInfo> branches, std::string commits,
std::vector<RemoteMetadataReleaseInfo> releases, std::vector<RemoteMetadataReleaseInfo> tags,
std::string default_branch, std::string web_page);
// The fetchFinished() signal is emitted when a fetch() call for a database is finished
void fetchFinished(QString filename, QString identity, const QUrl& url, std::string new_commit_id, std::string branch,
QDateTime last_modified, QIODevice* device);
// The pushFinished() signal is emitted when a push() call is finished, i.e. a database upload has completed.
void pushFinished(QString filename, QString identity, const QUrl& url, std::string new_commit_id, std::string branch, QString source_file);
private:
RemoteNetwork();
~RemoteNetwork() override;
void gotEncrypted(QNetworkReply* reply);
void gotReply(QNetworkReply* reply);
void gotError(QNetworkReply* reply, const QList<QSslError>& errors);
void updateProgress(qint64 bytesTransmitted, qint64 bytesTotal);
bool prepareSsl(QNetworkRequest* request, const QString& clientCert);
void prepareProgressDialog(QNetworkReply* reply, bool upload, const QUrl& url);
// Helper functions for building multi-part HTTP requests
void addPart(QHttpMultiPart* multipart, const QString& name, const QString& value) const;
void addPart(QHttpMultiPart* multipart, const QString& name, QFile* file, const QString& filename) const;
// Before using a new client certificate we need to clear the access and authentication cache of the network manager
// object. Otherwise Qt might reuse the old certificate if the requested URL has been used before.
void clearAccessCache(const QString& clientCert);
QNetworkAccessManager* m_manager;
QNetworkConfigurationManager* m_configurationManager;
QProgressDialog* m_progress;
QSslConfiguration m_sslConfiguration;
std::map<QString, QSslCertificate> m_clientCertFiles;
};
#endif

View File

@@ -4,16 +4,15 @@
#include "RemotePushDialog.h"
#include "ui_RemotePushDialog.h"
#include "RemoteDatabase.h"
#include "RemoteNetwork.h"
RemotePushDialog::RemotePushDialog(QWidget* parent, RemoteDatabase& remote, const QString& host, const QString& clientCert,
RemotePushDialog::RemotePushDialog(QWidget* parent, const QString& host, const QString& clientCert,
const QString& name, const QString& branch) :
QDialog(parent),
ui(new Ui::RemotePushDialog),
m_host(host),
m_clientCert(clientCert),
m_suggestedBranch(branch),
remoteDatabase(remote),
m_nameValidator(new QRegExpValidator(QRegExp("^[a-z,A-Z,0-9,\\.,\\-,\\_,\\(,\\),\\+,\\ ]+$"), this)),
m_branchValidator(new QRegExpValidator(QRegExp("^[a-z,A-Z,0-9,\\^,\\.,\\-,\\_,\\/,\\(,\\),\\:,\\&,\\ )]+$"), this))
{
@@ -29,11 +28,11 @@ RemotePushDialog::RemotePushDialog(QWidget* parent, RemoteDatabase& remote, cons
checkInput();
// Fetch list of available licences
connect(&remoteDatabase, &RemoteDatabase::gotLicenceList, this, &RemotePushDialog::fillInLicences);
remoteDatabase.fetch(host + "licence/list", RemoteDatabase::RequestTypeLicenceList, clientCert);
connect(&RemoteNetwork::get(), &RemoteNetwork::gotLicenceList, this, &RemotePushDialog::fillInLicences);
RemoteNetwork::get().fetch(host + "licence/list", RemoteNetwork::RequestTypeLicenceList, clientCert);
// Prepare fetching list of available branches
connect(&remoteDatabase, &RemoteDatabase::gotBranchList, this, &RemotePushDialog::fillInBranches);
connect(&RemoteNetwork::get(), &RemoteNetwork::gotBranchList, this, &RemotePushDialog::fillInBranches);
reloadBranchList();
}
@@ -139,11 +138,11 @@ void RemotePushDialog::reloadBranchList()
// Assemble query URL
QUrl url(m_host + "branch/list");
QUrlQuery query;
query.addQueryItem("username", remoteDatabase.getInfoFromClientCert(m_clientCert, RemoteDatabase::CertInfoUser));
query.addQueryItem("username", RemoteNetwork::get().getInfoFromClientCert(m_clientCert, RemoteNetwork::CertInfoUser));
query.addQueryItem("folder", "/");
query.addQueryItem("dbname", ui->editName->text());
url.setQuery(query);
// Send request
remoteDatabase.fetch(url.toString(), RemoteDatabase::RequestTypeBranchList, m_clientCert);
RemoteNetwork::get().fetch(url.toString(), RemoteNetwork::RequestTypeBranchList, m_clientCert);
}

View File

@@ -3,7 +3,6 @@
#include <QDialog>
class RemoteDatabase;
class QRegExpValidator;
namespace Ui {
@@ -15,7 +14,7 @@ class RemotePushDialog : public QDialog
Q_OBJECT
public:
explicit RemotePushDialog(QWidget* parent, RemoteDatabase& remote, const QString& host, const QString& clientCert,
explicit RemotePushDialog(QWidget* parent, const QString& host, const QString& clientCert,
const QString& name = QString(), const QString& branch = QString());
~RemotePushDialog() override;
@@ -36,9 +35,6 @@ private:
// Suggested branch to preselect
QString m_suggestedBranch;
// Reference to the remote database object which is stored somewhere in the main window
RemoteDatabase& remoteDatabase;
// Validators
QRegExpValidator* m_nameValidator;
QRegExpValidator* m_branchValidator;

View File

@@ -24,6 +24,7 @@ HEADERS += \
ImageViewer.h \
RemoteCommitsModel.h \
RemoteLocalFilesModel.h \
RemoteNetwork.h \
sqlitedb.h \
MainWindow.h \
EditIndexDialog.h \
@@ -87,6 +88,7 @@ SOURCES += \
ImageViewer.cpp \
RemoteCommitsModel.cpp \
RemoteLocalFilesModel.cpp \
RemoteNetwork.cpp \
sqlitedb.cpp \
MainWindow.cpp \
EditIndexDialog.cpp \