This commit is contained in:
62
runner/src/base64.cpp
Normal file
62
runner/src/base64.cpp
Normal 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
42
runner/src/main.cpp
Normal 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
466
runner/src/runner.cpp
Normal 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
|
Reference in New Issue
Block a user