305 lines
12 KiB
C
305 lines
12 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "../../nostr_core_lib/nostr_core/blossom_client.h"
|
|
|
|
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 = (strcmp(cwd, ".") == 0) ? snprintf(out, out_size, "%s", rel_path)
|
|
: snprintf(out, out_size, "%s/%s", cwd, rel_path);
|
|
if (n < 0 || (size_t)n >= out_size) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int is_https_server_local(const char* server) {
|
|
return server && strncmp(server, "https://", 8) == 0;
|
|
}
|
|
|
|
static int file_exists_local(const char* path) {
|
|
if (!path || path[0] == '\0') return 0;
|
|
return access(path, F_OK) == 0;
|
|
}
|
|
|
|
static int file_size_local(const char* path, size_t* out_size) {
|
|
if (!path || !out_size) return -1;
|
|
struct stat st;
|
|
if (stat(path, &st) != 0) return -1;
|
|
if (st.st_size < 0) return -1;
|
|
*out_size = (size_t)st.st_size;
|
|
return 0;
|
|
}
|
|
|
|
static void descriptor_to_json(cJSON* obj, const blossom_blob_descriptor_t* d) {
|
|
if (!obj || !d) return;
|
|
cJSON_AddStringToObject(obj, "sha256", d->sha256);
|
|
cJSON_AddStringToObject(obj, "url", d->url);
|
|
cJSON_AddNumberToObject(obj, "size", d->size);
|
|
cJSON_AddStringToObject(obj, "content_type", d->content_type);
|
|
cJSON_AddNumberToObject(obj, "created", d->created);
|
|
}
|
|
|
|
char* execute_blossom_upload(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* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
|
cJSON* file_path = cJSON_GetObjectItemCaseSensitive(args, "file_path");
|
|
cJSON* content_type = cJSON_GetObjectItemCaseSensitive(args, "content_type");
|
|
|
|
if (!server || !cJSON_IsString(server) || !server->valuestring || server->valuestring[0] == '\0' ||
|
|
!file_path || !cJSON_IsString(file_path) || !file_path->valuestring || file_path->valuestring[0] == '\0') {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_upload requires server and file_path");
|
|
}
|
|
|
|
if (!is_https_server_local(server->valuestring)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_upload requires an https:// server URL");
|
|
}
|
|
|
|
char full_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, file_path->valuestring, full_path, sizeof(full_path)) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_upload file_path is not allowed");
|
|
}
|
|
|
|
size_t input_size = 0;
|
|
if (file_size_local(full_path, &input_size) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_upload could not stat file_path");
|
|
}
|
|
|
|
int upload_max = ctx->cfg->tools.blossom_max_upload_bytes > 0 ? ctx->cfg->tools.blossom_max_upload_bytes : (16 * 1024 * 1024);
|
|
if (input_size > (size_t)upload_max) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_upload file exceeds configured max upload size");
|
|
}
|
|
|
|
blossom_blob_descriptor_t d;
|
|
int rc = blossom_upload_file(server->valuestring,
|
|
full_path,
|
|
(content_type && cJSON_IsString(content_type) && content_type->valuestring) ? content_type->valuestring : NULL,
|
|
ctx->cfg->keys.private_key,
|
|
30,
|
|
&d);
|
|
cJSON_Delete(args);
|
|
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_upload failed");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "server", server->valuestring);
|
|
descriptor_to_json(out, &d);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_blossom_download(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* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
|
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
|
cJSON* output_path = cJSON_GetObjectItemCaseSensitive(args, "output_path");
|
|
|
|
if (!server || !cJSON_IsString(server) || !server->valuestring || server->valuestring[0] == '\0' ||
|
|
!sha || !cJSON_IsString(sha) || !sha->valuestring ||
|
|
!output_path || !cJSON_IsString(output_path) || !output_path->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_download requires server, sha256, and output_path");
|
|
}
|
|
|
|
if (!is_https_server_local(server->valuestring)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_download requires an https:// server URL");
|
|
}
|
|
|
|
char full_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, output_path->valuestring, full_path, sizeof(full_path)) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_download output_path is not allowed");
|
|
}
|
|
|
|
cJSON* overwrite = cJSON_GetObjectItemCaseSensitive(args, "overwrite");
|
|
int allow_overwrite = (overwrite && cJSON_IsBool(overwrite) && cJSON_IsTrue(overwrite)) ? 1 : 0;
|
|
if (!allow_overwrite && file_exists_local(full_path)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_download output_path exists (set overwrite=true to replace)");
|
|
}
|
|
|
|
int download_max = ctx->cfg->tools.blossom_max_download_bytes > 0 ? ctx->cfg->tools.blossom_max_download_bytes : (16 * 1024 * 1024);
|
|
blossom_blob_descriptor_t d;
|
|
int rc = blossom_download_to_file(server->valuestring, sha->valuestring, full_path, 30, (size_t)download_max, &d);
|
|
cJSON_Delete(args);
|
|
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_download failed");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "output_path", full_path);
|
|
descriptor_to_json(out, &d);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_blossom_head(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* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
|
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
|
if (!server || !cJSON_IsString(server) || !server->valuestring || !sha || !cJSON_IsString(sha) || !sha->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_head requires server and sha256");
|
|
}
|
|
|
|
if (!is_https_server_local(server->valuestring)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_head requires an https:// server URL");
|
|
}
|
|
|
|
blossom_blob_descriptor_t d;
|
|
int rc = blossom_head(server->valuestring, sha->valuestring, 15, &d);
|
|
cJSON_Delete(args);
|
|
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_head failed");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddBoolToObject(out, "exists", 1);
|
|
descriptor_to_json(out, &d);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_blossom_delete(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* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
|
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
|
if (!server || !cJSON_IsString(server) || !server->valuestring || !sha || !cJSON_IsString(sha) || !sha->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_delete requires server and sha256");
|
|
}
|
|
|
|
if (!is_https_server_local(server->valuestring)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_delete requires an https:// server URL");
|
|
}
|
|
|
|
int rc = blossom_delete(server->valuestring, sha->valuestring, ctx->cfg->keys.private_key, 15);
|
|
cJSON_Delete(args);
|
|
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_delete failed");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddBoolToObject(out, "deleted", 1);
|
|
cJSON_AddStringToObject(out, "sha256", sha->valuestring);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|
|
|
|
char* execute_blossom_list(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* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
|
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
|
|
if (!server || !cJSON_IsString(server) || !server->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_list requires server");
|
|
}
|
|
|
|
if (!is_https_server_local(server->valuestring)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("blossom_list requires an https:// server URL");
|
|
}
|
|
|
|
const char* pk = (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0')
|
|
? pubkey->valuestring
|
|
: ctx->cfg->keys.public_key_hex;
|
|
|
|
blossom_blob_descriptor_t* items = NULL;
|
|
int count = 0;
|
|
int rc = blossom_list(server->valuestring, pk, 20, &items, &count);
|
|
cJSON_Delete(args);
|
|
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_list failed");
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
cJSON* arr = cJSON_CreateArray();
|
|
if (!out || !arr) {
|
|
free(items);
|
|
cJSON_Delete(out);
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "pubkey", pk);
|
|
cJSON_AddNumberToObject(out, "count", count);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
cJSON* it = cJSON_CreateObject();
|
|
if (!it) continue;
|
|
descriptor_to_json(it, &items[i]);
|
|
cJSON_AddItemToArray(arr, it);
|
|
}
|
|
free(items);
|
|
|
|
cJSON_AddItemToObject(out, "blobs", arr);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|