mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-02-11 06:08:33 -06:00
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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
635
src/RemoteNetwork.cpp
Normal 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
151
src/RemoteNetwork.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user