516 lines
17 KiB
C
516 lines
17 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <curl/curl.h>
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
|
|
typedef struct {
|
|
char* data;
|
|
size_t len;
|
|
size_t cap;
|
|
size_t max_bytes;
|
|
int truncated;
|
|
} local_http_fetch_buffer_t;
|
|
|
|
static char* json_error_local(const char* msg) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) return NULL;
|
|
cJSON_AddBoolToObject(root, "success", 0);
|
|
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|
|
|
|
static cJSON* parse_args_local(const char* args_json) {
|
|
const char* raw = args_json ? args_json : "{}";
|
|
cJSON* args = cJSON_Parse(raw);
|
|
if (!args || !cJSON_IsObject(args)) {
|
|
cJSON_Delete(args);
|
|
return NULL;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
static int is_safe_relative_path_local(const char* path) {
|
|
if (!path || path[0] == '\0') return 0;
|
|
if (path[0] == '/') return 0;
|
|
if (strstr(path, "..") != NULL) return 0;
|
|
if (strchr(path, '\\') != NULL) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
|
|
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
|
|
if (!is_safe_relative_path_local(rel_path)) return -1;
|
|
|
|
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
|
|
? ctx->cfg->tools.shell.working_directory
|
|
: ".";
|
|
|
|
int n = 0;
|
|
if (strcmp(cwd, ".") == 0) {
|
|
n = snprintf(out, out_size, "%s", rel_path);
|
|
} else {
|
|
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
|
|
}
|
|
|
|
if (n < 0 || (size_t)n >= out_size) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static char* shell_quote_single_local(const char* in) {
|
|
if (!in) return NULL;
|
|
|
|
size_t len = strlen(in);
|
|
size_t extra = 2U;
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (in[i] == '\'') {
|
|
extra += 4U;
|
|
} else {
|
|
extra += 1U;
|
|
}
|
|
}
|
|
|
|
char* out = (char*)malloc(extra + 1U);
|
|
if (!out) return NULL;
|
|
|
|
size_t j = 0;
|
|
out[j++] = '\'';
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (in[i] == '\'') {
|
|
out[j++] = '\'';
|
|
out[j++] = '\\';
|
|
out[j++] = '\'';
|
|
out[j++] = '\'';
|
|
} else {
|
|
out[j++] = in[i];
|
|
}
|
|
}
|
|
out[j++] = '\'';
|
|
out[j] = '\0';
|
|
|
|
return out;
|
|
}
|
|
|
|
static size_t local_http_fetch_write_cb_local(void* contents, size_t size, size_t nmemb, void* userp) {
|
|
local_http_fetch_buffer_t* rb = (local_http_fetch_buffer_t*)userp;
|
|
size_t total = size * nmemb;
|
|
if (!rb || total == 0) return total;
|
|
|
|
if (rb->len >= rb->max_bytes) {
|
|
rb->truncated = 1;
|
|
return total;
|
|
}
|
|
|
|
size_t allowed = rb->max_bytes - rb->len;
|
|
size_t to_copy = total <= allowed ? total : allowed;
|
|
|
|
if (rb->len + to_copy + 1U > rb->cap) {
|
|
size_t new_cap = rb->cap == 0 ? 1024U : rb->cap;
|
|
while (new_cap < rb->len + to_copy + 1U) {
|
|
new_cap *= 2U;
|
|
}
|
|
char* bigger = (char*)realloc(rb->data, new_cap);
|
|
if (!bigger) return 0;
|
|
rb->data = bigger;
|
|
rb->cap = new_cap;
|
|
}
|
|
|
|
memcpy(rb->data + rb->len, contents, to_copy);
|
|
rb->len += to_copy;
|
|
rb->data[rb->len] = '\0';
|
|
|
|
if (to_copy < total) {
|
|
rb->truncated = 1;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
static const char* detect_ca_bundle_path_for_tools_local(void) {
|
|
const char* env = getenv("SSL_CERT_FILE");
|
|
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
|
return env;
|
|
}
|
|
|
|
static const char* candidates[] = {
|
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
"/etc/ssl/cert.pem",
|
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
"/etc/ssl/ca-bundle.pem"
|
|
};
|
|
|
|
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
|
if (access(candidates[i], R_OK) == 0) {
|
|
return candidates[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* url = cJSON_GetObjectItemCaseSensitive(args, "url");
|
|
cJSON* method = cJSON_GetObjectItemCaseSensitive(args, "method");
|
|
cJSON* headers = cJSON_GetObjectItemCaseSensitive(args, "headers");
|
|
cJSON* body = cJSON_GetObjectItemCaseSensitive(args, "body");
|
|
cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_seconds");
|
|
cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");
|
|
|
|
if (!url || !cJSON_IsString(url) || !url->valuestring || url->valuestring[0] == '\0') {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_http_fetch requires string url");
|
|
}
|
|
if (headers && !cJSON_IsArray(headers)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_http_fetch headers must be an array when provided");
|
|
}
|
|
if (body && !cJSON_IsString(body)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_http_fetch body must be a string when provided");
|
|
}
|
|
|
|
const char* method_str = (method && cJSON_IsString(method) && method->valuestring && method->valuestring[0] != '\0')
|
|
? method->valuestring
|
|
: "GET";
|
|
|
|
if (body && cJSON_IsString(body) && body->valuestring && strcasecmp(method_str, "GET") == 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_http_fetch GET requests cannot include body");
|
|
}
|
|
|
|
int default_timeout = ctx->cfg->tools.local_http_fetch_default_timeout_seconds > 0
|
|
? ctx->cfg->tools.local_http_fetch_default_timeout_seconds
|
|
: 20;
|
|
int max_timeout = ctx->cfg->tools.local_http_fetch_max_timeout_seconds > 0
|
|
? ctx->cfg->tools.local_http_fetch_max_timeout_seconds
|
|
: 120;
|
|
if (default_timeout > max_timeout) {
|
|
default_timeout = max_timeout;
|
|
}
|
|
|
|
int timeout_seconds = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : default_timeout;
|
|
if (timeout_seconds <= 0) timeout_seconds = default_timeout;
|
|
if (timeout_seconds > max_timeout) timeout_seconds = max_timeout;
|
|
|
|
int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
|
|
int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
|
|
if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;
|
|
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_http_fetch failed to initialize curl");
|
|
}
|
|
|
|
local_http_fetch_buffer_t rb;
|
|
memset(&rb, 0, sizeof(rb));
|
|
rb.max_bytes = (size_t)max_bytes;
|
|
|
|
struct curl_slist* req_headers = NULL;
|
|
req_headers = curl_slist_append(req_headers, "Accept: */*");
|
|
|
|
if (headers && cJSON_IsArray(headers)) {
|
|
int n = cJSON_GetArraySize(headers);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* h = cJSON_GetArrayItem(headers, i);
|
|
if (h && cJSON_IsString(h) && h->valuestring && h->valuestring[0] != '\0') {
|
|
req_headers = curl_slist_append(req_headers, h->valuestring);
|
|
}
|
|
}
|
|
}
|
|
|
|
curl_easy_setopt(curl, CURLOPT_URL, url->valuestring);
|
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout_seconds);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, local_http_fetch_write_cb_local);
|
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
|
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers);
|
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "didactyl/local_http_fetch");
|
|
|
|
const char* ca_bundle = detect_ca_bundle_path_for_tools_local();
|
|
if (ca_bundle) {
|
|
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
|
}
|
|
|
|
if (strcasecmp(method_str, "GET") == 0) {
|
|
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
|
} else if (strcasecmp(method_str, "POST") == 0) {
|
|
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
|
if (body && cJSON_IsString(body) && body->valuestring) {
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring));
|
|
}
|
|
} else {
|
|
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method_str);
|
|
if (body && cJSON_IsString(body) && body->valuestring) {
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring);
|
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring));
|
|
}
|
|
}
|
|
|
|
CURLcode res = curl_easy_perform(curl);
|
|
long status_code = 0;
|
|
char* content_type = NULL;
|
|
char* content_type_copy = NULL;
|
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code);
|
|
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
|
|
if (content_type && content_type[0] != '\0') {
|
|
content_type_copy = strdup(content_type);
|
|
}
|
|
|
|
curl_slist_free_all(req_headers);
|
|
curl_easy_cleanup(curl);
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(rb.data);
|
|
return NULL;
|
|
}
|
|
|
|
int http_ok = (status_code >= 200 && status_code < 300) ? 1 : 0;
|
|
int success = (res == CURLE_OK && http_ok) ? 1 : 0;
|
|
|
|
cJSON_AddBoolToObject(out, "success", success);
|
|
cJSON_AddStringToObject(out, "url", url->valuestring);
|
|
cJSON_AddStringToObject(out, "method", method_str);
|
|
cJSON_AddNumberToObject(out, "status_code", status_code);
|
|
cJSON_AddBoolToObject(out, "http_ok", http_ok);
|
|
cJSON_AddBoolToObject(out, "truncated", rb.truncated ? 1 : 0);
|
|
cJSON_AddNumberToObject(out, "bytes_received", (double)rb.len);
|
|
|
|
if (content_type_copy && content_type_copy[0] != '\0') {
|
|
cJSON_AddStringToObject(out, "content_type", content_type_copy);
|
|
}
|
|
|
|
if (res != CURLE_OK) {
|
|
cJSON_AddStringToObject(out, "curl_error", curl_easy_strerror(res));
|
|
}
|
|
|
|
cJSON_AddStringToObject(out, "body", rb.data ? rb.data : "");
|
|
|
|
free(rb.data);
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
cJSON_Delete(args);
|
|
free(content_type_copy);
|
|
return json;
|
|
}
|
|
|
|
char* execute_local_shell_exec(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
if (!ctx->cfg->tools.shell.enabled) return json_error_local("shell tool disabled");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command");
|
|
if (!command || !cJSON_IsString(command) || !command->valuestring || command->valuestring[0] == '\0') {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_shell_exec requires string command");
|
|
}
|
|
|
|
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
|
|
? ctx->cfg->tools.shell.working_directory
|
|
: ".";
|
|
int timeout_s = ctx->cfg->tools.shell.timeout_seconds > 0 ? ctx->cfg->tools.shell.timeout_seconds : 30;
|
|
|
|
char* quoted_cwd = shell_quote_single_local(cwd);
|
|
char* quoted_cmd = shell_quote_single_local(command->valuestring);
|
|
cJSON_Delete(args);
|
|
|
|
if (!quoted_cwd || !quoted_cmd) {
|
|
free(quoted_cwd);
|
|
free(quoted_cmd);
|
|
return json_error_local("allocation failure");
|
|
}
|
|
|
|
int needed = snprintf(NULL,
|
|
0,
|
|
"cd %s && timeout %ds sh -lc %s 2>&1",
|
|
quoted_cwd,
|
|
timeout_s,
|
|
quoted_cmd);
|
|
if (needed <= 0) {
|
|
free(quoted_cwd);
|
|
free(quoted_cmd);
|
|
return json_error_local("failed to build shell command");
|
|
}
|
|
|
|
char* cmd = (char*)malloc((size_t)needed + 1U);
|
|
if (!cmd) {
|
|
free(quoted_cwd);
|
|
free(quoted_cmd);
|
|
return json_error_local("allocation failure");
|
|
}
|
|
|
|
snprintf(cmd,
|
|
(size_t)needed + 1U,
|
|
"cd %s && timeout %ds sh -lc %s 2>&1",
|
|
quoted_cwd,
|
|
timeout_s,
|
|
quoted_cmd);
|
|
|
|
free(quoted_cwd);
|
|
free(quoted_cmd);
|
|
|
|
FILE* fp = popen(cmd, "r");
|
|
free(cmd);
|
|
if (!fp) return json_error_local("failed to execute command");
|
|
|
|
int max_bytes = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
|
|
char* output = (char*)calloc((size_t)max_bytes + 1U, 1U);
|
|
if (!output) {
|
|
pclose(fp);
|
|
return json_error_local("allocation failure");
|
|
}
|
|
|
|
size_t used = 0;
|
|
while (!feof(fp) && used < (size_t)max_bytes) {
|
|
size_t n = fread(output + used, 1, (size_t)max_bytes - used, fp);
|
|
used += n;
|
|
if (n == 0) break;
|
|
}
|
|
|
|
int raw_status = pclose(fp);
|
|
int exit_status = raw_status;
|
|
if (raw_status != -1) {
|
|
if (WIFEXITED(raw_status)) {
|
|
exit_status = WEXITSTATUS(raw_status);
|
|
} else if (WIFSIGNALED(raw_status)) {
|
|
exit_status = 128 + WTERMSIG(raw_status);
|
|
}
|
|
}
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(output);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", exit_status == 0 ? 1 : 0);
|
|
cJSON_AddNumberToObject(out, "exit_status", exit_status);
|
|
cJSON_AddStringToObject(out, "output", output);
|
|
free(output);
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_local_file_read(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
|
|
cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes");
|
|
if (!path || !cJSON_IsString(path) || !path->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_file_read requires string path");
|
|
}
|
|
|
|
int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536;
|
|
int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
|
|
if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;
|
|
|
|
char file_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_file_read path is not allowed");
|
|
}
|
|
|
|
FILE* fp = fopen(file_path, "rb");
|
|
cJSON_Delete(args);
|
|
if (!fp) return json_error_local("local_file_read failed to open file");
|
|
|
|
char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U);
|
|
if (!buf) {
|
|
fclose(fp);
|
|
return json_error_local("allocation failure");
|
|
}
|
|
|
|
size_t n = fread(buf, 1, (size_t)max_bytes, fp);
|
|
int truncated = !feof(fp) ? 1 : 0;
|
|
fclose(fp);
|
|
buf[n] = '\0';
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "path", file_path);
|
|
cJSON_AddNumberToObject(out, "bytes_read", (double)n);
|
|
cJSON_AddBoolToObject(out, "truncated", truncated);
|
|
cJSON_AddStringToObject(out, "content", buf);
|
|
free(buf);
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_local_file_write(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path");
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
|
|
cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append");
|
|
if (!path || !cJSON_IsString(path) || !path->valuestring ||
|
|
!content || !cJSON_IsString(content) || !content->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_file_write requires string path and content");
|
|
}
|
|
|
|
char file_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("local_file_write path is not allowed");
|
|
}
|
|
|
|
const char* content_str = content->valuestring;
|
|
size_t len = strlen(content_str);
|
|
int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0;
|
|
|
|
FILE* fp = fopen(file_path, do_append ? "ab" : "wb");
|
|
cJSON_Delete(args);
|
|
if (!fp) return json_error_local("local_file_write failed to open file");
|
|
|
|
size_t n = fwrite(content_str, 1, len, fp);
|
|
fclose(fp);
|
|
if (n != len) return json_error_local("local_file_write failed to write all bytes");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "path", file_path);
|
|
cJSON_AddNumberToObject(out, "bytes_written", (double)n);
|
|
cJSON_AddBoolToObject(out, "append", do_append);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|