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

This commit is contained in:
Your Name 2025-05-10 12:43:25 +12:00
parent 2bcf6c530d
commit b5bc7b611d
20 changed files with 1502 additions and 0 deletions

28
runner/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Build directory
build/
cmake-build-*/
# IDE files
.idea/
.vscode/
*.swp
*~
# Compiled object files
*.o
*.obj
# Compiled dynamic libraries
*.so
*.dylib
*.dll
# Executables
*.exe
runner
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile

90
runner/CMakeLists.txt Normal file
View File

@ -0,0 +1,90 @@
cmake_minimum_required(VERSION 3.10)
project(runner VERSION 1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Include directories
include_directories(include)
# 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()
# 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)

212
runner/README.md Normal file
View File

@ -0,0 +1,212 @@
# Runner
Simple c++ demonstration program of the dropshell runner library.
use:
runner BASE64COMMAND
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
{
"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
},
"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)
}
}
```
If SSH information is provided, the command will be executed on the remote server.
## 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
```
This will install all required dependencies (cmake, g++, libssh-dev, jq).
##### Manual Installation
###### Ubuntu/Debian
```bash
sudo apt-get install cmake g++ libssh-dev jq
```
###### 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"]}'
```

78
runner/build.sh Executable file
View File

@ -0,0 +1,78 @@
#!/bin/bash
# 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"
exit 1
}
cd build
# 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"
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 .."
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"

View File

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

64
runner/examples/find_libssh.sh Executable file
View File

@ -0,0 +1,64 @@
#!/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

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

View File

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

View File

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

View File

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

View File

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

13
runner/include/base64.h Normal file
View File

@ -0,0 +1,13 @@
#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

98
runner/include/runner.h Normal file
View File

@ -0,0 +1,98 @@
#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 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::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 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::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 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::map<std::string, std::string>& env,
bool& silent,
bool& interactive
);
};
} // namespace dropshell
#endif // DROPSHELL_RUNNER_H

64
runner/install_deps.sh Executable file
View File

@ -0,0 +1,64 @@
#!/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

96
runner/minimal_test.sh Executable file
View File

@ -0,0 +1,96 @@
#!/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."

83
runner/run.sh Executable file
View File

@ -0,0 +1,83 @@
#!/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

62
runner/src/base64.cpp Normal file
View File

@ -0,0 +1,62 @@
#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;
}

42
runner/src/main.cpp Normal file
View File

@ -0,0 +1,42 @@
#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;
}

466
runner/src/runner.cpp Normal file
View File

@ -0,0 +1,466 @@
#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::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, env, silent, interactive);
} else {
exit_code = execute_local(command, args, 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::map<std::string, std::string> env;
bool silent, interactive;
if (!parse_json(run_json, command, args, env, silent, interactive)) {
return false;
}
int exit_code;
if (run_json.contains("ssh")) {
exit_code = execute_ssh(run_json["ssh"], command, args, env, silent, interactive, &output, true);
} else {
exit_code = execute_local(command, args, 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::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);
}
}
// 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::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);
}
}
// 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::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());
// 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 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
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) {
std::cerr << "Error requesting PTY: " << ssh_get_error(session) << std::endl;
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
return -1;
}
}
// 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

100
runner/test.sh Executable file
View File

@ -0,0 +1,100 @@
#!/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='{"command":"echo","args":["Hello from runner test!"]}'
BASE64=$(echo -n "$JSON" | base64)
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='{"command":"bash","args":["-c","echo Value of TEST_VAR: $TEST_VAR"],"env":{"TEST_VAR":"This is a test environment variable"}}'
BASE64=$(echo -n "$JSON" | base64)
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='{"command":"echo","args":["This should not be displayed"],"options":{"silent":true}}'
BASE64=$(echo -n "$JSON" | base64)
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='{"command":"false"}'
BASE64=$(echo -n "$JSON" | base64)
echo "JSON: $JSON"
echo "Running command (should fail)..."
build/runner "$BASE64" || true
RC=$?
if [ $RC -ne 0 ]; then
echo "✅ Test 4 passed (command correctly reported failure)"
else
echo "❌ Test 4 failed (command should have returned non-zero)"
exit 1
fi
echo
# Optional SSH test (disabled by default)
# Set ENABLE_SSH_TEST=1 to enable this test
if [ "${ENABLE_SSH_TEST}" = "1" ]; then
echo "Test 5: 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 5 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 5 passed"
else
echo "❌ Test 5 failed: examples/local_ssh.json not found"
exit 1
fi
echo
else
echo "Test 5: SSH test disabled (set ENABLE_SSH_TEST=1 to enable)"
echo
fi
echo "All tests passed! 🎉"