diff --git a/runner/CMakeLists.txt b/runner/CMakeLists.txt index df002a3..2b3661e 100644 --- a/runner/CMakeLists.txt +++ b/runner/CMakeLists.txt @@ -4,9 +4,14 @@ project(runner_demo) set(CMAKE_CXX_STANDARD 17) find_package(OpenSSL REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBSSH REQUIRED libssh) +include_directories(${LIBSSH_INCLUDE_DIRS}) +link_directories(${LIBSSH_LIBRARY_DIRS}) add_library(runner_lib runner.cpp) target_include_directories(runner_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(runner_lib ${LIBSSH_LIBRARIES}) add_executable(runner runner_demo.cpp) -target_link_libraries(runner runner_lib OpenSSL::SSL OpenSSL::Crypto) \ No newline at end of file +target_link_libraries(runner runner_lib OpenSSL::SSL OpenSSL::Crypto ${LIBSSH_LIBRARIES}) \ No newline at end of file diff --git a/runner/README.md b/runner/README.md index 8959991..9085a4d 100644 --- a/runner/README.md +++ b/runner/README.md @@ -3,9 +3,10 @@ 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. + runner + + +The exit code is that of the command run, or -1 if the command couldn't be run. @@ -39,45 +40,3 @@ If interactive is true, then an interactive session is created - e.g. for runnin 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 - -``` -{ - "command": "ls", - "args": ["-l", "/tmp"], - "working_dir": "/", - "env": {"FOO": "bar"}, - "silent": false, - "interactive": false -} -``` - -To use, encode the JSON as a single line, then base64 encode it: - -``` -echo -n '{"command":"ls","args":["-l","/tmp"],"working_dir":"/","env":{"FOO":"bar"},"silent":false,"interactive":false}' | base64 -``` - -Then run: - -``` -./build/runner -``` - diff --git a/runner/runner.cpp b/runner/runner.cpp index d513eb8..76c14cf 100644 --- a/runner/runner.cpp +++ b/runner/runner.cpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include +#include +#include namespace runner { @@ -19,89 +23,167 @@ int execute_cmd( std::string* output ) { if (sshinfo) { - // Build remote command string + ssh_session session = ssh_new(); + if (session == nullptr) { + if (output) *output = "Failed to create SSH session."; + return -1; + } + ssh_options_set(session, SSH_OPTIONS_HOST, sshinfo->host.c_str()); + if (!sshinfo->port.empty()) { + int port = std::stoi(sshinfo->port); + ssh_options_set(session, SSH_OPTIONS_PORT, &port); + } + if (!sshinfo->user.empty()) { + ssh_options_set(session, SSH_OPTIONS_USER, sshinfo->user.c_str()); + } + int rc = ssh_connect(session); + if (rc != SSH_OK) { + if (output) *output = std::string("SSH connection failed: ") + ssh_get_error(session); + ssh_free(session); + return -1; + } + // Try public key, then password if needed + rc = ssh_userauth_publickey_auto(session, nullptr, nullptr); + if (rc != SSH_AUTH_SUCCESS) { + // Try password from env["SSHPASS"] if present + auto it = env.find("SSHPASS"); + if (it != env.end()) { + rc = ssh_userauth_password(session, nullptr, it->second.c_str()); + } + } + if (rc != SSH_AUTH_SUCCESS) { + if (output) *output = std::string("SSH authentication failed: ") + ssh_get_error(session); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + ssh_channel channel = ssh_channel_new(session); + if (channel == nullptr) { + if (output) *output = "Failed to create SSH channel."; + ssh_disconnect(session); + ssh_free(session); + return -1; + } + rc = ssh_channel_open_session(channel); + if (rc != SSH_OK) { + if (output) *output = std::string("Failed to open SSH channel: ") + ssh_get_error(session); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } std::ostringstream remote_cmd; - // Set environment variables for (const auto& kv : env) { + if (kv.first == "SSHPASS") continue; remote_cmd << kv.first << "='" << kv.second << "' "; } - // Change working directory if (!working_dir.empty()) { remote_cmd << "cd '" << working_dir << "' && "; } - // Command and args remote_cmd << command; for (const auto& arg : args) { remote_cmd << " '" << arg << "'"; } std::string remote_cmd_str = remote_cmd.str(); - - // Build ssh command - std::vector ssh_argv = {"ssh"}; - if (!sshinfo->port.empty()) { - ssh_argv.push_back("-p"); - ssh_argv.push_back(sshinfo->port); - } - std::string userhost = sshinfo->user.empty() ? sshinfo->host : (sshinfo->user + "@" + sshinfo->host); - ssh_argv.push_back(userhost); - ssh_argv.push_back(remote_cmd_str); - - // Prepare for exec - std::vector argv; - for (auto& s : ssh_argv) argv.push_back(const_cast(s.c_str())); - argv.push_back(nullptr); - - int pipefd[2]; - bool use_pipe = output && !interactive; - if (use_pipe && pipe(pipefd) == -1) { - perror("pipe"); - return -1; - } - pid_t pid = fork(); - if (pid == -1) { - perror("fork"); - return -1; - } - if (pid == 0) { - // Child process - if (use_pipe) { - close(pipefd[0]); - dup2(pipefd[1], STDOUT_FILENO); - dup2(pipefd[1], STDERR_FILENO); - close(pipefd[1]); - } else if (silent && !interactive) { - int devnull = open("/dev/null", O_WRONLY); - dup2(devnull, STDOUT_FILENO); - dup2(devnull, STDERR_FILENO); - close(devnull); - } - if (!interactive) { - setsid(); - } - execvp("ssh", argv.data()); - perror("execvp ssh"); - exit(-1); - } else { - // Parent process - if (use_pipe) { - 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 { + if (interactive) { + rc = ssh_channel_request_pty(channel); + if (rc != SSH_OK) { + if (output) *output = std::string("Failed to request pty: ") + ssh_get_error(session); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); return -1; } + rc = ssh_channel_request_shell(channel); + if (rc != SSH_OK) { + if (output) *output = std::string("Failed to request shell: ") + ssh_get_error(session); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + // Set local terminal to raw mode + struct termios orig_termios, raw_termios; + tcgetattr(STDIN_FILENO, &orig_termios); + raw_termios = orig_termios; + cfmakeraw(&raw_termios); + tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios); + // If a command is provided, send it to the shell + if (!command.empty()) { + ssh_channel_write(channel, remote_cmd_str.c_str(), remote_cmd_str.size()); + ssh_channel_write(channel, "\n", 1); + } + // Forward input/output using select() + int maxfd = STDIN_FILENO > STDOUT_FILENO ? STDIN_FILENO : STDOUT_FILENO; + maxfd = maxfd > ssh_get_fd(session) ? maxfd : ssh_get_fd(session); + char buffer[4096]; + bool done = false; + while (!done) { + fd_set fds_read; + FD_ZERO(&fds_read); + FD_SET(STDIN_FILENO, &fds_read); + FD_SET(ssh_get_fd(session), &fds_read); + int ret = select(maxfd + 1, &fds_read, nullptr, nullptr, nullptr); + if (ret < 0) break; + // Read from stdin, send to channel + if (FD_ISSET(STDIN_FILENO, &fds_read)) { + ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer)); + if (n > 0) { + ssh_channel_write(channel, buffer, n); + } else { + ssh_channel_send_eof(channel); + done = true; + } + } + // Read from channel, write to stdout + if (FD_ISSET(ssh_get_fd(session), &fds_read)) { + int n = ssh_channel_read(channel, buffer, sizeof(buffer), 0); + if (n > 0) { + write(STDOUT_FILENO, buffer, n); + } else if (n == 0) { + done = true; + } + } + if (ssh_channel_is_closed(channel) || ssh_channel_is_eof(channel)) { + done = true; + } + } + // Restore terminal + tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios); + } else { + rc = ssh_channel_request_exec(channel, remote_cmd_str.c_str()); + if (rc != SSH_OK) { + if (output) *output = std::string("Failed to exec remote command: ") + ssh_get_error(session); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + if (output) { + std::ostringstream oss; + char buffer[4096]; + int nbytes; + while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) { + oss.write(buffer, nbytes); + } + *output = oss.str(); + } else if (!silent) { + char buffer[4096]; + int nbytes; + while ((nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0)) > 0) { + write(1, buffer, nbytes); + } + } } + ssh_channel_send_eof(channel); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return 0; } else { int pipefd[2]; bool use_pipe = output && !interactive; diff --git a/runner/runner_demo.cpp b/runner/runner_demo.cpp index 9f3fac6..5ace633 100644 --- a/runner/runner_demo.cpp +++ b/runner/runner_demo.cpp @@ -76,4 +76,5 @@ int main(int argc, char* argv[]) { ssh.port = "22"; runner::execute_cmd(command, args, working_dir, env, silent, interactive, &ssh); -} \ No newline at end of file +} +