// Admin event handler for Kind 23458/23459 admin commands #include #include #include #include #include #include #include "ginxsom.h" // Forward declarations for nostr_core_lib functions int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len); int nostr_nip44_decrypt(const unsigned char* recipient_private_key, const unsigned char* sender_public_key, const char* encrypted_data, char* output, size_t output_size); int nostr_nip44_encrypt(const unsigned char* sender_private_key, const unsigned char* recipient_public_key, const char* plaintext, char* output, size_t output_size); cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t created_at); // Use global database path from main.c extern char g_db_path[]; // Forward declarations static int get_server_privkey(unsigned char* privkey_bytes); static int get_server_pubkey(char* pubkey_hex, size_t size); static int handle_config_query_command(cJSON* response_data); static int handle_query_view_command(cJSON* command_array, cJSON* response_data); static int send_admin_response_event(const char* admin_pubkey, const char* request_id, cJSON* response_data); static cJSON* parse_authorization_header(void); static int process_admin_event(cJSON* event); /** * Handle Kind 23458 admin command event * Supports two delivery methods: * 1. POST body with JSON event * 2. Authorization header with Nostr event */ void handle_admin_event_request(void) { cJSON* event = NULL; int should_free_event = 1; // First, try to get event from Authorization header event = parse_authorization_header(); // If not in header, try POST body if (!event) { const char* content_length_str = getenv("CONTENT_LENGTH"); if (!content_length_str) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Event required in POST body or Authorization header\"}\n"); return; } long content_length = atol(content_length_str); if (content_length <= 0 || content_length > 65536) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid content length\"}\n"); return; } char* json_body = malloc(content_length + 1); if (!json_body) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Memory allocation failed\"}\n"); return; } size_t bytes_read = fread(json_body, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(json_body); printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to read complete request body\"}\n"); return; } json_body[content_length] = '\0'; // Parse event JSON event = cJSON_Parse(json_body); // Debug: Log the received JSON app_log(LOG_DEBUG, "ADMIN_EVENT: Received POST body: %s", json_body); free(json_body); if (!event) { app_log(LOG_ERROR, "ADMIN_EVENT: Failed to parse JSON"); printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid JSON\"}\n"); return; } // Debug: Log parsed event char* event_str = cJSON_Print(event); if (event_str) { app_log(LOG_DEBUG, "ADMIN_EVENT: Parsed event: %s", event_str); free(event_str); } } // Process the event (handles validation, decryption, command execution, response) int result = process_admin_event(event); // Clean up if (should_free_event && event) { cJSON_Delete(event); } (void)result; // Result already handled by process_admin_event } /** * Parse Kind 23458 event from Authorization header * Format: Authorization: Nostr * Returns: cJSON event object or NULL if not present/invalid */ static cJSON* parse_authorization_header(void) { const char* auth_header = getenv("HTTP_AUTHORIZATION"); if (!auth_header) { return NULL; } // Check for "Nostr " prefix (case-insensitive) if (strncasecmp(auth_header, "Nostr ", 6) != 0) { return NULL; } // Skip "Nostr " prefix const char* base64_event = auth_header + 6; // Decode base64 (simple implementation - in production use proper base64 decoder) // For now, assume the event is JSON directly (not base64 encoded) // This matches the pattern from c-relay's admin interface cJSON* event = cJSON_Parse(base64_event); return event; } /** * Process a Kind 23458 admin event (from POST body or Authorization header) * Returns: 0 on success, -1 on error (error response already sent) */ static int process_admin_event(cJSON* event) { if (!event) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid event\"}\n"); return -1; } // Verify it's Kind 23458 cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || !cJSON_IsNumber(kind_obj) || (int)cJSON_GetNumberValue(kind_obj) != 23458) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Event must be Kind 23458\"}\n"); return -1; } // Get event ID for response correlation cJSON* id_obj = cJSON_GetObjectItem(event, "id"); if (!id_obj || !cJSON_IsString(id_obj)) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Event missing id\"}\n"); return -1; } const char* request_id = cJSON_GetStringValue(id_obj); // Get admin pubkey from event cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Event missing pubkey\"}\n"); return -1; } const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj); // Verify admin pubkey sqlite3* db; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Database error\"}\n"); return -1; } sqlite3_stmt* stmt; const char* sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; int is_admin = 0; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char* db_admin_pubkey = (const char*)sqlite3_column_text(stmt, 0); if (db_admin_pubkey && strcmp(admin_pubkey, db_admin_pubkey) == 0) { is_admin = 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); if (!is_admin) { printf("Status: 403 Forbidden\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Not authorized as admin\"}\n"); return -1; } // Get encrypted content cJSON* content_obj = cJSON_GetObjectItem(event, "content"); if (!content_obj || !cJSON_IsString(content_obj)) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Event missing content\"}\n"); return -1; } const char* encrypted_content = cJSON_GetStringValue(content_obj); // Get server private key for decryption unsigned char server_privkey[32]; if (get_server_privkey(server_privkey) != 0) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to get server private key\"}\n"); return -1; } // Convert admin pubkey to bytes unsigned char admin_pubkey_bytes[32]; if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid admin pubkey format\"}\n"); return -1; } // Decrypt content using NIP-44 (or use plaintext for testing) char decrypted_content[8192]; const char* content_to_parse = encrypted_content; // Check if content is already plaintext JSON (starts with '[') if (encrypted_content[0] != '[') { // Content is encrypted, decrypt it int decrypt_result = nostr_nip44_decrypt( server_privkey, admin_pubkey_bytes, encrypted_content, decrypted_content, sizeof(decrypted_content) ); if (decrypt_result != 0) { app_log(LOG_ERROR, "ADMIN_EVENT: Decryption failed with result: %d", decrypt_result); app_log(LOG_ERROR, "ADMIN_EVENT: Encrypted content: %s", encrypted_content); printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to decrypt content\"}\n"); return -1; } content_to_parse = decrypted_content; app_log(LOG_DEBUG, "ADMIN_EVENT: Decrypted content: %s", decrypted_content); } else { app_log(LOG_DEBUG, "ADMIN_EVENT: Using plaintext content (starts with '['): %s", encrypted_content); } // Parse command array (either decrypted or plaintext) app_log(LOG_DEBUG, "ADMIN_EVENT: Parsing command array from: %s", content_to_parse); cJSON* command_array = cJSON_Parse(content_to_parse); if (!command_array || !cJSON_IsArray(command_array)) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Decrypted content is not a valid command array\"}\n"); return -1; } // Get command type cJSON* command_type = cJSON_GetArrayItem(command_array, 0); if (!command_type || !cJSON_IsString(command_type)) { cJSON_Delete(command_array); printf("Status: 400 Bad Request\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid command format\"}\n"); return -1; } const char* cmd = cJSON_GetStringValue(command_type); // Create response data object cJSON* response_data = cJSON_CreateObject(); cJSON_AddStringToObject(response_data, "query_type", cmd); cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL)); // Handle command int result = -1; if (strcmp(cmd, "config_query") == 0) { app_log(LOG_DEBUG, "ADMIN_EVENT: Handling config_query command"); result = handle_config_query_command(response_data); app_log(LOG_DEBUG, "ADMIN_EVENT: config_query result: %d", result); } else if (strcmp(cmd, "query_view") == 0) { app_log(LOG_DEBUG, "ADMIN_EVENT: Handling query_view command"); result = handle_query_view_command(command_array, response_data); app_log(LOG_DEBUG, "ADMIN_EVENT: query_view result: %d", result); } else { app_log(LOG_WARN, "ADMIN_EVENT: Unknown command: %s", cmd); cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "Unknown command"); result = -1; } cJSON_Delete(command_array); if (result == 0) { app_log(LOG_DEBUG, "ADMIN_EVENT: Sending Kind 23459 response"); // Send Kind 23459 response int send_result = send_admin_response_event(admin_pubkey, request_id, response_data); app_log(LOG_DEBUG, "ADMIN_EVENT: Response sent with result: %d", send_result); return send_result; } else { app_log(LOG_ERROR, "ADMIN_EVENT: Command processing failed"); cJSON_Delete(response_data); printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Command processing failed\"}\n"); return -1; } } /** * Get server private key from database (stored in blossom_seckey table) */ static int get_server_privkey(unsigned char* privkey_bytes) { sqlite3* db; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_stmt* stmt; const char* sql = "SELECT seckey FROM blossom_seckey LIMIT 1"; int result = -1; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char* privkey_hex = (const char*)sqlite3_column_text(stmt, 0); if (privkey_hex && nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) == 0) { result = 0; } } sqlite3_finalize(stmt); } sqlite3_close(db); return result; } /** * Get server public key from database (stored in config table as blossom_pubkey) */ static int get_server_pubkey(char* pubkey_hex, size_t size) { sqlite3* db; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_stmt* stmt; const char* sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; int result = -1; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); if (pubkey) { strncpy(pubkey_hex, pubkey, size - 1); pubkey_hex[size - 1] = '\0'; result = 0; } } sqlite3_finalize(stmt); } sqlite3_close(db); return result; } /** * Handle config_query command - returns all config values */ static int handle_config_query_command(cJSON* response_data) { sqlite3* db; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "Database error"); return -1; } cJSON_AddStringToObject(response_data, "status", "success"); cJSON* data = cJSON_CreateObject(); // Query all config settings sqlite3_stmt* stmt; const char* sql = "SELECT key, value FROM config ORDER BY key"; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == 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); } cJSON_AddItemToObject(response_data, "data", data); sqlite3_close(db); return 0; } /** * Handle query_view command - returns data from a specified database view * Command format: ["query_view", "view_name"] */ static int handle_query_view_command(cJSON* command_array, cJSON* response_data) { app_log(LOG_DEBUG, "ADMIN_EVENT: handle_query_view_command called"); // Get view name from command array cJSON* view_name_obj = cJSON_GetArrayItem(command_array, 1); if (!view_name_obj || !cJSON_IsString(view_name_obj)) { app_log(LOG_ERROR, "ADMIN_EVENT: View name missing or not a string"); cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "View name required"); return -1; } const char* view_name = cJSON_GetStringValue(view_name_obj); app_log(LOG_DEBUG, "ADMIN_EVENT: Querying view: %s", view_name); // Validate view name (whitelist approach for security) const char* allowed_views[] = { "blob_overview", "blob_type_distribution", "blob_time_stats", "top_uploaders", NULL }; int view_allowed = 0; for (int i = 0; allowed_views[i] != NULL; i++) { if (strcmp(view_name, allowed_views[i]) == 0) { view_allowed = 1; break; } } if (!view_allowed) { cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "Invalid view name"); app_log(LOG_WARN, "ADMIN_EVENT: Attempted to query invalid view: %s", view_name); return -1; } app_log(LOG_DEBUG, "ADMIN_EVENT: View '%s' is allowed, opening database: %s", view_name, g_db_path); // Open database sqlite3* db; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "ADMIN_EVENT: Failed to open database: %s (error: %s)", g_db_path, sqlite3_errmsg(db)); cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "Database error"); return -1; } // Build SQL query char sql[256]; snprintf(sql, sizeof(sql), "SELECT * FROM %s", view_name); app_log(LOG_DEBUG, "ADMIN_EVENT: Executing SQL: %s", sql); sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { app_log(LOG_ERROR, "ADMIN_EVENT: Failed to prepare query: %s (error: %s)", sql, sqlite3_errmsg(db)); sqlite3_close(db); cJSON_AddStringToObject(response_data, "status", "error"); cJSON_AddStringToObject(response_data, "error", "Failed to prepare query"); return -1; } // Get column count and names int col_count = sqlite3_column_count(stmt); // Create results array cJSON* results = cJSON_CreateArray(); // Fetch all rows while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* row = cJSON_CreateObject(); for (int i = 0; i < col_count; i++) { const char* col_name = sqlite3_column_name(stmt, i); int col_type = sqlite3_column_type(stmt, i); switch (col_type) { case SQLITE_INTEGER: cJSON_AddNumberToObject(row, col_name, (double)sqlite3_column_int64(stmt, i)); break; case SQLITE_FLOAT: cJSON_AddNumberToObject(row, col_name, sqlite3_column_double(stmt, i)); break; case SQLITE_TEXT: cJSON_AddStringToObject(row, col_name, (const char*)sqlite3_column_text(stmt, i)); break; case SQLITE_NULL: cJSON_AddNullToObject(row, col_name); break; default: // For BLOB or unknown types, skip break; } } cJSON_AddItemToArray(results, row); } sqlite3_finalize(stmt); sqlite3_close(db); // Build response cJSON_AddStringToObject(response_data, "status", "success"); cJSON_AddStringToObject(response_data, "view_name", view_name); cJSON_AddItemToObject(response_data, "data", results); app_log(LOG_DEBUG, "ADMIN_EVENT: Query view '%s' returned %d rows", view_name, cJSON_GetArraySize(results)); return 0; } /** * Send Kind 23459 admin response event */ static int send_admin_response_event(const char* admin_pubkey, const char* request_id, cJSON* response_data) { // Get server keys unsigned char server_privkey[32]; char server_pubkey[65]; if (get_server_privkey(server_privkey) != 0 || get_server_pubkey(server_pubkey, sizeof(server_pubkey)) != 0) { cJSON_Delete(response_data); printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to get server keys\"}\n"); return -1; } // Convert response data to JSON string char* response_json = cJSON_PrintUnformatted(response_data); cJSON_Delete(response_data); if (!response_json) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to serialize response\"}\n"); return -1; } // Convert admin pubkey to bytes for encryption unsigned char admin_pubkey_bytes[32]; if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) { free(response_json); printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Invalid admin pubkey\"}\n"); return -1; } // Encrypt response using NIP-44 char encrypted_response[131072]; int encrypt_result = nostr_nip44_encrypt( server_privkey, admin_pubkey_bytes, response_json, encrypted_response, sizeof(encrypted_response) ); free(response_json); if (encrypt_result != 0) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to encrypt response\"}\n"); return -1; } // Create Kind 23459 response event cJSON* response_event = cJSON_CreateObject(); cJSON_AddStringToObject(response_event, "pubkey", server_pubkey); cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL)); cJSON_AddNumberToObject(response_event, "kind", 23459); cJSON_AddStringToObject(response_event, "content", encrypted_response); // Add tags cJSON* tags = cJSON_CreateArray(); // p tag for admin cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey)); cJSON_AddItemToArray(tags, p_tag); // e tag for request correlation cJSON* e_tag = cJSON_CreateArray(); cJSON_AddItemToArray(e_tag, cJSON_CreateString("e")); cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id)); cJSON_AddItemToArray(tags, e_tag); cJSON_AddItemToObject(response_event, "tags", tags); // Sign the event cJSON* signed_event = nostr_create_and_sign_event( 23459, encrypted_response, tags, server_privkey, time(NULL) ); cJSON_Delete(response_event); if (!signed_event) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to sign response event\"}\n"); return -1; } // Return the signed event as HTTP response char* event_json = cJSON_PrintUnformatted(signed_event); cJSON_Delete(signed_event); if (!event_json) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\"error\":\"Failed to serialize event\"}\n"); return -1; } printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n"); printf("Cache-Control: no-cache\r\n"); printf("\r\n"); printf("%s\n", event_json); free(event_json); return 0; }