From f58b0c9573ed13cf8145716b0b687f57d09f9f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 14 Mar 2018 09:22:09 +0100 Subject: [PATCH] Add remote ssh plugin --- debian/control | 16 ++ debian/nymea-plugin-remotessh.install.in | 1 + nymea-plugins.pro | 3 +- remotessh/devicepluginremotessh.cpp | 205 +++++++++++++++++++++++ remotessh/devicepluginremotessh.h | 68 ++++++++ remotessh/devicepluginremotessh.json | 94 +++++++++++ remotessh/remotessh.pro | 10 ++ 7 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 debian/nymea-plugin-remotessh.install.in create mode 100644 remotessh/devicepluginremotessh.cpp create mode 100644 remotessh/devicepluginremotessh.h create mode 100644 remotessh/devicepluginremotessh.json create mode 100644 remotessh/remotessh.pro diff --git a/debian/control b/debian/control index a46e6198..70074afb 100644 --- a/debian/control +++ b/debian/control @@ -557,6 +557,21 @@ Description: nymea.io plugin for keba This package will install the nymea.io plugin for keba +Package: nymea-plugin-remotessh +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for remote ssh connection + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for remote ssh connection + + Package: nymea-plugins-translations Section: misc Architecture: all @@ -611,6 +626,7 @@ Depends: nymea-plugin-commandlauncher, nymea-plugin-genericelements, nymea-plugin-avahimonitor, nymea-plugin-gpio, + nymea-plugin-remotessh, Replaces: guh-plugins-maker Description: Plugins for nymea IoT server - Meta package for makers, tinkers and hackers The nymea daemon is a plugin based IoT (Internet of Things) server. The diff --git a/debian/nymea-plugin-remotessh.install.in b/debian/nymea-plugin-remotessh.install.in new file mode 100644 index 00000000..aa3836c4 --- /dev/null +++ b/debian/nymea-plugin-remotessh.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginremotessh.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 60118e7a..b40ea038 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -34,7 +34,8 @@ PLUGIN_DIRS = \ gpio \ snapd \ simulation \ - keba \ + keba \ + remotessh \ CONFIG+=all diff --git a/remotessh/devicepluginremotessh.cpp b/remotessh/devicepluginremotessh.cpp new file mode 100644 index 00000000..a6cd0cd3 --- /dev/null +++ b/remotessh/devicepluginremotessh.cpp @@ -0,0 +1,205 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginremotessh.h" +#include "plugininfo.h" + +#include +#include + +DevicePluginRemoteSsh::DevicePluginRemoteSsh() +{ + +} + +void DevicePluginRemoteSsh::init() +{ + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(10); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginRemoteSsh::onPluginTimeout); +} + +DeviceManager::DeviceSetupStatus DevicePluginRemoteSsh::setupDevice(Device *device) +{ + qCDebug(dcRemoteSsh()) << "Setup" << device->name() << device->params(); + + if (device->deviceClassId() == reverseSshDeviceClassId) { + m_identityFilePath = QString("%1/.ssh/id_rsa_guh").arg(QDir::homePath()); + return DeviceManager::DeviceSetupStatusSuccess; + } + return DeviceManager::DeviceSetupStatusFailure; +} + +DeviceManager::DeviceError DevicePluginRemoteSsh::executeAction(Device *device, const Action &action) +{ + if (device->deviceClassId() == reverseSshDeviceClassId ) { + + if (action.actionTypeId() == reverseSshConnectedActionTypeId) { + + if (action.param(reverseSshConnectedStateParamTypeId).value().toBool() == true) { + QProcess *process = startReverseSSHProcess(device); + m_reverseSSHProcess.insert(process, device); + m_startingProcess.insert(process, action.id()); + return DeviceManager::DeviceErrorAsync; + } else { + QProcess *process = m_reverseSSHProcess.key(device); + + // Check if the application is running... + if (!process) + return DeviceManager::DeviceErrorNoError; + + if (process->state() == QProcess::NotRunning) + return DeviceManager::DeviceErrorNoError; + + process->kill(); + m_killingProcess.insert(process, action.id()); + return DeviceManager::DeviceErrorAsync; + } + return DeviceManager::DeviceErrorNoError; + } + return DeviceManager::DeviceErrorActionTypeNotFound; + } + return DeviceManager::DeviceErrorDeviceClassNotFound; +} + + +void DevicePluginRemoteSsh::deviceRemoved(Device *device) +{ + if (device->deviceClassId() == reverseSshDeviceClassId) { + QProcess *process = m_reverseSSHProcess.key(device); + if (!process) + return; + + m_reverseSSHProcess.remove(process); + if (process->state() != QProcess::NotRunning) { + process->kill(); + } + process->deleteLater(); + } +} + +void DevicePluginRemoteSsh::processReadyRead() +{ + QByteArray data = static_cast(sender())->readAll(); + qCWarning(dcRemoteSsh()) << "process read" << data; +} + + +QProcess * DevicePluginRemoteSsh::startReverseSSHProcess(Device *device) +{ + qCDebug(dcRemoteSsh()) << "Start reverse SSH"; + QProcess *process = new QProcess(this); + process->setProcessChannelMode(QProcess::MergedChannels); + + connect(process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus))); + connect(process, &QProcess::stateChanged, this, &DevicePluginRemoteSsh::processStateChanged); + connect(process, &QProcess::readyRead, this, &DevicePluginRemoteSsh::processReadyRead); + + QStringList arguments; + int localPort = device->paramValue(reverseSshLocalPortParamTypeId).toInt(); + int remotePort = device->paramValue(reverseSshRemotePortParamTypeId).toInt(); + QString user = device->paramValue(reverseSshUserParamTypeId).toString(); + QString password = device->paramValue(reverseSshPasswordParamTypeId).toString(); + QString address = device->paramValue(reverseSshAddressParamTypeId).toString(); + + arguments << "-p" << password; + arguments << "ssh" << "-o StrictHostKeyChecking=no" << "-oUserKnownHostsFile=/dev/null"; + arguments << "-TN" << "-R" << QString("%1:localhost:%2").arg(remotePort).arg(localPort) << QString("%1@%2").arg(user, address); + process->start(QStringLiteral("sshpass"), arguments); + qCDebug(dcRemoteSsh()) << "Command:" << process->program() << process->arguments(); + return process; +} + +void DevicePluginRemoteSsh::onPluginTimeout() +{ + foreach(QProcess *process, m_reverseSSHProcess.keys()) { + if (process->state() == QProcess::NotRunning) + qCDebug(dcRemoteSsh()) << "SSH Process not running"; + } +} + + +void DevicePluginRemoteSsh::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + Q_UNUSED(exitCode); + QProcess *process = static_cast(sender()); + + if(exitStatus != QProcess::NormalExit || exitCode != 0) { + qCWarning(dcRemoteSsh()) << "Error:" << process->readAllStandardError(); + } + + if (m_reverseSSHProcess.contains(process)) { + qCDebug(dcRemoteSsh()) << "SSH process finished"; + Device *device = m_reverseSSHProcess.value(process); + device->setStateValue(reverseSshConnectedStateTypeId, false); + m_reverseSSHProcess.remove(process); + + } else if (m_sshKeyGenProcess.contains(process)) { + qCDebug(dcRemoteSsh()) << "SSH Key generation process finished" << process->readAll(); + Device *device = m_sshKeyGenProcess.value(process); + QFile file(QString(m_identityFilePath + ".pub")); + if(!file.open(QIODevice::ReadOnly)) + qCWarning(dcRemoteSsh()) << "error" << file.errorString(); + + QTextStream in(&file); + QString sshKey = in.readLine(); + device->setStateValue(reverseSshSshKeyStateTypeId, sshKey); + process->kill(); + m_sshKeyGenProcess.remove(process); + file.close(); + } +} + + +void DevicePluginRemoteSsh::processStateChanged(QProcess::ProcessState state) +{ + QProcess *process = static_cast(sender()); + Device *device = m_reverseSSHProcess.value(process); + + switch (state) { + case QProcess::Running: + device->setStateValue(reverseSshConnectedStateTypeId, true); + if (m_startingProcess.contains(process)) { + emit actionExecutionFinished(m_startingProcess.value(process), DeviceManager::DeviceErrorNoError); + m_startingProcess.remove(process); + } + break; + + case QProcess::NotRunning: + if (device) + device->setStateValue(reverseSshConnectedStateTypeId, false); + + if (m_startingProcess.contains(process)) { + emit actionExecutionFinished(m_startingProcess.value(process), DeviceManager::DeviceErrorInvalidParameter); + m_startingProcess.remove(process); + } + + if (m_killingProcess.contains(process)) { + emit actionExecutionFinished(m_killingProcess.value(process), DeviceManager::DeviceErrorNoError); + m_reverseSSHProcess.remove(process); + m_killingProcess.remove(process); + } + break; + default: + break; + } +} diff --git a/remotessh/devicepluginremotessh.h b/remotessh/devicepluginremotessh.h new file mode 100644 index 00000000..87f646a8 --- /dev/null +++ b/remotessh/devicepluginremotessh.h @@ -0,0 +1,68 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2017 Bernhard Trinnes * + * Copyright (C) 2018 Simon Stürz * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINREMOTESSH_H +#define DEVICEPLUGINREMOTESSH_H + +#include "plugintimer.h" +#include "plugin/deviceplugin.h" + +#include + +class DevicePluginRemoteSsh : public DevicePlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginremotessh.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginRemoteSsh(); + + void init() override; + DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + void deviceRemoved(Device *device) override; + DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; + +private: + QHash m_reverseSSHProcess; + QHash m_sshKeyGenProcess; + + QHash m_startingProcess; + QHash m_killingProcess; + + PluginTimer *m_pluginTimer = nullptr; + + bool m_aboutToQuit = false; + QString m_identityFilePath; + + QProcess *startReverseSSHProcess(Device *device); + +private slots: + void onPluginTimeout(); + void processReadyRead(); + void processStateChanged(QProcess::ProcessState state); + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); +}; + +#endif // DEVICEPLUGINREMOTESSH_H diff --git a/remotessh/devicepluginremotessh.json b/remotessh/devicepluginremotessh.json new file mode 100644 index 00000000..a20bb652 --- /dev/null +++ b/remotessh/devicepluginremotessh.json @@ -0,0 +1,94 @@ +{ + "id": "cd75d899-3f53-43fa-9ee8-f6b36646a27d", + "name": "RemoteSsh", + "displayName": "Remote SSH", + "vendors": [ + { + "id": "e87ad7b1-1705-46b1-a962-282126646b4d", + "name": "remoteAccess", + "displayName": "Remote Access", + "deviceClasses": [ + { + "id": "a4f12741-4f30-40ca-a319-7f15e9c0c43a", + "name": "reverseSsh", + "displayName": "Reverse SSH", + "deviceIcon": "Network", + "createMethods": ["user"], + "basicTags": ["Service"], + "interfaces": ["connectable"], + "paramTypes": [ + { + "id": "92747d75-d18a-4915-bd48-0edd5cc5f19a", + "name": "address", + "displayName": "Address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "127.0.0.1" + }, + { + "id": "7f7aa198-c719-415e-b31c-7a676b9d8e01", + "name": "localPort", + "displayName": "Local Port", + "type": "int", + "defaultValue": 22 + }, + { + "id": "988aec42-1026-4aef-85d1-329ee1a34208", + "name": "remotePort", + "displayName": "Remote Port", + "type": "int", + "defaultValue": 2022 + }, + { + "id": "c675f7ea-f94a-46e9-bf0f-92682182d6dd", + "name": "user", + "displayName": "User Name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "Enter your user" + }, + { + "id": "d8cc7177-bf35-4394-ab7b-881184bd8c8b", + "name": "password", + "displayName": "Password", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "Enter your password" + } + ], + "stateTypes":[ + { + "id": "19f079f0-1654-44c3-ab10-e7d7f9742e09", + "name": "reachable", + "displayName": "Server Reachable", + "displayNameEvent": "reachable status changed", + "type": "bool", + "defaultValue": true + }, + { + "id": "1ae425b2-d642-42ca-be41-4d06dff5c5cd", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "displayNameEvent": "Connected changed", + "displayNameAction": "Connect to Server", + "writable": true + }, + { + "id": "d8bb619e-6602-4c89-8654-85e111520561", + "name": "sshKey", + "displayName": "SSH public key", + "displayNameEvent": "SSH key changed", + "type": "QString", + "defaultValue": "-" + } + ] + } + ] + } + ] +} + + + diff --git a/remotessh/remotessh.pro b/remotessh/remotessh.pro new file mode 100644 index 00000000..390f2eea --- /dev/null +++ b/remotessh/remotessh.pro @@ -0,0 +1,10 @@ +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(nymea_devicepluginremotessh) + +SOURCES += \ + devicepluginremotessh.cpp \ + +HEADERS += \ + devicepluginremotessh.h \ +