diff --git a/runner/.gitignore b/runner/.gitignore new file mode 100644 index 0000000..b1fc6d6 --- /dev/null +++ b/runner/.gitignore @@ -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 \ No newline at end of file diff --git a/runner/CMakeLists.txt b/runner/CMakeLists.txt new file mode 100644 index 0000000..712b394 --- /dev/null +++ b/runner/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/runner/README.md b/runner/README.md new file mode 100644 index 0000000..8312ec7 --- /dev/null +++ b/runner/README.md @@ -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"]}' +``` diff --git a/runner/build.sh b/runner/build.sh new file mode 100755 index 0000000..caf2db4 --- /dev/null +++ b/runner/build.sh @@ -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" \ No newline at end of file diff --git a/runner/examples/env_vars.json b/runner/examples/env_vars.json new file mode 100644 index 0000000..46e3803 --- /dev/null +++ b/runner/examples/env_vars.json @@ -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"}} diff --git a/runner/examples/find_libssh.sh b/runner/examples/find_libssh.sh new file mode 100755 index 0000000..244edeb --- /dev/null +++ b/runner/examples/find_libssh.sh @@ -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 \ No newline at end of file diff --git a/runner/examples/interactive_mode.json b/runner/examples/interactive_mode.json new file mode 100644 index 0000000..abcb98d --- /dev/null +++ b/runner/examples/interactive_mode.json @@ -0,0 +1 @@ +{"command":"bash","options":{"interactive":true}} diff --git a/runner/examples/local_command.json b/runner/examples/local_command.json new file mode 100644 index 0000000..502ab49 --- /dev/null +++ b/runner/examples/local_command.json @@ -0,0 +1 @@ +{"command":"ls","args":["-la"],"options":{"silent":false}} diff --git a/runner/examples/local_ssh.json b/runner/examples/local_ssh.json new file mode 100644 index 0000000..8a55940 --- /dev/null +++ b/runner/examples/local_ssh.json @@ -0,0 +1 @@ +{"ssh":{"host":"localhost","key":"auto"},"command":"echo","args":["Hello from SSH on localhost!"]} diff --git a/runner/examples/silent_mode.json b/runner/examples/silent_mode.json new file mode 100644 index 0000000..700c082 --- /dev/null +++ b/runner/examples/silent_mode.json @@ -0,0 +1 @@ +{"command":"ls","args":["-la"],"options":{"silent":true}} diff --git a/runner/examples/ssh_command.json b/runner/examples/ssh_command.json new file mode 100644 index 0000000..abbd723 --- /dev/null +++ b/runner/examples/ssh_command.json @@ -0,0 +1 @@ +{"ssh":{"host":"localhost","port":22,"user":"USERNAME","key":"auto"},"command":"hostname","options":{"interactive":false}} diff --git a/runner/include/base64.h b/runner/include/base64.h new file mode 100644 index 0000000..d4c104f --- /dev/null +++ b/runner/include/base64.h @@ -0,0 +1,13 @@ +#ifndef BASE64_H +#define BASE64_H + +#include + +/** + * 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 \ No newline at end of file diff --git a/runner/include/runner.h b/runner/include/runner.h new file mode 100644 index 0000000..73158b2 --- /dev/null +++ b/runner/include/runner.h @@ -0,0 +1,98 @@ +#ifndef DROPSHELL_RUNNER_H +#define DROPSHELL_RUNNER_H + +#include +#include +#include +#include + +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& args, + const std::map& 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& args, + const std::map& 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& args, + std::map& env, + bool& silent, + bool& interactive + ); +}; + +} // namespace dropshell + +#endif // DROPSHELL_RUNNER_H \ No newline at end of file diff --git a/runner/install_deps.sh b/runner/install_deps.sh new file mode 100755 index 0000000..dfc8024 --- /dev/null +++ b/runner/install_deps.sh @@ -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 \ No newline at end of file diff --git a/runner/minimal_test.sh b/runner/minimal_test.sh new file mode 100755 index 0000000..353bc4b --- /dev/null +++ b/runner/minimal_test.sh @@ -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 +#include +#include + +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." \ No newline at end of file diff --git a/runner/run.sh b/runner/run.sh new file mode 100755 index 0000000..b7e89ff --- /dev/null +++ b/runner/run.sh @@ -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 " + 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 \ No newline at end of file diff --git a/runner/src/base64.cpp b/runner/src/base64.cpp new file mode 100644 index 0000000..cc1d234 --- /dev/null +++ b/runner/src/base64.cpp @@ -0,0 +1,62 @@ +#include "base64.h" +#include +#include + +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; +} \ No newline at end of file diff --git a/runner/src/main.cpp b/runner/src/main.cpp new file mode 100644 index 0000000..7425634 --- /dev/null +++ b/runner/src/main.cpp @@ -0,0 +1,42 @@ +#include "runner.h" +#include "base64.h" +#include +#include + +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; +} \ No newline at end of file diff --git a/runner/src/runner.cpp b/runner/src/runner.cpp new file mode 100644 index 0000000..8222651 --- /dev/null +++ b/runner/src/runner.cpp @@ -0,0 +1,466 @@ +#include "runner.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dropshell { + +bool Runner::run(const nlohmann::json& run_json) { + std::string command; + std::vector args; + std::map 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 args; + std::map 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& args, + std::map& 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& args, + const std::map& 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 c_args; + c_args.push_back(const_cast(command.c_str())); + for (const auto& arg : args) { + c_args.push_back(const_cast(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 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 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& args, + const std::map& 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 \ No newline at end of file diff --git a/runner/test.sh b/runner/test.sh new file mode 100755 index 0000000..c20f2df --- /dev/null +++ b/runner/test.sh @@ -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! 🎉" \ No newline at end of file