Commit 05484db2 authored by Tomaz  Canabrava's avatar Tomaz Canabrava Committed by Tomaz Canabrava
Browse files

Add the SSH Manager Plugin

This plugin adds a SSH Manager, just like PuTTY, for Konsole.
It reads and parses the ~/.ssh/config file and generates entries
based on that, it also accepts entries manually.

The file used to store the information is a json, stored locally.
Clicking on the entry: request to connect to the server
Middle click on the entry: new tab, request to connect to the server

You can also filter entries, and disable / enable the view by
the new Plugins submenu.
parent 2c1a4113
......@@ -106,6 +106,7 @@ add_subdirectory(profile)
add_subdirectory(session)
add_subdirectory(characters)
add_subdirectory(decoders)
add_subdirectory(plugins)
set(konsoleprivate_SRCS ${windowadaptors_SRCS}
BookmarkHandler.cpp
......
add_subdirectory(SSHManager)
qt_wrap_ui(EXTRA_SSHPLUGIN_SRCS sshwidget.ui)
kcoreaddons_add_plugin(konsole_sshmanagerplugin
SOURCES
sshmanagerplugin.cpp
sshmanagerpluginwidget.cpp
sshmanagermodel.cpp
sshmanagerfiltermodel.cpp
sshtreeview.cpp
${EXTRA_SSHPLUGIN_SRCS}
INSTALL_NAMESPACE
"konsoleplugins"
)
target_link_libraries(konsole_sshmanagerplugin
konsoleprivate
konsoleapp
)
{
"KPlugin": {
"Name": "SSH Manager"
}
}
/* This file was part of the KDE libraries
SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef SSHCONFIGURATIONDATA_H
#define SSHCONFIGURATIONDATA_H
#include <QString>
class SSHConfigurationData {
public:
QString name;
QString host;
QString port;
QString sshKey;
QString username;
QString profileName;
bool useSshConfig = false;
bool importedFromSshConfig = false;
};
Q_DECLARE_METATYPE(SSHConfigurationData);
#endif
/* 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 "sshmanagerfiltermodel.h"
SSHManagerFilterModel::SSHManagerFilterModel(QObject* parent)
: QSortFilterProxyModel(parent)
{
}
bool SSHManagerFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
auto text = filterRegularExpression().pattern();
if (text.isEmpty()) {
return true;
}
const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
if (sourceModel()->rowCount(idx) != 0) {
return true;
}
bool result = idx.data(Qt::DisplayRole).toString().toLower().contains(text.toLower());
return m_invertFilter == false ? result : !result;
}
void SSHManagerFilterModel::setInvertFilter(bool invert)
{
m_invertFilter = invert;
invalidateFilter();
}
/* This file was part of the KDE libraries
SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef SSHMANAGERFILTERMODEL_H
#define SSHMANAGERFILTERMODEL_H
#include <QSortFilterProxyModel>
class SSHManagerFilterModel : public QSortFilterProxyModel {
Q_OBJECT
public:
SSHManagerFilterModel(QObject *parent);
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
void setInvertFilter(bool invert);
private:
bool m_invertFilter = false;
};
#endif
/* 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 <KConfigGroup>
#include <KConfig>
#include <QDebug>
#include <QStandardPaths>
#include <QFile>
#include <QTextStream>
#include <QLoggingCategory>
#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 (invisibleRootItem()->rowCount() == 0) {
addTopLevelItem(i18n("Default"));
}
}
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);
invisibleRootItem()->appendRow(newItem);
invisibleRootItem()->sortChildren(0);
return newItem;
}
void SSHManagerModel::addChildItem(const SSHConfigurationData &config, const QString& parentName)
{
QStandardItem *item = nullptr;
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
if (invisibleRootItem()->child(i)->text() == parentName) {
item = invisibleRootItem()->child(i);
break;
}
}
if (!item) {
item = addTopLevelItem(parentName);
}
auto newChild = new QStandardItem();
newChild->setData(QVariant::fromValue(config), SSHRole);
newChild->setData(config.name, Qt::DisplayRole);
item->appendRow(newChild);
item->sortChildren(0);
}
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;
}
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.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, end = groupItem->rowCount(); e < end; 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("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)
{
removeRow(idx.row(), idx.parent());
}
void SSHManagerModel::startImportFromSshConfig()
{
for (int i = 0, end = invisibleRootItem()->rowCount(); i < end; i++) {
QStandardItem *groupItem = invisibleRootItem()->child(i);
if (groupItem->data(Qt::DisplayRole).toString() == tr("SSH Config")) {
removeIndex(indexFromItem(groupItem));
break;
}
}
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 untill the next Host appears.
ignoreEntry = true;
continue;
} else {
ignoreEntry = false;
}
if (!data.host.isEmpty()) {
if (data.name.isEmpty()) {
data.name = data.host;
}
data.useSshConfig = true;
data.importedFromSshConfig = true;
addChildItem(data, tr("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 (data.name.isEmpty()) {
data.name = data.host.trimmed();
}
data.useSshConfig = true;
data.importedFromSshConfig = true;
addChildItem(data, tr("SSH Config"));
}
}
/* This file was part of the KDE libraries
SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef SSHMANAGERMODEL_H
#define SSHMANAGERMODEL_H
#include <QStandardItemModel>
#include <memory>
class SSHConfigurationData;
class SSHManagerModel : public QStandardItemModel {
Q_OBJECT
public:
enum Roles {
SSHRole = Qt::UserRole + 1
};
SSHManagerModel(QObject *parent = nullptr);
~SSHManagerModel();
QStandardItem *addTopLevelItem(const QString& toplevel);
void addChildItem(const SSHConfigurationData &config, const QString &parentName);
void editChildItem(const SSHConfigurationData &config, const QModelIndex& idx);
void removeIndex(const QModelIndex& idx);
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
QStringList folders() const;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void startImportFromSshConfig();
void importFromSshConfigFile(const QString& file);
void load();
void save();
};
#endif
/* 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 "sshmanagerplugin.h"
#include "sshmanagerpluginwidget.h"
#include "sshmanagermodel.h"
#include "session/SessionController.h"
#include <QMainWindow>
#include <QDockWidget>
#include <QListView>
#include <QTimer>
#include <QMenuBar>
#include <KLocalizedString>
#include <KActionCollection>
#include "MainWindow.h"
K_PLUGIN_CLASS_WITH_JSON(SSHManagerPlugin, "konsole_sshmanager.json")
struct SSHManagerPluginPrivate {
SSHManagerModel model;
QMap<Konsole::MainWindow*, SSHManagerTreeWidget *> widgetForWindow;
QMap<Konsole::MainWindow*, QDockWidget *> dockForWindow;
};
SSHManagerPlugin::SSHManagerPlugin(QObject *object, const QVariantList &args)
: Konsole::IKonsolePlugin(object, args)
, d(std::make_unique<SSHManagerPluginPrivate>())
{
setName(QStringLiteral("SshManager"));
}
SSHManagerPlugin::~SSHManagerPlugin() = default;
void SSHManagerPlugin::createWidgetsForMainWindow(Konsole::MainWindow *mainWindow)
{
auto *sshDockWidget = new QDockWidget(mainWindow);
auto *managerWidget = new SSHManagerTreeWidget();
managerWidget->setModel(&d->model);
sshDockWidget->setWidget(managerWidget);
sshDockWidget->setWindowTitle(i18n("SSH Manager"));
sshDockWidget->setObjectName(QStringLiteral("SSHManagerDock"));
sshDockWidget->setVisible(false);
mainWindow->addDockWidget(Qt::LeftDockWidgetArea, sshDockWidget);
d->widgetForWindow[mainWindow] = managerWidget;
d->dockForWindow[mainWindow] = sshDockWidget;
connect(managerWidget, &SSHManagerTreeWidget::requestNewTab, this, [mainWindow]{
mainWindow->newTab();
});
}
QList<QAction *> SSHManagerPlugin::menuBarActions(Konsole::MainWindow* mainWindow) const
{
Q_UNUSED(mainWindow);
const QString show = i18n("Show SSH Manager");
const QString hide = i18n("Hide SSH Manager");
QAction *toggleVisibilityAction = new QAction(show, mainWindow);
toggleVisibilityAction->setCheckable(true);
toggleVisibilityAction->setChecked(false);
toggleVisibilityAction->setText(show);
connect(toggleVisibilityAction, &QAction::triggered, this, [=] (bool visible) {
d->dockForWindow[mainWindow]->setVisible(visible);
toggleVisibilityAction->setText(visible ? hide : show);
});
return {toggleVisibilityAction};
}
void SSHManagerPlugin::activeViewChanged(Konsole::SessionController *controller)
{
auto mainWindow = qobject_cast<Konsole::MainWindow*>(controller->view()->topLevelWidget());
// if we don't get a mainwindow here this *might* be just opening, call it again
// later on.
if (!mainWindow) {
QTimer::singleShot(500, this, [this, controller]{ activeViewChanged(controller); });
return;
}
d->widgetForWindow[mainWindow]->setCurrentController(controller);
}
#include "sshmanagerplugin.moc"
/* This file was part of the KDE libraries
SPDX-FileCopyrightText: 2021 Tomaz Canabrava <tcanabrava@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef SSHMANAGERPLUGIN_H
#define SSHMANAGERPLUGIN_H
#include <pluginsystem/IKonsolePlugin.h>
#include <memory>
namespace Konsole {
class SessionController;
class MainWindow;
}
struct SSHManagerPluginPrivate;
class SSHManagerPlugin : public Konsole::IKonsolePlugin {
Q_OBJECT
public:
SSHManagerPlugin(QObject *object, const QVariantList &args);
~SSHManagerPlugin();
void createWidgetsForMainWindow(Konsole::MainWindow *mainWindow) override;
void activeViewChanged(Konsole::SessionController *controller) override;
QList<QAction*> menuBarActions(Konsole::MainWindow* mainWindow) const override;
private:
std::unique_ptr<SSHManagerPluginPrivate> d;
};
#endif
/* 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 "sshmanagerpluginwidget.h"
#include "sshmanagermodel.h"
#include "sshconfigurationdata.h"
#include "session/SessionController.h"
#include "terminalDisplay/TerminalDisplay.h"
#include "profile/ProfileModel.h"
#include "ui_sshwidget.h"
#include "sshmanagerfiltermodel.h"
#include <KLocalizedString>
#include <QAction>
#include <QRegularExpression>
#include <QIntValidator>