mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-16 16:41:33 +10:00
Very WIP dmshell
This commit is contained in:
@@ -27,6 +27,9 @@ PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
|
||||
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DMShell, meshtastic_DMShell, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
|
||||
|
||||
|
||||
@@ -129,6 +132,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -308,6 +308,8 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126,
|
||||
/* Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen. */
|
||||
meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 = 127,
|
||||
/* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio, GPS, button, buzzer, and sensors. */
|
||||
meshtastic_HardwareModel_TRACKER_T1000_E_PRO = 128,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
@@ -511,6 +513,27 @@ typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
|
||||
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
|
||||
} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
|
||||
|
||||
/* Frame op code for PTY session control and stream transport.
|
||||
|
||||
Values 1-63 are client->server requests.
|
||||
Values 64-127 are server->client responses/events. */
|
||||
typedef enum _meshtastic_DMShell_OpCode {
|
||||
meshtastic_DMShell_OpCode_OP_UNSET = 0,
|
||||
/* Client -> server */
|
||||
meshtastic_DMShell_OpCode_OPEN = 1,
|
||||
meshtastic_DMShell_OpCode_INPUT = 2,
|
||||
meshtastic_DMShell_OpCode_RESIZE = 3,
|
||||
meshtastic_DMShell_OpCode_CLOSE = 4,
|
||||
meshtastic_DMShell_OpCode_PING = 5,
|
||||
meshtastic_DMShell_OpCode_ACK = 6,
|
||||
/* Server -> client */
|
||||
meshtastic_DMShell_OpCode_OPEN_OK = 64,
|
||||
meshtastic_DMShell_OpCode_OUTPUT = 65,
|
||||
meshtastic_DMShell_OpCode_CLOSED = 66,
|
||||
meshtastic_DMShell_OpCode_ERROR = 67,
|
||||
meshtastic_DMShell_OpCode_PONG = 68
|
||||
} meshtastic_DMShell_OpCode;
|
||||
|
||||
/* The priority of this message for sending.
|
||||
Higher priorities are sent first (when managing the transmit queue).
|
||||
This field is never sent over the air, it is only used internally inside of a local device node.
|
||||
@@ -843,6 +866,27 @@ typedef struct _meshtastic_StoreForwardPlusPlus {
|
||||
uint32_t chain_count;
|
||||
} meshtastic_StoreForwardPlusPlus;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(200) meshtastic_DMShell_payload_t;
|
||||
/* The actual over-the-mesh message doing DMShell */
|
||||
typedef struct _meshtastic_DMShell {
|
||||
/* Structured frame operation. */
|
||||
meshtastic_DMShell_OpCode op;
|
||||
/* Logical PTY session identifier. */
|
||||
uint32_t session_id;
|
||||
/* Monotonic sequence number for this frame. */
|
||||
uint32_t seq;
|
||||
/* Cumulative ack sequence number. */
|
||||
uint32_t ack_seq;
|
||||
/* Opaque bytes payload for INPUT/OUTPUT/ERROR and other frame bodies. */
|
||||
meshtastic_DMShell_payload_t payload;
|
||||
/* Terminal size columns used for OPEN/RESIZE signaling. */
|
||||
uint32_t cols;
|
||||
/* Terminal size rows used for OPEN/RESIZE signaling. */
|
||||
uint32_t rows;
|
||||
/* Bit flags for protocol extensions. */
|
||||
uint32_t flags;
|
||||
} meshtastic_DMShell;
|
||||
|
||||
/* Waypoint message, used to share arbitrary locations across the mesh */
|
||||
typedef struct _meshtastic_Waypoint {
|
||||
/* Id of the waypoint */
|
||||
@@ -1383,6 +1427,10 @@ extern "C" {
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
|
||||
|
||||
#define _meshtastic_DMShell_OpCode_MIN meshtastic_DMShell_OpCode_OP_UNSET
|
||||
#define _meshtastic_DMShell_OpCode_MAX meshtastic_DMShell_OpCode_PONG
|
||||
#define _meshtastic_DMShell_OpCode_ARRAYSIZE ((meshtastic_DMShell_OpCode)(meshtastic_DMShell_OpCode_PONG+1))
|
||||
|
||||
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
|
||||
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
|
||||
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
|
||||
@@ -1413,6 +1461,8 @@ extern "C" {
|
||||
|
||||
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
|
||||
|
||||
#define meshtastic_DMShell_op_ENUMTYPE meshtastic_DMShell_OpCode
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1457,6 +1507,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_DMShell_init_default {_meshtastic_DMShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_default {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1490,6 +1541,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_DMShell_init_zero {_meshtastic_DMShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_zero {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1579,6 +1631,14 @@ extern "C" {
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
|
||||
#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10
|
||||
#define meshtastic_DMShell_op_tag 1
|
||||
#define meshtastic_DMShell_session_id_tag 2
|
||||
#define meshtastic_DMShell_seq_tag 3
|
||||
#define meshtastic_DMShell_ack_seq_tag 4
|
||||
#define meshtastic_DMShell_payload_tag 5
|
||||
#define meshtastic_DMShell_cols_tag 6
|
||||
#define meshtastic_DMShell_rows_tag 7
|
||||
#define meshtastic_DMShell_flags_tag 8
|
||||
#define meshtastic_Waypoint_id_tag 1
|
||||
#define meshtastic_Waypoint_latitude_i_tag 2
|
||||
#define meshtastic_Waypoint_longitude_i_tag 3
|
||||
@@ -1811,6 +1871,18 @@ X(a, STATIC, SINGULAR, UINT32, chain_count, 10)
|
||||
#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
|
||||
#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
|
||||
|
||||
#define meshtastic_DMShell_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, op, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, session_id, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, seq, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, ack_seq, 4) \
|
||||
X(a, STATIC, SINGULAR, BYTES, payload, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, cols, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, rows, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT32, flags, 8)
|
||||
#define meshtastic_DMShell_CALLBACK NULL
|
||||
#define meshtastic_DMShell_DEFAULT NULL
|
||||
|
||||
#define meshtastic_Waypoint_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 1) \
|
||||
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
|
||||
@@ -2093,6 +2165,7 @@ extern const pb_msgdesc_t meshtastic_Routing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Data_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
|
||||
extern const pb_msgdesc_t meshtastic_DMShell_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Waypoint_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StatusMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
|
||||
@@ -2128,6 +2201,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_Data_fields &meshtastic_Data_msg
|
||||
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
|
||||
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
|
||||
#define meshtastic_DMShell_fields &meshtastic_DMShell_msg
|
||||
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
|
||||
#define meshtastic_StatusMessage_fields &meshtastic_StatusMessage_msg
|
||||
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
|
||||
@@ -2162,6 +2236,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_ChunkedPayload_size 245
|
||||
#define meshtastic_ClientNotification_size 482
|
||||
#define meshtastic_Compressed_size 239
|
||||
#define meshtastic_DMShell_size 241
|
||||
#define meshtastic_Data_size 269
|
||||
#define meshtastic_DeviceMetadata_size 54
|
||||
#define meshtastic_DuplicatedPublicKey_size 0
|
||||
|
||||
@@ -76,6 +76,8 @@ typedef enum _meshtastic_PortNum {
|
||||
meshtastic_PortNum_ALERT_APP = 11,
|
||||
/* Module/port for handling key verification requests. */
|
||||
meshtastic_PortNum_KEY_VERIFICATION_APP = 12,
|
||||
/* Module/port for handling key verification requests. */
|
||||
meshtastic_PortNum_DM_SHELL_APP = 13,
|
||||
/* Provides a 'ping' service that replies to any packet it receives.
|
||||
Also serves as a small example module.
|
||||
ENCODING: ASCII Plaintext */
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
#include "DMShell.h"
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
#include "Channels.h"
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Throttle.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "pb_decode.h"
|
||||
#include "pb_encode.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pty.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
DMShellModule *dmShellModule;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint16_t PTY_COLS_DEFAULT = 120;
|
||||
constexpr uint16_t PTY_ROWS_DEFAULT = 40;
|
||||
constexpr size_t MAX_PTY_READ_SIZE = 200;
|
||||
|
||||
struct BytesDecodeState {
|
||||
uint8_t *buf;
|
||||
size_t maxLen;
|
||||
size_t outLen;
|
||||
};
|
||||
|
||||
struct BytesEncodeState {
|
||||
const uint8_t *buf;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
bool decodeBytesField(pb_istream_t *stream, const pb_field_iter_t *field, void **arg)
|
||||
{
|
||||
(void)field;
|
||||
auto *state = static_cast<BytesDecodeState *>(*arg);
|
||||
const size_t fieldLen = stream->bytes_left;
|
||||
if (fieldLen > state->maxLen) {
|
||||
return false;
|
||||
}
|
||||
if (!pb_read(stream, state->buf, fieldLen)) {
|
||||
return false;
|
||||
}
|
||||
state->outLen = fieldLen;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeBytesField(pb_ostream_t *stream, const pb_field_iter_t *field, void *const *arg)
|
||||
{
|
||||
auto *state = static_cast<const BytesEncodeState *>(*arg);
|
||||
if (!state || !state->buf || state->len == 0) {
|
||||
return true;
|
||||
}
|
||||
if (!pb_encode_tag_for_field(stream, field)) {
|
||||
return false;
|
||||
}
|
||||
return pb_encode_string(stream, state->buf, state->len);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DMShellModule::DMShellModule()
|
||||
: SinglePortModule("DMShellModule", meshtastic_PortNum_DM_SHELL_APP), concurrency::OSThread("DMShell", 100)
|
||||
{
|
||||
LOG_WARN("DMShell enabled on Portduino: remote shell access is dangerous and intended for trusted debugging only");
|
||||
}
|
||||
|
||||
ProcessMessage DMShellModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
DMShellFrame frame;
|
||||
if (!parseFrame(mp, frame)) {
|
||||
LOG_WARN("DMShell: ignoring malformed frame");
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
|
||||
if (!isAuthorizedPacket(mp)) {
|
||||
LOG_WARN("DMShell: unauthorized sender 0x%x", mp.from);
|
||||
myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp);
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
|
||||
if (frame.op == meshtastic_DMShell_OpCode_OPEN) {
|
||||
LOG_WARN("DMShell: received OPEN from 0x%x sessionId=0x%x", mp.from, frame.sessionId);
|
||||
if (!openSession(mp, frame)) {
|
||||
const char *msg = "open_failed";
|
||||
sendFrameToPeer(getFrom(&mp), mp.channel, meshtastic_DMShell_OpCode_ERROR, frame.sessionId, frame.seq,
|
||||
reinterpret_cast<const uint8_t *>(msg), strlen(msg));
|
||||
}
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
|
||||
if (!session.active || frame.sessionId != session.sessionId || getFrom(&mp) != session.peer) {
|
||||
const char *msg = "invalid_session";
|
||||
sendFrameToPeer(getFrom(&mp), mp.channel, meshtastic_DMShell_OpCode_ERROR, frame.sessionId, frame.seq,
|
||||
reinterpret_cast<const uint8_t *>(msg), strlen(msg));
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
|
||||
session.lastActivityMs = millis();
|
||||
|
||||
switch (frame.op) {
|
||||
case meshtastic_DMShell_OpCode_INPUT:
|
||||
if (!writeSessionInput(frame)) {
|
||||
sendError("input_write_failed");
|
||||
}
|
||||
break;
|
||||
case meshtastic_DMShell_OpCode_RESIZE:
|
||||
if (frame.rows > 0 && frame.cols > 0) {
|
||||
struct winsize ws = {};
|
||||
ws.ws_row = frame.rows;
|
||||
ws.ws_col = frame.cols;
|
||||
if (session.masterFd >= 0) {
|
||||
ioctl(session.masterFd, TIOCSWINSZ, &ws);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case meshtastic_DMShell_OpCode_PING:
|
||||
sendControl(meshtastic_DMShell_OpCode_PONG, nullptr, 0, frame.seq);
|
||||
break;
|
||||
case meshtastic_DMShell_OpCode_CLOSE:
|
||||
closeSession("peer_close", true);
|
||||
break;
|
||||
default:
|
||||
sendError("unsupported_op");
|
||||
break;
|
||||
}
|
||||
|
||||
return ProcessMessage::STOP;
|
||||
}
|
||||
|
||||
int32_t DMShellModule::runOnce()
|
||||
{
|
||||
if (!session.active) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
reapChildIfExited();
|
||||
if (!session.active) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (Throttle::isWithinTimespanMs(session.lastActivityMs, SESSION_IDLE_TIMEOUT_MS) == false) {
|
||||
closeSession("idle_timeout", true);
|
||||
return 100;
|
||||
}
|
||||
|
||||
uint8_t outBuf[MAX_PTY_READ_SIZE];
|
||||
while (session.masterFd >= 0) {
|
||||
const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf));
|
||||
if (bytesRead > 0) {
|
||||
LOG_WARN("DMShell: read %d bytes from PTY", bytesRead);
|
||||
sendControl(meshtastic_DMShell_OpCode_OUTPUT, outBuf, static_cast<size_t>(bytesRead), session.txSeq++);
|
||||
session.lastActivityMs = millis();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytesRead == 0) {
|
||||
closeSession("pty_eof", true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_WARN("DMShell: PTY read error errno=%d", errno);
|
||||
closeSession("pty_read_error", true);
|
||||
break;
|
||||
}
|
||||
|
||||
return 100;
|
||||
}
|
||||
|
||||
bool DMShellModule::parseFrame(const meshtastic_MeshPacket &mp, DMShellFrame &outFrame)
|
||||
{
|
||||
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
meshtastic_DMShell decodedMsg = meshtastic_DMShell_init_zero;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_DMShell_fields, &decodedMsg)) {
|
||||
LOG_INFO("Received a DMShell message");
|
||||
} else {
|
||||
LOG_ERROR("Error decoding DMShell message!");
|
||||
return false;
|
||||
}
|
||||
|
||||
outFrame.op = decodedMsg.op;
|
||||
outFrame.sessionId = decodedMsg.session_id;
|
||||
outFrame.seq = decodedMsg.seq;
|
||||
outFrame.ackSeq = decodedMsg.ack_seq;
|
||||
outFrame.cols = decodedMsg.cols;
|
||||
outFrame.rows = decodedMsg.rows;
|
||||
outFrame.flags = decodedMsg.flags;
|
||||
outFrame.payloadLen = decodedMsg.payload.size;
|
||||
memcpy(outFrame.payload, decodedMsg.payload.bytes, outFrame.payloadLen);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DMShellModule::isAuthorizedPacket(const meshtastic_MeshPacket &mp) const
|
||||
{
|
||||
if (mp.from == 0) {
|
||||
return !config.security.is_managed;
|
||||
}
|
||||
|
||||
const meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
||||
if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) {
|
||||
return config.security.admin_channel_enabled;
|
||||
}
|
||||
|
||||
if (mp.pki_encrypted) {
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
if (config.security.admin_key[i].size == 32 &&
|
||||
memcmp(mp.public_key.bytes, config.security.admin_key[i].bytes, 32) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DMShellModule::openSession(const meshtastic_MeshPacket &mp, const DMShellFrame &frame)
|
||||
{
|
||||
if (session.active) {
|
||||
closeSession("preempted", true);
|
||||
}
|
||||
|
||||
int masterFd = -1;
|
||||
struct winsize ws = {};
|
||||
ws.ws_col = PTY_COLS_DEFAULT;
|
||||
ws.ws_row = PTY_ROWS_DEFAULT;
|
||||
const pid_t childPid = forkpty(&masterFd, nullptr, nullptr, &ws);
|
||||
if (childPid < 0) {
|
||||
LOG_ERROR("DMShell: forkpty failed errno=%d", errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (childPid == 0) {
|
||||
const char *shell = getenv("SHELL");
|
||||
if (!shell || !*shell) {
|
||||
shell = "/bin/sh";
|
||||
}
|
||||
execl(shell, shell, "-i", static_cast<char *>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
const int flags = fcntl(masterFd, F_GETFL, 0);
|
||||
if (flags >= 0) {
|
||||
fcntl(masterFd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
session.active = true;
|
||||
session.sessionId = (frame.sessionId != 0) ? frame.sessionId : static_cast<uint32_t>(random(1, 0x7fffffff));
|
||||
session.peer = getFrom(&mp);
|
||||
session.channel = mp.channel;
|
||||
session.masterFd = masterFd;
|
||||
session.childPid = childPid;
|
||||
session.txSeq = 0;
|
||||
session.lastActivityMs = millis();
|
||||
|
||||
uint8_t payload[sizeof(uint32_t)] = {0};
|
||||
uint32_t pidBE = (static_cast<uint32_t>(session.childPid) << 24) | ((static_cast<uint32_t>(session.childPid) >> 8) & 0xff00) |
|
||||
((static_cast<uint32_t>(session.childPid) << 8) & 0xff0000) |
|
||||
(static_cast<uint32_t>(session.childPid) >> 24);
|
||||
memcpy(payload, &pidBE, sizeof(payload));
|
||||
sendFrameToPeer(session.peer, session.channel, meshtastic_DMShell_OpCode_OPEN_OK, session.sessionId, frame.seq, payload,
|
||||
sizeof(payload), PTY_COLS_DEFAULT, PTY_ROWS_DEFAULT);
|
||||
|
||||
LOG_INFO("DMShell: opened session=0x%x peer=0x%x pid=%d", session.sessionId, session.peer, session.childPid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DMShellModule::writeSessionInput(const DMShellFrame &frame)
|
||||
{
|
||||
if (session.masterFd < 0) {
|
||||
return false;
|
||||
}
|
||||
if (frame.payloadLen == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ssize_t bytesWritten = write(session.masterFd, frame.payload, frame.payloadLen);
|
||||
return bytesWritten >= 0;
|
||||
}
|
||||
|
||||
void DMShellModule::closeSession(const char *reason, bool notifyPeer)
|
||||
{
|
||||
if (!session.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notifyPeer) {
|
||||
const size_t reasonLen = strnlen(reason, 256);
|
||||
sendControl(meshtastic_DMShell_OpCode_CLOSED, reinterpret_cast<const uint8_t *>(reason), reasonLen, session.txSeq++);
|
||||
}
|
||||
|
||||
if (session.masterFd >= 0) {
|
||||
close(session.masterFd);
|
||||
session.masterFd = -1;
|
||||
}
|
||||
|
||||
if (session.childPid > 0) {
|
||||
kill(session.childPid, SIGTERM);
|
||||
int status = 0;
|
||||
waitpid(session.childPid, &status, WNOHANG);
|
||||
session.childPid = -1;
|
||||
}
|
||||
|
||||
LOG_INFO("DMShell: closed session=0x%x reason=%s", session.sessionId, reason);
|
||||
session = DMShellSession{};
|
||||
}
|
||||
|
||||
void DMShellModule::reapChildIfExited()
|
||||
{
|
||||
if (!session.active || session.childPid <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
const pid_t result = waitpid(session.childPid, &status, WNOHANG);
|
||||
if (result == session.childPid) {
|
||||
closeSession("shell_exited", true);
|
||||
}
|
||||
}
|
||||
|
||||
void DMShellModule::sendControl(meshtastic_DMShell_OpCode op, const uint8_t *payload, size_t payloadLen, uint32_t seq)
|
||||
{
|
||||
sendFrameToPeer(session.peer, session.channel, op, session.sessionId, seq, payload, payloadLen);
|
||||
}
|
||||
|
||||
void DMShellModule::sendFrameToPeer(NodeNum peer, uint8_t channel, meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq,
|
||||
const uint8_t *payload, size_t payloadLen, uint32_t cols, uint32_t rows, uint32_t ackSeq,
|
||||
uint32_t flags)
|
||||
{
|
||||
meshtastic_MeshPacket *packet = buildFramePacket(op, sessionId, seq, payload, payloadLen, cols, rows, ackSeq, flags);
|
||||
if (!packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
packet->to = peer;
|
||||
packet->channel = channel;
|
||||
packet->want_ack = false;
|
||||
packet->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||
service->sendToMesh(packet);
|
||||
}
|
||||
|
||||
void DMShellModule::sendError(const char *message)
|
||||
{
|
||||
const size_t len = strnlen(message, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
sendControl(meshtastic_DMShell_OpCode_ERROR, reinterpret_cast<const uint8_t *>(message), len, session.txSeq++);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *DMShellModule::buildFramePacket(meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq,
|
||||
const uint8_t *payload, size_t payloadLen, uint32_t cols, uint32_t rows,
|
||||
uint32_t ackSeq, uint32_t flags)
|
||||
{
|
||||
meshtastic_DMShell frame = meshtastic_DMShell_init_zero;
|
||||
frame.op = op;
|
||||
frame.session_id = sessionId;
|
||||
frame.seq = seq;
|
||||
frame.ack_seq = ackSeq;
|
||||
frame.cols = cols;
|
||||
frame.rows = rows;
|
||||
frame.flags = flags;
|
||||
|
||||
if (payload && payloadLen > 0) {
|
||||
memcpy(frame.payload.bytes, payload, payloadLen);
|
||||
frame.payload.size = payloadLen;
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *packet = allocDataPacket();
|
||||
if (!packet) {
|
||||
return nullptr;
|
||||
}
|
||||
LOG_WARN("DMShell: building packet op=%d session=0x%x seq=%d payloadLen=%d", op, sessionId, seq, payloadLen);
|
||||
const size_t encoded = pb_encode_to_bytes(packet->decoded.payload.bytes, sizeof(packet->decoded.payload.bytes),
|
||||
meshtastic_DMShell_fields, &frame);
|
||||
if (encoded == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
packet->decoded.payload.size = encoded;
|
||||
return packet;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "MeshModule.h"
|
||||
#include "Router.h"
|
||||
#include "SinglePortModule.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
|
||||
struct DMShellFrame {
|
||||
meshtastic_DMShell_OpCode op = meshtastic_DMShell_OpCode_ERROR;
|
||||
uint32_t sessionId = 0;
|
||||
uint32_t seq = 0;
|
||||
uint32_t ackSeq = 0;
|
||||
uint32_t cols = 0;
|
||||
uint32_t rows = 0;
|
||||
uint32_t flags = 0;
|
||||
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||
size_t payloadLen = 0;
|
||||
};
|
||||
|
||||
struct DMShellSession {
|
||||
bool active = false;
|
||||
uint32_t sessionId = 0;
|
||||
NodeNum peer = 0;
|
||||
uint8_t channel = 0;
|
||||
int masterFd = -1;
|
||||
int childPid = -1;
|
||||
uint32_t txSeq = 0;
|
||||
uint32_t lastActivityMs = 0;
|
||||
};
|
||||
|
||||
class DMShellModule : private concurrency::OSThread, public SinglePortModule
|
||||
{
|
||||
|
||||
public:
|
||||
DMShellModule();
|
||||
|
||||
protected:
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
private:
|
||||
static constexpr uint32_t SESSION_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
DMShellSession session;
|
||||
|
||||
bool parseFrame(const meshtastic_MeshPacket &mp, DMShellFrame &outFrame);
|
||||
bool isAuthorizedPacket(const meshtastic_MeshPacket &mp) const;
|
||||
bool openSession(const meshtastic_MeshPacket &mp, const DMShellFrame &frame);
|
||||
bool writeSessionInput(const DMShellFrame &frame);
|
||||
void closeSession(const char *reason, bool notifyPeer);
|
||||
void reapChildIfExited();
|
||||
|
||||
void sendControl(meshtastic_DMShell_OpCode op, const uint8_t *payload, size_t payloadLen, uint32_t seq = 0);
|
||||
void sendFrameToPeer(NodeNum peer, uint8_t channel, meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq,
|
||||
const uint8_t *payload, size_t payloadLen, uint32_t cols = 0, uint32_t rows = 0, uint32_t ackSeq = 0,
|
||||
uint32_t flags = 0);
|
||||
void sendError(const char *message);
|
||||
meshtastic_MeshPacket *buildFramePacket(meshtastic_DMShell_OpCode op, uint32_t sessionId, uint32_t seq,
|
||||
const uint8_t *payload, size_t payloadLen, uint32_t cols, uint32_t rows,
|
||||
uint32_t ackSeq, uint32_t flags);
|
||||
};
|
||||
|
||||
extern DMShellModule *dmShellModule;
|
||||
|
||||
#endif
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "modules/WaypointModule.h"
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "modules/DMShell.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -184,6 +185,7 @@ void setupModules()
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
dmShellModule = new DMShellModule();
|
||||
#endif
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
|
||||
Reference in New Issue
Block a user