Compare commits

..

4 Commits

Author SHA1 Message Date
Your Name
1690b58c67 v0.3.4 - Implement secure relay private key storage
- Add relay_seckey table for secure private key storage
- Implement store_relay_private_key() and get_relay_private_key() functions
- Remove relay private key from public configuration events (kind 33334)
- Update first-time startup sequence to store keys securely after DB init
- Add proper validation and error handling for private key operations
- Fix timing issue where private key storage was attempted before DB initialization
- Security improvement: relay private keys no longer exposed in public events
2025-09-07 07:35:51 -04:00
Your Name
2e8eda5c67 v0.3.3 - Fix function naming consistency: rename find_existing_nrdb_files to find_existing_db_files
- Update function declaration in config.h
- Update function definition in config.c
- Update function calls in config.c and main.c
- Maintain consistency with .db file extension naming convention

This resolves the inconsistency between database file extension (.db) and function names (nrdb)
2025-09-07 06:58:50 -04:00
Your Name
74a4dc2533 v0.3.2 - Implement -p/--port CLI option for first-time startup port override
- Add cli_options_t structure for extensible command line options
- Implement port override in create_default_config_event()
- Update main() with robust CLI parsing and validation
- Add comprehensive help text documenting first-time only behavior
- Ensure CLI options only affect initial configuration event creation
- Maintain event-based configuration architecture for ongoing operation
- Include comprehensive error handling and input validation
- Add documentation in CLI_PORT_OVERRIDE_IMPLEMENTATION.md

Tested: First-time startup uses CLI port, subsequent startups use database config
2025-09-07 06:54:56 -04:00
Your Name
be7ae2b580 v0.3.1 - Implement database location and extension changes
- Change database extension from .nrdb to .db for standard SQLite convention
- Modify make_and_restart_relay.sh to run executable from build/ directory
- Database files now created in build/ directory alongside executable
- Enhanced --preserve-database flag with backup/restore functionality
- Updated source code references in config.c and main.c
- Port auto-increment functionality remains fully functional
2025-09-07 06:15:49 -04:00
10 changed files with 743 additions and 50 deletions

5
.roo/commands/push.md Normal file
View File

@@ -0,0 +1,5 @@
---
description: "Brief description of what this command does"
---
Run build_and_push.sh, and supply a good git commit message.

Binary file not shown.

View File

