v0.1.15 - Fix config page display and rebrand admin UI from relay to Blossom terminology

This commit is contained in:
Your Name
2025-12-12 13:12:46 -04:00
parent 3da7b62a95
commit d0bf851e86
25 changed files with 102416 additions and 1635 deletions

View File

@@ -1,8 +1,8 @@
/*
* Ginxsom Admin Commands Interface
*
* Handles encrypted admin commands sent via Kind 23456 events
* and generates encrypted responses as Kind 23457 events.
* Handles encrypted admin commands sent via Kind 23458 events
* and generates encrypted responses as Kind 23459 events.
*/
#ifndef ADMIN_COMMANDS_H

View File

@@ -1,4 +1,4 @@
// Admin event handler for Kind 23456/23457 admin commands
// Admin event handler for Kind 23458/23459 admin commands
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -27,90 +27,161 @@ extern char g_db_path[];
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 send_admin_response_event(const char* admin_pubkey, const char* request_id,
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 23456 admin command event
* Expects POST to /api/admin with JSON body containing the 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) {
// Read request body
const char* content_length_str = getenv("CONTENT_LENGTH");
if (!content_length_str) {
printf("Status: 411 Length Required\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Content-Length header required\"}\n");
return;
}
cJSON* event = NULL;
int should_free_event = 1;
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
cJSON* event = cJSON_Parse(json_body);
free(json_body);
// 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 <base64-encoded-event-json>
* 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 JSON\"}\n");
return;
printf("{\"error\":\"Invalid event\"}\n");
return -1;
}
// Verify it's Kind 23456
// Verify it's Kind 23458
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
cJSON_Delete(event);
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 23456\"}\n");
return;
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)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing id\"}\n");
return;
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)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing pubkey\"}\n");
return;
return -1;
}
const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj);
@@ -118,11 +189,10 @@ void handle_admin_event_request(void) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_Delete(event);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Database error\"}\n");
return;
return -1;
}
sqlite3_stmt* stmt;
@@ -141,42 +211,38 @@ void handle_admin_event_request(void) {
sqlite3_close(db);
if (!is_admin) {
cJSON_Delete(event);
printf("Status: 403 Forbidden\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Not authorized as admin\"}\n");
return;
return -1;
}
// Get encrypted content
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing content\"}\n");
return;
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) {
cJSON_Delete(event);
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;
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) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid admin pubkey format\"}\n");
return;
return -1;
}
// Decrypt content using NIP-44 (or use plaintext for testing)
@@ -195,11 +261,12 @@ void handle_admin_event_request(void) {
);
if (decrypt_result != 0) {
cJSON_Delete(event);
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;
return -1;
}
content_to_parse = decrypted_content;
}
@@ -207,22 +274,20 @@ void handle_admin_event_request(void) {
// Parse command array (either decrypted or plaintext)
cJSON* command_array = cJSON_Parse(content_to_parse);
if (!command_array || !cJSON_IsArray(command_array)) {
cJSON_Delete(event);
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;
return -1;
}
// Get command type
cJSON* command_type = cJSON_GetArrayItem(command_array, 0);
if (!command_type || !cJSON_IsString(command_type)) {
cJSON_Delete(command_array);
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid command format\"}\n");
return;
return -1;
}
const char* cmd = cJSON_GetStringValue(command_type);
@@ -242,16 +307,17 @@ void handle_admin_event_request(void) {
}
cJSON_Delete(command_array);
cJSON_Delete(event);
if (result == 0) {
// Send Kind 23457 response
// Send Kind 23459 response
send_admin_response_event(admin_pubkey, request_id, response_data);
return 0;
} else {
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;
}
}
@@ -350,7 +416,7 @@ static int handle_config_query_command(cJSON* response_data) {
}
/**
* Send Kind 23457 admin response event
* Send Kind 23459 admin response event
*/
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data) {
@@ -407,11 +473,11 @@ static int send_admin_response_event(const char* admin_pubkey, const char* reque
return -1;
}
// Create Kind 23457 response event
// 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", 23457);
cJSON_AddNumberToObject(response_event, "kind", 23459);
cJSON_AddStringToObject(response_event, "content", encrypted_response);
// Add tags
@@ -433,7 +499,7 @@ static int send_admin_response_event(const char* admin_pubkey, const char* reque
// Sign the event
cJSON* signed_event = nostr_create_and_sign_event(
23457,
23459,
encrypted_response,
tags,
server_privkey,

62
src/admin_interface.c Normal file
View File

@@ -0,0 +1,62 @@
// Admin interface handler - serves embedded web UI files
#include <stdio.h>
#include <string.h>
#include "ginxsom.h"
#include "admin_interface_embedded.h"
/**
* Serve embedded file with appropriate content type
*/
static void serve_embedded_file(const unsigned char* data, size_t size, const char* content_type) {
printf("Status: 200 OK\r\n");
printf("Content-Type: %s\r\n", content_type);
printf("Content-Length: %zu\r\n", size);
printf("Cache-Control: public, max-age=3600\r\n");
printf("\r\n");
fwrite((void*)data, 1, size, stdout);
fflush(stdout);
}
/**
* Handle admin interface requests
* Serves embedded web UI files from /api path (consistent with c-relay)
*/
void handle_admin_interface_request(const char* path) {
// Normalize path - remove trailing slash
char normalized_path[256];
strncpy(normalized_path, path, sizeof(normalized_path) - 1);
normalized_path[sizeof(normalized_path) - 1] = '\0';
size_t len = strlen(normalized_path);
if (len > 1 && normalized_path[len - 1] == '/') {
normalized_path[len - 1] = '\0';
}
// Route to appropriate embedded file
// All paths use /api/ prefix for consistency with c-relay
if (strcmp(normalized_path, "/api") == 0 || strcmp(normalized_path, "/api/index.html") == 0) {
serve_embedded_file(embedded_index_html, embedded_index_html_size, "text/html; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/index.css") == 0) {
serve_embedded_file(embedded_index_css, embedded_index_css_size, "text/css; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/index.js") == 0) {
serve_embedded_file(embedded_index_js, embedded_index_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/nostr-lite.js") == 0) {
serve_embedded_file(embedded_nostr_lite_js, embedded_nostr_lite_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/nostr.bundle.js") == 0) {
serve_embedded_file(embedded_nostr_bundle_js, embedded_nostr_bundle_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/text_graph.js") == 0) {
serve_embedded_file(embedded_text_graph_js, embedded_text_graph_js_size, "application/javascript; charset=utf-8");
}
else {
// 404 Not Found
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/html; charset=utf-8\r\n");
printf("\r\n");
printf("<html><body><h1>404 Not Found</h1><p>File not found: %s</p></body></html>\n", normalized_path);
}
}

62954
src/admin_interface_embedded.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 14
#define VERSION "v0.1.14"
#define VERSION_PATCH 15
#define VERSION "v0.1.15"
#include <stddef.h>
#include <stdint.h>
@@ -272,9 +272,12 @@ int validate_sha256_format(const char* sha256);
// Admin API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
// Admin event handler (Kind 23456/23457)
// Admin event handler (Kind 23458/23459)
void handle_admin_event_request(void);
// Admin interface handler (serves embedded web UI)
void handle_admin_interface_request(const char* path);
// Individual endpoint handlers
void handle_stats_api(void);
void handle_config_get_api(void);

View File

@@ -2314,13 +2314,14 @@ if (!config_loaded /* && !initialize_server_config() */) {
// Special case: Root endpoint is public and doesn't require authentication
if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/") == 0) {
// Handle GET / requests - Server info endpoint
// Handle GET / requests - Server info endpoint (NIP-11)
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("Content-Type: application/nostr+json\r\n\r\n");
printf("{\n");
printf(" \"server\": \"ginxsom\",\n");
printf(" \"version\": \"%s\",\n", VERSION);
printf(" \"description\": \"Ginxsom Blossom Server\",\n");
printf(" \"pubkey\": \"%s\",\n", g_blossom_pubkey);
printf(" \"endpoints\": {\n");
printf(" \"blob_get\": \"GET /<sha256>\",\n");
printf(" \"blob_head\": \"HEAD /<sha256>\",\n");
@@ -2382,12 +2383,28 @@ if (!config_loaded /* && !initialize_server_config() */) {
operation = "mirror";
} else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/report") == 0) {
operation = "report";
} else if (strncmp(request_uri, "/admin", 6) == 0) {
operation = "admin_interface"; // Public static files - no auth required
} else if (strncmp(request_uri, "/api/", 5) == 0) {
operation = "admin";
// Special case: POST /api/admin uses Kind 23456 events for authentication
// Skip centralized validation for these requests
if (strcmp(request_method, "POST") == 0 && strcmp(request_uri, "/api/admin") == 0) {
operation = "admin_event"; // Mark as special case
// Check if this is a static file request or API request
const char *path = request_uri + 5; // Skip "/api/"
int is_static_file = 0;
// Check for static file extensions or root /api path
if (strstr(path, ".html") || strstr(path, ".css") || strstr(path, ".js") ||
strlen(path) == 0 || strcmp(path, "/") == 0) {
is_static_file = 1;
}
if (is_static_file) {
operation = "admin_interface"; // Public static files - no auth required
} else {
operation = "admin";
// Special case: POST /api/admin uses Kind 23458 events for authentication
// Skip centralized validation for these requests
if (strcmp(request_method, "POST") == 0 && strcmp(request_uri, "/api/admin") == 0) {
operation = "admin_event"; // Mark as special case
}
}
} else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
operation = "list";
@@ -2424,6 +2441,8 @@ if (!config_loaded /* && !initialize_server_config() */) {
// Special case: challenge generation failure should be handled by the endpoint
if (strcmp(operation, "challenge") == 0) {
// Let the /auth endpoint handle this - it will generate its own error response
} else if (strcmp(operation, "admin_interface") == 0) {
// Admin interface serves public static files - no auth required
} else if (strcmp(operation, "head") == 0 || strcmp(operation, "head_upload") == 0) {
// HEAD requests might not require auth depending on config - let handler decide
} else if (strcmp(operation, "list") == 0) {
@@ -2431,7 +2450,7 @@ if (!config_loaded /* && !initialize_server_config() */) {
} else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) {
// Health endpoint is public and doesn't require authentication - let handler decide
} else if (strcmp(operation, "admin_event") == 0) {
// POST /api/admin uses Kind 23456 events - authentication handled by admin_event.c
// POST /api/admin uses Kind 23458 events - authentication handled by admin_event.c
// Skip centralized validation and let the handler validate the event
} else {
// For other operations, validation failure means auth failure
@@ -2528,10 +2547,34 @@ if (!config_loaded /* && !initialize_server_config() */) {
} else if (strcmp(request_method, "POST") == 0 &&
strcmp(request_uri, "/api/admin") == 0) {
// Handle POST /api/admin requests (Kind 23458 admin events)
handle_admin_event_request();
} else if (strncmp(request_uri, "/admin", 6) == 0) {
// Handle admin web interface requests (embedded files)
handle_admin_interface_request(request_uri);
} else if (strncmp(request_uri, "/api/", 5) == 0) {
// Handle admin API requests with pre-validated auth
const char *validated_pubkey = (result.valid && strlen(result.pubkey) == 64) ? result.pubkey : NULL;
handle_admin_api_request(request_method, request_uri, validated_pubkey, result.valid);
// Check if this is a static file request (no auth required) or API request (auth required)
const char *path = request_uri + 5; // Skip "/api/"
int is_static_file = 0;
// Check for static file extensions
if (strstr(path, ".html") || strstr(path, ".css") || strstr(path, ".js") ||
strcmp(request_uri, "/api") == 0 || strcmp(request_uri, "/api/") == 0) {
is_static_file = 1;
}
if (is_static_file) {
// Serve static files without authentication
handle_admin_interface_request(request_uri);
} else {
// Handle admin API requests with pre-validated auth
const char *validated_pubkey = (result.valid && strlen(result.pubkey) == 64) ? result.pubkey : NULL;
handle_admin_api_request(request_method, request_uri, validated_pubkey, result.valid);
}
} else if (strcmp(request_method, "GET") == 0 &&
@@ -2562,13 +2605,14 @@ if (!config_loaded /* && !initialize_server_config() */) {
}
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/") == 0) {
// Handle GET / requests - Server info endpoint
// Handle GET / requests - Server info endpoint (NIP-11)
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("Content-Type: application/nostr+json\r\n\r\n");
printf("{\n");
printf(" \"server\": \"ginxsom\",\n");
printf(" \"version\": \"%s\",\n", VERSION);
printf(" \"description\": \"Ginxsom Blossom Server\",\n");
printf(" \"pubkey\": \"%s\",\n", g_blossom_pubkey);
printf(" \"endpoints\": {\n");
printf(" \"blob_get\": \"GET /<sha256>\",\n");
printf(" \"blob_head\": \"HEAD /<sha256>\",\n");