New Plugin: Tmate

This commit is contained in:
Michael Zanetti 2023-11-03 23:46:22 +01:00
parent 10c54614d6
commit 122fb6e7d2
11 changed files with 549 additions and 0 deletions

9
debian/control vendored
View File

@ -581,6 +581,15 @@ Description: nymea integration plugin for Tempo time tracking
This package contains the nymea integration plugin for Tempo time tracking.
Package: nymea-plugin-tmate
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
tmate,
Description: nymea integration plugin for tmate
This package contains the nymea integration plugin for tmate terminal access.
Package: nymea-plugin-tplink
Architecture: any
Depends: ${shlibs:Depends},

2
debian/nymea-plugin-tmate.install.in vendored Normal file
View File

@ -0,0 +1,2 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugintmate.so
tmate/translations/*qm usr/share/nymea/translations/

View File

@ -75,6 +75,7 @@ PLUGIN_DIRS = \
telegram \
tempo \
texasinstruments \
tmate \
tplink \
tuya \
udpcommander \

26
tmate/README.md Normal file
View File

@ -0,0 +1,26 @@
# Tmate
This plugin allows to establish a terminal connection to the device where nymea is running.
This is useful when maintaining remote nymea setups which may be hidden behind a firewall
and cannot be accessed from the public internet. A user can easily enable SSH access for
a nymea setup by adding a Thing using the app without having to deal with DNS and NAT.
## Setup
A tmate thing can be created without any additional information. Leaving all the fields
empty during setup will establish a default tmate session to tmate.io servers and generate
a new random token. This is the equivalent of just running `tmate` in a terminal.
For a more permanent solution, an API key can be registered at tmate.io. Using the api key a custom session name can be set which will be used for every connection establishment, persisting across nymea restarts. In the
next step, provide the SSH credentials for the user on the SSH server which has been created before. Once the login succeeds, the thing should become connected.
> Note: Using custom/self hosted tmate servers is not supported at this point.
### Connecting clients
The thing will inform about the session names via states and can be accessed via ssh or web.
## More
https://tmate.io/

View File

@ -0,0 +1,194 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project 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 project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationplugintmate.h"
#include "plugininfo.h"
#include <QFile>
#include <QDir>
#include <QRegularExpression>
IntegrationPluginTmate::IntegrationPluginTmate()
{
}
IntegrationPluginTmate::~IntegrationPluginTmate()
{
foreach (QProcess *process, m_processes) {
process->terminate();
}
}
void IntegrationPluginTmate::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
QStringList arguments;
QString apiKey = thing->paramValue(tmateThingApiKeyParamTypeId).toString();
QString sessionName = thing->paramValue(tmateThingSessionNameParamTypeId).toString();
arguments << "-F";
if (!apiKey.isEmpty()) {
arguments << "-k" << apiKey;
if (!sessionName.isEmpty()) {
arguments << "-n" << sessionName;
arguments << "-r" << "ro-" + sessionName;
}
}
QProcess *process = new QProcess(thing);
process->setProgram("tmate");
process->setArguments(arguments);
process->setProcessChannelMode(QProcess::MergedChannels);
m_processes.insert(info->thing(), process);
connect(process, &QProcess::stateChanged, thing, [=](QProcess::ProcessState newState){
switch (newState) {
case QProcess::Starting:
qCDebug(dcTmate()) << "Connection starting for" << thing->name();
return ;
case QProcess::Running:
qCInfo(dcTmate()) << "Reverse SSH connected for" << thing->name();
thing->setStateValue(tmateConnectedStateTypeId, true);
return;
case QProcess::NotRunning:
qCInfo(dcTmate()) << "Reverse SSH disconnected for" << thing->name();
thing->setStateValue(tmateConnectedStateTypeId, false);
thing->setStateValue(tmateSshStateTypeId, "");
thing->setStateValue(tmateSshRoStateTypeId, "");
thing->setStateValue(tmateWebStateTypeId, "");
thing->setStateValue(tmateWebRoStateTypeId, "");
thing->setStateValue(tmateClientsStateTypeId, 0);
return;
}
});
connect(process, &QProcess::readyRead, thing, [=](){
while (process->canReadLine()) {
QByteArray data = process->readLine();
qCDebug(dcTmate()) << thing->name() << ":" << data;
auto extractSession = [thing](const StateTypeId &stateTypeId, const QString &type, const QString &input) {
int sessionStart = input.indexOf(type);
if (sessionStart >= 0) {
int sessionEnd = input.indexOf(QChar('\n'), sessionStart);
qCInfo(dcTmate()) << input << "Session start" << sessionStart << "session end" << sessionEnd;
QString session =input.mid(sessionStart + type.length(), sessionEnd);
thing->setStateValue(stateTypeId, session);
}
};
extractSession(tmateSshStateTypeId, "ssh session: ssh ", data);
extractSession(tmateSshRoStateTypeId, "ssh session read only: ssh ", data);
extractSession(tmateWebStateTypeId, "web session: ", data);
extractSession(tmateWebRoStateTypeId, "web session read only: ", data);
QRegularExpression joinAddressRegex("joined \\(([0-9\\.]+)\\)");
QRegularExpressionMatchIterator it = joinAddressRegex.globalMatch(data);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString word = match.captured(1);
qCInfo(dcTmate()) << "Connected:" << word;
thing->emitEvent(tmateClientConnectedEventTypeId, {{tmateClientConnectedEventClientAddressParamTypeId, word}});
thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()+1);
}
QRegularExpression leftAddressRegex("left \\(([0-9\\.]+)\\)");
it = leftAddressRegex.globalMatch(data);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString word = match.captured(1);
qCInfo(dcTmate()) << "Disconnected:" << word;
thing->emitEvent(tmateClientDisconnectedEventTypeId, {{tmateClientDisconnectedEventClientAddressParamTypeId, word}});
thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()-1);
}
}
});
// Start up now if enabled
bool enabled = thing->stateValue(tmateActiveStateTypeId).toBool();
if (enabled) {
process->start();
}
info->finish(Thing::ThingErrorNoError);
// Create a watchdog to reconnect if a connection drops...
if (!m_watchdog) {
m_watchdog = hardwareManager()->pluginTimerManager()->registerTimer(10);
connect(m_watchdog, &PluginTimer::timeout, this, [this](){
foreach (Thing *thing, m_processes.keys()) {
QProcess *process = m_processes.value(thing);
if (thing->stateValue(tmateActiveStateTypeId).toBool() && process->state() == QProcess::NotRunning) {
qCInfo(dcTmate()) << "Reconnecting tmate for" << thing->name();
process->start();
}
}
});
}
}
void IntegrationPluginTmate::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == tmateThingClassId) {
QProcess *process = m_processes.take(thing);
if (process->state() != QProcess::NotRunning) {
process->terminate();
process->waitForFinished();
}
}
if (myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_watchdog);
m_watchdog = nullptr;
}
}
void IntegrationPluginTmate::executeAction(ThingActionInfo *info)
{
if (info->action().actionTypeId() == tmateActiveActionTypeId) {
bool active = info->action().paramValue(tmateActiveActionActiveParamTypeId).toBool();
QProcess *process = m_processes.value(info->thing());
if (active) {
qCInfo(dcTmate()) << "Reconnecting tmate for" << info->thing()->name();
process->start();
} else {
qCInfo(dcTmate()) << "Terminating session for" << info->thing()->name();
process->terminate();
}
info->thing()->setStateValue(tmateActiveStateTypeId, active);
info->finish(Thing::ThingErrorNoError);
}
}

View File

@ -0,0 +1,62 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2023, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project 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 project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINREMOTESSH_H
#define INTEGRATIONPLUGINREMOTESSH_H
#include "plugintimer.h"
#include "integrations/integrationplugin.h"
#include "extern-plugininfo.h"
#include <QProcess>
class IntegrationPluginTmate : public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugintmate.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginTmate();
~IntegrationPluginTmate();
// void startPairing(ThingPairingInfo *info) override;
// void confirmPairing(ThingPairingInfo *info, const QString &user, const QString &secret) override;
void executeAction(ThingActionInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void thingRemoved(Thing *thing) override;
private:
QHash<Thing*, QProcess*> m_processes;
PluginTimer *m_watchdog = nullptr;
};
#endif // INTEGRATIONPLUGINREMOTESSH_H

View File

@ -0,0 +1,131 @@
{
"id": "d06ab0d1-dbfe-48af-b196-523cc37a1e5e",
"name": "Tmate",
"displayName": "Tmate",
"vendors": [
{
"id": "b948d5e2-bfc6-4e28-a2ba-e40e46f4c213",
"name": "tmate",
"displayName": "Tmate",
"thingClasses": [
{
"id": "3f06ad52-9514-41b1-9bf9-031241d34634",
"name": "tmate",
"displayName": "Tmate",
"createMethods": ["user"],
"setupMethod": "justadd",
"interfaces": [],
"paramTypes": [
{
"id": "01f0c818-55e1-4842-a9b9-cc58bbfe76c6",
"name": "apiKey",
"displayName": "API key (optional)",
"type": "QString",
"defaultValue": ""
},
{
"id": "e587e3dc-0beb-441f-8f07-b23c25580b10",
"name": "sessionName",
"displayName": "Session name (requires API key usage)",
"type": "QString",
"defaultValue": ""
}
],
"stateTypes":[
{
"id": "7009c176-e1aa-49bc-818c-63f7a9027306",
"name": "active",
"displayName": "Active",
"displayNameAction": "Set active",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "beac3113-04f1-4d70-875b-44ca8b307866",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"suggestLogging": true,
"cached": false
},
{
"id": "ef780fb1-b31a-4333-944b-a02bf3297fea",
"name": "sshRo",
"displayName": "SSH RO",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "e8ff1b90-7701-454c-a557-4b91dc8c649b",
"name": "ssh",
"displayName": "SSH",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "98248bc0-ddda-4ae6-8558-4d7155a39c33",
"name": "webRo",
"displayName": "Web RO",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "9c284ede-eea8-4a9b-a326-e59a6bc7bb7c",
"name": "web",
"displayName": "Web",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "786e7be7-917a-4062-83ff-aade80686ec5",
"name": "clients",
"displayName": "Clients",
"type": "uint",
"defaultValue": 0,
"cached": false
}
],
"eventTypes": [
{
"id": "0508a1e2-4ed2-42ee-ab70-ed7cdd1e261c",
"name": "clientConnected",
"displayName": "Client connected",
"suggestLogging": true,
"paramTypes": [
{
"id": "a334c6e7-dffc-4720-aa21-815636be1bc1",
"name": "clientAddress",
"displayName": "Client address",
"type": "QString"
}
]
},
{
"id": "2871e481-1b67-4d77-b1ce-e0965784aa89",
"name": "clientDisconnected",
"displayName": "Client disconnected",
"suggestLogging": true,
"paramTypes": [
{
"id": "0ad4ed71-4b9a-44ab-83b1-5c62482e1625",
"name": "clientAddress",
"displayName": "Client address",
"type": "QString"
}
]
}
]
}
]
}
]
}

BIN
tmate/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

13
tmate/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "Tmate",
"tagline": "Shell access to the machine running nymea from anywhere via tmate.",
"icon": "logo.png",
"stability": "community",
"offline": false,
"technologies": [
"cloud"
],
"categories": [
"tool"
]
}

8
tmate/tmate.pro Normal file
View File

@ -0,0 +1,8 @@
include(../plugins.pri)
SOURCES += \
integrationplugintmate.cpp \
HEADERS += \
integrationplugintmate.h \

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>Tmate</name>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="43"/>
<source>API key (optional)</source>
<extracomment>The name of the ParamType (ThingClass: tmate, Type: thing, ID: {01f0c818-55e1-4842-a9b9-cc58bbfe76c6})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="46"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="49"/>
<source>Active</source>
<extracomment>The name of the ParamType (ThingClass: tmate, ActionType: active, ID: {7009c176-e1aa-49bc-818c-63f7a9027306})
----------
The name of the StateType ({7009c176-e1aa-49bc-818c-63f7a9027306}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="52"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="55"/>
<source>Client address</source>
<extracomment>The name of the ParamType (ThingClass: tmate, EventType: clientDisconnected, ID: {0ad4ed71-4b9a-44ab-83b1-5c62482e1625})
----------
The name of the ParamType (ThingClass: tmate, EventType: clientConnected, ID: {a334c6e7-dffc-4720-aa21-815636be1bc1})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="58"/>
<source>Client connected</source>
<extracomment>The name of the EventType ({0508a1e2-4ed2-42ee-ab70-ed7cdd1e261c}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="61"/>
<source>Client disconnected</source>
<extracomment>The name of the EventType ({2871e481-1b67-4d77-b1ce-e0965784aa89}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="64"/>
<source>Clients</source>
<extracomment>The name of the StateType ({786e7be7-917a-4062-83ff-aade80686ec5}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="67"/>
<source>Connected</source>
<extracomment>The name of the StateType ({beac3113-04f1-4d70-875b-44ca8b307866}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="70"/>
<source>SSH</source>
<extracomment>The name of the StateType ({e8ff1b90-7701-454c-a557-4b91dc8c649b}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="73"/>
<source>SSH RO</source>
<extracomment>The name of the StateType ({ef780fb1-b31a-4333-944b-a02bf3297fea}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="76"/>
<source>Session name (requires API key usage)</source>
<extracomment>The name of the ParamType (ThingClass: tmate, Type: thing, ID: {e587e3dc-0beb-441f-8f07-b23c25580b10})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="79"/>
<source>Set active</source>
<extracomment>The name of the ActionType ({7009c176-e1aa-49bc-818c-63f7a9027306}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="82"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="85"/>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="88"/>
<source>Tmate</source>
<extracomment>The name of the ThingClass ({3f06ad52-9514-41b1-9bf9-031241d34634})
----------
The name of the vendor ({b948d5e2-bfc6-4e28-a2ba-e40e46f4c213})
----------
The name of the plugin Tmate ({d06ab0d1-dbfe-48af-b196-523cc37a1e5e})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="91"/>
<source>Web</source>
<extracomment>The name of the StateType ({9c284ede-eea8-4a9b-a326-e59a6bc7bb7c}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build/nymea-plugins-Desktop-Debug/tmate/plugininfo.h" line="94"/>
<source>Web RO</source>
<extracomment>The name of the StateType ({98248bc0-ddda-4ae6-8558-4d7155a39c33}) of ThingClass tmate</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>