File indexing completed on 2024-05-19 05:54:12

0001 /*  This file was part of the KDE libraries
0002 
0003     SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "sshmanagermodel.h"
0009 
0010 #include <QStandardItem>
0011 
0012 #include <KLocalizedString>
0013 
0014 #include <KConfig>
0015 #include <KConfigGroup>
0016 
0017 #include <QFile>
0018 #include <QFileInfo>
0019 #include <QLoggingCategory>
0020 #include <QStandardPaths>
0021 #include <QTextStream>
0022 
0023 #include "profile/ProfileManager.h"
0024 #include "session/Session.h"
0025 #include "session/SessionController.h"
0026 #include "session/SessionManager.h"
0027 
0028 #include "profile/ProfileManager.h"
0029 #include "profile/ProfileModel.h"
0030 
0031 #include "sshconfigurationdata.h"
0032 
0033 Q_LOGGING_CATEGORY(SshManagerPlugin, "org.kde.konsole.plugin.sshmanager")
0034 
0035 namespace
0036 {
0037 const QString sshDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QStringLiteral("/.ssh/");
0038 }
0039 
0040 SSHManagerModel::SSHManagerModel(QObject *parent)
0041     : QStandardItemModel(parent)
0042 {
0043     load();
0044     if (!m_sshConfigTopLevelItem) {
0045         // this also sets the m_sshConfigTopLevelItem if the text is `SSH Config`.
0046         addTopLevelItem(i18nc("@item:inlistbox Hosts from ssh/config file", "SSH Config"));
0047     }
0048     if (invisibleRootItem()->rowCount() == 0) {
0049         addTopLevelItem(i18nc("@item:inlistbox The default list of ssh hosts", "Default"));
0050     }
0051     if (QFileInfo::exists(sshDir + QStringLiteral("config"))) {
0052         m_sshConfigWatcher.addPath(sshDir + QStringLiteral("config"));
0053         connect(&m_sshConfigWatcher, &QFileSystemWatcher::fileChanged, this, [this] {
0054             startImportFromSshConfig();
0055         });
0056         startImportFromSshConfig();
0057     }
0058 }
0059 
0060 SSHManagerModel::~SSHManagerModel() noexcept
0061 {
0062     save();
0063 }
0064 
0065 QStandardItem *SSHManagerModel::addTopLevelItem(const QString &name)
0066 {
0067     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
0068         if (invisibleRootItem()->child(i)->text() == name) {
0069             return nullptr;
0070         }
0071     }
0072 
0073     auto *newItem = new QStandardItem();
0074     newItem->setText(name);
0075     newItem->setToolTip(i18n("%1 is a folder for SSH entries", name));
0076     invisibleRootItem()->appendRow(newItem);
0077     invisibleRootItem()->sortChildren(0);
0078 
0079     if (name == i18n("SSH Config")) {
0080         m_sshConfigTopLevelItem = newItem;
0081     }
0082 
0083     return newItem;
0084 }
0085 
0086 void SSHManagerModel::addChildItem(const SSHConfigurationData &config, const QString &parentName)
0087 {
0088     QStandardItem *parentItem = nullptr;
0089     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
0090         if (invisibleRootItem()->child(i)->text() == parentName) {
0091             parentItem = invisibleRootItem()->child(i);
0092             break;
0093         }
0094     }
0095 
0096     if (!parentItem) {
0097         parentItem = addTopLevelItem(parentName);
0098     }
0099 
0100     auto newChild = new QStandardItem();
0101     newChild->setData(QVariant::fromValue(config), SSHRole);
0102     newChild->setText(config.name);
0103     newChild->setToolTip(i18n("Host: %1", config.host));
0104     parentItem->appendRow(newChild);
0105     parentItem->sortChildren(0);
0106 }
0107 
0108 std::optional<QString> SSHManagerModel::profileForHost(const QString &host) const
0109 {
0110     auto *root = invisibleRootItem();
0111 
0112     // iterate through folders:
0113     for (int i = 0, end = root->rowCount(); i < end; ++i) {
0114         // iterate throguh the items on folders;
0115         auto folder = root->child(i);
0116         for (int e = 0, inner_end = folder->rowCount(); e < inner_end; ++e) {
0117             QStandardItem *ssh_item = folder->child(e);
0118             auto data = ssh_item->data(SSHRole).value<SSHConfigurationData>();
0119 
0120             // Return the profile name if the host matches.
0121             if (data.host == host) {
0122                 return data.profileName;
0123             }
0124         }
0125     }
0126 
0127     return {};
0128 }
0129 
0130 bool SSHManagerModel::setData(const QModelIndex &index, const QVariant &value, int role)
0131 {
0132     const bool ret = QStandardItemModel::setData(index, value, role);
0133     invisibleRootItem()->sortChildren(0);
0134     return ret;
0135 }
0136 
0137 void SSHManagerModel::editChildItem(const SSHConfigurationData &config, const QModelIndex &idx)
0138 {
0139     QStandardItem *item = itemFromIndex(idx);
0140     item->setData(QVariant::fromValue(config), SSHRole);
0141     item->setData(config.name, Qt::DisplayRole);
0142     item->parent()->sortChildren(0);
0143 }
0144 
0145 QStringList SSHManagerModel::folders() const
0146 {
0147     QStringList retList;
0148     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
0149         retList.push_back(invisibleRootItem()->child(i)->text());
0150     }
0151     return retList;
0152 }
0153 
0154 bool SSHManagerModel::hasHost(const QString &host) const
0155 {
0156     // runs in O(N), should be ok for the amount of data peophe have.
0157     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
0158         QStandardItem *iChild = invisibleRootItem()->child(i);
0159         for (int e = 0, crend = iChild->rowCount(); e < crend; e++) {
0160             QStandardItem *eChild = iChild->child(e);
0161             const auto data = eChild->data(SSHManagerModel::Roles::SSHRole).value<SSHConfigurationData>();
0162             if (data.host == host) {
0163                 return true;
0164             }
0165         }
0166     }
0167     return false;
0168 }
0169 
0170 void SSHManagerModel::setSessionController(Konsole::SessionController *controller)
0171 {
0172     if (m_session) {
0173         disconnect(m_session, nullptr, this, nullptr);
0174     }
0175     m_session = controller->session();
0176     Q_ASSERT(m_session);
0177 
0178     connect(m_session, &QObject::destroyed, this, [this] {
0179         m_session = nullptr;
0180     });
0181 
0182     connect(m_session, &Konsole::Session::hostnameChanged, this, &SSHManagerModel::triggerProfileChange);
0183 }
0184 
0185 void SSHManagerModel::triggerProfileChange(const QString &sshHost)
0186 {
0187     if (!manageProfile) {
0188         return;
0189     }
0190 
0191     auto *sm = Konsole::SessionManager::instance();
0192     QString profileToLoad;
0193 
0194     // This code is messy, Let's see if this can explain a bit.
0195     // This if sequence tries to do two things:
0196     // Stores the current profile, when we trigger a change - but only
0197     // if our hostname is the localhost.
0198     // and when we change to another profile (or go back to the local host)
0199     // we need to restore the previous profile, not go to the default one.
0200     // so this whole mess of m_sessionToProfile is just to load it correctly
0201     // later on.
0202     if (sshHost == QSysInfo::machineHostName()) {
0203         // It's the first time that we call this, using the hostname as host.
0204         // just prepare the session as a empty profile and set it as initialized to false.
0205         if (!m_sessionToProfileName.contains(m_session)) {
0206             m_sessionToProfileName[m_session] = QString();
0207             return;
0208         }
0209 
0210         // We just loaded the localhost again, after a probable different profile.
0211         // mark the profile to load as the one we stored previously.
0212         else if (m_sessionToProfileName[m_session].length()) {
0213             profileToLoad = m_sessionToProfileName[m_session];
0214             m_sessionToProfileName.remove(m_session);
0215         }
0216     } else {
0217         // We just loaded a hostname that's not the localhost. save the current profile
0218         // so we can restore it later on, and load the profile for it.
0219         if (m_sessionToProfileName[m_session].isEmpty()) {
0220             m_sessionToProfileName[m_session] = m_session->profile();
0221         }
0222     }
0223 
0224     // end of really bad code. can someone think of a better algorithm for this?
0225 
0226     if (profileToLoad.isEmpty()) {
0227         std::optional<QString> profileName = profileForHost(sshHost);
0228         if (profileName) {
0229             profileToLoad = *profileName;
0230         }
0231     }
0232 
0233     auto profiles = Konsole::ProfileManager::instance()->allProfiles();
0234 
0235     auto findIt = std::find_if(std::begin(profiles), std::end(profiles), [&profileToLoad](const Konsole::Profile::Ptr &pr) {
0236         if (pr) {
0237             return pr->name() == profileToLoad;
0238         }
0239         return false;
0240     });
0241 
0242     if (findIt == std::end(profiles)) {
0243         return;
0244     }
0245 
0246     sm->setSessionProfile(m_session, *findIt);
0247 }
0248 
0249 void SSHManagerModel::load()
0250 {
0251     auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
0252     for (const QString &groupName : config.groupList()) {
0253         KConfigGroup group = config.group(groupName);
0254         if (groupName == QStringLiteral("Global plugin config")) {
0255             manageProfile = group.readEntry<bool>("manageProfile", false);
0256             continue;
0257         }
0258         addTopLevelItem(groupName);
0259         for (const QString &sessionName : group.groupList()) {
0260             SSHConfigurationData data;
0261             KConfigGroup sessionGroup = group.group(sessionName);
0262             data.host = sessionGroup.readEntry("hostname");
0263             data.name = sessionGroup.readEntry("identifier");
0264             data.port = sessionGroup.readEntry("port");
0265             data.profileName = sessionGroup.readEntry("profileName");
0266             data.username = sessionGroup.readEntry("username");
0267             data.sshKey = sessionGroup.readEntry("sshkey");
0268             data.useSshConfig = sessionGroup.readEntry<bool>("useSshConfig", false);
0269             data.importedFromSshConfig = sessionGroup.readEntry<bool>("importedFromSshConfig", false);
0270             addChildItem(data, groupName);
0271         }
0272     }
0273 }
0274 
0275 void SSHManagerModel::save()
0276 {
0277     auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
0278     for (const QString &groupName : config.groupList()) {
0279         config.deleteGroup(groupName);
0280     }
0281 
0282     KConfigGroup globalGroup = config.group(QStringLiteral("Global plugin config"));
0283     globalGroup.writeEntry("manageProfile", manageProfile);
0284 
0285     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
0286         QStandardItem *groupItem = invisibleRootItem()->child(i);
0287         const QString groupName = groupItem->text();
0288         KConfigGroup baseGroup = config.group(groupName);
0289         for (int e = 0, rend = groupItem->rowCount(); e < rend; e++) {
0290             QStandardItem *sshElement = groupItem->child(e);
0291             const auto data = sshElement->data(SSHRole).value<SSHConfigurationData>();
0292             KConfigGroup sshGroup = baseGroup.group(data.name.trimmed());
0293             sshGroup.writeEntry("hostname", data.host.trimmed());
0294             sshGroup.writeEntry("identifier", data.name.trimmed());
0295             sshGroup.writeEntry("port", data.port.trimmed());
0296             sshGroup.writeEntry("profileName", data.profileName.trimmed());
0297             sshGroup.writeEntry("sshkey", data.sshKey.trimmed());
0298             sshGroup.writeEntry("useSshConfig", data.useSshConfig);
0299             sshGroup.writeEntry("username", data.username);
0300             sshGroup.writeEntry("importedFromSshConfig", data.importedFromSshConfig);
0301         }
0302     }
0303 
0304     config.sync();
0305 }
0306 
0307 Qt::ItemFlags SSHManagerModel::flags(const QModelIndex &index) const
0308 {
0309     if (indexFromItem(invisibleRootItem()) == index.parent()) {
0310         return QStandardItemModel::flags(index);
0311     } else {
0312         return QStandardItemModel::flags(index) & ~Qt::ItemIsEditable;
0313     }
0314 }
0315 
0316 void SSHManagerModel::removeIndex(const QModelIndex &idx)
0317 {
0318     if (idx.data(Qt::DisplayRole) == i18n("SSH Config")) {
0319         m_sshConfigTopLevelItem = nullptr;
0320     }
0321 
0322     removeRow(idx.row(), idx.parent());
0323 }
0324 
0325 void SSHManagerModel::startImportFromSshConfig()
0326 {
0327     importFromSshConfigFile(sshDir + QStringLiteral("config"));
0328 }
0329 
0330 void SSHManagerModel::importFromSshConfigFile(const QString &file)
0331 {
0332     QFile sshConfig(file);
0333     if (!sshConfig.open(QIODevice::ReadOnly)) {
0334         qCDebug(SshManagerPlugin) << "Can't open config file";
0335     }
0336     QTextStream stream(&sshConfig);
0337     QString line;
0338 
0339     SSHConfigurationData data;
0340 
0341     // If we hit a *, we ignore till the next Host.
0342     bool ignoreEntry = false;
0343     while (stream.readLineInto(&line)) {
0344         // ignore comments
0345         if (line.startsWith(QStringLiteral("#"))) {
0346             continue;
0347         }
0348 
0349         QStringList lists = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0350         // ignore lines that are not "Type Value"
0351         if (lists.count() != 2) {
0352             continue;
0353         }
0354 
0355         if (lists.at(0) == QStringLiteral("Import")) {
0356             if (lists.at(1).contains(QLatin1Char('*'))) {
0357                 // TODO: We don't handle globbing yet.
0358                 continue;
0359             }
0360 
0361             importFromSshConfigFile(sshDir + lists.at(1));
0362             continue;
0363         }
0364 
0365         if (lists.at(0) == QStringLiteral("Host")) {
0366             if (line.contains(QLatin1Char('*'))) {
0367                 // Panic, ignore everything until the next Host appears.
0368                 ignoreEntry = true;
0369                 continue;
0370             } else {
0371                 ignoreEntry = false;
0372             }
0373 
0374             // When we hit this, that means that we just finished reading the
0375             // *previous* host. so we need to add it to the list, if we can,
0376             // and read the next value.
0377             if (!data.host.isEmpty() && !hasHost(data.host)) {
0378                 // We already registered this entity.
0379 
0380                 if (data.name.isEmpty()) {
0381                     data.name = data.host;
0382                 }
0383                 data.useSshConfig = true;
0384                 data.importedFromSshConfig = true;
0385                 data.profileName = Konsole::ProfileManager::instance()->defaultProfile()->name();
0386                 addChildItem(data, i18n("SSH Config"));
0387             }
0388 
0389             data = {};
0390             data.host = lists.at(1);
0391         }
0392 
0393         if (ignoreEntry) {
0394             continue;
0395         }
0396 
0397         if (lists.at(0) == QStringLiteral("HostName")) {
0398             // hostname is always after Host, so this will be true.
0399             const QString currentHost = data.host;
0400             data.host = lists.at(1).trimmed();
0401             data.name = currentHost.trimmed();
0402         } else if (lists.at(0) == QStringLiteral("IdentityFile")) {
0403             data.sshKey = lists.at(1).trimmed();
0404         } else if (lists.at(0) == QStringLiteral("Port")) {
0405             data.port = lists.at(1).trimmed();
0406         } else if (lists.at(0) == QStringLiteral("User")) {
0407             data.username = lists.at(1).trimmed();
0408         }
0409     }
0410 
0411     // the last possible read
0412     if (data.host.length()) {
0413         if (!hasHost(data.host)) {
0414             if (data.name.isEmpty()) {
0415                 data.name = data.host.trimmed();
0416             }
0417             data.useSshConfig = true;
0418             data.importedFromSshConfig = true;
0419             addChildItem(data, i18n("SSH Config"));
0420         }
0421     }
0422 }
0423 
0424 void SSHManagerModel::setManageProfile(bool manage)
0425 {
0426     manageProfile = manage;
0427 }
0428 
0429 bool SSHManagerModel::getManageProfile()
0430 {
0431     return manageProfile;
0432 }
0433 
0434 #include "moc_sshmanagermodel.cpp"