1 /*  This file was part of the KDE libraries
2 
3     SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "sshmanagermodel.h"
9 
10 #include <QStandardItem>
11 
12 #include <KLocalizedString>
13 
14 #include <KConfig>
15 #include <KConfigGroup>
16 
17 #include <QDebug>
18 #include <QFile>
19 #include <QLoggingCategory>
20 #include <QStandardPaths>
21 #include <QTextStream>
22 
23 #include "sshconfigurationdata.h"
24 
25 Q_LOGGING_CATEGORY(SshManagerPlugin, "org.kde.konsole.plugin.sshmanager")
26 
27 namespace
28 {
29 const QString SshDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QStringLiteral("/.ssh/");
30 }
31 
SSHManagerModel(QObject * parent)32 SSHManagerModel::SSHManagerModel(QObject *parent)
33     : QStandardItemModel(parent)
34 {
35     load();
36     if (invisibleRootItem()->rowCount() == 0) {
37         addTopLevelItem(i18n("Default"));
38     }
39 }
40 
~SSHManagerModel()41 SSHManagerModel::~SSHManagerModel() noexcept
42 {
43     save();
44 }
45 
addTopLevelItem(const QString & name)46 QStandardItem *SSHManagerModel::addTopLevelItem(const QString &name)
47 {
48     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
49         if (invisibleRootItem()->child(i)->text() == name) {
50             return nullptr;
51         }
52     }
53 
54     auto *newItem = new QStandardItem();
55     newItem->setText(name);
56     invisibleRootItem()->appendRow(newItem);
57     invisibleRootItem()->sortChildren(0);
58     return newItem;
59 }
60 
addChildItem(const SSHConfigurationData & config,const QString & parentName)61 void SSHManagerModel::addChildItem(const SSHConfigurationData &config, const QString &parentName)
62 {
63     QStandardItem *item = nullptr;
64     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
65         if (invisibleRootItem()->child(i)->text() == parentName) {
66             item = invisibleRootItem()->child(i);
67             break;
68         }
69     }
70 
71     if (!item) {
72         item = addTopLevelItem(parentName);
73     }
74 
75     auto newChild = new QStandardItem();
76     newChild->setData(QVariant::fromValue(config), SSHRole);
77     newChild->setData(config.name, Qt::DisplayRole);
78     item->appendRow(newChild);
79     item->sortChildren(0);
80 }
81 
setData(const QModelIndex & index,const QVariant & value,int role)82 bool SSHManagerModel::setData(const QModelIndex &index, const QVariant &value, int role)
83 {
84     const bool ret = QStandardItemModel::setData(index, value, role);
85     invisibleRootItem()->sortChildren(0);
86     return ret;
87 }
88 
editChildItem(const SSHConfigurationData & config,const QModelIndex & idx)89 void SSHManagerModel::editChildItem(const SSHConfigurationData &config, const QModelIndex &idx)
90 {
91     QStandardItem *item = itemFromIndex(idx);
92     item->setData(QVariant::fromValue(config), SSHRole);
93     item->setData(config.name, Qt::DisplayRole);
94     item->parent()->sortChildren(0);
95 }
96 
folders() const97 QStringList SSHManagerModel::folders() const
98 {
99     QStringList retList;
100     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
101         retList.push_back(invisibleRootItem()->child(i)->text());
102     }
103     return retList;
104 }
105 
load()106 void SSHManagerModel::load()
107 {
108     auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
109     for (const QString &groupName : config.groupList()) {
110         KConfigGroup group = config.group(groupName);
111         addTopLevelItem(groupName);
112         for (const QString &sessionName : group.groupList()) {
113             SSHConfigurationData data;
114             KConfigGroup sessionGroup = group.group(sessionName);
115             data.host = sessionGroup.readEntry("hostname");
116             data.name = sessionGroup.readEntry("identifier");
117             data.port = sessionGroup.readEntry("port");
118             data.profileName = sessionGroup.readEntry("profilename");
119             data.sshKey = sessionGroup.readEntry("sshkey");
120             data.useSshConfig = sessionGroup.readEntry<bool>("useSshConfig", false);
121             data.importedFromSshConfig = sessionGroup.readEntry<bool>("importedFromSshConfig", false);
122             addChildItem(data, groupName);
123         }
124     }
125 }
126 
save()127 void SSHManagerModel::save()
128 {
129     auto config = KConfig(QStringLiteral("konsolesshconfig"), KConfig::OpenFlag::SimpleConfig);
130     for (const QString &groupName : config.groupList()) {
131         config.deleteGroup(groupName);
132     }
133 
134     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
135         QStandardItem *groupItem = invisibleRootItem()->child(i);
136         const QString groupName = groupItem->text();
137         KConfigGroup baseGroup = config.group(groupName);
138         for (int e = 0, rend = groupItem->rowCount(); e < rend; e++) {
139             QStandardItem *sshElement = groupItem->child(e);
140             const auto data = sshElement->data(SSHRole).value<SSHConfigurationData>();
141             KConfigGroup sshGroup = baseGroup.group(data.name.trimmed());
142             sshGroup.writeEntry("hostname", data.host.trimmed());
143             sshGroup.writeEntry("identifier", data.name.trimmed());
144             sshGroup.writeEntry("port", data.port.trimmed());
145             sshGroup.writeEntry("profileName", data.profileName.trimmed());
146             sshGroup.writeEntry("sshkey", data.sshKey.trimmed());
147             sshGroup.writeEntry("useSshConfig", data.useSshConfig);
148             sshGroup.writeEntry("importedFromSshConfig", data.importedFromSshConfig);
149             sshGroup.sync();
150         }
151         baseGroup.sync();
152     }
153     config.sync();
154 }
155 
flags(const QModelIndex & index) const156 Qt::ItemFlags SSHManagerModel::flags(const QModelIndex &index) const
157 {
158     if (indexFromItem(invisibleRootItem()) == index.parent()) {
159         return QStandardItemModel::flags(index);
160     } else {
161         return QStandardItemModel::flags(index) & ~Qt::ItemIsEditable;
162     }
163 }
164 
removeIndex(const QModelIndex & idx)165 void SSHManagerModel::removeIndex(const QModelIndex &idx)
166 {
167     removeRow(idx.row(), idx.parent());
168 }
169 
startImportFromSshConfig()170 void SSHManagerModel::startImportFromSshConfig()
171 {
172     for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
173         QStandardItem *groupItem = invisibleRootItem()->child(i);
174         if (groupItem->data(Qt::DisplayRole).toString() == tr("SSH Config")) {
175             removeIndex(indexFromItem(groupItem));
176             break;
177         }
178     }
179 
180     importFromSshConfigFile(SshDir + QStringLiteral("config"));
181 }
182 
importFromSshConfigFile(const QString & file)183 void SSHManagerModel::importFromSshConfigFile(const QString &file)
184 {
185     QFile sshConfig(file);
186     if (!sshConfig.open(QIODevice::ReadOnly)) {
187         qCDebug(SshManagerPlugin) << "Can't open config file";
188     }
189     QTextStream stream(&sshConfig);
190     QString line;
191 
192     SSHConfigurationData data;
193 
194     // If we hit a *, we ignore till the next Host.
195     bool ignoreEntry = false;
196     while (stream.readLineInto(&line)) {
197         // ignore comments
198         if (line.startsWith(QStringLiteral("#"))) {
199             continue;
200         }
201 
202         QStringList lists = line.split(QLatin1Char(' '), Qt::SkipEmptyParts);
203         // ignore lines that are not "Type Value"
204         if (lists.count() != 2) {
205             continue;
206         }
207 
208         if (lists.at(0) == QStringLiteral("Import")) {
209             if (lists.at(1).contains(QLatin1Char('*'))) {
210                 // TODO: We don't handle globbing yet.
211                 continue;
212             }
213 
214             importFromSshConfigFile(SshDir + lists.at(1));
215             continue;
216         }
217 
218         if (lists.at(0) == QStringLiteral("Host")) {
219             if (line.contains(QLatin1Char('*'))) {
220                 // Panic, ignore everything untill the next Host appears.
221                 ignoreEntry = true;
222                 continue;
223             } else {
224                 ignoreEntry = false;
225             }
226 
227             if (!data.host.isEmpty()) {
228                 if (data.name.isEmpty()) {
229                     data.name = data.host;
230                 }
231                 data.useSshConfig = true;
232                 data.importedFromSshConfig = true;
233                 addChildItem(data, tr("SSH Config"));
234                 data = {};
235             }
236 
237             data.host = lists.at(1);
238         }
239 
240         if (ignoreEntry) {
241             continue;
242         }
243 
244         if (lists.at(0) == QStringLiteral("HostName")) {
245             // hostname is always after Host, so this will be true.
246             const QString currentHost = data.host;
247             data.host = lists.at(1).trimmed();
248             data.name = currentHost.trimmed();
249         } else if (lists.at(0) == QStringLiteral("IdentityFile")) {
250             data.sshKey = lists.at(1).trimmed();
251         } else if (lists.at(0) == QStringLiteral("Port")) {
252             data.port = lists.at(1).trimmed();
253         } else if (lists.at(0) == QStringLiteral("User")) {
254             data.username = lists.at(1).trimmed();
255         }
256     }
257 
258     // the last possible read
259     if (data.host.count()) {
260         if (data.name.isEmpty()) {
261             data.name = data.host.trimmed();
262         }
263         data.useSshConfig = true;
264         data.importedFromSshConfig = true;
265         addChildItem(data, tr("SSH Config"));
266     }
267 }
268