.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s

This commit is contained in:
Your Name 2025-05-10 13:47:17 +12:00
parent 068ef34709
commit 8827ea5a42
28 changed files with 26043 additions and 1567 deletions

View File

@ -1,90 +1,12 @@
cmake_minimum_required(VERSION 3.10)
project(runner VERSION 1.0)
project(runner_demo)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Include directories
include_directories(include)
find_package(OpenSSL REQUIRED)
# Find required packages
find_package(nlohmann_json QUIET)
if(NOT nlohmann_json_FOUND)
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
endif()
add_library(runner_lib runner.cpp)
target_include_directories(runner_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# Try to find libssh using different methods
find_package(libssh QUIET)
if(NOT libssh_FOUND)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(LIBSSH libssh QUIET)
endif()
if(NOT LIBSSH_FOUND)
# Try to find manually if pkg-config failed too
find_path(LIBSSH_INCLUDE_DIR
NAMES libssh/libssh.h
PATHS /usr/include /usr/local/include
)
find_library(LIBSSH_LIBRARY
NAMES ssh libssh
PATHS /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu
)
if(LIBSSH_INCLUDE_DIR AND LIBSSH_LIBRARY)
set(LIBSSH_FOUND TRUE)
set(LIBSSH_LIBRARIES ${LIBSSH_LIBRARY})
set(LIBSSH_INCLUDE_DIRS ${LIBSSH_INCLUDE_DIR})
message(STATUS "Found libssh: ${LIBSSH_LIBRARY}")
else()
message(FATAL_ERROR "libssh not found. Please install libssh-dev package.\nOn Ubuntu/Debian: sudo apt install libssh-dev\nOn CentOS/RHEL: sudo yum install libssh-devel\nOn macOS: brew install libssh")
endif()
endif()
endif()
# Print libssh information for debugging
message(STATUS "LIBSSH_FOUND: ${LIBSSH_FOUND}")
message(STATUS "LIBSSH_LIBRARIES: ${LIBSSH_LIBRARIES}")
message(STATUS "LIBSSH_INCLUDE_DIRS: ${LIBSSH_INCLUDE_DIRS}")
find_package(Threads REQUIRED)
# Library target
add_library(dropshell_runner STATIC
src/runner.cpp
src/base64.cpp
)
target_include_directories(dropshell_runner PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${LIBSSH_INCLUDE_DIRS}
)
# Link with libssh
if(libssh_FOUND)
# libssh was found using the find_package method
target_link_libraries(dropshell_runner PUBLIC
nlohmann_json::nlohmann_json
ssh
Threads::Threads
)
else()
# libssh was found using pkg-config or manual search
target_link_libraries(dropshell_runner PUBLIC
nlohmann_json::nlohmann_json
${LIBSSH_LIBRARIES}
Threads::Threads
)
endif()
# Executable target
add_executable(runner src/main.cpp)
target_link_libraries(runner PRIVATE dropshell_runner)
add_executable(runner runner_demo.cpp)
target_link_libraries(runner runner_lib OpenSSL::SSL OpenSSL::Crypto)

View File

@ -7,207 +7,77 @@ use:
BASE64COMMAND is Base64 encoded json. The json format is as described below. The exit code is that of the command run, or -1 if the command couldn't be run.
The c++ library used, which is contained in this codebase, has two simple functions:
bool runner(nlohmann::json run_json); // no output capture
bool runner(nlohmann::json run_json, std::string & output); // with output capture.
## JSON Specification
```json
The c++ library used, which is contained in this codebase, has one simple function:
typedef struct sSSHInfo {
std::string host;
std::string user;
std::string port;
} sSSHInfo;
static int execute_cmd(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
sSSHInfo * sshinfo = nullptr,
std::string* output = nullptr,
);
If SSH information is provided, the command will be executed on the remote server. Otherwise on the local machine.
If output is provided, the output of the command is captured.
If interactive is true, then an interactive session is created - e.g. for running nano to remotely edit a file, or sshing into a docker container from the remote host.
If silent is true, then all output is suppressed.
Before the command is run, the current directory is changed to working_dir on the remote host (or local host if no ssh info provided).
## BASE64COMMAND JSON Format
The BASE64COMMAND argument should be a Base64-encoded JSON object with the following fields:
- `command` (string, required): The command to execute (e.g., "ls").
- `args` (array of strings, optional): Arguments to pass to the command (e.g., ["-l", "/tmp"]).
- `working_dir` (string, optional): Directory to change to before running the command.
- `env` (object, optional): Environment variables to set (e.g., {"FOO": "bar"}).
- `silent` (boolean, optional): If true, suppress all output. Default: false.
- `interactive` (boolean, optional): If true, run the command interactively. Default: false.
- `sshinfo` (object, optional): If present, run the command on a remote host (not implemented in demo):
- `host` (string): Hostname or IP.
- `user` (string): Username.
- `port` (string): SSH port.
### Example JSON
```
{
"ssh": { // Optional: SSH connection information
"host": "hostname", // Remote host to connect to
"port": 22, // Port number (default: 22)
"user": "username", // Username for SSH connection
"key": "path/to/key" // Path to SSH key file or "auto" to use current user's key
},
"working_directory": "/path", // Optional: Directory to change to before executing the command
"env": { // Optional: Environment variables
"VAR1": "value1",
"VAR2": "value2"
},
"command": "command_name", // Required: Command to execute
"args": ["arg1", "arg2"], // Optional: Command arguments
"options": { // Optional: Execution options
"silent": false, // Suppress all terminal output (default: false)
"interactive": false // Hook up TTY for interactive sessions (default: false)
}
"command": "ls",
"args": ["-l", "/tmp"],
"working_dir": "/",
"env": {"FOO": "bar"},
"silent": false,
"interactive": false
}
```
If SSH information is provided, the command will be executed on the remote server.
To use, encode the JSON as a single line, then base64 encode it:
## Build Instructions
### Prerequisites
- CMake 3.10 or newer
- C++17 compatible compiler
- libssh development libraries
- jq (for the helper scripts)
#### Installing Dependencies
##### Quick Installation (Ubuntu/Debian)
For Ubuntu/Debian systems, you can use the provided installation script:
```bash
sudo ./install_deps.sh
```
echo -n '{"command":"ls","args":["-l","/tmp"],"working_dir":"/","env":{"FOO":"bar"},"silent":false,"interactive":false}' | base64
```
This will install all required dependencies (cmake, g++, libssh-dev, jq).
Then run:
##### Manual Installation
###### Ubuntu/Debian
```bash
sudo apt-get install cmake g++ libssh-dev jq
```
./build/runner <BASE64COMMAND>
```
###### CentOS/RHEL
```bash
sudo yum install cmake gcc-c++ libssh-devel jq
```
###### macOS
```bash
brew install cmake libssh jq
```
###### Windows
Using vcpkg:
```bash
vcpkg install libssh nlohmann-json
```
### Building
To build the project, you can use the provided build script:
```bash
./build.sh
```
Or manually:
```bash
mkdir -p build
cd build
cmake ..
make
```
The executable will be created at `build/runner`.
### Testing
A simple test script is included to verify the functionality:
```bash
./test.sh
```
This will run basic tests for command execution, environment variables, silent mode, and return codes.
If you have SSH configured on your local machine and want to test the SSH functionality:
```bash
ENABLE_SSH_TEST=1 ./test.sh
```
### Troubleshooting
If CMake cannot find libssh, you can:
1. Run the libssh finder script to locate your installation:
```bash
./examples/find_libssh.sh
```
2. Specify its location manually:
```bash
cmake -DCMAKE_PREFIX_PATH=/path/to/libssh/installation ..
```
3. Or set the libssh_DIR environment variable:
```bash
export libssh_DIR=/path/to/libssh/installation
cmake ..
```
4. If the problem persists, specify the library and include paths directly:
```bash
cmake -DLIBSSH_LIBRARY=/path/to/libssh.so -DLIBSSH_INCLUDE_DIR=/path/to/include ..
```
## Usage Examples
### Running a local command
```bash
# Create a JSON configuration for the 'ls -l' command
JSON='{"command":"ls","args":["-l"]}'
# Base64 encode the JSON
BASE64=$(echo -n "$JSON" | base64)
# Run the command
./build/runner $BASE64
```
### Running a command with environment variables
```bash
# Create a JSON configuration with environment variables
JSON='{"command":"echo","args":["$GREETING"],"env":{"GREETING":"Hello, World!"}}'
# Base64 encode the JSON
BASE64=$(echo -n "$JSON" | base64)
# Run the command
./build/runner $BASE64
```
### Running a command on a remote server via SSH
```bash
# Create a JSON configuration for a remote command
JSON='{"ssh":{"host":"example.com","user":"username","key":"auto"},"command":"hostname"}'
# Base64 encode the JSON
BASE64=$(echo -n "$JSON" | base64)
# Run the command
./build/runner $BASE64
```
### Running an interactive command
```bash
# Create a JSON configuration for an interactive command
JSON='{"command":"vim","options":{"interactive":true}}'
# Base64 encode the JSON
BASE64=$(echo -n "$JSON" | base64)
# Run the command
./build/runner $BASE64
```
### Using the helper script
The `run.sh` script simplifies testing by handling the JSON validation and Base64 encoding:
```bash
# Run with a JSON file
./run.sh examples/local_command.json
# Run with a JSON string
./run.sh '{"command":"echo","args":["Hello World"]}'
```

View File

@ -1,78 +1,39 @@
#!/bin/bash
set -e
# Don't use set -e because we want to handle errors ourselves
# set -e
echo "Building Runner - Dropshell Command Execution Library"
echo "===================================================="
# Create build directory if it doesn't exist
mkdir -p build || {
echo "ERROR: Failed to create build directory"
# Check for cmake
if ! command -v cmake &> /dev/null; then
echo "Error: cmake is not installed. Please install cmake." >&2
exit 1
}
cd build
fi
# Check if pkg-config is available and can find libssh
if command -v pkg-config &> /dev/null && pkg-config --exists libssh; then
# If libssh is found through pkg-config, use it
LIBSSH_PREFIX=$(pkg-config --variable=prefix libssh)
echo "Found libssh through pkg-config at: $LIBSSH_PREFIX"
CMAKE_ARGS="-DCMAKE_PREFIX_PATH=$LIBSSH_PREFIX"
# Check for g++
if ! command -v g++ &> /dev/null; then
echo "Error: g++ is not installed. Please install g++." >&2
exit 1
fi
# Check for OpenSSL
if ! pkg-config --exists openssl; then
echo "Error: OpenSSL development libraries not found. Please install libssl-dev." >&2
exit 1
fi
# Check for nlohmann_json
if ! pkg-config --exists nlohmann_json; then
echo "Warning: nlohmann_json not found via pkg-config. Make sure it is installed or available to CMake." >&2
fi
BUILD_DIR=build
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
cmake ..
make -j$(nproc)
if [ -f runner ]; then
echo "Build successful. Run ./build/runner BASE64COMMAND to test."
else
# Otherwise, let CMake try to find it
CMAKE_ARGS=""
# Check if libssh-dev/libssh-devel is installed
if [ ! -f "/usr/include/libssh/libssh.h" ] && [ ! -f "/usr/local/include/libssh/libssh.h" ]; then
echo "WARNING: libssh development headers not found in standard locations."
echo "You may need to install the libssh development package:"
echo " - Ubuntu/Debian: sudo apt install libssh-dev"
echo " - CentOS/RHEL: sudo yum install libssh-devel"
echo " - macOS: brew install libssh"
echo ""
echo "Continuing build anyway, but it may fail..."
echo ""
# Offer to run the find_libssh.sh helper script
if [ -x "../examples/find_libssh.sh" ]; then
echo "Would you like to run the libssh finder helper script? (y/n)"
read -r answer
if [[ "$answer" =~ ^[Yy] ]]; then
cd ..
./examples/find_libssh.sh
cd build
echo ""
echo "Continuing with build..."
fi
fi
fi
fi
# Configure and build with error handling
echo "Running cmake with args: $CMAKE_ARGS"
if ! cmake $CMAKE_ARGS ..; then
echo ""
echo "ERROR: CMake configuration failed."
echo "This is likely due to missing dependencies. Please check the error message above."
echo "For libssh issues, try:"
echo " 1. Install libssh development package: sudo apt install libssh-dev"
echo " 2. Run our helper script: ./examples/find_libssh.sh"
echo " 3. If you know where libssh is installed, specify it with:"
echo " cmake -DLIBSSH_LIBRARY=/path/to/libssh.so -DLIBSSH_INCLUDE_DIR=/path/to/include .."
echo "Build failed. Check the output above for errors."
exit 1
fi
if ! make -j$(nproc); then
echo ""
echo "ERROR: Build failed."
echo "Please check the error messages above for details."
exit 1
fi
echo ""
echo "Build complete. Binary is at build/runner"
echo ""
echo "Examples:"
echo " ./run.sh examples/local_command.json # Run a local command"
echo " ./run.sh examples/env_vars.json # Run with environment variables"
fi

View File

@ -1 +0,0 @@
{"command":"bash","args":["-c","echo Hello $NAME, the current directory is $PWD"],"env":{"NAME":"Runner","CUSTOM_VAR":"This is a custom environment variable"}}

View File

@ -1,64 +0,0 @@
#!/bin/bash
# Helper script to find libssh installation on your system
echo "Looking for libssh installation..."
echo
# Check pkg-config path
if command -v pkg-config &> /dev/null; then
echo "Checking pkg-config for libssh:"
if pkg-config --exists libssh; then
echo " - Found libssh with pkg-config"
echo " - Version: $(pkg-config --modversion libssh)"
echo " - Include path: $(pkg-config --variable=includedir libssh)"
echo " - Library path: $(pkg-config --variable=libdir libssh)"
echo
echo "To use this in cmake:"
echo " cmake -DCMAKE_PREFIX_PATH=$(pkg-config --variable=prefix libssh) .."
echo
else
echo " - libssh not found with pkg-config"
echo
fi
else
echo "pkg-config not found on your system"
echo
fi
# Check common include paths
for DIR in /usr/include /usr/local/include /opt/local/include; do
if [ -d "$DIR" ] && [ -f "$DIR/libssh/libssh.h" ]; then
echo "Found libssh headers at: $DIR/libssh/libssh.h"
fi
done
# Check common library paths
LIB_PATHS=()
for DIR in /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu /usr/local/lib64 /opt/local/lib; do
if [ -d "$DIR" ]; then
LIBS=$(find "$DIR" -name "libssh.so*" -o -name "libssh.dylib" -o -name "libssh.a" 2>/dev/null)
if [ -n "$LIBS" ]; then
echo "Found libssh libraries in $DIR:"
echo "$LIBS" | sed 's/^/ - /'
LIB_DIR="$DIR"
LIB_PATHS+=("$DIR")
fi
fi
done
echo
if [ ${#LIB_PATHS[@]} -gt 0 ]; then
echo "To build with the detected libssh installation, you can use:"
echo " cmake -DLIBSSH_LIBRARY=${LIB_PATHS[0]}/libssh.so -DLIBSSH_INCLUDE_DIR=/usr/include .."
echo
echo "Or set environment variables:"
echo " export LIBSSH_LIBRARY=${LIB_PATHS[0]}/libssh.so"
echo " export LIBSSH_INCLUDE_DIR=/usr/include"
echo " cmake .."
else
echo "Could not find libssh on your system."
echo "Please install it with your package manager:"
echo " - Ubuntu/Debian: sudo apt install libssh-dev"
echo " - CentOS/RHEL: sudo yum install libssh-devel"
echo " - macOS: brew install libssh"
fi

View File

@ -1 +0,0 @@
{"command":"bash","options":{"interactive":true}}

View File

@ -1 +0,0 @@
{"command":"ls","args":["-la"],"options":{"silent":false}}

View File

@ -1 +0,0 @@
{"ssh":{"host":"localhost","key":"auto"},"command":"echo","args":["Hello from SSH on localhost!"]}

View File

@ -1 +0,0 @@
{"command":"ls","args":["-la"],"options":{"silent":true}}

View File

@ -1 +0,0 @@
{"ssh":{"host":"localhost","port":22,"user":"USERNAME","key":"auto"},"command":"hostname","options":{"interactive":false}}

View File

@ -1 +0,0 @@
{"ssh":{"host":"localhost","user":"USERNAME","key":"auto"},"command":"pwd","working_directory":"/tmp"}

View File

@ -1 +0,0 @@
{"command":"pwd","working_directory":"/tmp"}

View File

@ -1,2 +0,0 @@
lkjdfslka

View File

@ -1,13 +0,0 @@
#ifndef BASE64_H
#define BASE64_H
#include <string>
/**
* Decode a Base64 encoded string to its original form
* @param encoded The Base64 encoded string
* @return The decoded string
*/
std::string base64_decode(const std::string& encoded);
#endif // BASE64_H

View File

@ -1,104 +0,0 @@
#ifndef DROPSHELL_RUNNER_H
#define DROPSHELL_RUNNER_H
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <map>
namespace dropshell {
/**
* Runner library for executing commands locally or remotely via SSH
*/
class Runner {
public:
/**
* Execute a command according to the specification in the JSON
* @param run_json JSON specification for the command
* @return true if command executed successfully, false otherwise
*/
static bool run(const nlohmann::json& run_json);
/**
* Execute a command and capture its output
* @param run_json JSON specification for the command
* @param output Reference to string where output will be stored
* @return true if command executed successfully, false otherwise
*/
static bool run(const nlohmann::json& run_json, std::string& output);
private:
/**
* Execute a command locally
* @param command Command to execute
* @param args Command arguments
* @param working_dir Working directory to change to before execution
* @param env Environment variables
* @param silent Whether to suppress output
* @param interactive Whether to enable interactive mode
* @param output Reference to string where output will be stored (if capturing)
* @param capture_output Whether to capture output
* @return exit code of the command, or -1 if execution failed
*/
static int execute_local(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output = nullptr,
bool capture_output = false
);
/**
* Execute a command remotely via SSH
* @param ssh_config SSH configuration
* @param command Command to execute
* @param args Command arguments
* @param working_dir Working directory to change to on the remote server
* @param env Environment variables
* @param silent Whether to suppress output
* @param interactive Whether to enable interactive mode
* @param output Reference to string where output will be stored (if capturing)
* @param capture_output Whether to capture output
* @return exit code of the command, or -1 if execution failed
*/
static int execute_ssh(
const nlohmann::json& ssh_config,
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output = nullptr,
bool capture_output = false
);
/**
* Parse command specification from JSON
* @param run_json JSON specification
* @param command Output parameter for command
* @param args Output parameter for command arguments
* @param working_dir Output parameter for working directory
* @param env Output parameter for environment variables
* @param silent Output parameter for silent option
* @param interactive Output parameter for interactive option
* @return true if parsing was successful, false otherwise
*/
static bool parse_json(
const nlohmann::json& run_json,
std::string& command,
std::vector<std::string>& args,
std::string& working_dir,
std::map<std::string, std::string>& env,
bool& silent,
bool& interactive
);
};
} // namespace dropshell
#endif // DROPSHELL_RUNNER_H

View File

@ -1,64 +0,0 @@
#!/bin/bash
# Install dependencies script for Runner
# This script is for Ubuntu/Debian systems
echo "Installing dependencies for Runner - Dropshell Command Execution Library"
echo "======================================================================"
echo
# Check if running as root or with sudo
if [ "$EUID" -ne 0 ]; then
echo "Please run this script with sudo:"
echo " sudo $0"
exit 1
fi
echo "Updating package lists..."
apt-get update
echo "Installing required packages:"
echo " - build-essential (C++ compiler and build tools)"
echo " - cmake (build system)"
echo " - libssh-dev (SSH client library)"
echo " - jq (JSON parsing for helper scripts)"
echo
apt-get install -y build-essential cmake libssh-dev jq
# Check if installation was successful
if [ $? -eq 0 ]; then
echo
echo "Dependencies installed successfully!"
echo
echo "You can now build the project with:"
echo " ./build.sh"
echo
else
echo
echo "Error: Failed to install dependencies."
echo "Please check the error messages above."
exit 1
fi
# Verify libssh installation
if [ -f "/usr/include/libssh/libssh.h" ]; then
echo "Verified: libssh development headers are installed."
# Find libssh shared library
LIB=$(find /usr/lib -name "libssh.so*" | head -1)
if [ -n "$LIB" ]; then
echo "Verified: libssh shared library is installed at $LIB"
else
echo "Warning: Could not find libssh shared library."
fi
echo
echo "You're all set! Build the project with ./build.sh"
else
echo "Warning: Could not verify libssh installation."
echo "The package might have been installed in a non-standard location."
echo "You may need to manually specify the location when building:"
echo
echo " cmake -DLIBSSH_LIBRARY=/path/to/libssh.so -DLIBSSH_INCLUDE_DIR=/path/to/include .."
fi

View File

@ -1,20 +0,0 @@
#!/bin/bash
# JSON=$(cat <<'EOF'
# {"command":"nano","args":["-w","./hello.txt"]}
# EOF
# )
JSON=$(cat <<'EOF'
{"command":"nano","args":["-w","./hello.txt"],"options":{"interactive":true}}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command..."
build/runner "$BASE64"

View File

@ -1,96 +0,0 @@
#!/bin/bash
echo "Running minimal libssh test to diagnose linking issues"
echo "===================================================="
# Create test directory
TESTDIR="ssh_test_tmp"
mkdir -p $TESTDIR
cd $TESTDIR
# Create minimal C program that uses libssh
cat > test.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <libssh/libssh.h>
int main() {
ssh_session session = ssh_new();
if (session == NULL) {
fprintf(stderr, "Failed to create SSH session\n");
return 1;
}
printf("Successfully created SSH session\n");
ssh_free(session);
return 0;
}
EOF
echo "Created minimal test program. Attempting to compile..."
# Check pkg-config first
if command -v pkg-config &> /dev/null && pkg-config --exists libssh; then
CFLAGS=$(pkg-config --cflags libssh)
LIBS=$(pkg-config --libs libssh)
echo "Found libssh with pkg-config:"
echo " CFLAGS: $CFLAGS"
echo " LIBS: $LIBS"
echo
echo "Compiling with pkg-config flags..."
gcc -o test_pkg test.c $CFLAGS $LIBS
if [ $? -eq 0 ]; then
echo "Compilation successful!"
echo "Running the test program:"
./test_pkg
else
echo "Compilation failed with pkg-config flags."
fi
echo
fi
# Try with simple -lssh flag
echo "Compiling with simple -lssh flag..."
gcc -o test_simple test.c -lssh
if [ $? -eq 0 ]; then
echo "Compilation successful with -lssh!"
echo "Running the test program:"
./test_simple
else
echo "Compilation failed with simple -lssh flag."
echo "This indicates your libssh installation might not be in the standard location."
fi
echo
# Try to find libssh manually
echo "Searching for libssh.so in common locations..."
LIBSSH_PATHS=$(find /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu -name "libssh.so*" 2>/dev/null)
if [ -n "$LIBSSH_PATHS" ]; then
echo "Found libssh in the following locations:"
echo "$LIBSSH_PATHS" | sed 's/^/ - /'
# Extract directory of first result
LIBSSH_DIR=$(dirname $(echo "$LIBSSH_PATHS" | head -1))
echo
echo "For CMake, you can try:"
echo " cmake -DLIBSSH_LIBRARY=$LIBSSH_DIR/libssh.so -DLIBSSH_INCLUDE_DIR=/usr/include .."
else
echo "Could not find libssh.so."
echo "Consider installing libssh-dev package:"
echo " sudo apt install libssh-dev"
fi
# Clean up
cd ..
echo
echo "Cleaning up..."
rm -rf $TESTDIR
echo
echo "Test completed. Use this information to troubleshoot your libssh configuration."

25578
runner/nlohmann/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

View File

@ -1,83 +0,0 @@
#!/bin/bash
set -e
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed."
echo "Install it with: sudo apt-get install jq"
exit 1
fi
# Function to print usage
usage() {
echo "Usage: $0 <json_file>"
echo " $0 'json_string'"
exit 1
}
# Check if argument is provided
if [ $# -ne 1 ]; then
usage
fi
JSON=""
# Process and clean up JSON input to handle any whitespace or formatting issues
process_json() {
local input="$1"
# Use 'tr' to remove any trailing whitespace and control characters
# Then use jq to validate and normalize the JSON structure
echo "$input" | tr -d '\r' | sed 's/[[:space:]]*$//' | jq -c '.'
}
# Check if the argument is a file or a JSON string
if [ -f "$1" ]; then
# Validate JSON file
if ! jq . "$1" > /dev/null 2>&1; then
echo "Error: Invalid JSON in file $1"
exit 1
fi
# Read file content and process it
FILE_CONTENT=$(cat "$1")
JSON=$(process_json "$FILE_CONTENT")
else
# Check if it's a valid JSON string
if ! echo "$1" | jq . > /dev/null 2>&1; then
echo "Error: Invalid JSON string"
exit 1
fi
# Process the JSON string
JSON=$(process_json "$1")
fi
# Double-check that we have valid JSON (defensive programming)
if ! echo "$JSON" | jq . > /dev/null 2>&1; then
echo "Error: Something went wrong processing the JSON"
exit 1
fi
# Base64 encode the JSON, ensuring no trailing newlines
BASE64=$(echo -n "$JSON" | base64 | tr -d '\n')
# Ensure the binary exists
if [ ! -f "build/runner" ]; then
echo "Building the project first..."
./build.sh
fi
# Run the command
echo "Running command with JSON:"
echo "$JSON" | jq .
echo
echo "Base64 encoded: $BASE64"
echo
# Execute the runner with the processed base64 input
build/runner "$BASE64"
EXIT_CODE=$?
echo
echo "Command exited with code: $EXIT_CODE"
exit $EXIT_CODE

93
runner/runner.cpp Normal file
View File

@ -0,0 +1,93 @@
#include "runner.h"
#include <cstdlib>
#include <sstream>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
int execute_cmd(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
sSSHInfo* sshinfo,
std::string* output
) {
// SSH execution is not implemented in this demo
if (sshinfo) {
std::cerr << "Remote SSH execution is not implemented in this demo.\n";
return -1;
}
int pipefd[2];
if (output && pipe(pipefd) == -1) {
perror("pipe");
return -1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
// Child process
if (!working_dir.empty()) {
if (chdir(working_dir.c_str()) != 0) {
perror("chdir");
exit(-1);
}
}
// Set environment variables
for (const auto& kv : env) {
setenv(kv.first.c_str(), kv.second.c_str(), 1);
}
if (output) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
} else if (silent) {
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
if (!interactive) {
// Detach from terminal if not interactive
setsid();
}
std::vector<char*> argv;
argv.push_back(const_cast<char*>(command.c_str()));
for (const auto& arg : args) {
argv.push_back(const_cast<char*>(arg.c_str()));
}
argv.push_back(nullptr);
execvp(command.c_str(), argv.data());
perror("execvp");
exit(-1);
} else {
// Parent process
if (output) {
close(pipefd[1]);
std::ostringstream oss;
char buf[4096];
ssize_t n;
while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) {
oss.write(buf, n);
}
close(pipefd[0]);
*output = oss.str();
}
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -1;
}
}
}

21
runner/runner.h Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <vector>
#include <map>
struct sSSHInfo {
std::string host;
std::string user;
std::string port;
};
int execute_cmd(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
sSSHInfo* sshinfo = nullptr,
std::string* output = nullptr
);

63
runner/runner_demo.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "runner.h"
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <sstream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <nlohmann/json.hpp>
#include <openssl/bio.h>
#include <openssl/evp.h>
using json = nlohmann::json;
std::string base64_decode(const std::string& encoded) {
BIO* bio, *b64;
int decodeLen = (encoded.length() * 3) / 4;
std::string decoded(decodeLen, '\0');
bio = BIO_new_mem_buf(encoded.data(), encoded.length());
b64 = BIO_new(BIO_f_base64());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
int len = BIO_read(bio, &decoded[0], encoded.length());
decoded.resize(len > 0 ? len : 0);
BIO_free_all(bio);
return decoded;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: runner BASE64COMMAND\n";
return -1;
}
std::string decoded = base64_decode(argv[1]);
json j;
try {
j = json::parse(decoded);
} catch (...) {
std::cerr << "Invalid JSON in decoded command\n";
return -1;
}
std::string command = j.value("command", "");
std::vector<std::string> args = j.value("args", std::vector<std::string>{});
std::string working_dir = j.value("working_dir", "");
std::map<std::string, std::string> env = j.value("env", std::map<std::string, std::string>{});
bool silent = j.value("silent", false);
bool interactive = j.value("interactive", false);
sSSHInfo* sshinfo = nullptr;
sSSHInfo ssh;
if (j.contains("sshinfo")) {
ssh.host = j["sshinfo"].value("host", "");
ssh.user = j["sshinfo"].value("user", "");
ssh.port = j["sshinfo"].value("port", "");
sshinfo = &ssh;
}
std::string output;
int ret = execute_cmd(command, args, working_dir, env, silent, interactive, sshinfo, &output);
if (!silent && !output.empty()) {
std::cout << output;
}
return ret;
}

View File

@ -1,62 +0,0 @@
#include "base64.h"
#include <iostream>
#include <cctype>
std::string base64_decode(const std::string& encoded) {
const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
size_t in_len = encoded.size();
size_t i = 0;
size_t in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string decoded;
while (in_ < in_len && encoded[in_] != '=' &&
(std::isalnum(encoded[in_]) || encoded[in_] == '+' || encoded[in_] == '/')) {
char_array_4[i++] = encoded[in_++];
if (i == 4) {
// Translate values in char_array_4 from base64 alphabet to indices
for (i = 0; i < 4; i++) {
char_array_4[i] = base64_chars.find(char_array_4[i]);
}
// Decode to original bytes
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; i < 3; i++) {
decoded += char_array_3[i];
}
i = 0;
}
}
// Handle any remaining bytes
if (i) {
// Fill remaining positions with zeros
for (size_t j = i; j < 4; j++) {
char_array_4[j] = 0;
}
// Convert to indices
for (size_t j = 0; j < 4; j++) {
char_array_4[j] = base64_chars.find(char_array_4[j]);
}
// Decode remaining bytes
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
// Only add valid bytes based on how many characters we had
for (size_t j = 0; j < i - 1; j++) {
decoded += char_array_3[j];
}
}
return decoded;
}

View File

@ -1,42 +0,0 @@
#include "runner.h"
#include "base64.h"
#include <iostream>
#include <string>
void print_usage() {
std::cerr << "Usage: runner BASE64COMMAND" << std::endl;
std::cerr << " where BASE64COMMAND is a Base64 encoded JSON string" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
print_usage();
return 1;
}
std::string base64_command = argv[1];
std::string json_string;
try {
// Decode Base64
json_string = base64_decode(base64_command);
} catch (const std::exception& e) {
std::cerr << "Error decoding Base64: " << e.what() << std::endl;
return 1;
}
// Parse JSON
nlohmann::json run_json;
try {
run_json = nlohmann::json::parse(json_string);
} catch (const nlohmann::json::parse_error& e) {
std::cerr << "Error parsing JSON: " << e.what() << std::endl;
return 1;
}
// Execute command
bool success = dropshell::Runner::run(run_json);
// Return the exit code
return success ? 0 : 1;
}

View File

@ -1,513 +0,0 @@
#include "runner.h"
#include <iostream>
#include <sstream>
#include <cstdlib>
#include <array>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <libssh/libssh.h>
#include <libssh/sftp.h>
#include <pwd.h>
namespace dropshell {
bool Runner::run(const nlohmann::json& run_json) {
std::string command;
std::vector<std::string> args;
std::string working_dir;
std::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, working_dir, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, working_dir, env, silent, interactive);
} else {
exit_code = execute_local(command, args, working_dir, env, silent, interactive);
}
return exit_code == 0;
}
bool Runner::run(const nlohmann::json& run_json, std::string& output) {
std::string command;
std::vector<std::string> args;
std::string working_dir;
std::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, working_dir, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, working_dir, env, silent, interactive, &output, true);
} else {
exit_code = execute_local(command, args, working_dir, env, silent, interactive, &output, true);
}
return exit_code == 0;
}
bool Runner::parse_json(
const nlohmann::json& run_json,
std::string& command,
std::vector<std::string>& args,
std::string& working_dir,
std::map<std::string, std::string>& env,
bool& silent,
bool& interactive
) {
try {
// Command is required
if (!run_json.contains("command") || !run_json["command"].is_string()) {
std::cerr << "Error: 'command' field is required and must be a string" << std::endl;
return false;
}
command = run_json["command"];
// Args are optional
args.clear();
if (run_json.contains("args")) {
if (!run_json["args"].is_array()) {
std::cerr << "Error: 'args' field must be an array" << std::endl;
return false;
}
for (const auto& arg : run_json["args"]) {
if (!arg.is_string()) {
std::cerr << "Error: All arguments must be strings" << std::endl;
return false;
}
args.push_back(arg);
}
}
// Working directory is optional
working_dir = "";
if (run_json.contains("working_directory")) {
if (!run_json["working_directory"].is_string()) {
std::cerr << "Error: 'working_directory' field must be a string" << std::endl;
return false;
}
working_dir = run_json["working_directory"];
}
// Environment variables are optional
env.clear();
if (run_json.contains("env")) {
if (!run_json["env"].is_object()) {
std::cerr << "Error: 'env' field must be an object" << std::endl;
return false;
}
for (auto it = run_json["env"].begin(); it != run_json["env"].end(); ++it) {
if (!it.value().is_string()) {
std::cerr << "Error: All environment variable values must be strings" << std::endl;
return false;
}
env[it.key()] = it.value();
}
}
// Options are optional
silent = false;
interactive = false;
if (run_json.contains("options")) {
if (!run_json["options"].is_object()) {
std::cerr << "Error: 'options' field must be an object" << std::endl;
return false;
}
if (run_json["options"].contains("silent")) {
if (!run_json["options"]["silent"].is_boolean()) {
std::cerr << "Error: 'silent' option must be a boolean" << std::endl;
return false;
}
silent = run_json["options"]["silent"];
}
if (run_json["options"].contains("interactive")) {
if (!run_json["options"]["interactive"].is_boolean()) {
std::cerr << "Error: 'interactive' option must be a boolean" << std::endl;
return false;
}
interactive = run_json["options"]["interactive"];
}
}
return true;
} catch (const std::exception& e) {
std::cerr << "Error parsing JSON: " << e.what() << std::endl;
return false;
}
}
int Runner::execute_local(
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output,
bool capture_output
) {
int pipefd[2];
if (capture_output && pipe(pipefd) == -1) {
std::cerr << "Error creating pipe: " << strerror(errno) << std::endl;
return -1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Fork failed: " << strerror(errno) << std::endl;
if (capture_output) {
close(pipefd[0]);
close(pipefd[1]);
}
return -1;
}
if (pid == 0) {
// Child process
// Set up output redirection if needed
if (capture_output) {
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
} else if (silent) {
int devnull = open("/dev/null", O_WRONLY);
if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
}
// Change to working directory if specified
if (!working_dir.empty()) {
if (chdir(working_dir.c_str()) != 0) {
std::cerr << "Error changing to directory " << working_dir << ": " << strerror(errno) << std::endl;
exit(1);
}
}
// Set environment variables
for (const auto& [key, value] : env) {
setenv(key.c_str(), value.c_str(), 1);
}
// Prepare arguments
std::vector<char*> c_args;
c_args.push_back(const_cast<char*>(command.c_str()));
for (const auto& arg : args) {
c_args.push_back(const_cast<char*>(arg.c_str()));
}
c_args.push_back(nullptr);
// Execute command
execvp(command.c_str(), c_args.data());
// If exec fails
std::cerr << "exec failed: " << strerror(errno) << std::endl;
exit(1);
} else {
// Parent process
if (capture_output) {
close(pipefd[1]); // Close write end
std::array<char, 4096> buffer;
std::ostringstream oss;
ssize_t bytes_read;
while ((bytes_read = read(pipefd[0], buffer.data(), buffer.size() - 1)) > 0) {
buffer[bytes_read] = '\0';
oss << buffer.data();
if (!silent) {
std::cout << buffer.data();
}
}
close(pipefd[0]);
if (output) {
*output = oss.str();
}
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else {
return -1;
}
}
}
std::string find_ssh_key_for_user() {
const char* home_dir = getenv("HOME");
if (!home_dir) {
struct passwd* pw = getpwuid(getuid());
if (pw) {
home_dir = pw->pw_dir;
}
}
if (!home_dir) {
return "";
}
// Common SSH key locations
std::vector<std::string> key_paths = {
std::string(home_dir) + "/.ssh/id_rsa",
std::string(home_dir) + "/.ssh/id_ed25519",
std::string(home_dir) + "/.ssh/id_ecdsa",
std::string(home_dir) + "/.ssh/id_dsa"
};
for (const auto& path : key_paths) {
if (access(path.c_str(), F_OK) == 0) {
return path;
}
}
return "";
}
int Runner::execute_ssh(
const nlohmann::json& ssh_config,
const std::string& command,
const std::vector<std::string>& args,
const std::string& working_dir,
const std::map<std::string, std::string>& env,
bool silent,
bool interactive,
std::string* output,
bool capture_output
) {
if (!ssh_config.contains("host") || !ssh_config["host"].is_string()) {
std::cerr << "Error: SSH configuration requires 'host' field" << std::endl;
return -1;
}
std::string host = ssh_config["host"];
int port = 22;
if (ssh_config.contains("port")) {
if (ssh_config["port"].is_number_integer()) {
port = ssh_config["port"];
} else {
std::cerr << "Error: SSH 'port' must be an integer" << std::endl;
return -1;
}
}
std::string user = "";
if (ssh_config.contains("user") && ssh_config["user"].is_string()) {
user = ssh_config["user"];
} else {
// Get current username as default
char username[256];
if (getlogin_r(username, sizeof(username)) == 0) {
user = username;
} else {
struct passwd* pw = getpwuid(getuid());
if (pw) {
user = pw->pw_name;
}
}
}
std::string key_path = "";
if (ssh_config.contains("key") && ssh_config["key"].is_string()) {
std::string key = ssh_config["key"];
if (key == "auto") {
key_path = find_ssh_key_for_user();
if (key_path.empty()) {
std::cerr << "Error: Could not find SSH key automatically" << std::endl;
return -1;
}
} else {
key_path = key;
}
} else {
key_path = find_ssh_key_for_user();
}
// Initialize SSH session
ssh_session session = ssh_new();
if (session == nullptr) {
std::cerr << "Error: Failed to create SSH session" << std::endl;
return -1;
}
// Set SSH options
ssh_options_set(session, SSH_OPTIONS_HOST, host.c_str());
ssh_options_set(session, SSH_OPTIONS_PORT, &port);
ssh_options_set(session, SSH_OPTIONS_USER, user.c_str());
// Set pseudo-terminal flag (equivalent to ssh -tt)
int flag = 1;
ssh_options_set(session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &flag);
// Connect to server
int rc = ssh_connect(session);
if (rc != SSH_OK) {
std::cerr << "Error connecting to " << host << ": " << ssh_get_error(session) << std::endl;
ssh_free(session);
return -1;
}
// Authenticate with key
if (!key_path.empty()) {
rc = ssh_userauth_publickey_auto(session, nullptr, key_path.empty() ? nullptr : key_path.c_str());
if (rc != SSH_AUTH_SUCCESS) {
std::cerr << "Error authenticating with key: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
} else {
// Try default authentication methods
rc = ssh_userauth_publickey_auto(session, nullptr, nullptr);
if (rc != SSH_AUTH_SUCCESS) {
std::cerr << "Error authenticating: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
}
// Prepare command
std::ostringstream cmd_stream;
// Add environment variables
for (const auto& [key, value] : env) {
cmd_stream << "export " << key << "=\"" << value << "\"; ";
}
// Add cd command if working_directory is specified
if (!working_dir.empty()) {
cmd_stream << "cd \"" << working_dir << "\" && ";
}
// Add command and args
cmd_stream << command;
for (const auto& arg : args) {
cmd_stream << " " << arg;
}
std::string full_command = cmd_stream.str();
int exit_status = -1;
ssh_channel channel = ssh_channel_new(session);
if (channel == nullptr) {
std::cerr << "Error creating SSH channel: " << ssh_get_error(session) << std::endl;
ssh_disconnect(session);
ssh_free(session);
return -1;
}
rc = ssh_channel_open_session(channel);
if (rc != SSH_OK) {
std::cerr << "Error opening SSH session: " << ssh_get_error(session) << std::endl;
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
if (interactive) {
// Request a pseudo-terminal for interactive commands with specific term type
const char* term_type = "xterm-256color";
// Force PTY allocation first with basic settings
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) {
std::cerr << "Error requesting basic PTY: " << ssh_get_error(session) << std::endl;
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
// Then set the size and term type
rc = ssh_channel_request_pty_size(channel, term_type, 80, 24);
if (rc != SSH_OK) {
std::cerr << "Warning: Could not set PTY size, but continuing anyway" << std::endl;
}
// Set up terminal mode to be more raw (important for full-screen apps)
if (env.find("TERM") == env.end()) {
// Add TERM environment if not present
cmd_stream << "export TERM=\"" << term_type << "\"; ";
}
}
// Execute the command
rc = ssh_channel_request_exec(channel, full_command.c_str());
if (rc != SSH_OK) {
std::cerr << "Error executing command: " << ssh_get_error(session) << std::endl;
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
// Read command output
char buffer[4096];
int nbytes;
std::ostringstream oss;
// Read from stdout
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) {
if (capture_output) {
oss.write(buffer, nbytes);
}
if (!silent) {
std::cout.write(buffer, nbytes);
}
}
// Read from stderr if needed
while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 1)) > 0) {
if (capture_output) {
oss.write(buffer, nbytes);
}
if (!silent) {
std::cerr.write(buffer, nbytes);
}
}
if (capture_output && output) {
*output = oss.str();
}
// Get exit status
ssh_channel_send_eof(channel);
exit_status = ssh_channel_get_exit_status(channel);
// Clean up
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return exit_status;
}
} // namespace dropshell

View File

@ -1,148 +0,0 @@
#!/bin/bash
set -e
# Simple test script to verify that the runner works properly
echo "Testing Runner - Dropshell Command Execution Library"
echo "=================================================="
echo
# Ensure the binary is built
if [ ! -f "build/runner" ]; then
echo "Building the project first..."
./build.sh
fi
# Run a simple echo command
echo "Test 1: Basic command execution"
JSON=$(cat <<'EOF'
{"command":"echo","args":["Hello from runner test!"]}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command..."
build/runner "$BASE64"
if [ $? -eq 0 ]; then
echo "✅ Test 1 passed"
else
echo "❌ Test 1 failed"
exit 1
fi
echo
# Test with environment variables
echo "Test 2: Environment variables"
JSON=$(cat <<'EOF'
{"command":"bash","args":["-c","echo Value of TEST_VAR: $TEST_VAR"],"env":{"TEST_VAR":"This is a test environment variable"}}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command..."
build/runner "$BASE64"
if [ $? -eq 0 ]; then
echo "✅ Test 2 passed"
else
echo "❌ Test 2 failed"
exit 1
fi
echo
# Test silent mode
echo "Test 3: Silent mode"
JSON=$(cat <<'EOF'
{"command":"echo","args":["This should not be displayed"],"options":{"silent":true}}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command (no output expected)..."
OUTPUT=$(build/runner "$BASE64")
if [ $? -eq 0 ] && [ -z "$OUTPUT" ]; then
echo "✅ Test 3 passed"
else
echo "❌ Test 3 failed, unexpected output: '$OUTPUT'"
exit 1
fi
echo
# Test return code handling
echo "Test 4: Error return code"
JSON=$(cat <<'EOF'
{"command":"false"}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command (should fail)..."
set +e # Temporarily disable exiting on error
build/runner "$BASE64"
RC=$?
set -e # Re-enable exiting on error
if [ $RC -ne 0 ]; then
echo "✅ Test 4 passed (command correctly reported failure, exit code: $RC)"
else
echo "❌ Test 4 failed (command should have returned non-zero)"
exit 1
fi
echo
# Test working directory feature
echo "Test 5: Working directory"
JSON=$(cat <<'EOF'
{"command":"pwd","working_directory":"/tmp"}
EOF
)
BASE64=$(echo -n "$JSON" | base64 -w0)
echo "JSON: $JSON"
echo "Running command (should show /tmp)..."
OUTPUT=$(build/runner "$BASE64")
if [ $? -eq 0 ] && [[ "$OUTPUT" == "/tmp"* ]]; then
echo "✅ Test 5 passed (command ran in the correct directory)"
else
echo "❌ Test 5 failed, unexpected output: '$OUTPUT'"
exit 1
fi
echo
# Optional SSH test (disabled by default)
echo "Test 6: SSH to localhost (make sure SSH server is running and you can connect to localhost)"
if [ -f "examples/local_ssh.json" ]; then
echo "Using examples/local_ssh.json for the test..."
./run.sh examples/local_ssh.json || {
echo "❌ Test 6 failed"
echo "Note: This test requires SSH server running on localhost and proper key-based authentication."
echo "Check your SSH configuration or disable this test."
exit 1
}
echo "✅ Test 6 passed"
else
echo "❌ Test 6 failed: examples/local_ssh.json not found"
exit 1
fi
echo
echo "Test 7: SSH with working directory (make sure SSH server is running and you can connect to localhost)"
if [ -f "examples/ssh_working_dir.json" ]; then
echo "Using examples/ssh_working_dir.json for the test..."
OUTPUT=$(./run.sh examples/ssh_working_dir.json | grep "/tmp")
if [ $? -ne 0 ]; then
echo "❌ Test 7 failed, command did not run"
exit 1
fi
if [[ "$OUTPUT" == *"/tmp"* ]]; then
echo "✅ Test 7 passed (SSH command ran in the correct directory)"
else
echo "❌ Test 7 failed, directory was not changed correctly"
echo "Note: This test requires SSH server running on localhost and proper key-based authentication."
exit 1
fi
else
echo "❌ Test 7 failed: examples/ssh_working_dir.json not found"
exit 1
fi
echo
echo "All tests passed! 🎉"