@@ -38,7 +38,7 @@ if [ "$HELP" = true ]; then
echo "Event-Based Configuration:"
echo " This relay now uses event-based configuration stored directly in the database."
echo " On first startup, keys are automatically generated and printed once."
echo " Database file: <relay_pubkey>.nrdb (created automatically)"
echo " Database file: <relay_pubkey>.db (created automatically)"
echo ""
echo "Examples:"
echo " $0 # Fresh start with new keys (default)"
@@ -51,15 +51,22 @@ fi
# Handle database file cleanup for fresh start
if [ "$PRESERVE_DATABASE" = false ]; then
if ls *.nrdb >/dev/null 2>&1; then
if ls *.db >/dev/null 2>&1 || ls build/*.db >/dev/null 2>&1; then
echo "Removing existing database files to trigger fresh key generation..."
rm -f *.nrdb
rm -f *.db build/*.db
echo "✓ Database files removed - will generate new keys and database"
else
echo "No existing database found - will generate fresh setup"
fi
else
echo "Preserving existing database files as requested"
# Back up database files before clean build
if ls build/*.db >/dev/null 2>&1; then
echo "Backing up existing database files..."
mkdir -p /tmp/relay_backup_$$
cp build/*.db* /tmp/relay_backup_$$/ 2>/dev/null || true
echo "Database files backed up to temporary location"
fi
fi
# Clean up legacy files that are no longer used
@@ -70,6 +77,14 @@ rm -f db/c_nostr_relay.db* 2>/dev/null
echo "Building project..."
make clean all
# Restore database files if preserving
if [ "$PRESERVE_DATABASE" = true ] && [ -d "/tmp/relay_backup_$$" ]; then
echo "Restoring preserved database files..."
cp /tmp/relay_backup_$$/*.db* build/ 2>/dev/null || true
rm -rf /tmp/relay_backup_$$
echo "Database files restored to build directory"
fi
# Check if build was successful
if [ $? -ne 0 ]; then
echo "ERROR: Build failed. Cannot restart relay."
@@ -129,9 +144,13 @@ echo "Database will be initialized automatically on startup if needed"
echo "Starting relay server..."
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
# Change to build directory before starting relay so database files are created there
cd build
# Start relay in background and capture its PID (no command line arguments needed)
$BINARY_PATH > relay.log 2>&1 &
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
RELAY_PID=$!
# Change back to original directory
cd ..
echo "Started with PID: $RELAY_PID"

View File

@@ -1 +1 @@
1073070
1198669

View File

@@ -29,11 +29,14 @@ static cJSON* g_current_config = NULL;
// Cache for initial configuration event (before database is initialized)
static cJSON* g_pending_config_event = NULL;
// Temporary storage for relay private key during first-time setup
static char g_temp_relay_privkey[65] = {0};
// ================================
// UTILITY FUNCTIONS
// ================================
char** find_existing_nrdb_files(void) {
char** find_existing_db_files(void) {
DIR *dir;
struct dirent *entry;
char **files = NULL;
@@ -44,9 +47,9 @@ char** find_existing_nrdb_files(void) {
return NULL;
}
// Count .nrdb files
// Count .db files
while ((entry = readdir(dir)) != NULL) {
if (strstr(entry->d_name, ".nrdb") != NULL) {
if (strstr(entry->d_name, ".db") != NULL) {
count++;
}
}
@@ -67,7 +70,7 @@ char** find_existing_nrdb_files(void) {
// Store filenames
int i = 0;
while ((entry = readdir(dir)) != NULL && i < count) {
if (strstr(entry->d_name, ".nrdb") != NULL) {
if (strstr(entry->d_name, ".db") != NULL) {
files[i] = malloc(strlen(entry->d_name) + 1);
if (files[i]) {
strcpy(files[i], entry->d_name);
@@ -84,8 +87,8 @@ char** find_existing_nrdb_files(void) {
char* extract_pubkey_from_filename(const char* filename) {
if (!filename) return NULL;
// Find .nrdb extension
const char* dot = strstr(filename, ".nrdb");
// Find .db extension
const char* dot = strstr(filename, ".db");
if (!dot) return NULL;
// Calculate pubkey length
@@ -107,10 +110,10 @@ char* get_database_name_from_relay_pubkey(const char* relay_pubkey) {
return NULL;
}
char* db_name = malloc(strlen(relay_pubkey) + 6); // +6 for ".nrdb\0"
char* db_name = malloc(strlen(relay_pubkey) + 4); // +4 for ".db\0"
if (!db_name) return NULL;
sprintf(db_name, "%s.nrdb", relay_pubkey);
sprintf(db_name, "%s.db", relay_pubkey);
return db_name;
}
@@ -342,7 +345,7 @@ int get_config_bool(const char* key, int default_value) {
// ================================
int is_first_time_startup(void) {
char** existing_files = find_existing_nrdb_files();
char** existing_files = find_existing_db_files();
if (existing_files) {
// Free the array
for (int i = 0; existing_files[i]; i++) {
@@ -437,13 +440,111 @@ int generate_random_private_key_bytes(unsigned char* privkey_bytes) {
return 0;
}
// ================================
// SECURE RELAY PRIVATE KEY STORAGE
// ================================
int store_relay_private_key(const char* relay_privkey_hex) {
if (!relay_privkey_hex) {
log_error("Invalid relay private key for storage");
return -1;
}
// Validate private key format (must be 64 hex characters)
if (strlen(relay_privkey_hex) != 64) {
log_error("Invalid relay private key length (must be 64 hex characters)");
return -1;
}
// Validate hex format
for (int i = 0; i < 64; i++) {
char c = relay_privkey_hex[i];
if (!((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
log_error("Invalid relay private key format (must be hex characters only)");
return -1;
}
}
if (!g_db) {
log_error("Database not available for relay private key storage");
return -1;
}
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare relay private key storage query");
return -1;
}
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) {
log_success("Relay private key stored securely in database");
return 0;
} else {
log_error("Failed to store relay private key in database");
return -1;
}
}
char* get_relay_private_key(void) {
if (!g_db) {
log_error("Database not available for relay private key retrieval");
return NULL;
}
const char* sql = "SELECT private_key_hex FROM relay_seckey";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare relay private key retrieval query");
return NULL;
}
char* private_key = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0);
if (key_from_db && strlen(key_from_db) == 64) {
private_key = malloc(65); // 64 chars + null terminator
if (private_key) {
strcpy(private_key, key_from_db);
}
}
}
sqlite3_finalize(stmt);
if (!private_key) {
log_error("Relay private key not found in secure storage");
}
return private_key;
}
const char* get_temp_relay_private_key(void) {
if (strlen(g_temp_relay_privkey) == 64) {
return g_temp_relay_privkey;
}
return NULL;
}
// ================================
// DEFAULT CONFIG EVENT CREATION
// ================================
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
const char* relay_privkey_hex,
const char* relay_pubkey_hex) {
const char* relay_pubkey_hex,
const cli_options_t* cli_options) {
if (!admin_privkey_bytes || !relay_privkey_hex || !relay_pubkey_hex) {
log_error("Invalid parameters for creating default config event");
return NULL;
@@ -470,16 +571,31 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
cJSON_AddItemToArray(relay_pubkey_tag, cJSON_CreateString(relay_pubkey_hex));
cJSON_AddItemToArray(tags, relay_pubkey_tag);
cJSON* relay_privkey_tag = cJSON_CreateArray();
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString("relay_privkey"));
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString(relay_privkey_hex));
cJSON_AddItemToArray(tags, relay_privkey_tag);
// Note: relay_privkey is now stored securely in relay_seckey table
// It is no longer included in the public configuration event
// Add all default configuration values
// Add all default configuration values with command line overrides
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].key));
cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].value));
// Check for command line overrides
const char* value = DEFAULT_CONFIG_VALUES[i].value;
if (cli_options) {
// Override relay_port if specified on command line
if (cli_options->port_override > 0 && strcmp(DEFAULT_CONFIG_VALUES[i].key, "relay_port") == 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
cJSON_AddItemToArray(tag, cJSON_CreateString(port_str));
log_info("Using command line port override in configuration event");
printf(" Port: %d (overriding default %s)\n", cli_options->port_override, DEFAULT_CONFIG_VALUES[i].value);
} else {
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
}
} else {
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
}
cJSON_AddItemToArray(tags, tag);
}
@@ -516,7 +632,7 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
// IMPLEMENTED FUNCTIONS
// ================================
int first_time_startup_sequence(void) {
int first_time_startup_sequence(const cli_options_t* cli_options) {
log_info("Starting first-time startup sequence...");
// 1. Generate admin keypair using /dev/urandom + nostr_core_lib
@@ -565,14 +681,19 @@ int first_time_startup_sequence(void) {
return -1;
}
// 5. Create initial configuration event using defaults
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
// 5. Store relay private key in temporary storage for later secure storage
strncpy(g_temp_relay_privkey, relay_privkey, sizeof(g_temp_relay_privkey) - 1);
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
log_info("Relay private key cached for secure storage after database initialization");
// 6. Create initial configuration event using defaults (without private key)
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
if (!config_event) {
log_error("Failed to create default configuration event");
return -1;
}
// 6. Try to store configuration event in database, but cache it if database isn't ready
// 7. Try to store configuration event in database, but cache it if database isn't ready
if (store_config_event_in_database(config_event) == 0) {
log_success("Initial configuration event stored successfully");
} else {
@@ -584,16 +705,16 @@ int first_time_startup_sequence(void) {
g_pending_config_event = cJSON_Duplicate(config_event, 1);
}
// 7. Cache the current config
// 8. Cache the current config
if (g_current_config) {
cJSON_Delete(g_current_config);
}
g_current_config = cJSON_Duplicate(config_event, 1);
// 8. Clean up
// 9. Clean up
cJSON_Delete(config_event);
// 9. Print admin private key for user to save
// 10. Print admin private key for user to save
printf("\n");
printf("=================================================================\n");
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
@@ -642,6 +763,373 @@ int startup_existing_relay(const char* relay_pubkey) {
return 0;
}
// ================================
// CONFIGURATION FIELD VALIDATION
// ================================
// Validation helper functions
static int is_valid_port(const char* port_str) {
if (!port_str) return 0;
char* endptr;
long port = strtol(port_str, &endptr, 10);
// Must be valid number and in valid port range
return (endptr != port_str && *endptr == '\0' && port >= 1 && port <= 65535);
}
static int is_valid_boolean(const char* bool_str) {
if (!bool_str) return 0;
return (strcasecmp(bool_str, "true") == 0 ||
strcasecmp(bool_str, "false") == 0 ||
strcasecmp(bool_str, "yes") == 0 ||
strcasecmp(bool_str, "no") == 0 ||
strcasecmp(bool_str, "1") == 0 ||
strcasecmp(bool_str, "0") == 0);
}
static int is_valid_positive_integer(const char* int_str) {
if (!int_str) return 0;
char* endptr;
long val = strtol(int_str, &endptr, 10);
return (endptr != int_str && *endptr == '\0' && val >= 0);
}
static int is_valid_non_negative_integer(const char* int_str) {
if (!int_str) return 0;
char* endptr;
long val = strtol(int_str, &endptr, 10);
return (endptr != int_str && *endptr == '\0' && val >= 0);
}
static int is_valid_string_length(const char* str, size_t max_length) {
if (!str) return 1; // NULL strings are valid (use defaults)
return strlen(str) <= max_length;
}
static int is_valid_pow_mode(const char* mode_str) {
if (!mode_str) return 0;
return (strcasecmp(mode_str, "basic") == 0 ||
strcasecmp(mode_str, "strict") == 0 ||
strcasecmp(mode_str, "disabled") == 0);
}
static int is_valid_hex_key(const char* key_str) {
if (!key_str) return 0;
// Must be exactly 64 hex characters
if (strlen(key_str) != 64) return 0;
// Must contain only hex characters
for (int i = 0; i < 64; i++) {
char c = key_str[i];
if (!((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) {
return 0;
}
}
return 1;
}
// Main validation function for configuration fields
static int validate_config_field(const char* key, const char* value, char* error_msg, size_t error_size) {
if (!key || !value) {
snprintf(error_msg, error_size, "key or value is NULL");
return -1;
}
// Port validation
if (strcmp(key, "relay_port") == 0) {
if (!is_valid_port(value)) {
snprintf(error_msg, error_size, "invalid port number '%s' (must be 1-65535)", value);
return -1;
}
return 0;
}
// Connection limits
if (strcmp(key, "max_connections") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_connections '%s' (must be positive integer)", value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 10000) {
snprintf(error_msg, error_size, "max_connections '%s' out of range (1-10000)", value);
return -1;
}
return 0;
}
// Boolean fields
if (strcmp(key, "auth_enabled") == 0 ||
strcmp(key, "nip40_expiration_enabled") == 0 ||
strcmp(key, "nip40_expiration_strict") == 0 ||
strcmp(key, "nip40_expiration_filter") == 0) {
if (!is_valid_boolean(value)) {
snprintf(error_msg, error_size, "invalid boolean value '%s' for %s", value, key);
return -1;
}
return 0;
}
// String length validation
if (strcmp(key, "relay_description") == 0) {
if (!is_valid_string_length(value, RELAY_DESCRIPTION_MAX_LENGTH)) {
snprintf(error_msg, error_size, "relay_description too long (max %d characters)", RELAY_DESCRIPTION_MAX_LENGTH);
return -1;
}
return 0;
}
if (strcmp(key, "relay_contact") == 0) {
if (!is_valid_string_length(value, RELAY_CONTACT_MAX_LENGTH)) {
snprintf(error_msg, error_size, "relay_contact too long (max %d characters)", RELAY_CONTACT_MAX_LENGTH);
return -1;
}
return 0;
}
if (strcmp(key, "relay_software") == 0 ||
strcmp(key, "relay_version") == 0) {
if (!is_valid_string_length(value, 256)) {
snprintf(error_msg, error_size, "%s too long (max 256 characters)", key);
return -1;
}
return 0;
}
// PoW difficulty validation
if (strcmp(key, "pow_min_difficulty") == 0) {
if (!is_valid_non_negative_integer(value)) {
snprintf(error_msg, error_size, "invalid pow_min_difficulty '%s' (must be non-negative integer)", value);
return -1;
}
int val = atoi(value);
if (val > 32) { // 32 is practically impossible
snprintf(error_msg, error_size, "pow_min_difficulty '%s' too high (max 32)", value);
return -1;
}
return 0;
}
// PoW mode validation
if (strcmp(key, "pow_mode") == 0) {
if (!is_valid_pow_mode(value)) {
snprintf(error_msg, error_size, "invalid pow_mode '%s' (must be basic, strict, or disabled)", value);
return -1;
}
return 0;
}
// Time-based validation
if (strcmp(key, "nip40_expiration_grace_period") == 0) {
if (!is_valid_non_negative_integer(value)) {
snprintf(error_msg, error_size, "invalid grace period '%s' (must be non-negative integer)", value);
return -1;
}
int val = atoi(value);
if (val > 86400) { // Max 1 day
snprintf(error_msg, error_size, "grace period '%s' too long (max 86400 seconds)", value);
return -1;
}
return 0;
}
// Subscription limits
if (strcmp(key, "max_subscriptions_per_client") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_subscriptions_per_client '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 1000) {
snprintf(error_msg, error_size, "max_subscriptions_per_client '%s' out of range (1-1000)", value);
return -1;
}
return 0;
}
if (strcmp(key, "max_total_subscriptions") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_total_subscriptions '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 100000) {
snprintf(error_msg, error_size, "max_total_subscriptions '%s' out of range (1-100000)", value);
return -1;
}
return 0;
}
if (strcmp(key, "max_filters_per_subscription") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_filters_per_subscription '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 100) {
snprintf(error_msg, error_size, "max_filters_per_subscription '%s' out of range (1-100)", value);
return -1;
}
return 0;
}
// Event limits
if (strcmp(key, "max_event_tags") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_event_tags '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 1000) {
snprintf(error_msg, error_size, "max_event_tags '%s' out of range (1-1000)", value);
return -1;
}
return 0;
}
if (strcmp(key, "max_content_length") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_content_length '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 100 || val > 1048576) { // 1MB max
snprintf(error_msg, error_size, "max_content_length '%s' out of range (100-1048576)", value);
return -1;
}
return 0;
}
if (strcmp(key, "max_message_length") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid max_message_length '%s'", value);
return -1;
}
int val = atoi(value);
if (val < 1024 || val > 1048576) { // 1KB to 1MB
snprintf(error_msg, error_size, "max_message_length '%s' out of range (1024-1048576)", value);
return -1;
}
return 0;
}
// Performance limits
if (strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0) {
if (!is_valid_positive_integer(value)) {
snprintf(error_msg, error_size, "invalid %s '%s'", key, value);
return -1;
}
int val = atoi(value);
if (val < 1 || val > 50000) {
snprintf(error_msg, error_size, "%s '%s' out of range (1-50000)", key, value);
return -1;
}
return 0;
}
// Key validation for relay keys
if (strcmp(key, "relay_pubkey") == 0 || strcmp(key, "relay_privkey") == 0) {
if (!is_valid_hex_key(value)) {
snprintf(error_msg, error_size, "invalid %s format (must be 64-character hex)", key);
return -1;
}
return 0;
}
// Special validation for d tag (relay identifier)
if (strcmp(key, "d") == 0) {
if (!is_valid_hex_key(value)) {
snprintf(error_msg, error_size, "invalid relay identifier 'd' format (must be 64-character hex)");
return -1;
}
return 0;
}
// Unknown field - log warning but allow
log_warning("Unknown configuration field");
printf(" Field: %s = %s\n", key, value);
return 0;
}
// Validate all fields in a configuration event
static int validate_configuration_event_fields(const cJSON* event, char* error_msg, size_t error_size) {
if (!event) {
snprintf(error_msg, error_size, "null configuration event");
return -1;
}
log_info("Validating configuration event fields...");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
snprintf(error_msg, error_size, "missing or invalid tags array");
return -1;
}
int validated_fields = 0;
int validation_errors = 0;
char field_error[512];
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_key = cJSON_GetArrayItem(tag, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
if (tag_key && tag_value &&
cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) {
const char* key = cJSON_GetStringValue(tag_key);
const char* value = cJSON_GetStringValue(tag_value);
if (validate_config_field(key, value, field_error, sizeof(field_error)) != 0) {
// Safely truncate the error message if needed
size_t prefix_len = strlen("field validation failed: ");
size_t available_space = error_size > prefix_len ? error_size - prefix_len - 1 : 0;
if (available_space > 0) {
snprintf(error_msg, error_size, "field validation failed: %.*s",
(int)available_space, field_error);
} else {
strncpy(error_msg, "field validation failed", error_size - 1);
error_msg[error_size - 1] = '\0';
}
log_error("Configuration field validation failed");
printf(" Field: %s = %s\n", key, value);
printf(" Error: %s\n", field_error);
validation_errors++;
return -1; // Stop on first error
} else {
validated_fields++;
}
}
}
}
if (validation_errors > 0) {
char summary[256];
snprintf(summary, sizeof(summary), "%d configuration fields failed validation", validation_errors);
log_error(summary);
return -1;
}
char success_msg[256];
snprintf(success_msg, sizeof(success_msg), "%d configuration fields validated successfully", validated_fields);
log_success(success_msg);
return 0;
}
int process_configuration_event(const cJSON* event) {
if (!event) {
log_error("Invalid configuration event");
@@ -690,6 +1178,14 @@ int process_configuration_event(const cJSON* event) {
log_success("Configuration event structure and signature validated successfully");
// NEW: Validate configuration field values
char validation_error[512];
if (validate_configuration_event_fields(event, validation_error, sizeof(validation_error)) != 0) {
log_error("Configuration field validation failed");
printf(" Validation error: %s\n", validation_error);
return -1;
}
// Store in database
if (store_config_event_in_database(event) != 0) {
log_error("Failed to store configuration event");
@@ -702,7 +1198,7 @@ int process_configuration_event(const cJSON* event) {
return -1;
}
log_success("Configuration event processed successfully");
log_success("Configuration event processed successfully with field validation");
return 0;
}

View File

@@ -32,6 +32,12 @@ typedef struct {
char config_file_path[512]; // Temporary for compatibility
} config_manager_t;
// Command line options structure for first-time startup
typedef struct {
int port_override; // -1 = not set, >0 = port value
// Future CLI options can be added here
} cli_options_t;
// Global configuration manager
extern config_manager_t g_config_manager;
@@ -62,7 +68,7 @@ int get_config_bool(const char* key, int default_value);
// First-time startup functions
int is_first_time_startup(void);
int first_time_startup_sequence(void);
int first_time_startup_sequence(const cli_options_t* cli_options);
int startup_existing_relay(const char* relay_pubkey);
// Configuration application functions
@@ -70,7 +76,12 @@ int apply_configuration_from_event(const cJSON* event);
int apply_runtime_config_handlers(const cJSON* old_event, const cJSON* new_event);
// Utility functions
char** find_existing_nrdb_files(void);
char** find_existing_db_files(void);
char* extract_pubkey_from_filename(const char* filename);
// Secure relay private key storage functions
int store_relay_private_key(const char* relay_privkey_hex);
char* get_relay_private_key(void);
const char* get_temp_relay_private_key(void); // For first-time startup only
#endif /* CONFIG_H */

