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"