You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
14 KiB
406 lines
14 KiB
/* This file was part of the KDE libraries |
|
|
|
SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "sshmanagermodel.h" |
|
|
|
#include <QStandardItem> |
|
|
|
#include <KLocalizedString> |
|
|
|
#include <KConfig> |
|
#include <KConfigGroup> |
|
|
|
#include <QDebug> |
|
#include <QFile> |
|
#include <QLoggingCategory> |
|
#include <QStandardPaths> |
|
#include <QTextStream> |
|
|
|
#include "profile/ProfileManager.h" |
|
#include "session/Session.h" |
|
#include "session/SessionController.h" |
|
#include "session/SessionManager.h" |
|
|
|
#include "profile/ProfileManager.h" |
|
#include "profile/ProfileModel.h" |
|
|
|
#include "sshconfigurationdata.h" |
|
|
|
Q_LOGGING_CATEGORY(SshManagerPlugin, "org.kde.konsole.plugin.sshmanager") |
|
|
|
namespace |
|
{ |
|
const QString SshDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QStringLiteral("/.ssh/"); |
|
} |
|
|
|
SSHManagerModel::SSHManagerModel(QObject *parent) |
|
: QStandardItemModel(parent) |
|
{ |
|
load(); |
|
if (!m_sshConfigTopLevelItem) { |
|
// this also sets the m_sshConfigTopLevelItem if the text is `SSH Config`. |
|
addTopLevelItem(i18n("SSH Config")); |
|
} |
|
if (invisibleRootItem()->rowCount() == 0) { |
|
addTopLevelItem(i18n("Default")); |
|
} |
|
|
|
m_sshConfigWatcher.addPath(SshDir + QStringLiteral("config")); |
|
connect(&m_sshConfigWatcher, &QFileSystemWatcher::fileChanged, this, [this] { |
|
startImportFromSshConfig(); |
|
}); |
|
startImportFromSshConfig(); |
|
} |
|
|
|
SSHManagerModel::~SSHManagerModel() noexcept |
|
{ |
|
save(); |
|
} |
|
|
|
QStandardItem *SSHManagerModel::addTopLevelItem(const QString &name) |
|
{ |
|
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) { |
|
if (invisibleRootItem()->child(i)->text() == name) { |
|
return nullptr; |
|
} |
|
} |
|
|
|
auto *newItem = new QStandardItem(); |
|
newItem->setText(name); |
|
newItem->setToolTip(i18n("%1 is a folder for SSH entries", name)); |
|
invisibleRootItem()->appendRow(newItem); |
|
invisibleRootItem()->sortChildren(0); |
|
|
|
if (name == i18n("SSH Config")) { |
|
m_sshConfigTopLevelItem = newItem; |
|
} |
|
|
|
return newItem; |
|
} |
|
|
|
void SSHManagerModel::addChildItem(const SSHConfigurationData &config, const QString &parentName) |
|
{ |
|
QStandardItem *parentItem = nullptr; |
|
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) { |
|
if (invisibleRootItem()->child(i)->text() == parentName) { |
|
parentItem = invisibleRootItem()->child(i); |
|
break; |
|
} |
|
} |
|
|
|
if (!parentItem) { |
|
parentItem = addTopLevelItem(parentName); |
|
} |
|
|
|
auto newChild = new QStandardItem(); |
|
newChild->setData(QVariant::fromValue(config), SSHRole); |
|
newChild->setText(config.name); |
|
newChild->setToolTip(i18n("Host: %1", config.host)); |
|
parentItem->appendRow(newChild); |
|
parentItem->sortChildren(0); |
|
} |
|
|
|
std::optional<QString> SSHManagerModel::profileForHost(const QString &host) const |
|
{ |
|
auto *root = invisibleRootItem(); |
|
|
|
// iterate through folders: |
|
for (int i = 0, end = root->rowCount(); i < end; ++i) { |
|
// iterate throguh the items on folders; |
|
auto folder = root->child(i); |
|
for (int e = 0, inner_end = folder->rowCount(); e < inner_end; ++e) { |
|
QStandardItem *ssh_item = folder->child(e); |
|
auto data = ssh_item->data(SSHRole).value<SSHConfigurationData>(); |
|
|
|
// Return the profile name if the host matches. |
|
if (data.host == host) { |
|
return data.profileName; |
|
} |
|
} |
|
} |
|
|
|
return {}; |
|
} |
|
|
|
bool SSHManagerModel::setData(const QModelIndex &index, const QVariant &value, int role) |
|
{ |
|
const bool ret = QStandardItemModel::setData(index, value, role); |
|
invisibleRootItem()->sortChildren(0); |
|
return ret; |
|
} |
|
|
|
void SSHManagerModel::editChildItem(const SSHConfigurationData &config, const QModelIndex &idx) |
|
{ |
|
QStandardItem *item = itemFromIndex(idx); |
|
item->setData(QVariant::fromValue(config), SSHRole); |
|
item->setData(config.name, Qt::DisplayRole); |
|
item->parent()->sortChildren(0); |
|
} |
|
|
|
QStringList SSHManagerModel::folders() const |
|
{ |
|
QStringList retList; |
|
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) { |
|
retList.push_back(invisibleRootItem()->child(i)->text()); |
|
} |
|
return retList; |
|
} |
|
|
|
bool SSHManagerModel::hasHost(const QString &host) const |
|
{ |
|
// runs in O(N), should be ok for the amount of data peophe have. |
|
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) { |
|
QStandardItem *iChild = invisibleRootItem()->child(i); |
|
for (int e = 0, end = iChild->rowCount(); e < end; e++) { |
|
QStandardItem *eChild = iChild->child(e); |
|
const auto data = eChild->data(SSHManagerModel::Roles::SSHRole).value<SSHConfigurationData>(); |
|
if (data.host == host) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
void SSHManagerModel::setSessionController(Konsole::SessionController *controller) |
|
{ |
|
if (m_session) { |
|
disconnect(m_session, nullptr, this, nullptr); |
|
} |
|
m_session = controller->session(); |
|
Q_ASSERT(m_session); |
|
|
|
connect(m_session, &QObject::destroyed, this, [this] { |
|
m_session = nullptr; |
|
}); |
|
|
|
connect(m_session, &Konsole::Session::hostnameChanged, this, &SSHManagerModel::triggerProfileChange); |
|
} |
|
|
|
void SSHManagerModel::triggerProfileChange(const QString &sshHost) |
|
{ |
|
auto *sm = Konsole::SessionManager::instance(); |
|
QString profileToLoad; |
|
|
|
// This code is messy, Let's see if this can explain a bit. |
|
// This if sequence tries to do two things: |
|
// Stores the current profile, when we trigger a change - but only |
|
// if our hostname is the localhost. |
|
// and when we change to another profile (or go back to the local host) |
|
// we need to restore the previous profile, not go to the default one. |
|
// so this whole mess of m_sessionToProfile is just to load it correctly |
|
// later on. |
|
if (sshHost == QSysInfo::machineHostName()) { |
|
// It's the first time that we call this, using the hostname as host. |
|
// just prepare the session as a empty profile and set it as initialized to false. |
|
if (!m_sessionToProfileName.contains(m_session)) { |
|
m_sessionToProfileName[m_session] = QString(); |
|
return; |
|
} |
|
|
|
// We just loaded the localhost again, after a probable different profile. |
|
// mark the profile to load as the one we stored previously. |
|
else if (m_sessionToProfileName[m_session].count()) { |
|
profileToLoad = m_sessionToProfileName[m_session]; |
|
m_sessionToProfileName.remove(m_session); |
|
} |
|
} else { |
|
// We just loaded a hostname that's not the localhost. save the current profile |
|
// so we can restore it later on, and load the profile for it. |
|
if (m_sessionToProfileName[m_session].isEmpty()) { |
|
m_sessionToProfileName[m_session] = m_session->profile(); |
|
} |
|
} |
|
|
|
// end of really bad code. can someone think of a better algorithm for this? |
|
|
|
if (profileToLoad.isEmpty()) { |
|
std::optional<QString> profileName = profileForHost(sshHost); |
|
if (profileName) { |
|
profileToLoad = *profileName; |
|
} |
|
} |
|
|
|
auto profiles = Konsole::ProfileManager::instance()->allProfiles(); |
|
auto findIt = std::find_if(std::begin(profiles), std::end(profiles), [&profileToLoad](const Konsole::Profile::Ptr &pr) { |
|
return pr->name() == profileToLoad; |
|
}); |
|
if (findIt == std::end(profiles)) { |
|
return; |
|
} |
|
|
|
sm->setSessionProfile(m_session, *findIt); |
|
} |
|
|
|
void SSHManagerModel::load() |
|
{ |
|
auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig); |
|
for (const QString &groupName : config.groupList()) { |
|
KConfigGroup group = config.group(groupName); |
|
addTopLevelItem(groupName); |
|
for (const QString &sessionName : group.groupList()) { |
|
SSHConfigurationData data; |
|
KConfigGroup sessionGroup = group.group(sessionName); |
|
data.host = sessionGroup.readEntry("hostname"); |
|
data.name = sessionGroup.readEntry("identifier"); |
|
data.port = sessionGroup.readEntry("port"); |
|
data.profileName = sessionGroup.readEntry("profileName"); |
|
data.username = sessionGroup.readEntry("username"); |
|
data.sshKey = sessionGroup.readEntry("sshkey"); |
|
data.useSshConfig = sessionGroup.readEntry<bool>("useSshConfig", false); |
|
data.importedFromSshConfig = sessionGroup.readEntry<bool>("importedFromSshConfig", false); |
|
addChildItem(data, groupName); |
|
} |
|
} |
|
} |
|
|
|
void SSHManagerModel::save() |
|
{ |
|
auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig); |
|
for (const QString &groupName : config.groupList()) { |
|
config.deleteGroup(groupName); |
|
} |
|
|
|
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) { |
|
QStandardItem *groupItem = invisibleRootItem()->child(i); |
|
const QString groupName = groupItem->text(); |
|
KConfigGroup baseGroup = config.group(groupName); |
|
for (int e = 0, rend = groupItem->rowCount(); e < rend; e++) { |
|
QStandardItem *sshElement = groupItem->child(e); |
|
const auto data = sshElement->data(SSHRole).value<SSHConfigurationData>(); |
|
KConfigGroup sshGroup = baseGroup.group(data.name.trimmed()); |
|
sshGroup.writeEntry("hostname", data.host.trimmed()); |
|
sshGroup.writeEntry("identifier", data.name.trimmed()); |
|
sshGroup.writeEntry("port", data.port.trimmed()); |
|
sshGroup.writeEntry("profileName", data.profileName.trimmed()); |
|
sshGroup.writeEntry("sshkey", data.sshKey.trimmed()); |
|
sshGroup.writeEntry("useSshConfig", data.useSshConfig); |
|
sshGroup.writeEntry("username", data.username); |
|
sshGroup.writeEntry("importedFromSshConfig", data.importedFromSshConfig); |
|
sshGroup.sync(); |
|
} |
|
baseGroup.sync(); |
|
} |
|
config.sync(); |
|
} |
|
|
|
Qt::ItemFlags SSHManagerModel::flags(const QModelIndex &index) const |
|
{ |
|
if (indexFromItem(invisibleRootItem()) == index.parent()) { |
|
return QStandardItemModel::flags(index); |
|
} else { |
|
return QStandardItemModel::flags(index) & ~Qt::ItemIsEditable; |
|
} |
|
} |
|
|
|
void SSHManagerModel::removeIndex(const QModelIndex &idx) |
|
{ |
|
if (idx.data(Qt::DisplayRole) == i18n("SSH Config")) { |
|
m_sshConfigTopLevelItem = nullptr; |
|
} |
|
|
|
removeRow(idx.row(), idx.parent()); |
|
} |
|
|
|
void SSHManagerModel::startImportFromSshConfig() |
|
{ |
|
importFromSshConfigFile(SshDir + QStringLiteral("config")); |
|
} |
|
|
|
void SSHManagerModel::importFromSshConfigFile(const QString &file) |
|
{ |
|
QFile sshConfig(file); |
|
if (!sshConfig.open(QIODevice::ReadOnly)) { |
|
qCDebug(SshManagerPlugin) << "Can't open config file"; |
|
} |
|
QTextStream stream(&sshConfig); |
|
QString line; |
|
|
|
SSHConfigurationData data; |
|
|
|
// If we hit a *, we ignore till the next Host. |
|
bool ignoreEntry = false; |
|
while (stream.readLineInto(&line)) { |
|
// ignore comments |
|
if (line.startsWith(QStringLiteral("#"))) { |
|
continue; |
|
} |
|
|
|
QStringList lists = line.split(QLatin1Char(' '), Qt::SkipEmptyParts); |
|
// ignore lines that are not "Type Value" |
|
if (lists.count() != 2) { |
|
continue; |
|
} |
|
|
|
if (lists.at(0) == QStringLiteral("Import")) { |
|
if (lists.at(1).contains(QLatin1Char('*'))) { |
|
// TODO: We don't handle globbing yet. |
|
continue; |
|
} |
|
|
|
importFromSshConfigFile(SshDir + lists.at(1)); |
|
continue; |
|
} |
|
|
|
if (lists.at(0) == QStringLiteral("Host")) { |
|
if (line.contains(QLatin1Char('*'))) { |
|
// Panic, ignore everything until the next Host appears. |
|
ignoreEntry = true; |
|
continue; |
|
} else { |
|
ignoreEntry = false; |
|
} |
|
|
|
// When we hit this, that means that we just finished reading the |
|
// *previous* host. so we need to add it to the list, if we can, |
|
// and read the next value. |
|
if (!data.host.isEmpty() && !hasHost(data.host)) { |
|
// We already registered this entity. |
|
|
|
if (data.name.isEmpty()) { |
|
data.name = data.host; |
|
} |
|
data.useSshConfig = true; |
|
data.importedFromSshConfig = true; |
|
data.profileName = Konsole::ProfileManager::instance()->defaultProfile()->name(); |
|
addChildItem(data, i18n("SSH Config")); |
|
} |
|
|
|
data = {}; |
|
data.host = lists.at(1); |
|
} |
|
|
|
if (ignoreEntry) { |
|
continue; |
|
} |
|
|
|
if (lists.at(0) == QStringLiteral("HostName")) { |
|
// hostname is always after Host, so this will be true. |
|
const QString currentHost = data.host; |
|
data.host = lists.at(1).trimmed(); |
|
data.name = currentHost.trimmed(); |
|
} else if (lists.at(0) == QStringLiteral("IdentityFile")) { |
|
data.sshKey = lists.at(1).trimmed(); |
|
} else if (lists.at(0) == QStringLiteral("Port")) { |
|
data.port = lists.at(1).trimmed(); |
|
} else if (lists.at(0) == QStringLiteral("User")) { |
|
data.username = lists.at(1).trimmed(); |
|
} |
|
} |
|
|
|
// the last possible read |
|
if (data.host.count()) { |
|
if (!hasHost(data.host)) { |
|
if (data.name.isEmpty()) { |
|
data.name = data.host.trimmed(); |
|
} |
|
data.useSshConfig = true; |
|
data.importedFromSshConfig = true; |
|
addChildItem(data, i18n("SSH Config")); |
|
} |
|
} |
|
}
|
|
|