v0.1.10 - In the middle of working on getting admin api working
This commit is contained in:
@@ -11,8 +11,8 @@
|
||||
#include <unistd.h>
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Database path (consistent with main.c)
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
// Use global database path from main.c
|
||||
extern char g_db_path[];
|
||||
|
||||
// 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);
|
||||
@@ -44,7 +44,7 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
// Default on DB error
|
||||
strncpy(out, "http://localhost:9001", out_size - 1);
|
||||
@@ -130,8 +130,12 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
|
||||
}
|
||||
|
||||
// Authentication now handled by centralized validation system
|
||||
// Health endpoint is exempt from authentication requirement
|
||||
if (strcmp(path, "/health") != 0) {
|
||||
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
|
||||
// Kind 23456 events authenticate themselves via signed event validation
|
||||
int skip_auth = (strcmp(path, "/health") == 0) ||
|
||||
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0);
|
||||
|
||||
if (!skip_auth) {
|
||||
if (!is_authenticated || !validated_pubkey) {
|
||||
send_json_error(401, "admin_auth_required", "Valid admin authentication required");
|
||||
return;
|
||||
@@ -157,6 +161,13 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
|
||||
} else {
|
||||
send_json_error(404, "not_found", "API endpoint not found");
|
||||
}
|
||||
} else if (strcmp(method, "POST") == 0) {
|
||||
if (strcmp(path, "/admin") == 0) {
|
||||
// Handle Kind 23456/23457 admin event commands
|
||||
handle_admin_event_request();
|
||||
} 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();
|
||||
@@ -201,7 +212,7 @@ int verify_admin_pubkey(const char* event_pubkey) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc, is_admin = 0;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
return 0;
|
||||
}
|
||||
@@ -228,7 +239,7 @@ int is_admin_enabled(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc, enabled = 0;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
return 0; // Default disabled if can't access DB
|
||||
}
|
||||
@@ -254,7 +265,7 @@ void handle_stats_api(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
send_json_error(500, "database_error", "Failed to open database");
|
||||
return;
|
||||
@@ -349,7 +360,7 @@ void handle_config_get_api(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
send_json_error(500, "database_error", "Failed to open database");
|
||||
return;
|
||||
@@ -423,7 +434,7 @@ void handle_config_put_api(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc) {
|
||||
free(json_body);
|
||||
cJSON_Delete(config_data);
|
||||
@@ -541,7 +552,7 @@ void handle_config_key_put_api(const char* key) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc) {
|
||||
free(json_body);
|
||||
cJSON_Delete(request_data);
|
||||
@@ -621,7 +632,7 @@ void handle_files_api(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
send_json_error(500, "database_error", "Failed to open database");
|
||||
return;
|
||||
@@ -715,7 +726,7 @@ void handle_health_api(void) {
|
||||
|
||||
// Check database connection
|
||||
sqlite3* db;
|
||||
int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
cJSON_AddStringToObject(data, "database", "connected");
|
||||
sqlite3_close(db);
|
||||
|
||||
471
src/admin_event.c
Normal file
471
src/admin_event.c
Normal file
@@ -0,0 +1,471 @@
|
||||
// Admin event handler for Kind 23456/23457 admin commands
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#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 send_admin_response_event(const char* admin_pubkey, const char* request_id,
|
||||
cJSON* response_data);
|
||||
|
||||
/**
|
||||
* Handle Kind 23456 admin command event
|
||||
* Expects POST to /api/admin with JSON body containing the 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!event) {
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\"error\":\"Invalid JSON\"}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify it's Kind 23456
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
|
||||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
|
||||
cJSON_Delete(event);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
cJSON_Delete(event);
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\"error\":\"Failed to decrypt content\"}\n");
|
||||
return;
|
||||
}
|
||||
content_to_parse = decrypted_content;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
result = handle_config_query_command(response_data);
|
||||
} else {
|
||||
cJSON_AddStringToObject(response_data, "status", "error");
|
||||
cJSON_AddStringToObject(response_data, "error", "Unknown command");
|
||||
}
|
||||
|
||||
cJSON_Delete(command_array);
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (result == 0) {
|
||||
// Send Kind 23457 response
|
||||
send_admin_response_event(admin_pubkey, request_id, response_data);
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Kind 23457 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 23457 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_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(
|
||||
23457,
|
||||
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;
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
#include <stdint.h>
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Database path
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
// Use global database path from main.c
|
||||
extern char g_db_path[];
|
||||
|
||||
// Check if NIP-94 metadata emission is enabled
|
||||
int nip94_is_enabled(void) {
|
||||
@@ -19,7 +19,7 @@ int nip94_is_enabled(void) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc, enabled = 1; // Default enabled
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
return 1; // Default enabled on DB error
|
||||
}
|
||||
@@ -50,7 +50,7 @@ int nip94_get_origin(char* out, size_t out_size) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
#include <time.h>
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Database path (should match main.c)
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
// Use global database path from main.c
|
||||
extern char g_db_path[];
|
||||
|
||||
// Forward declarations for helper functions
|
||||
void send_error_response(int status_code, const char* error_type, const char* message, const char* details);
|
||||
@@ -154,7 +154,7 @@ int store_blob_report(const char* event_json, const char* reporter_pubkey) {
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 1
|
||||
#define VERSION_PATCH 9
|
||||
#define VERSION "v0.1.9"
|
||||
#define VERSION_PATCH 10
|
||||
#define VERSION "v0.1.10"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -262,6 +262,9 @@ 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)
|
||||
void handle_admin_event_request(void);
|
||||
|
||||
// Individual endpoint handlers
|
||||
void handle_stats_api(void);
|
||||
void handle_config_get_api(void);
|
||||
|
||||
491
src/main.c
491
src/main.c
@@ -26,13 +26,14 @@
|
||||
#define MAX_MIME_LEN 128
|
||||
|
||||
// Configuration variables - can be overridden via command line
|
||||
char g_db_path[MAX_PATH_LEN] = "db/ginxsom.db";
|
||||
char g_db_path[MAX_PATH_LEN] = ""; // Will be set based on pubkey
|
||||
char g_storage_dir[MAX_PATH_LEN] = ".";
|
||||
|
||||
// Key management variables
|
||||
char g_admin_pubkey[65] = ""; // Admin public key for authorization
|
||||
char g_blossom_seckey[65] = ""; // Blossom server private key for decryption/signing
|
||||
int g_generate_keys = 0; // Flag to generate keys on startup
|
||||
char g_blossom_pubkey[65] = ""; // Blossom server public key (derived from seckey)
|
||||
int g_generate_keys = 0; // Flag to generate keys on startup (deprecated)
|
||||
|
||||
// Use global configuration variables
|
||||
#define DB_PATH g_db_path
|
||||
@@ -192,7 +193,7 @@ int initialize_database(const char *db_path) {
|
||||
" ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads'),"
|
||||
" ('server_name', 'ginxsom', 'Server name for responses'),"
|
||||
" ('admin_pubkey', '', 'Admin public key for API access'),"
|
||||
" ('admin_enabled', 'false', 'Whether admin API is enabled'),"
|
||||
" ('admin_enabled', 'true', 'Whether admin API is enabled'),"
|
||||
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
|
||||
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
|
||||
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');";
|
||||
@@ -245,6 +246,9 @@ int generate_server_keypair(void);
|
||||
int load_server_keys(void);
|
||||
int store_blossom_private_key(const char *seckey);
|
||||
int get_blossom_private_key(char *seckey_out, size_t max_len);
|
||||
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out);
|
||||
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey);
|
||||
int set_db_path_from_pubkey(const char *pubkey);
|
||||
|
||||
// External validator function declarations
|
||||
const char *nostr_request_validator_get_last_violation_type(void);
|
||||
@@ -258,6 +262,76 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
|
||||
|
||||
// Key management function implementations
|
||||
|
||||
// Derive public key from private key hex string
|
||||
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out) {
|
||||
if (!privkey_hex || strlen(privkey_hex) != 64) {
|
||||
fprintf(stderr, "ERROR: Invalid private key format\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned char seckey_bytes[32];
|
||||
if (nostr_hex_to_bytes(privkey_hex, seckey_bytes, 32) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "ERROR: Failed to parse private key hex\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned char pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "ERROR: Failed to derive public key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set database path based on pubkey (db/<pubkey>.db)
|
||||
int set_db_path_from_pubkey(const char *pubkey) {
|
||||
if (!pubkey || strlen(pubkey) != 64) {
|
||||
fprintf(stderr, "ERROR: Invalid pubkey for database naming\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(g_db_path, sizeof(g_db_path), "db/%s.db", pubkey);
|
||||
fprintf(stderr, "DATABASE: Set database path to %s\n", g_db_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate that database filename matches the pubkey stored inside
|
||||
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey) {
|
||||
// Extract pubkey from database filename
|
||||
const char *filename = strrchr(db_path, '/');
|
||||
if (!filename) {
|
||||
filename = db_path;
|
||||
} else {
|
||||
filename++; // Skip the '/'
|
||||
}
|
||||
|
||||
// Check if filename matches pattern: <pubkey>.db
|
||||
size_t filename_len = strlen(filename);
|
||||
if (filename_len != 67) { // 64 chars + ".db" = 67
|
||||
fprintf(stderr, "WARNING: Database filename doesn't match pubkey pattern: %s\n", filename);
|
||||
return 0; // Don't enforce for non-standard names (backward compatibility)
|
||||
}
|
||||
|
||||
// Extract pubkey from filename (first 64 chars)
|
||||
char filename_pubkey[65];
|
||||
strncpy(filename_pubkey, filename, 64);
|
||||
filename_pubkey[64] = '\0';
|
||||
|
||||
// Compare with expected pubkey
|
||||
if (strcasecmp(filename_pubkey, expected_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Database pubkey mismatch!\n");
|
||||
fprintf(stderr, " Database filename: %s\n", filename_pubkey);
|
||||
fprintf(stderr, " Expected pubkey: %s\n", expected_pubkey);
|
||||
fprintf(stderr, " → Database filename doesn't match the keys stored inside\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "DATABASE: Pubkey validation passed: %s\n", filename_pubkey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Generate random private key bytes using /dev/urandom
|
||||
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) {
|
||||
FILE *fp = fopen("/dev/urandom", "rb");
|
||||
@@ -309,6 +383,26 @@ int generate_server_keypair(void) {
|
||||
|
||||
// Convert public key to hex
|
||||
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
|
||||
|
||||
// Store pubkey in global variable
|
||||
strncpy(g_blossom_pubkey, pubkey_hex, sizeof(g_blossom_pubkey) - 1);
|
||||
g_blossom_pubkey[64] = '\0';
|
||||
|
||||
// Store seckey in global variable
|
||||
strncpy(g_blossom_seckey, seckey_hex, sizeof(g_blossom_seckey) - 1);
|
||||
g_blossom_seckey[64] = '\0';
|
||||
|
||||
// Set database path based on pubkey (MUST be done before storing keys)
|
||||
if (set_db_path_from_pubkey(pubkey_hex) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to set database path\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Initialize database with new path
|
||||
if (initialize_database(g_db_path) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to initialize database at %s\n", g_db_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store private key securely
|
||||
if (store_blossom_private_key(seckey_hex) != 0) {
|
||||
@@ -321,7 +415,7 @@ int generate_server_keypair(void) {
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc) {
|
||||
fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
@@ -354,6 +448,7 @@ int generate_server_keypair(void) {
|
||||
fprintf(stderr, "========================================\n");
|
||||
fprintf(stderr, "Blossom Public Key: %s\n", pubkey_hex);
|
||||
fprintf(stderr, "Blossom Private Key: %s\n", seckey_hex);
|
||||
fprintf(stderr, "Database Path: %s\n", g_db_path);
|
||||
fprintf(stderr, "========================================\n");
|
||||
fprintf(stderr, "IMPORTANT: Save the private key securely!\n");
|
||||
fprintf(stderr, "This key is used for decrypting admin messages.\n");
|
||||
@@ -372,26 +467,30 @@ int load_server_keys(void) {
|
||||
// Try to load blossom private key
|
||||
fprintf(stderr, "DEBUG: Trying to load blossom private key...\n");
|
||||
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
||||
fprintf(stderr, "DEBUG: No blossom private key found\n");
|
||||
// No private key found - check if we should generate one
|
||||
if (g_generate_keys) {
|
||||
fprintf(stderr, "STARTUP: No blossom private key found, generating new keypair...\n");
|
||||
if (generate_server_keypair() != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to generate server keypair\n");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "WARNING: No blossom private key found. Use --generate-keys to create one.\n");
|
||||
// This is not fatal - server can still operate without admin features
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n");
|
||||
fprintf(stderr, "DEBUG: No blossom private key found in database\n");
|
||||
return -1; // Keys must exist in database
|
||||
}
|
||||
|
||||
fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n");
|
||||
|
||||
// Derive public key from private key
|
||||
if (derive_pubkey_from_privkey(g_blossom_seckey, g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to derive public key from private key\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "STARTUP: Derived blossom pubkey: %s\n", g_blossom_pubkey);
|
||||
|
||||
// Validate database filename matches pubkey
|
||||
if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Database pubkey validation failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Load admin pubkey from command line or config
|
||||
if (strlen(g_admin_pubkey) == 0) {
|
||||
// Try to load from database config
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
@@ -410,7 +509,7 @@ int load_server_keys(void) {
|
||||
}
|
||||
} else {
|
||||
// Store admin pubkey in config if provided via command line
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
@@ -441,7 +540,7 @@ int store_blossom_private_key(const char *seckey) {
|
||||
}
|
||||
|
||||
// Create blossom_seckey table if it doesn't exist
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
if (rc) {
|
||||
fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
@@ -484,7 +583,7 @@ int get_blossom_private_key(char *seckey_out, size_t max_len) {
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
return -1;
|
||||
}
|
||||
@@ -520,7 +619,7 @@ int insert_blob_metadata(const char *sha256, long size, const char *type,
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
if (rc) {
|
||||
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
||||
return 0;
|
||||
@@ -575,7 +674,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
|
||||
if (rc) {
|
||||
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
||||
return 0;
|
||||
@@ -845,7 +944,7 @@ void handle_list_request(const char *pubkey) {
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc) {
|
||||
|
||||
send_error_response(500, "database_error", "Failed to access database",
|
||||
@@ -992,7 +1091,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||
if (rc) {
|
||||
|
||||
send_error_response(500, "database_error", "Failed to access database",
|
||||
@@ -1737,10 +1836,13 @@ int main(int argc, char *argv[]) {
|
||||
// Parse command line arguments
|
||||
int use_test_keys = 0;
|
||||
char test_server_privkey[65] = "";
|
||||
char specified_db_path[MAX_PATH_LEN] = "";
|
||||
int db_path_specified = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--db-path") == 0 && i + 1 < argc) {
|
||||
strncpy(g_db_path, argv[i + 1], sizeof(g_db_path) - 1);
|
||||
strncpy(specified_db_path, argv[i + 1], sizeof(specified_db_path) - 1);
|
||||
db_path_specified = 1;
|
||||
i++; // Skip next argument
|
||||
} else if (strcmp(argv[i], "--storage-dir") == 0 && i + 1 < argc) {
|
||||
strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1);
|
||||
@@ -1750,13 +1852,24 @@ int main(int argc, char *argv[]) {
|
||||
const char *help_text =
|
||||
"Usage: ginxsom-fcgi [options]\n"
|
||||
"Options:\n"
|
||||
" --db-path PATH Database file path (default: db/ginxsom.db)\n"
|
||||
" --storage-dir DIR Storage directory for files (default: blobs)\n"
|
||||
" --admin-pubkey KEY Admin public key for management (64 hex chars)\n"
|
||||
" --server-privkey KEY Server private key (64 hex chars, for testing)\n"
|
||||
" --db-path PATH Database file path (must match pubkey if keys exist)\n"
|
||||
" --storage-dir DIR Storage directory for files (default: .)\n"
|
||||
" --admin-pubkey KEY Admin public key (only used when creating new database)\n"
|
||||
" --server-privkey KEY Server private key (creates new DB or validates existing)\n"
|
||||
" --test-keys Use test keys from .test_keys file\n"
|
||||
" --generate-keys Generate server keypair on startup\n"
|
||||
" --help, -h Show this help message\n";
|
||||
" --generate-keys Generate new keypair and create database (deprecated)\n"
|
||||
" --help, -h Show this help message\n"
|
||||
"\n"
|
||||
"Database Naming:\n"
|
||||
" Databases are named after the server's public key: db/<pubkey>.db\n"
|
||||
" This ensures database-key consistency and prevents mismatches.\n"
|
||||
"\n"
|
||||
"Startup Scenarios:\n"
|
||||
" 1. No arguments → Generate new keys, create db/<new_pubkey>.db\n"
|
||||
" 2. --db-path <path> → Open existing database, validate keys match filename\n"
|
||||
" 3. --server-privkey <key>→ Create new database with those keys\n"
|
||||
" 4. --test-keys → Use test keys, create/overwrite test database\n"
|
||||
" 5. --db-path + --server-privkey → Validate keys match or error\n";
|
||||
ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text));
|
||||
(void)written; // Suppress unused variable warning
|
||||
return 0;
|
||||
@@ -1770,6 +1883,7 @@ int main(int argc, char *argv[]) {
|
||||
use_test_keys = 1;
|
||||
} else if (strcmp(argv[i], "--generate-keys") == 0) {
|
||||
g_generate_keys = 1;
|
||||
fprintf(stderr, "WARNING: --generate-keys is deprecated. Keys are generated automatically when needed.\n");
|
||||
} else {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
fprintf(stderr, "Use --help for usage information\n");
|
||||
@@ -1777,8 +1891,27 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load test keys if requested
|
||||
fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir);
|
||||
|
||||
// CRITICAL: Initialize nostr crypto system BEFORE key operations
|
||||
fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n");
|
||||
int crypto_init_result = nostr_crypto_init();
|
||||
fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result);
|
||||
if (crypto_init_result != 0) {
|
||||
fprintf(stderr, "FATAL ERROR: Failed to initialize nostr crypto system\r\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n");
|
||||
|
||||
// ========================================================================
|
||||
// DATABASE AND KEY INITIALIZATION - 5 SCENARIOS
|
||||
// ========================================================================
|
||||
|
||||
// Scenario 4: Test Mode (--test-keys)
|
||||
if (use_test_keys) {
|
||||
fprintf(stderr, "\n=== SCENARIO 4: TEST MODE ===\n");
|
||||
|
||||
// Load test keys from .test_keys file
|
||||
FILE *keys_file = fopen(".test_keys", "r");
|
||||
if (!keys_file) {
|
||||
fprintf(stderr, "ERROR: Cannot open .test_keys file\n");
|
||||
@@ -1787,7 +1920,6 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), keys_file)) {
|
||||
// Parse ADMIN_PUBKEY='...'
|
||||
if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) {
|
||||
char *start = line + 14;
|
||||
char *end = strchr(start, '\'');
|
||||
@@ -1796,7 +1928,6 @@ int main(int argc, char *argv[]) {
|
||||
g_admin_pubkey[64] = '\0';
|
||||
}
|
||||
}
|
||||
// Parse SERVER_PRIVKEY='...'
|
||||
else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) {
|
||||
char *start = line + 16;
|
||||
char *end = strchr(start, '\'');
|
||||
@@ -1808,110 +1939,232 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
fclose(keys_file);
|
||||
|
||||
fprintf(stderr, "STARTUP: Using test keys from .test_keys\n");
|
||||
fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey);
|
||||
fprintf(stderr, "STARTUP: Server privkey loaded from test keys\n");
|
||||
}
|
||||
|
||||
fprintf(stderr, "STARTUP: Using database path: %s\n", g_db_path);
|
||||
fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir);
|
||||
if (strlen(g_admin_pubkey) > 0) {
|
||||
fprintf(stderr, "STARTUP: Admin pubkey specified: %s\n", g_admin_pubkey);
|
||||
}
|
||||
if (g_generate_keys) {
|
||||
fprintf(stderr, "STARTUP: Will generate server keypair\n");
|
||||
}
|
||||
|
||||
fprintf(stderr, "DEBUG: About to initialize database\n");
|
||||
|
||||
// Initialize database (create if doesn't exist)
|
||||
fprintf(stderr, "STARTUP: Initializing database...\n");
|
||||
if (initialize_database(g_db_path) != 0) {
|
||||
fprintf(stderr, "FATAL ERROR: Failed to initialize database\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "STARTUP: Database ready\n");
|
||||
|
||||
// CRITICAL: Initialize nostr crypto system BEFORE key operations
|
||||
fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n");
|
||||
int crypto_init_result = nostr_crypto_init();
|
||||
fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result);
|
||||
if (crypto_init_result != 0) {
|
||||
fprintf(stderr,
|
||||
"FATAL ERROR: Failed to initialize nostr crypto system\r\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n");
|
||||
|
||||
// Initialize server keys (now that crypto is initialized)
|
||||
fprintf(stderr, "STARTUP: Initializing server keys...\n");
|
||||
fflush(stderr);
|
||||
|
||||
// If test keys were provided via command line, store them in database
|
||||
if (test_server_privkey[0] != '\0') {
|
||||
// Store test private key in database
|
||||
fprintf(stderr, "TEST MODE: Loaded keys from .test_keys\n");
|
||||
fprintf(stderr, "TEST MODE: Admin pubkey: %s\n", g_admin_pubkey);
|
||||
|
||||
// Derive pubkey from test privkey
|
||||
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to derive pubkey from test privkey\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "TEST MODE: Server pubkey: %s\n", g_blossom_pubkey);
|
||||
|
||||
// Set database path based on test pubkey
|
||||
if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to set database path\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test mode ALWAYS overwrites database for clean testing
|
||||
fprintf(stderr, "TEST MODE: Creating/overwriting database: %s\n", g_db_path);
|
||||
unlink(g_db_path); // Remove if exists
|
||||
|
||||
// Initialize new database
|
||||
if (initialize_database(g_db_path) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to initialize test database\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Store test keys
|
||||
strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1);
|
||||
g_blossom_seckey[64] = '\0';
|
||||
|
||||
fprintf(stderr, "STARTUP: Storing test server private key in database...\n");
|
||||
if (store_blossom_private_key(test_server_privkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to store test private key\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Derive and store public key
|
||||
unsigned char seckey_bytes[32];
|
||||
if (nostr_hex_to_bytes(test_server_privkey, seckey_bytes, 32) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "ERROR: Failed to parse test private key\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
unsigned char pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "ERROR: Failed to derive public key from test private key\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char pubkey_hex[65];
|
||||
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
|
||||
|
||||
// Store server public key in config
|
||||
// Store pubkey and admin pubkey in config
|
||||
sqlite3 *db;
|
||||
sqlite3_stmt *stmt;
|
||||
int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, "Blossom server's public key (TEST MODE)", -1, SQLITE_STATIC);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
if (strlen(g_admin_pubkey) > 0) {
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, "Admin public key (TEST MODE)", -1, SQLITE_STATIC);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
fprintf(stderr, "STARTUP: Test server keys stored in database\n");
|
||||
fprintf(stderr, "STARTUP: Server pubkey: %s\n", pubkey_hex);
|
||||
fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey);
|
||||
fprintf(stderr, "TEST MODE: Database initialized successfully\n");
|
||||
}
|
||||
|
||||
// Scenario 3: Keys Specified (--server-privkey)
|
||||
else if (test_server_privkey[0] != '\0') {
|
||||
fprintf(stderr, "\n=== SCENARIO 3: KEYS SPECIFIED ===\n");
|
||||
|
||||
// Now call load_server_keys to ensure admin_pubkey is also stored
|
||||
int key_init_result = load_server_keys();
|
||||
if (key_init_result != 0) {
|
||||
fprintf(stderr, "WARNING: Failed to complete key initialization\n");
|
||||
}
|
||||
} else {
|
||||
// Load keys from database (production mode)
|
||||
int key_init_result = load_server_keys();
|
||||
fprintf(stderr, "KEY INIT RESULT: %d\n", key_init_result);
|
||||
fflush(stderr);
|
||||
if (key_init_result != 0) {
|
||||
fprintf(stderr, "FATAL ERROR: Failed to initialize server keys\n");
|
||||
// Derive pubkey from provided privkey
|
||||
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Invalid server private key\n");
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "STARTUP: Server keys initialized successfully\n");
|
||||
|
||||
fprintf(stderr, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey);
|
||||
|
||||
// Scenario 5: Both database and keys specified - validate match
|
||||
if (db_path_specified) {
|
||||
fprintf(stderr, "\n=== SCENARIO 5: DATABASE + KEYS (VALIDATION) ===\n");
|
||||
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
|
||||
|
||||
// Check if database exists
|
||||
struct stat st;
|
||||
if (stat(g_db_path, &st) != 0) {
|
||||
fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load keys from database
|
||||
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
||||
fprintf(stderr, "ERROR: Invalid database: missing server keys\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Compare with provided key
|
||||
if (strcmp(g_blossom_seckey, test_server_privkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Server private key doesn't match database\n");
|
||||
fprintf(stderr, " Provided key and database keys are different\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "VALIDATION: Keys match database - continuing\n");
|
||||
|
||||
// Validate pubkey matches filename
|
||||
if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// Scenario 3 continued: Create new database with provided keys
|
||||
else {
|
||||
// Set database path based on derived pubkey
|
||||
if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to set database path\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check if database already exists
|
||||
struct stat st;
|
||||
if (stat(g_db_path, &st) == 0) {
|
||||
fprintf(stderr, "ERROR: Database already exists for this pubkey: %s\n", g_db_path);
|
||||
fprintf(stderr, " Use --db-path to open existing database or use different keys\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Initialize new database
|
||||
if (initialize_database(g_db_path) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to initialize database\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Store keys
|
||||
strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1);
|
||||
g_blossom_seckey[64] = '\0';
|
||||
|
||||
if (store_blossom_private_key(test_server_privkey) != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to store private key\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Store pubkey in config
|
||||
sqlite3 *db;
|
||||
sqlite3_stmt *stmt;
|
||||
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, "Blossom server's public key", -1, SQLITE_STATIC);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
if (strlen(g_admin_pubkey) > 0) {
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, "Admin public key", -1, SQLITE_STATIC);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
fprintf(stderr, "KEYS: New database created successfully\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario 2: Database Specified (--db-path)
|
||||
else if (db_path_specified) {
|
||||
fprintf(stderr, "\n=== SCENARIO 2: DATABASE SPECIFIED ===\n");
|
||||
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
|
||||
|
||||
// Check if database exists
|
||||
struct stat st;
|
||||
if (stat(g_db_path, &st) != 0) {
|
||||
fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path);
|
||||
fprintf(stderr, " → Specify a different database or let the application create a new one\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "DATABASE: Opening existing database: %s\n", g_db_path);
|
||||
|
||||
// Load keys from database
|
||||
if (load_server_keys() != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to load keys from database\n");
|
||||
fprintf(stderr, " → Database may be corrupted or not a valid ginxsom database\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "DATABASE: Keys loaded and validated successfully\n");
|
||||
}
|
||||
|
||||
// Scenario 1: No Arguments (Fresh Start)
|
||||
else {
|
||||
fprintf(stderr, "\n=== SCENARIO 1: FRESH START (NO ARGUMENTS) ===\n");
|
||||
fprintf(stderr, "FRESH START: Generating new server keypair...\n");
|
||||
|
||||
// Generate new keypair (this will set g_db_path based on pubkey)
|
||||
if (generate_server_keypair() != 0) {
|
||||
fprintf(stderr, "ERROR: Failed to generate server keypair\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "FRESH START: New instance created successfully\n");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// END DATABASE AND KEY INITIALIZATION
|
||||
// ========================================================================
|
||||
|
||||
fprintf(stderr, "\n=== FINAL CONFIGURATION ===\n");
|
||||
fprintf(stderr, "Database path: %s\n", g_db_path);
|
||||
fprintf(stderr, "Storage directory: %s\n", g_storage_dir);
|
||||
fprintf(stderr, "Server pubkey: %s\n", g_blossom_pubkey);
|
||||
if (strlen(g_admin_pubkey) > 0) {
|
||||
fprintf(stderr, "Admin pubkey: %s\n", g_admin_pubkey);
|
||||
}
|
||||
fprintf(stderr, "===========================\n\n");
|
||||
|
||||
fflush(stderr);
|
||||
|
||||
// If --generate-keys was specified, exit after key generation
|
||||
@@ -1940,7 +2193,7 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
||||
// Initialize request validator system
|
||||
fprintf(stderr, "STARTUP: Initializing request validator system...\r\n");
|
||||
int validator_init_result =
|
||||
ginxsom_request_validator_init(DB_PATH, "ginxsom");
|
||||
ginxsom_request_validator_init(g_db_path, "ginxsom");
|
||||
fprintf(stderr, "MAIN: validator init return code: %d\r\n",
|
||||
validator_init_result);
|
||||
if (validator_init_result != NOSTR_SUCCESS) {
|
||||
@@ -2054,6 +2307,11 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
||||
operation = "report";
|
||||
} 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
|
||||
}
|
||||
} else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
|
||||
operation = "list";
|
||||
} else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) {
|
||||
@@ -2095,6 +2353,9 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
||||
// List operation might be optional auth - let handler decide
|
||||
} 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
|
||||
// Skip centralized validation and let the handler validate the event
|
||||
} else {
|
||||
// For other operations, validation failure means auth failure
|
||||
const char *message = result.reason[0] ? result.reason : "Authentication failed";
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
|
||||
// nostr_core_lib/nostr_core/nostr_common.h
|
||||
|
||||
// Database path (consistent with main.c)
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
// Use global database path from main.c
|
||||
extern char g_db_path[];
|
||||
|
||||
// NIP-42 challenge management constants
|
||||
#define MAX_CHALLENGES 1000
|
||||
@@ -1064,7 +1064,7 @@ static int reload_auth_config(void) {
|
||||
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
|
||||
|
||||
// Open database
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
validator_debug_log("VALIDATOR: Could not open database\n");
|
||||
// Use defaults
|
||||
@@ -1345,7 +1345,7 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||
validator_debug_log(rules_msg);
|
||||
|
||||
// Open database
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
validator_debug_log(
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");
|
||||
|
||||
Reference in New Issue
Block a user