Files
c-relay/src/config.c

1002 lines
33 KiB
C

#include "config.h"
#include "version.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <cjson/cJSON.h>
#include "../nostr_core_lib/nostr_core/nostr_core.h"
// External database connection (from main.c)
extern sqlite3* g_db;
// Global configuration manager instance
config_manager_t g_config_manager = {0};
// Logging functions (defined in main.c)
extern void log_info(const char* message);
extern void log_success(const char* message);
extern void log_warning(const char* message);
extern void log_error(const char* message);
// ================================
// CORE CONFIGURATION FUNCTIONS
// ================================
//
int init_configuration_system(void) {
log_info("Initializing configuration system...");
// Clear configuration manager state
memset(&g_config_manager, 0, sizeof(config_manager_t));
g_config_manager.db = g_db;
// Get XDG configuration directory
if (get_xdg_config_dir(g_config_manager.config_dir_path, sizeof(g_config_manager.config_dir_path)) != 0) {
log_error("Failed to determine XDG configuration directory");
return -1;
}
// Build configuration file path
snprintf(g_config_manager.config_file_path, sizeof(g_config_manager.config_file_path),
"%s/%s", g_config_manager.config_dir_path, CONFIG_FILE_NAME);
log_info("Configuration directory: %s");
printf(" %s\n", g_config_manager.config_dir_path);
log_info("Configuration file: %s");
printf(" %s\n", g_config_manager.config_file_path);
// Initialize database prepared statements
if (init_config_database_statements() != 0) {
log_error("Failed to initialize configuration database statements");
return -1;
}
// Generate configuration file if missing
if (generate_config_file_if_missing() != 0) {
log_warning("Failed to generate configuration file, continuing with database configuration");
}
// Load configuration from all sources
if (load_configuration() != 0) {
log_error("Failed to load configuration");
return -1;
}
// Apply configuration to global variables
if (apply_configuration_to_globals() != 0) {
log_error("Failed to apply configuration to global variables");
return -1;
}
log_success("Configuration system initialized successfully");
return 0;
}
void cleanup_configuration_system(void) {
log_info("Cleaning up configuration system...");
// Finalize prepared statements
if (g_config_manager.get_config_stmt) {
sqlite3_finalize(g_config_manager.get_config_stmt);
g_config_manager.get_config_stmt = NULL;
}
if (g_config_manager.set_config_stmt) {
sqlite3_finalize(g_config_manager.set_config_stmt);
g_config_manager.set_config_stmt = NULL;
}
if (g_config_manager.log_change_stmt) {
sqlite3_finalize(g_config_manager.log_change_stmt);
g_config_manager.log_change_stmt = NULL;
}
// Clear manager state
memset(&g_config_manager, 0, sizeof(config_manager_t));
log_success("Configuration system cleaned up");
}
int load_configuration(void) {
log_info("Loading configuration from all sources...");
// Try to load configuration from file first
if (config_file_exists()) {
log_info("Configuration file found, attempting to load...");
if (load_config_from_file() == 0) {
g_config_manager.file_config_loaded = 1;
log_success("File configuration loaded successfully");
} else {
log_warning("Failed to load file configuration, falling back to database");
}
} else {
log_info("No configuration file found, checking database");
}
// Load configuration from database (either as primary or fallback)
if (load_config_from_database() == 0) {
g_config_manager.database_config_loaded = 1;
log_success("Database configuration loaded");
} else {
log_error("Failed to load database configuration");
return -1;
}
g_config_manager.last_reload = time(NULL);
return 0;
}
int apply_configuration_to_globals(void) {
log_info("Applying configuration to global variables...");
// Apply configuration values to existing global variables
// This would update the existing hardcoded values with database values
// For now, this is a placeholder - in Phase 4 we'll implement
// the actual mapping to existing global variables
log_success("Configuration applied to global variables");
return 0;
}
// ================================
// DATABASE CONFIGURATION FUNCTIONS
// ================================
int init_config_database_statements(void) {
if (!g_db) {
log_error("Database connection not available for configuration");
return -1;
}
log_info("Initializing configuration database statements...");
// Prepare statement for getting configuration values
const char* get_sql = "SELECT value FROM server_config WHERE key = ?";
int rc = sqlite3_prepare_v2(g_db, get_sql, -1, &g_config_manager.get_config_stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare get_config statement");
return -1;
}
// Prepare statement for setting configuration values
const char* set_sql = "INSERT OR REPLACE INTO server_config (key, value, updated_at) VALUES (?, ?, strftime('%s', 'now'))";
rc = sqlite3_prepare_v2(g_db, set_sql, -1, &g_config_manager.set_config_stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare set_config statement");
return -1;
}
// Prepare statement for logging configuration changes
const char* log_sql = "INSERT INTO config_history (config_key, old_value, new_value, changed_by) VALUES (?, ?, ?, ?)";
rc = sqlite3_prepare_v2(g_db, log_sql, -1, &g_config_manager.log_change_stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare log_change statement");
return -1;
}
log_success("Configuration database statements initialized");
return 0;
}
int get_database_config(const char* key, char* value, size_t value_size) {
if (!key || !value || !g_config_manager.get_config_stmt) {
return -1;
}
// Reset and bind parameters
sqlite3_reset(g_config_manager.get_config_stmt);
sqlite3_bind_text(g_config_manager.get_config_stmt, 1, key, -1, SQLITE_STATIC);
int result = -1;
if (sqlite3_step(g_config_manager.get_config_stmt) == SQLITE_ROW) {
const char* db_value = (const char*)sqlite3_column_text(g_config_manager.get_config_stmt, 0);
if (db_value) {
strncpy(value, db_value, value_size - 1);
value[value_size - 1] = '\0';
result = 0;
}
}
return result;
}
int set_database_config(const char* key, const char* new_value, const char* changed_by) {
if (!key || !new_value || !g_config_manager.set_config_stmt) {
return -1;
}
// Get old value for logging
char old_value[CONFIG_VALUE_MAX_LENGTH] = {0};
get_database_config(key, old_value, sizeof(old_value));
// Set new value
sqlite3_reset(g_config_manager.set_config_stmt);
sqlite3_bind_text(g_config_manager.set_config_stmt, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(g_config_manager.set_config_stmt, 2, new_value, -1, SQLITE_STATIC);
int result = 0;
if (sqlite3_step(g_config_manager.set_config_stmt) != SQLITE_DONE) {
log_error("Failed to set configuration value");
result = -1;
} else {
// Log the change
if (g_config_manager.log_change_stmt) {
sqlite3_reset(g_config_manager.log_change_stmt);
sqlite3_bind_text(g_config_manager.log_change_stmt, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(g_config_manager.log_change_stmt, 2, strlen(old_value) > 0 ? old_value : NULL, -1, SQLITE_STATIC);
sqlite3_bind_text(g_config_manager.log_change_stmt, 3, new_value, -1, SQLITE_STATIC);
sqlite3_bind_text(g_config_manager.log_change_stmt, 4, changed_by ? changed_by : "system", -1, SQLITE_STATIC);
sqlite3_step(g_config_manager.log_change_stmt);
}
}
return result;
}
int load_config_from_database(void) {
log_info("Loading configuration from database...");
// Database configuration is already populated by schema defaults
// This function validates that the configuration tables exist and are accessible
const char* test_sql = "SELECT COUNT(*) FROM server_config WHERE config_type IN ('system', 'user')";
sqlite3_stmt* test_stmt;
int rc = sqlite3_prepare_v2(g_db, test_sql, -1, &test_stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare database configuration test query");
return -1;
}
int config_count = 0;
if (sqlite3_step(test_stmt) == SQLITE_ROW) {
config_count = sqlite3_column_int(test_stmt, 0);
}
sqlite3_finalize(test_stmt);
if (config_count > 0) {
log_success("Database configuration validated (%d entries)");
printf(" Found %d configuration entries\n", config_count);
return 0;
} else {
log_error("No configuration entries found in database");
return -1;
}
}
// ================================
// FILE CONFIGURATION FUNCTIONS
// ================================
int get_xdg_config_dir(char* path, size_t path_size) {
const char* xdg_config_home = getenv("XDG_CONFIG_HOME");
if (xdg_config_home && strlen(xdg_config_home) > 0) {
// Use XDG_CONFIG_HOME if set
snprintf(path, path_size, "%s/%s", xdg_config_home, CONFIG_XDG_DIR_NAME);
} else {
// Fall back to ~/.config
const char* home = getenv("HOME");
if (!home) {
log_error("Neither XDG_CONFIG_HOME nor HOME environment variable is set");
return -1;
}
snprintf(path, path_size, "%s/.config/%s", home, CONFIG_XDG_DIR_NAME);
}
return 0;
}
int config_file_exists(void) {
struct stat st;
return (stat(g_config_manager.config_file_path, &st) == 0);
}
int load_config_from_file(void) {
log_info("Loading configuration from file...");
FILE* file = fopen(g_config_manager.config_file_path, "r");
if (!file) {
log_error("Failed to open configuration file");
return -1;
}
// Read file contents
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
fseek(file, 0, SEEK_SET);
char* file_content = malloc(file_size + 1);
if (!file_content) {
log_error("Failed to allocate memory for configuration file");
fclose(file);
return -1;
}
size_t read_size = fread(file_content, 1, file_size, file);
file_content[read_size] = '\0';
fclose(file);
// Parse JSON
cJSON* json = cJSON_Parse(file_content);
free(file_content);
if (!json) {
log_error("Failed to parse configuration file as JSON");
return -1;
}
// Validate Nostr event structure
int result = validate_and_apply_config_event(json);
cJSON_Delete(json);
if (result == 0) {
log_success("Configuration loaded from file successfully");
} else {
log_error("Configuration file validation failed");
}
return result;
}
// ================================
// NOSTR EVENT VALIDATION FUNCTIONS
// ================================
int validate_nostr_event_structure(const cJSON* event) {
if (!event || !cJSON_IsObject(event)) {
log_error("Configuration event is not a valid JSON object");
return -1;
}
// Check required fields
cJSON* kind = cJSON_GetObjectItem(event, "kind");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* sig = cJSON_GetObjectItem(event, "sig");
if (!kind || !cJSON_IsNumber(kind)) {
log_error("Configuration event missing or invalid 'kind' field");
return -1;
}
if (cJSON_GetNumberValue(kind) != 33334) {
log_error("Configuration event has wrong kind (expected 33334)");
return -1;
}
if (!created_at || !cJSON_IsNumber(created_at)) {
log_error("Configuration event missing or invalid 'created_at' field");
return -1;
}
if (!tags || !cJSON_IsArray(tags)) {
log_error("Configuration event missing or invalid 'tags' field");
return -1;
}
if (!content || !cJSON_IsString(content)) {
log_error("Configuration event missing or invalid 'content' field");
return -1;
}
if (!pubkey || !cJSON_IsString(pubkey)) {
log_error("Configuration event missing or invalid 'pubkey' field");
return -1;
}
if (!id || !cJSON_IsString(id)) {
log_error("Configuration event missing or invalid 'id' field");
return -1;
}
if (!sig || !cJSON_IsString(sig)) {
log_error("Configuration event missing or invalid 'sig' field");
return -1;
}
// Validate pubkey format (64 hex characters)
const char* pubkey_str = cJSON_GetStringValue(pubkey);
if (strlen(pubkey_str) != 64) {
log_error("Configuration event pubkey has invalid length");
return -1;
}
// Validate id format (64 hex characters)
const char* id_str = cJSON_GetStringValue(id);
if (strlen(id_str) != 64) {
log_error("Configuration event id has invalid length");
return -1;
}
// Validate signature format (128 hex characters)
const char* sig_str = cJSON_GetStringValue(sig);
if (strlen(sig_str) != 128) {
log_error("Configuration event signature has invalid length");
return -1;
}
log_info("Configuration event structure validation passed");
return 0;
}
int validate_config_tags(const cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
return -1;
}
int tag_count = 0;
const cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) {
log_error("Configuration tag is not an array");
return -1;
}
int tag_size = cJSON_GetArraySize(tag);
if (tag_size < 2) {
log_error("Configuration tag has insufficient elements");
return -1;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* value = cJSON_GetArrayItem(tag, 1);
if (!key || !cJSON_IsString(key) || !value || !cJSON_IsString(value)) {
log_error("Configuration tag key or value is not a string");
return -1;
}
tag_count++;
}
log_info("Configuration tags validation passed (%d tags)");
printf(" Found %d configuration tags\n", tag_count);
return 0;
}
int extract_and_apply_config_tags(const cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
return -1;
}
int applied_count = 0;
const cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* value = cJSON_GetArrayItem(tag, 1);
if (!key || !value) continue;
const char* key_str = cJSON_GetStringValue(key);
const char* value_str = cJSON_GetStringValue(value);
if (!key_str || !value_str) continue;
// Validate configuration value
config_validation_result_t validation = validate_config_value(key_str, value_str);
if (validation != CONFIG_VALID) {
log_config_validation_error(key_str, value_str, "Value failed validation");
continue;
}
// Apply configuration to database
if (set_database_config(key_str, value_str, "file") == 0) {
applied_count++;
} else {
log_error("Failed to apply configuration");
printf(" Key: %s, Value: %s\n", key_str, value_str);
}
}
if (applied_count > 0) {
log_success("Applied configuration from file");
printf(" Applied %d configuration values\n", applied_count);
return 0;
} else {
log_warning("No valid configuration values found in file");
return -1;
}
}
int validate_and_apply_config_event(const cJSON* event) {
log_info("Validating configuration event...");
// Step 1: Validate event structure
if (validate_nostr_event_structure(event) != 0) {
return -1;
}
// Step 2: Extract and validate tags
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (validate_config_tags(tags) != 0) {
return -1;
}
// Step 3: For now, skip signature verification (would require Nostr crypto library)
// In production, this would verify the event signature against admin pubkeys
log_warning("Signature verification not yet implemented - accepting event");
// Step 4: Extract and apply configuration
if (extract_and_apply_config_tags(tags) != 0) {
return -1;
}
log_success("Configuration event validation and application completed");
return 0;
}
// ================================
// CONFIGURATION ACCESS FUNCTIONS
// ================================
const char* get_config_value(const char* key) {
static char buffer[CONFIG_VALUE_MAX_LENGTH];
if (!key) {
return NULL;
}
// Priority 1: Database configuration (updated from file)
if (get_database_config(key, buffer, sizeof(buffer)) == 0) {
return buffer;
}
// Priority 2: Environment variables (fallback)
const char* env_value = getenv(key);
if (env_value) {
return env_value;
}
// No value found
return NULL;
}
int get_config_int(const char* key, int default_value) {
const char* str_value = get_config_value(key);
if (!str_value) {
return default_value;
}
char* endptr;
long val = strtol(str_value, &endptr, 10);
if (endptr == str_value || *endptr != '\0') {
// Invalid integer format
return default_value;
}
return (int)val;
}
int get_config_bool(const char* key, int default_value) {
const char* str_value = get_config_value(key);
if (!str_value) {
return default_value;
}
// Check for boolean values
if (strcasecmp(str_value, "true") == 0 ||
strcasecmp(str_value, "yes") == 0 ||
strcasecmp(str_value, "1") == 0) {
return 1;
} else if (strcasecmp(str_value, "false") == 0 ||
strcasecmp(str_value, "no") == 0 ||
strcasecmp(str_value, "0") == 0) {
return 0;
}
return default_value;
}
int set_config_value(const char* key, const char* value) {
if (!key || !value) {
return -1;
}
return set_database_config(key, value, "api");
}
// ================================
// CONFIGURATION VALIDATION
// ================================
config_validation_result_t validate_config_value(const char* key, const char* value) {
// Placeholder for validation logic
// Will implement full validation in Phase 3
if (!key || !value) {
return CONFIG_MISSING_REQUIRED;
}
// Basic validation - all values are valid for now
return CONFIG_VALID;
}
void log_config_validation_error(const char* key, const char* value, const char* error) {
log_error("Configuration validation error");
printf(" Key: %s\n", key ? key : "NULL");
printf(" Value: %s\n", value ? value : "NULL");
printf(" Error: %s\n", error ? error : "Unknown error");
}
// ================================
// UTILITY FUNCTIONS
// ================================
const char* config_type_to_string(config_type_t type) {
switch (type) {
case CONFIG_TYPE_SYSTEM: return "system";
case CONFIG_TYPE_USER: return "user";
case CONFIG_TYPE_RUNTIME: return "runtime";
default: return "unknown";
}
}
const char* config_data_type_to_string(config_data_type_t type) {
switch (type) {
case CONFIG_DATA_STRING: return "string";
case CONFIG_DATA_INTEGER: return "integer";
case CONFIG_DATA_BOOLEAN: return "boolean";
case CONFIG_DATA_JSON: return "json";
default: return "unknown";
}
}
config_type_t string_to_config_type(const char* str) {
if (!str) return CONFIG_TYPE_USER;
if (strcmp(str, "system") == 0) return CONFIG_TYPE_SYSTEM;
if (strcmp(str, "user") == 0) return CONFIG_TYPE_USER;
if (strcmp(str, "runtime") == 0) return CONFIG_TYPE_RUNTIME;
return CONFIG_TYPE_USER;
}
config_data_type_t string_to_config_data_type(const char* str) {
if (!str) return CONFIG_DATA_STRING;
if (strcmp(str, "string") == 0) return CONFIG_DATA_STRING;
if (strcmp(str, "integer") == 0) return CONFIG_DATA_INTEGER;
if (strcmp(str, "boolean") == 0) return CONFIG_DATA_BOOLEAN;
if (strcmp(str, "json") == 0) return CONFIG_DATA_JSON;
return CONFIG_DATA_STRING;
}
int config_requires_restart(const char* key) {
if (!key) return 0;
// Check database for requires_restart flag
const char* sql = "SELECT requires_restart FROM server_config WHERE key = ?";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return 0;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
int requires_restart = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
requires_restart = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return requires_restart;
}
// ================================
// NOSTR EVENT GENERATION FUNCTIONS
// ================================
#include <sys/stat.h>
cJSON* create_config_nostr_event(const char* privkey_hex) {
log_info("Creating configuration Nostr event...");
// Convert hex private key to bytes
unsigned char privkey_bytes[32];
if (nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) != 0) {
log_error("Failed to convert private key from hex");
return NULL;
}
// Create tags array with default configuration values
cJSON* tags = cJSON_CreateArray();
// Default configuration values (moved from schema.sql)
typedef struct {
const char* key;
const char* value;
} default_config_t;
static const default_config_t defaults[] = {
// Administrative settings
{"admin_enabled", "false"},
// Server core settings
{"relay_port", "8888"},
{"database_path", "db/c_nostr_relay.db"},
{"max_connections", "100"},
// NIP-11 Relay Information
{"relay_name", "C Nostr Relay"},
{"relay_description", "High-performance C Nostr relay with SQLite storage"},
{"relay_contact", ""},
{"relay_pubkey", ""},
{"relay_software", "https://git.laantungir.net/laantungir/c-relay.git"},
{"relay_version", VERSION},
// NIP-13 Proof of Work
{"pow_enabled", "true"},
{"pow_min_difficulty", "0"},
{"pow_mode", "basic"},
// NIP-40 Expiration Timestamp
{"expiration_enabled", "true"},
{"expiration_strict", "true"},
{"expiration_filter", "true"},
{"expiration_grace_period", "300"},
// Subscription limits
{"max_subscriptions_per_client", "25"},
{"max_total_subscriptions", "5000"},
{"max_filters_per_subscription", "10"},
// Event processing limits
{"max_event_tags", "100"},
{"max_content_length", "8196"},
{"max_message_length", "16384"},
// Performance settings
{"default_limit", "500"},
{"max_limit", "5000"}
};
int defaults_count = sizeof(defaults) / sizeof(defaults[0]);
// First try to load from database, fall back to defaults
const char* sql = "SELECT key, value FROM server_config WHERE config_type IN ('system', 'user') ORDER BY key";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
// Load existing values from database
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);
// Skip admin_pubkey since it's redundant (already in event.pubkey)
if (key && value && strcmp(key, "admin_pubkey") != 0) {
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString(key));
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
cJSON_AddItemToArray(tags, tag);
}
}
sqlite3_finalize(stmt);
}
// If database is empty, use defaults
if (cJSON_GetArraySize(tags) == 0) {
log_info("Database empty, using default configuration values");
for (int i = 0; i < defaults_count; i++) {
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString(defaults[i].key));
cJSON_AddItemToArray(tag, cJSON_CreateString(defaults[i].value));
cJSON_AddItemToArray(tags, tag);
}
}
// Create and sign event using nostr_core_lib
cJSON* event = nostr_create_and_sign_event(
33334, // kind
"C Nostr Relay configuration event", // content
tags, // tags
privkey_bytes, // private key bytes for signing
time(NULL) // created_at timestamp
);
cJSON_Delete(tags); // Clean up tags as they were duplicated in nostr_create_and_sign_event
if (!event) {
log_error("Failed to create and sign configuration event");
return NULL;
}
// Log success information
cJSON* id_obj = cJSON_GetObjectItem(event, "id");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (id_obj && pubkey_obj) {
log_success("Configuration Nostr event created successfully");
printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj));
printf(" Public Key: %s\n", cJSON_GetStringValue(pubkey_obj));
}
return event;
}
int write_config_event_to_file(const cJSON* event) {
if (!event) {
return -1;
}
// Ensure config directory exists
struct stat st = {0};
if (stat(g_config_manager.config_dir_path, &st) == -1) {
if (mkdir(g_config_manager.config_dir_path, 0700) != 0) {
log_error("Failed to create configuration directory");
return -1;
}
log_info("Created configuration directory: %s");
printf(" %s\n", g_config_manager.config_dir_path);
}
// Write to file with custom formatting for better readability
FILE* file = fopen(g_config_manager.config_file_path, "w");
if (!file) {
log_error("Failed to open configuration file for writing");
return -1;
}
// Custom formatting: tags first, then other fields
fprintf(file, "{\n");
// First, write tags array with each tag on its own line
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
fprintf(file, " \"tags\": [\n");
int tag_count = cJSON_GetArraySize(tags);
for (int i = 0; i < tag_count; i++) {
cJSON* tag = cJSON_GetArrayItem(tags, i);
if (tag && cJSON_IsArray(tag)) {
char* tag_str = cJSON_Print(tag);
if (tag_str) {
fprintf(file, " %s%s\n", tag_str, (i < tag_count - 1) ? "," : "");
free(tag_str);
}
}
}
fprintf(file, " ],\n");
}
// Then write other fields in order
const char* field_order[] = {"id", "pubkey", "created_at", "kind", "content", "sig"};
int field_count = sizeof(field_order) / sizeof(field_order[0]);
for (int i = 0; i < field_count; i++) {
cJSON* field = cJSON_GetObjectItem(event, field_order[i]);
if (field) {
fprintf(file, " \"%s\": ", field_order[i]);
if (cJSON_IsString(field)) {
fprintf(file, "\"%s\"", cJSON_GetStringValue(field));
} else if (cJSON_IsNumber(field)) {
fprintf(file, "%ld", (long)cJSON_GetNumberValue(field));
}
if (i < field_count - 1) {
fprintf(file, ",");
}
fprintf(file, "\n");
}
}
fprintf(file, "}\n");
fclose(file);
log_success("Configuration file written successfully");
printf(" File: %s\n", g_config_manager.config_file_path);
return 0;
}
int generate_config_file_if_missing(void) {
// Check if config file already exists
if (config_file_exists()) {
log_info("Configuration file already exists, skipping generation");
return 0;
}
log_info("Generating missing configuration file...");
// Get private key from environment variable or generate random one
char privkey_hex[65];
const char* env_privkey = getenv(CONFIG_PRIVKEY_ENV);
if (env_privkey && strlen(env_privkey) == 64) {
// Use provided private key
strncpy(privkey_hex, env_privkey, sizeof(privkey_hex) - 1);
privkey_hex[sizeof(privkey_hex) - 1] = '\0';
log_info("Using private key from environment variable");
} else {
// Generate random private key manually (nostr_core_lib doesn't have a key generation function)
FILE* urandom = fopen("/dev/urandom", "rb");
if (!urandom) {
log_error("Failed to open /dev/urandom for key generation");
return -1;
}
unsigned char privkey_bytes[32];
if (fread(privkey_bytes, 1, 32, urandom) != 32) {
log_error("Failed to read random bytes for private key");
fclose(urandom);
return -1;
}
fclose(urandom);
// Convert to hex
nostr_bytes_to_hex(privkey_bytes, 32, privkey_hex);
// Generate corresponding public key
char pubkey_hex[65];
unsigned char pubkey_bytes[32];
if (nostr_ec_public_key_from_private_key(privkey_bytes, pubkey_bytes) == 0) {
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
} else {
log_error("Failed to derive public key from private key");
return -1;
}
log_info("Generated random private key for configuration signing");
// Print the generated private key prominently for the administrator
printf("\n");
printf("=================================================================\n");
printf("IMPORTANT: GENERATED RELAY ADMIN PRIVATE KEY\n");
printf("=================================================================\n");
printf("Private Key: %s\n", privkey_hex);
printf("Public Key: %s\n", pubkey_hex);
printf("\nSAVE THIS PRIVATE KEY SECURELY - IT CONTROLS YOUR RELAY!\n");
printf("\nTo use this key in future sessions:\n");
printf(" export %s=%s\n", CONFIG_PRIVKEY_ENV, privkey_hex);
printf("=================================================================\n");
printf("\n");
char warning_msg[256];
snprintf(warning_msg, sizeof(warning_msg),
"To use a specific private key, set the %s environment variable", CONFIG_PRIVKEY_ENV);
log_warning(warning_msg);
}
// Create Nostr event
cJSON* event = create_config_nostr_event(privkey_hex);
if (!event) {
log_error("Failed to create configuration event");
return -1;
}
// Extract and store the admin public key in database configuration
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (pubkey_obj && cJSON_IsString(pubkey_obj)) {
const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj);
if (set_database_config("admin_pubkey", admin_pubkey, "system") == 0) {
log_info("Stored admin public key in configuration database");
printf(" Admin Public Key: %s\n", admin_pubkey);
} else {
log_warning("Failed to store admin public key in database");
}
}
// Write to file
int result = write_config_event_to_file(event);
cJSON_Delete(event);
if (result == 0) {
log_success("Configuration file generated successfully");
} else {
log_error("Failed to write configuration file");
}
return result;
}