View File

@@ -2,6 +2,7 @@
#define DEFAULT_CONFIG_EVENT_H
#include <cjson/cJSON.h>
#include "config.h" // For cli_options_t definition
/*
* Default Configuration Event Template
@@ -61,8 +62,9 @@ static const struct {
#define DEFAULT_CONFIG_COUNT (sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]))
// Function to create default configuration event
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
const char* relay_privkey_hex,
const char* relay_pubkey_hex);
const char* relay_pubkey_hex,
const cli_options_t* cli_options);
#endif /* DEFAULT_CONFIG_EVENT_H */

View File

@@ -10,6 +10,10 @@
#include <pthread.h>
#include <sqlite3.h>
#include <libwebsockets.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// Include nostr_core_lib for Nostr functionality
#include "../nostr_core_lib/cjson/cJSON.h"
@@ -2956,6 +2960,34 @@ static struct lws_protocols protocols[] = {
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
};
// Check if a port is available for binding
int check_port_available(int port) {
int sockfd;
struct sockaddr_in addr;
int result;
// Create a socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
return 0; // Cannot create socket, assume port unavailable
}
// Set up the address structure
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
// Try to bind to the port
result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// Close the socket
close(sockfd);
// Return 1 if bind succeeded (port available), 0 if failed (port in use)
return (result == 0) ? 1 : 0;
}
// Start libwebsockets-based WebSocket Nostr relay server
int start_websocket_relay(int port_override) {
struct lws_context_creation_info info;
@@ -2964,12 +2996,15 @@ int start_websocket_relay(int port_override) {
memset(&info, 0, sizeof(info));
// Use port override if provided, otherwise use configuration
info.port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int actual_port = configured_port;
int port_attempts = 0;
const int max_port_attempts = 5;
// Minimal libwebsockets configuration
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
// Minimal libwebsockets configuration
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
// Remove interface restrictions - let system choose
@@ -2983,15 +3018,82 @@ int start_websocket_relay(int port_override) {
// Max payload size for Nostr events
info.max_http_header_data = 4096;
ws_context = lws_create_context(&info);
// Find an available port with pre-checking
while (port_attempts < max_port_attempts) {
char attempt_msg[256];
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
log_info(attempt_msg);
// Pre-check if port is available
if (!check_port_available(actual_port)) {
port_attempts++;
if (port_attempts < max_port_attempts) {
char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
log_warning(retry_msg);
actual_port++;
continue;
} else {
char error_msg[512];
snprintf(error_msg, sizeof(error_msg),
"Failed to find available port after %d attempts (tried ports %d-%d)",
max_port_attempts, configured_port, actual_port);
log_error(error_msg);
return -1;
}
}
// Port appears available, try creating libwebsockets context
info.port = actual_port;
char binding_msg[256];
snprintf(binding_msg, sizeof(binding_msg), "Attempting to bind libwebsockets to port %d", actual_port);
log_info(binding_msg);
ws_context = lws_create_context(&info);
if (ws_context) {
// Success! Port binding worked
break;
}
// libwebsockets failed even though port check passed
// This could be due to timing or different socket options
int errno_saved = errno;
char lws_error_msg[256];
snprintf(lws_error_msg, sizeof(lws_error_msg),
"libwebsockets failed to bind to port %d (errno: %d)", actual_port, errno_saved);
log_warning(lws_error_msg);
port_attempts++;
if (port_attempts < max_port_attempts) {
actual_port++;
continue;
}
// If we get here, we've exhausted attempts
break;
}
if (!ws_context) {
log_error("Failed to create libwebsockets context");
char error_msg[512];
snprintf(error_msg, sizeof(error_msg),
"Failed to create libwebsockets context after %d attempts. Last attempted port: %d",
port_attempts, actual_port);
log_error(error_msg);
perror("libwebsockets creation error");
return -1;
}
char startup_msg[256];
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", info.port);
if (actual_port != configured_port) {
snprintf(startup_msg, sizeof(startup_msg),
"WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)",
actual_port, configured_port);
log_warning(startup_msg);
} else {
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
}
log_success(startup_msg);
// Main event loop with proper signal handling
@@ -3030,14 +3132,19 @@ void print_usage(const char* program_name) {
printf("Options:\n");
printf(" -h, --help Show this help message\n");
printf(" -v, --version Show version information\n");
printf(" -p, --port PORT Override relay port (first-time startup only)\n");
printf("\n");
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
printf(" On first startup, keys are automatically generated and printed once.\n");
printf(" Database file: <relay_pubkey>.nrdb (created automatically)\n");
printf(" Command line options like --port only apply during first-time setup.\n");
printf(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\n");
printf("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name);
printf(" %s -p 8080 # First-time setup with port 8080\n", program_name);
printf(" %s --port 9000 # First-time setup with port 9000\n", program_name);
printf(" %s --help # Show this help\n", program_name);
printf(" %s --version # Show version info\n", program_name);
printf("\n");
@@ -3052,7 +3159,12 @@ void print_version() {
}
int main(int argc, char* argv[]) {
// Parse minimal command line arguments (no configuration overrides)
// Initialize CLI options structure
cli_options_t cli_options = {
.port_override = -1 // -1 = not set
};
// Parse command line arguments
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
@@ -3060,6 +3172,30 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
print_version();
return 0;
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
// Port override option
if (i + 1 >= argc) {
log_error("Port option requires a value. Use --help for usage information.");
print_usage(argv[0]);
return 1;
}
// Parse port number
char* endptr;
long port = strtol(argv[i + 1], &endptr, 10);
if (endptr == argv[i + 1] || *endptr != '\0' || port < 1 || port > 65535) {
log_error("Invalid port number. Port must be between 1 and 65535.");
print_usage(argv[0]);
return 1;
}
cli_options.port_override = (int)port;
i++; // Skip the port argument
char port_msg[128];
snprintf(port_msg, sizeof(port_msg), "Port override specified: %d", cli_options.port_override);
log_info(port_msg);
} else {
log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]);
@@ -3091,8 +3227,8 @@ int main(int argc, char* argv[]) {
return 1;
}
// Run first-time startup sequence (generates keys, creates database, etc.)
if (first_time_startup_sequence() != 0) {
// Run first-time startup sequence (generates keys, sets up database path, but doesn't store private key yet)
if (first_time_startup_sequence(&cli_options) != 0) {
log_error("Failed to complete first-time startup sequence");
cleanup_configuration_system();
nostr_cleanup();
@@ -3107,6 +3243,23 @@ int main(int argc, char* argv[]) {
return 1;
}
// Now that database is available, store the relay private key securely
const char* relay_privkey = get_temp_relay_private_key();
if (relay_privkey) {
if (store_relay_private_key(relay_privkey) != 0) {
log_error("Failed to store relay private key securely after database initialization");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
log_success("Relay private key stored securely in database");
} else {
log_error("Relay private key not available from first-time startup");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
// Retry storing the configuration event now that database is initialized
if (retry_store_initial_config_event() != 0) {
log_warning("Failed to store initial configuration event after database init");
@@ -3115,7 +3268,7 @@ int main(int argc, char* argv[]) {
log_info("Existing relay detected");
// Find existing database file
char** existing_files = find_existing_nrdb_files();
char** existing_files = find_existing_db_files();
if (!existing_files || !existing_files[0]) {
log_error("No existing relay database found");
nostr_cleanup();

View File

@@ -1,12 +1,12 @@
/* Embedded SQL Schema for C Nostr Relay
* Generated from db/schema.sql - Do not edit manually
* Schema Version: 4
* Schema Version: 5
*/
#ifndef SQL_SCHEMA_H
#define SQL_SCHEMA_H
/* Schema version constant */
#define EMBEDDED_SCHEMA_VERSION "4"
#define EMBEDDED_SCHEMA_VERSION "5"
/* Embedded SQL schema as C string literal */
static const char* const EMBEDDED_SCHEMA_SQL =
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
-- Event-based configuration system using kind 33334 Nostr events\n\
\n\
-- Schema version tracking\n\
PRAGMA user_version = 4;\n\
PRAGMA user_version = 5;\n\
\n\
-- Enable foreign key support\n\
PRAGMA foreign_keys = ON;\n\
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
\n\
-- Insert schema metadata\n\
INSERT INTO schema_info (key, value) VALUES\n\
('version', '4'),\n\
('description', 'Event-based Nostr relay schema with kind 33334 configuration events'),\n\
('version', '5'),\n\
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
('created_at', strftime('%s', 'now'));\n\
\n\
-- Helper views for common queries\n\
@@ -128,6 +128,13 @@ BEGIN\n\
AND id != NEW.id;\n\
END;\n\
\n\
-- Relay Private Key Secure Storage\n\
-- Stores the relay's private key separately from public configuration\n\
CREATE TABLE relay_seckey (\n\
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),\n\
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
);\n\
\n\
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
-- Optional database logging for subscription analytics and debugging\n\
\n\