Compare commits

...

2 Commits

Author SHA1 Message Date
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
13 changed files with 856 additions and 31 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.

View File

@@ -0,0 +1,145 @@
# CLI Port Override Implementation
## Overview
This document describes the implementation of the `-p <port>` command line option for the C Nostr Relay, which allows overriding the default relay port during first-time startup only.
## Design Principles
1. **First-time startup only**: Command line options only affect the initial configuration event creation
2. **Event-based persistence**: After first startup, all configuration is managed through database events
3. **Proper encapsulation**: All configuration logic is contained within `config.c`
4. **Extensible design**: The CLI options structure can easily accommodate future command line options
## Implementation Details
### Files Modified
#### `src/config.h`
- Added `cli_options_t` structure to encapsulate command line options
- Updated `first_time_startup_sequence()` function signature
#### `src/config.c`
- Updated `first_time_startup_sequence()` to accept CLI options parameter
- Updated `create_default_config_event()` to accept CLI options parameter
- Implemented port override logic in DEFAULT_CONFIG_VALUES array processing
#### `src/default_config_event.h`
- Updated function signature for `create_default_config_event()`
- Added proper header include for `cli_options_t` definition
#### `src/main.c`
- Added command line parsing for `-p <port>` and `--port <port>`
- Updated help text to document the new option
- Added proper error handling for invalid port values
- Updated function call to pass CLI options to configuration system
### CLI Options Structure
```c
typedef struct {
int port_override; // -1 = not set, >0 = port value
// Future CLI options can be added here
} cli_options_t;
```
### Command Line Usage
```bash
# First-time startup with port override
./c_relay_x86 -p 9090
./c_relay_x86 --port 9090
# Show help (includes new option)
./c_relay_x86 --help
# Show version
./c_relay_x86 --version
```
### Error Handling
The implementation includes robust error handling for:
- Missing port argument: `./c_relay_x86 -p`
- Invalid port format: `./c_relay_x86 -p invalid_port`
- Out-of-range ports: `./c_relay_x86 -p 0` or `./c_relay_x86 -p 99999`
## Behavior Verification
### First-Time Startup
When no database exists:
1. Command line is parsed and `-p <port>` is processed
2. CLI options are passed to `first_time_startup_sequence()`
3. Port override is applied in `create_default_config_event()`
4. Configuration event is created with overridden port value
5. Relay starts on the specified port
6. Port setting is persisted in database for future startups
### Subsequent Startups
When database already exists:
1. Command line is still parsed (for consistency)
2. Existing relay path is taken
3. Configuration is loaded from database events
4. CLI options are ignored
5. Relay starts on port from database configuration
## Testing Results
### Test 1: First-time startup with port override
```bash
./c_relay_x86 -p 9090
```
**Result**: ✅ Relay starts on port 9090, configuration stored in database
### Test 2: Subsequent startup ignores CLI options
```bash
./c_relay_x86 -p 7777
```
**Result**: ✅ Relay starts on port 9090 (from database), ignores `-p 7777`
### Test 3: Error handling
```bash
./c_relay_x86 -p invalid_port
./c_relay_x86 -p
```
**Result**: ✅ Proper error messages and help text displayed
### Test 4: Help text
```bash
./c_relay_x86 --help
```
**Result**: ✅ Displays updated help with `-p, --port PORT` option
## Database Verification
The port setting is correctly stored in the database:
```sql
SELECT json_extract(tags, '$') FROM events WHERE kind = 33334;
```
Shows the overridden port value in the configuration event tags.
## Future Extensions
The `cli_options_t` structure is designed to be easily extended:
```c
typedef struct {
int port_override; // -1 = not set, >0 = port value
char* description_override; // Future: relay description override
int max_connections_override; // Future: connection limit override
// Add more options as needed
} cli_options_t;
```
## Key Design Benefits
1. **Separation of Concerns**: Main function handles CLI parsing, config system handles application
2. **First-time Only**: Prevents confusion about configuration precedence
3. **Event-based Architecture**: Maintains consistency with the relay's event-based configuration system
4. **Extensible**: Easy to add new command line options in the future
5. **Robust**: Comprehensive error handling and validation
6. **Documented**: Clear help text explains behavior to users
## Summary
The `-p <port>` command line option implementation successfully provides a way to override the default relay port during first-time startup while maintaining the event-based configuration architecture for ongoing operation. The implementation is robust, well-tested, and ready for production use.

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
1187428

