816 lines
27 KiB
C
816 lines
27 KiB
C
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
#ifdef __linux__
|
|
#include <sys/statvfs.h>
|
|
#else
|
|
#include <sys/mount.h>
|
|
#endif
|
|
#include <unistd.h>
|
|
#include "ginxsom.h"
|
|
|
|
// Database path (consistent with main.c)
|
|
#define DB_PATH "db/ginxsom.db"
|
|
|
|
// Function declarations (moved from admin_api.h)
|
|
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
|
|
void handle_stats_api(void);
|
|
void handle_config_get_api(void);
|
|
void handle_config_put_api(void);
|
|
void handle_config_key_put_api(const char* key);
|
|
void handle_files_api(void);
|
|
void handle_health_api(void);
|
|
int authenticate_admin_request(const char* auth_header);
|
|
int is_admin_enabled(void);
|
|
int verify_admin_pubkey(const char* event_pubkey);
|
|
void send_json_response(int status, const char* json_content);
|
|
void send_json_error(int status, const char* error, const char* message);
|
|
int parse_query_params(const char* query_string, char params[][256], int max_params);
|
|
|
|
// Forward declarations for local utility functions
|
|
static int admin_nip94_get_origin(char* out, size_t out_size);
|
|
static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
|
|
static const char* admin_mime_to_extension(const char* mime_type);
|
|
|
|
// Local utility functions (from main.c but implemented here for admin API)
|
|
static int admin_nip94_get_origin(char* out, size_t out_size) {
|
|
if (!out || out_size == 0) {
|
|
return 0;
|
|
}
|
|
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
// Default on DB error
|
|
strncpy(out, "http://localhost:9001", out_size - 1);
|
|
out[out_size - 1] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (value) {
|
|
strncpy(out, value, out_size - 1);
|
|
out[out_size - 1] = '\0';
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
return 1;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
sqlite3_close(db);
|
|
|
|
// Default fallback
|
|
strncpy(out, "http://localhost:9001", out_size - 1);
|
|
out[out_size - 1] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
static void admin_nip94_build_blob_url(const char* origin, const char* sha256,
|
|
const char* mime_type, char* out, size_t out_size) {
|
|
if (!origin || !sha256 || !out || out_size == 0) {
|
|
return;
|
|
}
|
|
|
|
// Use local admin implementation for extension mapping
|
|
const char* extension = admin_mime_to_extension(mime_type);
|
|
snprintf(out, out_size, "%s/%s%s", origin, sha256, extension);
|
|
}
|
|
|
|
// Centralized MIME type to file extension mapping (from main.c)
|
|
static const char* admin_mime_to_extension(const char* mime_type) {
|
|
if (!mime_type) {
|
|
return ".bin";
|
|
}
|
|
|
|
if (strstr(mime_type, "image/jpeg")) {
|
|
return ".jpg";
|
|
} else if (strstr(mime_type, "image/webp")) {
|
|
return ".webp";
|
|
} else if (strstr(mime_type, "image/png")) {
|
|
return ".png";
|
|
} else if (strstr(mime_type, "image/gif")) {
|
|
return ".gif";
|
|
} else if (strstr(mime_type, "video/mp4")) {
|
|
return ".mp4";
|
|
} else if (strstr(mime_type, "video/webm")) {
|
|
return ".webm";
|
|
} else if (strstr(mime_type, "audio/mpeg")) {
|
|
return ".mp3";
|
|
} else if (strstr(mime_type, "audio/ogg")) {
|
|
return ".ogg";
|
|
} else if (strstr(mime_type, "text/plain")) {
|
|
return ".txt";
|
|
} else if (strstr(mime_type, "application/pdf")) {
|
|
return ".pdf";
|
|
} else {
|
|
return ".bin";
|
|
}
|
|
}
|
|
|
|
// Main API request handler
|
|
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) {
|
|
const char* path = uri + 4; // Skip "/api"
|
|
|
|
// Check if admin interface is enabled
|
|
if (!is_admin_enabled()) {
|
|
send_json_error(503, "admin_disabled", "Admin interface is disabled");
|
|
return;
|
|
}
|
|
|
|
// Authentication now handled by centralized validation system
|
|
// Health endpoint is exempt from authentication requirement
|
|
if (strcmp(path, "/health") != 0) {
|
|
if (!is_authenticated || !validated_pubkey) {
|
|
send_json_error(401, "admin_auth_required", "Valid admin authentication required");
|
|
return;
|
|
}
|
|
|
|
// Verify the authenticated pubkey has admin privileges
|
|
if (!verify_admin_pubkey(validated_pubkey)) {
|
|
send_json_error(403, "admin_forbidden", "Admin privileges required");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Route to appropriate handler
|
|
if (strcmp(method, "GET") == 0) {
|
|
if (strcmp(path, "/stats") == 0) {
|
|
handle_stats_api();
|
|
} else if (strcmp(path, "/config") == 0) {
|
|
handle_config_get_api();
|
|
} else if (strncmp(path, "/files", 6) == 0) {
|
|
handle_files_api();
|
|
} else if (strcmp(path, "/health") == 0) {
|
|
handle_health_api();
|
|
} else {
|
|
send_json_error(404, "not_found", "API endpoint not found");
|
|
}
|
|
} else if (strcmp(method, "PUT") == 0) {
|
|
if (strcmp(path, "/config") == 0) {
|
|
handle_config_put_api();
|
|
} else if (strncmp(path, "/config/", 8) == 0) {
|
|
const char* key = path + 8; // Skip "/config/"
|
|
if (strlen(key) > 0) {
|
|
handle_config_key_put_api(key);
|
|
} else {
|
|
send_json_error(400, "invalid_key", "Configuration key cannot be empty");
|
|
}
|
|
} else {
|
|
send_json_error(405, "method_not_allowed", "Method not allowed");
|
|
}
|
|
} else {
|
|
send_json_error(405, "method_not_allowed", "Method not allowed");
|
|
}
|
|
}
|
|
|
|
// Admin authentication functions
|
|
int authenticate_admin_request(const char* auth_header) {
|
|
if (!auth_header) {
|
|
return 0; // No auth header
|
|
}
|
|
|
|
// NOTE: Authentication now handled by centralized validation system in main.c
|
|
// This function is kept for compatibility but should receive validation results
|
|
// from the centralized system rather than performing duplicate validation
|
|
|
|
// TODO: Modify to accept validation results from centralized system
|
|
// For now, assume validation was successful if we reach here
|
|
// and extract pubkey from global context or parameters
|
|
|
|
return 0; // Temporarily disabled - needs integration with centralized system
|
|
}
|
|
|
|
int verify_admin_pubkey(const char* event_pubkey) {
|
|
if (!event_pubkey) {
|
|
return 0;
|
|
}
|
|
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc, is_admin = 0;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
return 0;
|
|
}
|
|
|
|
const char* sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* admin_pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
|
if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) {
|
|
is_admin = 1;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
sqlite3_close(db);
|
|
|
|
return is_admin;
|
|
}
|
|
|
|
int is_admin_enabled(void) {
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc, enabled = 0;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
return 0; // Default disabled if can't access DB
|
|
}
|
|
|
|
const char* sql = "SELECT value FROM config WHERE key = 'admin_enabled'";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 0);
|
|
enabled = (value && strcmp(value, "true") == 0) ? 1 : 0;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
sqlite3_close(db);
|
|
|
|
return enabled;
|
|
}
|
|
|
|
// Individual endpoint handlers
|
|
void handle_stats_api(void) {
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
send_json_error(500, "database_error", "Failed to open database");
|
|
return;
|
|
}
|
|
|
|
// Create consolidated statistics view if it doesn't exist
|
|
const char* create_view =
|
|
"CREATE VIEW IF NOT EXISTS storage_stats AS "
|
|
"SELECT "
|
|
" COUNT(*) as total_blobs, "
|
|
" SUM(size) as total_bytes, "
|
|
" AVG(size) as avg_blob_size, "
|
|
" COUNT(DISTINCT uploader_pubkey) as unique_uploaders, "
|
|
" MIN(uploaded_at) as first_upload, "
|
|
" MAX(uploaded_at) as last_upload "
|
|
"FROM blobs";
|
|
|
|
rc = sqlite3_exec(db, create_view, NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
sqlite3_close(db);
|
|
send_json_error(500, "database_error", "Failed to create stats view");
|
|
return;
|
|
}
|
|
|
|
// Query storage_stats view
|
|
const char* sql = "SELECT total_blobs, total_bytes, avg_blob_size, "
|
|
"unique_uploaders, first_upload, last_upload FROM storage_stats";
|
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
sqlite3_close(db);
|
|
send_json_error(500, "database_error", "Failed to prepare query");
|
|
return;
|
|
}
|
|
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON* data = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddItemToObject(response, "data", data);
|
|
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
int total_files = sqlite3_column_int(stmt, 0);
|
|
long long total_bytes = sqlite3_column_int64(stmt, 1);
|
|
double avg_size = sqlite3_column_double(stmt, 2);
|
|
int unique_uploaders = sqlite3_column_int(stmt, 3);
|
|
|
|
cJSON_AddNumberToObject(data, "total_files", total_files);
|
|
cJSON_AddNumberToObject(data, "total_bytes", (double)total_bytes);
|
|
cJSON_AddNumberToObject(data, "total_size_mb", (double)total_bytes / (1024 * 1024));
|
|
cJSON_AddNumberToObject(data, "unique_uploaders", unique_uploaders);
|
|
cJSON_AddNumberToObject(data, "first_upload", sqlite3_column_int64(stmt, 4));
|
|
cJSON_AddNumberToObject(data, "last_upload", sqlite3_column_int64(stmt, 5));
|
|
cJSON_AddNumberToObject(data, "avg_file_size", avg_size);
|
|
|
|
// Get file type distribution
|
|
sqlite3_stmt* type_stmt;
|
|
const char* type_sql = "SELECT type, COUNT(*) FROM blobs GROUP BY type ORDER BY COUNT(*) DESC LIMIT 5";
|
|
cJSON* file_types = cJSON_CreateObject();
|
|
|
|
rc = sqlite3_prepare_v2(db, type_sql, -1, &type_stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
while (sqlite3_step(type_stmt) == SQLITE_ROW) {
|
|
const char* type_name = (const char*)sqlite3_column_text(type_stmt, 0);
|
|
int count = sqlite3_column_int(type_stmt, 1);
|
|
cJSON_AddNumberToObject(file_types, type_name ? type_name : "unknown", count);
|
|
}
|
|
sqlite3_finalize(type_stmt);
|
|
}
|
|
cJSON_AddItemToObject(data, "file_types", file_types);
|
|
} else {
|
|
// No data - return zeros
|
|
cJSON_AddNumberToObject(data, "total_files", 0);
|
|
cJSON_AddNumberToObject(data, "total_bytes", 0);
|
|
cJSON_AddNumberToObject(data, "total_size_mb", 0.0);
|
|
cJSON_AddNumberToObject(data, "unique_uploaders", 0);
|
|
cJSON_AddNumberToObject(data, "avg_file_size", 0);
|
|
cJSON_AddItemToObject(data, "file_types", cJSON_CreateObject());
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
}
|
|
|
|
void handle_config_get_api(void) {
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
send_json_error(500, "database_error", "Failed to open database");
|
|
return;
|
|
}
|
|
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON* data = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddItemToObject(response, "data", data);
|
|
|
|
// Query all config settings
|
|
const char* sql = "SELECT key, value FROM config ORDER BY key";
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const char* key = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char* value = (const char*)sqlite3_column_text(stmt, 1);
|
|
if (key && value) {
|
|
cJSON_AddStringToObject(data, key, value);
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
sqlite3_close(db);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
}
|
|
|
|
void handle_config_put_api(void) {
|
|
// Read request body
|
|
const char* content_length_str = getenv("CONTENT_LENGTH");
|
|
if (!content_length_str) {
|
|
send_json_error(411, "length_required", "Content-Length header required");
|
|
return;
|
|
}
|
|
|
|
long content_length = atol(content_length_str);
|
|
if (content_length <= 0 || content_length > 4096) {
|
|
send_json_error(400, "invalid_content_length", "Invalid content length");
|
|
return;
|
|
}
|
|
|
|
char* json_body = malloc(content_length + 1);
|
|
if (!json_body) {
|
|
send_json_error(500, "memory_error", "Failed to allocate memory");
|
|
return;
|
|
}
|
|
|
|
size_t bytes_read = fread(json_body, 1, content_length, stdin);
|
|
if (bytes_read != (size_t)content_length) {
|
|
free(json_body);
|
|
send_json_error(400, "incomplete_body", "Failed to read complete request body");
|
|
return;
|
|
}
|
|
json_body[content_length] = '\0';
|
|
|
|
// Parse JSON
|
|
cJSON* config_data = cJSON_Parse(json_body);
|
|
if (!config_data) {
|
|
free(json_body);
|
|
send_json_error(400, "invalid_json", "Invalid JSON in request body");
|
|
return;
|
|
}
|
|
|
|
// Update database
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
|
if (rc) {
|
|
free(json_body);
|
|
cJSON_Delete(config_data);
|
|
send_json_error(500, "database_error", "Failed to open database");
|
|
return;
|
|
}
|
|
|
|
// Collect updated keys for response
|
|
cJSON* updated_keys = cJSON_CreateArray();
|
|
|
|
// Update each config value
|
|
const char* update_sql = "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)";
|
|
|
|
cJSON* item = NULL;
|
|
cJSON_ArrayForEach(item, config_data) {
|
|
if (cJSON_IsString(item) && item->string) {
|
|
rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, item->string, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(item), -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 3, time(NULL));
|
|
|
|
rc = sqlite3_step(stmt);
|
|
if (rc == SQLITE_DONE) {
|
|
cJSON_AddItemToArray(updated_keys, cJSON_CreateString(item->string));
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
}
|
|
}
|
|
|
|
free(json_body);
|
|
cJSON_Delete(config_data);
|
|
sqlite3_close(db);
|
|
|
|
// Send response
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddStringToObject(response, "message", "Configuration updated successfully");
|
|
cJSON_AddItemToObject(response, "updated_keys", updated_keys);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
|
|
// Force cache refresh after configuration update
|
|
nostr_request_validator_force_cache_refresh();
|
|
}
|
|
|
|
void handle_config_key_put_api(const char* key) {
|
|
if (!key || strlen(key) == 0) {
|
|
send_json_error(400, "invalid_key", "Configuration key cannot be empty");
|
|
return;
|
|
}
|
|
|
|
// Read request body
|
|
const char* content_length_str = getenv("CONTENT_LENGTH");
|
|
if (!content_length_str) {
|
|
send_json_error(411, "length_required", "Content-Length header required");
|
|
return;
|
|
}
|
|
|
|
long content_length = atol(content_length_str);
|
|
if (content_length <= 0 || content_length > 4096) {
|
|
send_json_error(400, "invalid_content_length", "Invalid content length");
|
|
return;
|
|
}
|
|
|
|
char* json_body = malloc(content_length + 1);
|
|
if (!json_body) {
|
|
send_json_error(500, "memory_error", "Failed to allocate memory");
|
|
return;
|
|
}
|
|
|
|
size_t bytes_read = fread(json_body, 1, content_length, stdin);
|
|
if (bytes_read != (size_t)content_length) {
|
|
free(json_body);
|
|
send_json_error(400, "incomplete_body", "Failed to read complete request body");
|
|
return;
|
|
}
|
|
json_body[content_length] = '\0';
|
|
|
|
// Parse JSON - expect {"value": "..."}
|
|
cJSON* request_data = cJSON_Parse(json_body);
|
|
if (!request_data) {
|
|
free(json_body);
|
|
send_json_error(400, "invalid_json", "Invalid JSON in request body");
|
|
return;
|
|
}
|
|
|
|
cJSON* value_item = cJSON_GetObjectItem(request_data, "value");
|
|
if (!cJSON_IsString(value_item)) {
|
|
free(json_body);
|
|
cJSON_Delete(request_data);
|
|
send_json_error(400, "missing_value", "Request must contain 'value' field");
|
|
return;
|
|
}
|
|
|
|
const char* value = cJSON_GetStringValue(value_item);
|
|
if (!value) {
|
|
free(json_body);
|
|
cJSON_Delete(request_data);
|
|
send_json_error(400, "invalid_value", "Value must be a string");
|
|
return;
|
|
}
|
|
|
|
// Make a safe copy of the value string BEFORE deleting cJSON object
|
|
char safe_value[256];
|
|
strncpy(safe_value, value, sizeof(safe_value) - 1);
|
|
safe_value[sizeof(safe_value) - 1] = '\0';
|
|
|
|
// Update database
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
|
if (rc) {
|
|
free(json_body);
|
|
cJSON_Delete(request_data);
|
|
send_json_error(500, "database_error", "Failed to open database");
|
|
return;
|
|
}
|
|
|
|
// Update or insert the config value
|
|
const char* update_sql = "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)";
|
|
rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
free(json_body);
|
|
cJSON_Delete(request_data);
|
|
sqlite3_close(db);
|
|
send_json_error(500, "database_error", "Failed to prepare update statement");
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(stmt, 2, safe_value, -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 3, time(NULL));
|
|
|
|
rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
|
|
free(json_body);
|
|
cJSON_Delete(request_data);
|
|
|
|
if (rc != SQLITE_DONE) {
|
|
send_json_error(500, "database_error", "Failed to update configuration");
|
|
return;
|
|
}
|
|
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddStringToObject(response, "message", "Configuration updated successfully");
|
|
cJSON_AddStringToObject(response, "key", key);
|
|
cJSON_AddStringToObject(response, "value", safe_value);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
|
|
// Force cache refresh after configuration update
|
|
nostr_request_validator_force_cache_refresh();
|
|
}
|
|
|
|
void handle_files_api(void) {
|
|
// Parse query parameters for pagination
|
|
const char* query_string = getenv("QUERY_STRING");
|
|
int limit = 50;
|
|
int offset = 0;
|
|
|
|
if (query_string) {
|
|
char params[10][256];
|
|
int param_count = parse_query_params(query_string, params, 10);
|
|
|
|
for (int i = 0; i < param_count; i++) {
|
|
char* key = params[i];
|
|
char* value = strchr(key, '=');
|
|
if (value) {
|
|
*value++ = '\0';
|
|
if (strcmp(key, "limit") == 0) {
|
|
limit = atoi(value);
|
|
if (limit <= 0 || limit > 200) limit = 50;
|
|
} else if (strcmp(key, "offset") == 0) {
|
|
offset = atoi(value);
|
|
if (offset < 0) offset = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sqlite3* db;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc) {
|
|
send_json_error(500, "database_error", "Failed to open database");
|
|
return;
|
|
}
|
|
|
|
// Query recent files with pagination
|
|
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
|
|
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
|
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
sqlite3_close(db);
|
|
send_json_error(500, "database_error", "Failed to prepare query");
|
|
return;
|
|
}
|
|
|
|
sqlite3_bind_int(stmt, 1, limit);
|
|
sqlite3_bind_int(stmt, 2, offset);
|
|
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON* data = cJSON_CreateObject();
|
|
cJSON* files_array = cJSON_CreateArray();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddItemToObject(response, "data", data);
|
|
cJSON_AddItemToObject(data, "files", files_array);
|
|
cJSON_AddNumberToObject(data, "limit", limit);
|
|
cJSON_AddNumberToObject(data, "offset", offset);
|
|
|
|
int total_count = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
total_count++;
|
|
|
|
cJSON* file_obj = cJSON_CreateObject();
|
|
cJSON_AddItemToArray(files_array, file_obj);
|
|
|
|
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
|
|
const char* type = (const char*)sqlite3_column_text(stmt, 2);
|
|
const char* filename = (const char*)sqlite3_column_text(stmt, 5);
|
|
|
|
cJSON_AddStringToObject(file_obj, "sha256", sha256 ? sha256 : "");
|
|
cJSON_AddNumberToObject(file_obj, "size", sqlite3_column_int64(stmt, 1));
|
|
cJSON_AddStringToObject(file_obj, "type", type ? type : "");
|
|
cJSON_AddNumberToObject(file_obj, "uploaded_at", sqlite3_column_int64(stmt, 3));
|
|
|
|
const char* uploader = (const char*)sqlite3_column_text(stmt, 4);
|
|
cJSON_AddStringToObject(file_obj, "uploader_pubkey",
|
|
uploader ? uploader : "");
|
|
|
|
cJSON_AddStringToObject(file_obj, "filename",
|
|
filename ? filename : "");
|
|
|
|
// Build URL for file
|
|
char url[1024];
|
|
if (type && sha256) {
|
|
// Use local admin implementation for URL building
|
|
char origin_url[256];
|
|
admin_nip94_get_origin(origin_url, sizeof(origin_url));
|
|
admin_nip94_build_blob_url(origin_url, sha256, type, url, sizeof(url));
|
|
cJSON_AddStringToObject(file_obj, "url", url);
|
|
}
|
|
}
|
|
|
|
// Get total count for pagination info
|
|
const char* count_sql = "SELECT COUNT(*) FROM blobs";
|
|
sqlite3_stmt* count_stmt;
|
|
|
|
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
rc = sqlite3_step(count_stmt);
|
|
if (rc == SQLITE_ROW) {
|
|
int total = sqlite3_column_int(count_stmt, 0);
|
|
cJSON_AddNumberToObject(data, "total", total);
|
|
}
|
|
sqlite3_finalize(count_stmt);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
}
|
|
|
|
void handle_health_api(void) {
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON* data = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "success");
|
|
cJSON_AddItemToObject(response, "data", data);
|
|
|
|
// Check database connection
|
|
sqlite3* db;
|
|
int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
cJSON_AddStringToObject(data, "database", "connected");
|
|
sqlite3_close(db);
|
|
} else {
|
|
cJSON_AddStringToObject(data, "database", "disconnected");
|
|
}
|
|
|
|
// Check blob directory
|
|
struct stat st;
|
|
if (stat("blobs", &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
cJSON_AddStringToObject(data, "blob_directory", "accessible");
|
|
} else {
|
|
cJSON_AddStringToObject(data, "blob_directory", "inaccessible");
|
|
}
|
|
|
|
// Get disk usage
|
|
cJSON* disk_usage = cJSON_CreateObject();
|
|
struct statvfs vfs;
|
|
if (statvfs(".", &vfs) == 0) {
|
|
unsigned long long total_bytes = (unsigned long long)vfs.f_blocks * vfs.f_frsize;
|
|
unsigned long long free_bytes = (unsigned long long)vfs.f_bavail * vfs.f_frsize;
|
|
unsigned long long used_bytes = total_bytes - free_bytes;
|
|
double usage_percent = (double)used_bytes / (double)total_bytes * 100.0;
|
|
|
|
cJSON_AddNumberToObject(disk_usage, "total_bytes", (double)total_bytes);
|
|
cJSON_AddNumberToObject(disk_usage, "used_bytes", (double)used_bytes);
|
|
cJSON_AddNumberToObject(disk_usage, "available_bytes", (double)free_bytes);
|
|
cJSON_AddNumberToObject(disk_usage, "usage_percent", usage_percent);
|
|
}
|
|
cJSON_AddItemToObject(data, "disk_usage", disk_usage);
|
|
|
|
// Add server info
|
|
cJSON_AddNumberToObject(data, "server_time", (double)time(NULL));
|
|
cJSON_AddNumberToObject(data, "uptime", 0); // Would need to track process start time
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
send_json_response(200, response_str);
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
}
|
|
|
|
// Utility functions
|
|
void send_json_response(int status, const char* json_content) {
|
|
printf("Status: %d OK\r\n", status);
|
|
printf("Content-Type: application/json\r\n");
|
|
printf("Cache-Control: no-cache\r\n");
|
|
printf("\r\n");
|
|
printf("%s\n", json_content);
|
|
}
|
|
|
|
void send_json_error(int status, const char* error, const char* message) {
|
|
cJSON* response = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(response, "status", "error");
|
|
cJSON_AddStringToObject(response, "error", error);
|
|
cJSON_AddStringToObject(response, "message", message);
|
|
|
|
char* response_str = cJSON_PrintUnformatted(response);
|
|
printf("Status: %d %s\r\n",
|
|
status,
|
|
status == 400 ? "Bad Request" :
|
|
status == 401 ? "Unauthorized" :
|
|
status == 403 ? "Forbidden" :
|
|
status == 404 ? "Not Found" :
|
|
status == 500 ? "Internal Server Error" :
|
|
status == 503 ? "Service Unavailable" :
|
|
"Error");
|
|
printf("Content-Type: application/json\r\n");
|
|
printf("Cache-Control: no-cache\r\n");
|
|
printf("\r\n");
|
|
printf("%s\n", response_str);
|
|
|
|
free(response_str);
|
|
cJSON_Delete(response);
|
|
}
|
|
|
|
int parse_query_params(const char* query_string, char params[][256], int max_params) {
|
|
if (!query_string || !params) return 0;
|
|
|
|
size_t query_len = strlen(query_string);
|
|
char* query_copy = malloc(query_len + 1);
|
|
if (!query_copy) return 0;
|
|
memcpy(query_copy, query_string, query_len + 1);
|
|
|
|
int count = 0;
|
|
char* token = strtok(query_copy, "&");
|
|
|
|
while (token && count < max_params) {
|
|
if (strlen(token) >= sizeof(params[0])) {
|
|
token[sizeof(params[0]) - 1] = '\0';
|
|
}
|
|
strcpy(params[count], token);
|
|
count++;
|
|
token = strtok(NULL, "&");
|
|
}
|
|
|
|
free(query_copy);
|
|
return count;
|
|
} |