35
relay2.log Normal file
View File

@@ -0,0 +1,35 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[WARNING] No configuration event found in existing database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode (default)
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Checking port availability: 8888
[WARNING] Port 8888 is in use, trying port 8889 (attempt 2/5)
[INFO] Checking port availability: 8889
[INFO] Attempting to bind libwebsockets to port 8889
[WARNING] WebSocket relay started on ws://127.0.0.1:8889 (configured port 8888 was unavailable)
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8889 (configured port 8888 was unavailable)
[INFO] Received shutdown signal
[INFO] Shutting down WebSocket server...
[SUCCESS] WebSocket relay shut down cleanly
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[SUCCESS] Server shutdown complete

37
relay3.log Normal file
View File

@@ -0,0 +1,37 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[WARNING] No configuration event found in existing database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode (default)
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Checking port availability: 8888
[WARNING] Port 8888 is in use, trying port 8889 (attempt 2/5)
[INFO] Checking port availability: 8889
[WARNING] Port 8889 is in use, trying port 8890 (attempt 3/5)
[INFO] Checking port availability: 8890
[INFO] Attempting to bind libwebsockets to port 8890
[WARNING] WebSocket relay started on ws://127.0.0.1:8890 (configured port 8888 was unavailable)
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8890 (configured port 8888 was unavailable)
[INFO] Received shutdown signal
[INFO] Shutting down WebSocket server...
[SUCCESS] WebSocket relay shut down cleanly
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[SUCCESS] Server shutdown complete

View File

@@ -44,9 +44,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 +67,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 +84,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 +107,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;
}
@@ -443,7 +443,8 @@ int generate_random_private_key_bytes(unsigned char* 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) {
if (!admin_privkey_bytes || !relay_privkey_hex || !relay_pubkey_hex) {
log_error("Invalid parameters for creating default config event");
return NULL;
@@ -475,11 +476,28 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString(relay_privkey_hex));
cJSON_AddItemToArray(tags, relay_privkey_tag);
// 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 +534,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
@@ -566,7 +584,7 @@ int first_time_startup_sequence(void) {
}
// 5. Create initial configuration event using defaults
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
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;
@@ -642,6 +660,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 +1075,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 +1095,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

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]);
@@ -3092,7 +3228,7 @@ int main(int argc, char* argv[]) {
}
// Run first-time startup sequence (generates keys, creates database, etc.)
if (first_time_startup_sequence() != 0) {
if (first_time_startup_sequence(&cli_options) != 0) {
log_error("Failed to complete first-time startup sequence");
cleanup_configuration_system();
nostr_cleanup();

47
test_port_increment.log Normal file
View File

@@ -0,0 +1,47 @@
=== C Nostr Relay Server ===
Event-based configuration system
[INFO] Existing relay detected
[INFO] Initializing event-based configuration system...
[SUCCESS] Event-based configuration system initialized
[INFO] Starting existing relay...
Relay pubkey: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a
[SUCCESS] Existing relay startup prepared
[SUCCESS] Database connection established: 6df436471c7965d6473e89998162e6b87cc3547d71a2db12f559a39f4596059a.nrdb
[INFO] Database schema already exists, skipping initialization
[INFO] Existing database schema version: 4
[INFO] Applying configuration from event...
[INFO] Checking for runtime configuration changes...
[INFO] Subscription limits changed - updating subscription manager
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] PoW configuration changed - reinitializing PoW system
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Expiration configuration changed - reinitializing expiration system
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Relay information changed - reinitializing relay info
[SUCCESS] Relay information initialized with default values
[SUCCESS] Configuration updated via kind 33334 event - 4 system components reinitialized
[SUCCESS] Configuration applied from event (4 handlers executed)
[SUCCESS] Configuration loaded from database
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW configured in basic validation mode
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Subscription limits: max_per_client=25, max_total=5000
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[INFO] Attempting to bind to port 8888
[2025/09/06 20:34:16:8170] E: ERROR on binding fd 8 to port 8888 (-1 98)
[2025/09/06 20:34:16:8172] E: init server failed
[2025/09/06 20:34:16:8172] E: Failed to create default vhost
[ERROR] Failed to create libwebsockets context after 1 attempts. Last attempted port: 8888
libwebsockets creation error: Inappropriate ioctl for device
[INFO] Cleaning up configuration system...
[SUCCESS] Configuration system cleaned up
[INFO] Database connection closed
[ERROR] Server shutdown with errors