Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3210b9e752 | ||
|
|
2d66b8bf1d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ src/version.h
|
||||
dev-config/
|
||||
db/
|
||||
copy_executable_local.sh
|
||||
nostr_login_lite/
|
||||
32
07.md
Normal file
32
07.md
Normal file
@@ -0,0 +1,32 @@
|
||||
NIP-07
|
||||
======
|
||||
|
||||
`window.nostr` capability for web browsers
|
||||
------------------------------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability.
|
||||
|
||||
That object must define the following methods:
|
||||
|
||||
```
|
||||
async window.nostr.getPublicKey(): string // returns a public key as hex
|
||||
async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it
|
||||
```
|
||||
|
||||
Aside from these two basic above, the following functions can also be implemented optionally:
|
||||
```
|
||||
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated)
|
||||
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated)
|
||||
async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44
|
||||
async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44
|
||||
```
|
||||
|
||||
### Recommendation to Extension Authors
|
||||
To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest.
|
||||
|
||||
|
||||
### Implementation
|
||||
|
||||
See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions.
|
||||
59
40.md
59
40.md
@@ -1,59 +0,0 @@
|
||||
NIP-40
|
||||
======
|
||||
|
||||
Expiration Timestamp
|
||||
--------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
The `expiration` tag enables users to specify a unix timestamp at which the message SHOULD be considered expired (by relays and clients) and SHOULD be deleted by relays.
|
||||
|
||||
#### Spec
|
||||
|
||||
```
|
||||
tag: expiration
|
||||
values:
|
||||
- [UNIX timestamp in seconds]: required
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"pubkey": "<pub-key>",
|
||||
"created_at": 1000000000,
|
||||
"kind": 1,
|
||||
"tags": [
|
||||
["expiration", "1600000000"]
|
||||
],
|
||||
"content": "This message will expire at the specified timestamp and be deleted by relays.\n",
|
||||
"id": "<event-id>"
|
||||
}
|
||||
```
|
||||
|
||||
Note: The timestamp should be in the same format as the created_at timestamp and should be interpreted as the time at which the message should be deleted by relays.
|
||||
|
||||
Client Behavior
|
||||
---------------
|
||||
|
||||
Clients SHOULD use the `supported_nips` field to learn if a relay supports this NIP. Clients SHOULD NOT send expiration events to relays that do not support this NIP.
|
||||
|
||||
Clients SHOULD ignore events that have expired.
|
||||
|
||||
Relay Behavior
|
||||
--------------
|
||||
|
||||
Relays MAY NOT delete expired messages immediately on expiration and MAY persist them indefinitely.
|
||||
Relays SHOULD NOT send expired events to clients, even if they are stored.
|
||||
Relays SHOULD drop any events that are published to them if they are expired.
|
||||
An expiration timestamp does not affect storage of ephemeral events.
|
||||
|
||||
Suggested Use Cases
|
||||
-------------------
|
||||
|
||||
* Temporary announcements - This tag can be used to make temporary announcements. For example, an event organizer could use this tag to post announcements about an upcoming event.
|
||||
* Limited-time offers - This tag can be used by businesses to make limited-time offers that expire after a certain amount of time. For example, a business could use this tag to make a special offer that is only available for a limited time.
|
||||
|
||||
#### Warning
|
||||
The events could be downloaded by third parties as they are publicly accessible all the time on the relays.
|
||||
So don't consider expiring messages as a security feature for your conversations or other uses.
|
||||
109
42.md
109
42.md
@@ -1,109 +0,0 @@
|
||||
NIP-42
|
||||
======
|
||||
|
||||
Authentication of clients to relays
|
||||
-----------------------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
This NIP defines a way for clients to authenticate to relays by signing an ephemeral event.
|
||||
|
||||
## Motivation
|
||||
|
||||
A relay may want to require clients to authenticate to access restricted resources. For example,
|
||||
|
||||
- A relay may request payment or other forms of whitelisting to publish events -- this can naïvely be achieved by limiting publication to events signed by the whitelisted key, but with this NIP they may choose to accept any events as long as they are published from an authenticated user;
|
||||
- A relay may limit access to `kind: 4` DMs to only the parties involved in the chat exchange, and for that it may require authentication before clients can query for that kind.
|
||||
- A relay may limit subscriptions of any kind to paying users or users whitelisted through any other means, and require authentication.
|
||||
|
||||
## Definitions
|
||||
|
||||
### New client-relay protocol messages
|
||||
|
||||
This NIP defines a new message, `AUTH`, which relays CAN send when they support authentication and clients can send to relays when they want to authenticate. When sent by relays the message has the following form:
|
||||
|
||||
```
|
||||
["AUTH", <challenge-string>]
|
||||
```
|
||||
|
||||
And, when sent by clients, the following form:
|
||||
|
||||
```
|
||||
["AUTH", <signed-event-json>]
|
||||
```
|
||||
|
||||
Clients MAY provide signed events from multiple pubkeys in a sequence of `AUTH` messages. Relays MUST treat all pubkeys as authenticated accordingly.
|
||||
|
||||
`AUTH` messages sent by clients MUST be answered with an `OK` message, like any `EVENT` message.
|
||||
|
||||
### Canonical authentication event
|
||||
|
||||
The signed event is an ephemeral event not meant to be published or queried, it must be of `kind: 22242` and it should have at least two tags, one for the relay URL and one for the challenge string as received from the relay. Relays MUST exclude `kind: 22242` events from being broadcasted to any client. `created_at` should be the current time. Example:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 22242,
|
||||
"tags": [
|
||||
["relay", "wss://relay.example.com/"],
|
||||
["challenge", "challengestringhere"]
|
||||
],
|
||||
// other fields...
|
||||
}
|
||||
```
|
||||
|
||||
### `OK` and `CLOSED` machine-readable prefixes
|
||||
|
||||
This NIP defines two new prefixes that can be used in `OK` (in response to event writes by clients) and `CLOSED` (in response to rejected subscriptions by clients):
|
||||
|
||||
- `"auth-required: "` - for when a client has not performed `AUTH` and the relay requires that to fulfill the query or write the event.
|
||||
- `"restricted: "` - for when a client has already performed `AUTH` but the key used to perform it is still not allowed by the relay or is exceeding its authorization.
|
||||
|
||||
## Protocol flow
|
||||
|
||||
At any moment the relay may send an `AUTH` message to the client containing a challenge. The challenge is valid for the duration of the connection or until another challenge is sent by the relay. The client MAY decide to send its `AUTH` event at any point and the authenticated session is valid afterwards for the duration of the connection.
|
||||
|
||||
### `auth-required` in response to a `REQ` message
|
||||
|
||||
Given that a relay is likely to require clients to perform authentication only for certain jobs, like answering a `REQ` or accepting an `EVENT` write, these are some expected common flows:
|
||||
|
||||
```
|
||||
relay: ["AUTH", "<challenge>"]
|
||||
client: ["REQ", "sub_1", {"kinds": [4]}]
|
||||
relay: ["CLOSED", "sub_1", "auth-required: we can't serve DMs to unauthenticated users"]
|
||||
client: ["AUTH", {"id": "abcdef...", ...}]
|
||||
client: ["AUTH", {"id": "abcde2...", ...}]
|
||||
relay: ["OK", "abcdef...", true, ""]
|
||||
relay: ["OK", "abcde2...", true, ""]
|
||||
client: ["REQ", "sub_1", {"kinds": [4]}]
|
||||
relay: ["EVENT", "sub_1", {...}]
|
||||
relay: ["EVENT", "sub_1", {...}]
|
||||
relay: ["EVENT", "sub_1", {...}]
|
||||
relay: ["EVENT", "sub_1", {...}]
|
||||
...
|
||||
```
|
||||
|
||||
In this case, the `AUTH` message from the relay could be sent right as the client connects or it can be sent immediately before the `CLOSED` is sent. The only requirement is that _the client must have a stored challenge associated with that relay_ so it can act upon that in response to the `auth-required` `CLOSED` message.
|
||||
|
||||
### `auth-required` in response to an `EVENT` message
|
||||
|
||||
The same flow is valid for when a client wants to write an `EVENT` to the relay, except now the relay sends back an `OK` message instead of a `CLOSED` message:
|
||||
|
||||
```
|
||||
relay: ["AUTH", "<challenge>"]
|
||||
client: ["EVENT", {"id": "012345...", ...}]
|
||||
relay: ["OK", "012345...", false, "auth-required: we only accept events from registered users"]
|
||||
client: ["AUTH", {"id": "abcdef...", ...}]
|
||||
relay: ["OK", "abcdef...", true, ""]
|
||||
client: ["EVENT", {"id": "012345...", ...}]
|
||||
relay: ["OK", "012345...", true, ""]
|
||||
```
|
||||
|
||||
## Signed Event Verification
|
||||
|
||||
To verify `AUTH` messages, relays must ensure:
|
||||
|
||||
- that the `kind` is `22242`;
|
||||
- that the event `created_at` is close (e.g. within ~10 minutes) of the current time;
|
||||
- that the `"challenge"` tag matches the challenge sent before;
|
||||
- that the `"relay"` tag matches the relay URL:
|
||||
- URL normalization techniques can be applied. For most cases just checking if the domain name is correct should be enough.
|
||||
1200
api/index.html
Normal file
1200
api/index.html
Normal file
File diff suppressed because it is too large
Load Diff
3358
api/nostr-lite.js
Normal file
3358
api/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
6860
api/nostr.bundle.js
Normal file
6860
api/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,61 @@ echo "=== C Nostr Relay Build and Restart Script ==="
|
||||
PRESERVE_DATABASE=false
|
||||
HELP=false
|
||||
USE_TEST_KEYS=false
|
||||
ADMIN_KEY=""
|
||||
RELAY_KEY=""
|
||||
PORT_OVERRIDE=""
|
||||
|
||||
# Key validation function
|
||||
validate_hex_key() {
|
||||
local key="$1"
|
||||
local key_type="$2"
|
||||
|
||||
if [ ${#key} -ne 64 ]; then
|
||||
echo "ERROR: $key_type key must be exactly 64 characters"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! [[ "$key" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
||||
echo "ERROR: $key_type key must contain only hex characters (0-9, a-f, A-F)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--preserve-database|-p)
|
||||
-a|--admin-key)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Admin key option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
-r|--relay-key)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Relay key option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
RELAY_KEY="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
-p|--port)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Port option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
PORT_OVERRIDE="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
--preserve-database)
|
||||
PRESERVE_DATABASE=true
|
||||
shift
|
||||
;;
|
||||
@@ -32,14 +83,38 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate custom keys if provided
|
||||
if [ -n "$ADMIN_KEY" ]; then
|
||||
if ! validate_hex_key "$ADMIN_KEY" "Admin"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$RELAY_KEY" ]; then
|
||||
if ! validate_hex_key "$RELAY_KEY" "Relay"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate port if provided
|
||||
if [ -n "$PORT_OVERRIDE" ]; then
|
||||
if ! [[ "$PORT_OVERRIDE" =~ ^[0-9]+$ ]] || [ "$PORT_OVERRIDE" -lt 1 ] || [ "$PORT_OVERRIDE" -gt 65535 ]; then
|
||||
echo "ERROR: Port must be a number between 1 and 65535"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show help
|
||||
if [ "$HELP" = true ]; then
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --preserve-database, -p Keep existing database files (don't delete for fresh start)"
|
||||
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo " -a, --admin-key <hex> 64-character hex admin private key"
|
||||
echo " -r, --relay-key <hex> 64-character hex relay private key"
|
||||
echo " -p, --port <port> Custom port override (default: 8888)"
|
||||
echo " --preserve-database Keep existing database files (don't delete for fresh start)"
|
||||
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Event-Based Configuration:"
|
||||
echo " This relay now uses event-based configuration stored directly in the database."
|
||||
@@ -47,11 +122,14 @@ if [ "$HELP" = true ]; then
|
||||
echo " Database file: <relay_pubkey>.db (created automatically)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Fresh start with new keys (default)"
|
||||
echo " $0 -p # Preserve existing database and keys"
|
||||
echo " $0 -t # Use test keys for consistent development"
|
||||
echo " $0 -t -p # Use test keys and preserve database"
|
||||
echo " $0 # Fresh start with random keys"
|
||||
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
||||
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
||||
echo " $0 --preserve-database # Preserve existing database and keys"
|
||||
echo " $0 --test-keys # Use test keys for consistent development"
|
||||
echo " $0 -t --preserve-database # Use test keys and preserve database"
|
||||
echo ""
|
||||
echo "Key Format: Keys must be exactly 64 hexadecimal characters (0-9, a-f, A-F)"
|
||||
echo "Default behavior: Deletes existing database files to start fresh with new keys"
|
||||
echo " for development purposes"
|
||||
exit 0
|
||||
@@ -152,14 +230,36 @@ 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')"
|
||||
|
||||
# Build command line arguments for relay binary
|
||||
RELAY_ARGS=""
|
||||
|
||||
if [ -n "$ADMIN_KEY" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -a $ADMIN_KEY"
|
||||
echo "Using custom admin key: ${ADMIN_KEY:0:16}..."
|
||||
fi
|
||||
|
||||
if [ -n "$RELAY_KEY" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -r $RELAY_KEY"
|
||||
echo "Using custom relay key: ${RELAY_KEY:0:16}..."
|
||||
fi
|
||||
|
||||
if [ -n "$PORT_OVERRIDE" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -p $PORT_OVERRIDE"
|
||||
echo "Using custom port: $PORT_OVERRIDE"
|
||||
fi
|
||||
|
||||
# Change to build directory before starting relay so database files are created there
|
||||
cd build
|
||||
# Start relay in background and capture its PID
|
||||
if [ "$USE_TEST_KEYS" = true ]; then
|
||||
echo "Using deterministic test keys for development..."
|
||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 &
|
||||
elif [ -n "$RELAY_ARGS" ]; then
|
||||
echo "Starting relay with custom configuration..."
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS > ../relay.log 2>&1 &
|
||||
else
|
||||
# No command line arguments needed for random key generation
|
||||
echo "Starting relay with random key generation..."
|
||||
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
|
||||
fi
|
||||
RELAY_PID=$!
|
||||
|
||||
797
src/config.c
797
src/config.c
@@ -18,12 +18,57 @@ extern sqlite3* g_db;
|
||||
config_manager_t g_config_manager = {0};
|
||||
char g_database_path[512] = {0};
|
||||
|
||||
// ================================
|
||||
// NEW ADMIN API STRUCTURES
|
||||
// ================================
|
||||
|
||||
// Migration state management
|
||||
typedef enum {
|
||||
MIGRATION_NOT_NEEDED,
|
||||
MIGRATION_NEEDED,
|
||||
MIGRATION_IN_PROGRESS,
|
||||
MIGRATION_COMPLETED,
|
||||
MIGRATION_FAILED
|
||||
} migration_state_t;
|
||||
|
||||
typedef struct {
|
||||
migration_state_t state;
|
||||
int event_config_count;
|
||||
int table_config_count;
|
||||
int migration_errors;
|
||||
time_t migration_started;
|
||||
time_t migration_completed;
|
||||
char error_message[512];
|
||||
} migration_status_t;
|
||||
|
||||
static migration_status_t g_migration_status = {0};
|
||||
|
||||
// Configuration source type
|
||||
typedef enum {
|
||||
CONFIG_SOURCE_EVENT, // Current event-based system
|
||||
CONFIG_SOURCE_TABLE, // New table-based system
|
||||
CONFIG_SOURCE_HYBRID // During migration
|
||||
} config_source_t;
|
||||
|
||||
// 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);
|
||||
|
||||
// Forward declarations for new admin API functions
|
||||
int populate_default_config_values(void);
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
|
||||
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size);
|
||||
void invalidate_config_cache(void);
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value, const char* action);
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value);
|
||||
int is_config_table_ready(void);
|
||||
int migrate_config_from_events_to_table(void);
|
||||
int populate_config_table_from_event(const cJSON* event);
|
||||
|
||||
// Current configuration cache
|
||||
static cJSON* g_current_config = NULL;
|
||||
|
||||
@@ -811,12 +856,12 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 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");
|
||||
// 7. Process configuration through admin API instead of storing in events table
|
||||
if (process_startup_config_event_with_fallback(config_event) == 0) {
|
||||
log_success("Initial configuration processed successfully through admin API");
|
||||
} else {
|
||||
log_warning("Failed to store initial configuration event - will retry after database init");
|
||||
// Cache the event for later storage
|
||||
log_warning("Failed to process initial configuration - will retry after database init");
|
||||
// Cache the event for later processing
|
||||
if (g_pending_config_event) {
|
||||
cJSON_Delete(g_pending_config_event);
|
||||
}
|
||||
@@ -873,6 +918,9 @@ int startup_existing_relay(const char* relay_pubkey) {
|
||||
g_database_path[sizeof(g_database_path) - 1] = '\0';
|
||||
free(db_name);
|
||||
|
||||
// Configuration will be migrated from events to table after database initialization
|
||||
log_info("Configuration migration will be performed after database is available");
|
||||
|
||||
// Load configuration event from database (after database is initialized)
|
||||
// This will be done in apply_configuration_from_database()
|
||||
|
||||
@@ -1579,6 +1627,490 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// NEW ADMIN API IMPLEMENTATION
|
||||
// ================================
|
||||
|
||||
// ================================
|
||||
// CONFIG TABLE MANAGEMENT FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Note: Config table is now created via embedded schema in sql_schema.h
|
||||
|
||||
// Get value from config table
|
||||
const char* get_config_value_from_table(const char* key) {
|
||||
if (!g_db || !key) return NULL;
|
||||
|
||||
const char* sql = "SELECT value FROM config WHERE key = ?";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||
|
||||
static char config_value_buffer[CONFIG_VALUE_MAX_LENGTH];
|
||||
const char* result = NULL;
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* value = (char*)sqlite3_column_text(stmt, 0);
|
||||
if (value) {
|
||||
strncpy(config_value_buffer, value, sizeof(config_value_buffer) - 1);
|
||||
config_value_buffer[sizeof(config_value_buffer) - 1] = '\0';
|
||||
result = config_value_buffer;
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set value in config table
|
||||
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
|
||||
const char* description, const char* category, int requires_restart) {
|
||||
if (!g_db || !key || !value || !data_type) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int(stmt, 6, requires_restart);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||
}
|
||||
|
||||
// Update config in table (simpler version of set_config_value_in_table)
|
||||
int update_config_in_table(const char* key, const char* value) {
|
||||
if (!g_db || !key || !value) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||
}
|
||||
|
||||
// Populate default config values
|
||||
int populate_default_config_values(void) {
|
||||
log_info("Populating default configuration values in table...");
|
||||
|
||||
// Add all default configuration values to the table
|
||||
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
|
||||
const char* key = DEFAULT_CONFIG_VALUES[i].key;
|
||||
const char* value = DEFAULT_CONFIG_VALUES[i].value;
|
||||
|
||||
// Determine data type
|
||||
const char* data_type = "string";
|
||||
if (strcmp(key, "relay_port") == 0 ||
|
||||
strcmp(key, "max_connections") == 0 ||
|
||||
strcmp(key, "pow_min_difficulty") == 0 ||
|
||||
strcmp(key, "max_subscriptions_per_client") == 0 ||
|
||||
strcmp(key, "max_total_subscriptions") == 0 ||
|
||||
strcmp(key, "max_filters_per_subscription") == 0 ||
|
||||
strcmp(key, "max_event_tags") == 0 ||
|
||||
strcmp(key, "max_content_length") == 0 ||
|
||||
strcmp(key, "max_message_length") == 0 ||
|
||||
strcmp(key, "default_limit") == 0 ||
|
||||
strcmp(key, "max_limit") == 0 ||
|
||||
strcmp(key, "nip42_challenge_expiration") == 0 ||
|
||||
strcmp(key, "nip40_expiration_grace_period") == 0) {
|
||||
data_type = "integer";
|
||||
} else if (strcmp(key, "auth_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_strict") == 0 ||
|
||||
strcmp(key, "nip40_expiration_filter") == 0 ||
|
||||
strcmp(key, "nip42_auth_required") == 0) {
|
||||
data_type = "boolean";
|
||||
}
|
||||
|
||||
// Set category
|
||||
const char* category = "general";
|
||||
if (strstr(key, "relay_")) {
|
||||
category = "relay";
|
||||
} else if (strstr(key, "nip40_")) {
|
||||
category = "expiration";
|
||||
} else if (strstr(key, "nip42_") || strstr(key, "auth_")) {
|
||||
category = "authentication";
|
||||
} else if (strstr(key, "pow_")) {
|
||||
category = "proof_of_work";
|
||||
} else if (strstr(key, "max_")) {
|
||||
category = "limits";
|
||||
}
|
||||
|
||||
// Determine if requires restart
|
||||
int requires_restart = 0;
|
||||
if (strcmp(key, "relay_port") == 0) {
|
||||
requires_restart = 1;
|
||||
}
|
||||
|
||||
if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) != 0) {
|
||||
char error_msg[256];
|
||||
snprintf(error_msg, sizeof(error_msg), "Failed to set default config: %s = %s", key, value);
|
||||
log_error(error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
log_success("Default configuration values populated");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// ADMIN EVENT PROCESSING FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Process admin events (moved from main.c)
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) {
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_obj || !cJSON_IsNumber(kind_obj)) {
|
||||
snprintf(error_message, error_size, "invalid: missing or invalid kind");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Verify admin authorization
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
|
||||
snprintf(error_message, error_size, "invalid: missing pubkey");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* event_pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||
const char* admin_pubkey = get_config_value("admin_pubkey");
|
||||
|
||||
if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) {
|
||||
snprintf(error_message, error_size, "auth-required: not authorized admin");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
switch (kind) {
|
||||
case 33334:
|
||||
return process_admin_config_event(event, error_message, error_size);
|
||||
case 33335:
|
||||
return process_admin_auth_event(event, error_message, error_size);
|
||||
default:
|
||||
snprintf(error_message, error_size, "invalid: unsupported admin event kind");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle kind 33334 config events
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||
snprintf(error_message, error_size, "invalid: configuration event must have tags");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Config table should already exist from embedded schema
|
||||
|
||||
// Begin transaction for atomic config updates
|
||||
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
snprintf(error_message, error_size, "failed to begin config transaction");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int updates_applied = 0;
|
||||
|
||||
// Process each tag as a configuration parameter
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags_obj) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* key = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
// Skip relay identifier tag
|
||||
if (strcmp(key, "d") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update configuration in table
|
||||
if (update_config_in_table(key, value) == 0) {
|
||||
updates_applied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updates_applied > 0) {
|
||||
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||
invalidate_config_cache();
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates", updates_applied);
|
||||
log_success(success_msg);
|
||||
} else {
|
||||
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||
snprintf(error_message, error_size, "no valid configuration parameters found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle kind 33335 auth rule events
|
||||
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) {
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||
snprintf(error_message, error_size, "invalid: auth rule event must have tags");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract action from content or tags
|
||||
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : "";
|
||||
|
||||
// Parse the action from content (should be "add" or "remove")
|
||||
cJSON* content_json = cJSON_Parse(content);
|
||||
const char* action = "add"; // default
|
||||
if (content_json) {
|
||||
cJSON* action_obj = cJSON_GetObjectItem(content_json, "action");
|
||||
if (action_obj && cJSON_IsString(action_obj)) {
|
||||
action = cJSON_GetStringValue(action_obj);
|
||||
}
|
||||
cJSON_Delete(content_json);
|
||||
}
|
||||
|
||||
// Begin transaction for atomic auth rule updates
|
||||
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
snprintf(error_message, error_size, "failed to begin auth rule transaction");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rules_processed = 0;
|
||||
|
||||
// Process each tag as an auth rule specification
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags_obj) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* rule_type_obj = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1);
|
||||
cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2);
|
||||
|
||||
if (!cJSON_IsString(rule_type_obj) ||
|
||||
!cJSON_IsString(pattern_type_obj) ||
|
||||
!cJSON_IsString(pattern_value_obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* rule_type = cJSON_GetStringValue(rule_type_obj);
|
||||
const char* pattern_type = cJSON_GetStringValue(pattern_type_obj);
|
||||
const char* pattern_value = cJSON_GetStringValue(pattern_value_obj);
|
||||
|
||||
// Process the auth rule based on action
|
||||
if (strcmp(action, "add") == 0) {
|
||||
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
|
||||
rules_processed++;
|
||||
}
|
||||
} else if (strcmp(action, "remove") == 0) {
|
||||
if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
|
||||
rules_processed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rules_processed > 0) {
|
||||
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed);
|
||||
log_success(success_msg);
|
||||
} else {
|
||||
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||
snprintf(error_message, error_size, "no valid auth rules found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// AUTH RULES MANAGEMENT FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Add auth rule from configuration
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value, const char* action) {
|
||||
if (!g_db || !rule_type || !pattern_type || !pattern_value || !action) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action) "
|
||||
"VALUES (?, ?, ?, ?)";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 4, action, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||
}
|
||||
|
||||
// Remove auth rule from configuration
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value) {
|
||||
if (!g_db || !rule_type || !pattern_type || !pattern_value) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return (rc == SQLITE_DONE) ? 0 : -1;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// CONFIGURATION CACHE MANAGEMENT
|
||||
// ================================
|
||||
|
||||
// Invalidate configuration cache
|
||||
void invalidate_config_cache(void) {
|
||||
// For now, just log that cache was invalidated
|
||||
// In a full implementation, this would clear any cached config values
|
||||
log_info("Configuration cache invalidated");
|
||||
}
|
||||
|
||||
// Reload configuration from table
|
||||
int reload_config_from_table(void) {
|
||||
// For now, just log that config was reloaded
|
||||
// In a full implementation, this would reload all cached values from the table
|
||||
log_info("Configuration reloaded from table");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// HYBRID CONFIG ACCESS FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Hybrid config getter (tries table first, falls back to event)
|
||||
const char* get_config_value_hybrid(const char* key) {
|
||||
// Try table-based config first if available
|
||||
if (is_config_table_ready()) {
|
||||
const char* table_value = get_config_value_from_table(key);
|
||||
if (table_value) {
|
||||
return table_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to event-based config
|
||||
return get_config_value(key);
|
||||
}
|
||||
|
||||
// Check if config table is ready
|
||||
int is_config_table_ready(void) {
|
||||
if (!g_db) return 0;
|
||||
|
||||
const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int table_exists = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
table_exists = sqlite3_column_int(stmt, 0) > 0;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (!table_exists) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if table has configuration data
|
||||
const char* count_sql = "SELECT COUNT(*) FROM config";
|
||||
rc = sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
config_count = sqlite3_column_int(stmt, 0);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return config_count > 0;
|
||||
}
|
||||
|
||||
// Initialize configuration system with migration support
|
||||
int initialize_config_system_with_migration(void) {
|
||||
log_info("Initializing configuration system with migration support...");
|
||||
|
||||
// Initialize config manager
|
||||
memset(&g_config_manager, 0, sizeof(g_config_manager));
|
||||
memset(&g_migration_status, 0, sizeof(g_migration_status));
|
||||
|
||||
// For new installations, config table should already exist from embedded schema
|
||||
log_success("Configuration system initialized with table support");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// RETRY INITIAL CONFIG EVENT STORAGE
|
||||
// ================================
|
||||
@@ -1591,16 +2123,263 @@ int retry_store_initial_config_event(void) {
|
||||
|
||||
log_info("Retrying storage of initial configuration event...");
|
||||
|
||||
// Try to store the cached configuration event
|
||||
if (store_config_event_in_database(g_pending_config_event) == 0) {
|
||||
log_success("Initial configuration event stored successfully on retry");
|
||||
// Try to process the cached configuration event through admin API
|
||||
if (process_startup_config_event_with_fallback(g_pending_config_event) == 0) {
|
||||
log_success("Initial configuration processed successfully through admin API on retry");
|
||||
|
||||
// Clean up the pending event
|
||||
cJSON_Delete(g_pending_config_event);
|
||||
g_pending_config_event = NULL;
|
||||
return 0;
|
||||
} else {
|
||||
log_error("Failed to store initial configuration event on retry");
|
||||
log_error("Failed to process initial configuration through admin API on retry");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ================================
|
||||
// CONFIG MIGRATION FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Populate config table from a configuration event
|
||||
int populate_config_table_from_event(const cJSON* event) {
|
||||
if (!event || !g_db) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Populating config table from configuration event...");
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
log_error("Configuration event missing tags array");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int configs_populated = 0;
|
||||
|
||||
// Process each tag as a configuration parameter
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* key = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
// Skip relay identifier tag
|
||||
if (strcmp(key, "d") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine data type for the config value
|
||||
const char* data_type = "string";
|
||||
if (strcmp(key, "relay_port") == 0 ||
|
||||
strcmp(key, "max_connections") == 0 ||
|
||||
strcmp(key, "pow_min_difficulty") == 0 ||
|
||||
strcmp(key, "max_subscriptions_per_client") == 0 ||
|
||||
strcmp(key, "max_total_subscriptions") == 0 ||
|
||||
strcmp(key, "max_filters_per_subscription") == 0 ||
|
||||
strcmp(key, "max_event_tags") == 0 ||
|
||||
strcmp(key, "max_content_length") == 0 ||
|
||||
strcmp(key, "max_message_length") == 0 ||
|
||||
strcmp(key, "default_limit") == 0 ||
|
||||
strcmp(key, "max_limit") == 0 ||
|
||||
strcmp(key, "nip42_challenge_expiration") == 0 ||
|
||||
strcmp(key, "nip40_expiration_grace_period") == 0) {
|
||||
data_type = "integer";
|
||||
} else if (strcmp(key, "auth_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_strict") == 0 ||
|
||||
strcmp(key, "nip40_expiration_filter") == 0 ||
|
||||
strcmp(key, "nip42_auth_required") == 0) {
|
||||
data_type = "boolean";
|
||||
}
|
||||
|
||||
// Set category
|
||||
const char* category = "general";
|
||||
if (strstr(key, "relay_")) {
|
||||
category = "relay";
|
||||
} else if (strstr(key, "nip40_")) {
|
||||
category = "expiration";
|
||||
} else if (strstr(key, "nip42_") || strstr(key, "auth_")) {
|
||||
category = "authentication";
|
||||
} else if (strstr(key, "pow_")) {
|
||||
category = "proof_of_work";
|
||||
} else if (strstr(key, "max_")) {
|
||||
category = "limits";
|
||||
}
|
||||
|
||||
// Determine if requires restart
|
||||
int requires_restart = 0;
|
||||
if (strcmp(key, "relay_port") == 0) {
|
||||
requires_restart = 1;
|
||||
}
|
||||
|
||||
// Insert into config table
|
||||
if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) == 0) {
|
||||
configs_populated++;
|
||||
} else {
|
||||
char error_msg[256];
|
||||
snprintf(error_msg, sizeof(error_msg), "Failed to populate config: %s = %s", key, value);
|
||||
log_error(error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (configs_populated > 0) {
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "Populated %d configuration values from event", configs_populated);
|
||||
log_success(success_msg);
|
||||
return 0;
|
||||
} else {
|
||||
log_error("No configuration values were populated from event");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate configuration from existing events to config table
|
||||
int migrate_config_from_events_to_table(void) {
|
||||
if (!g_db) {
|
||||
log_error("Database not available for configuration migration");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Migrating configuration from events to config table...");
|
||||
|
||||
// Load the most recent configuration event from database
|
||||
cJSON* config_event = load_config_event_from_database(g_config_manager.relay_pubkey);
|
||||
if (!config_event) {
|
||||
log_info("No existing configuration event found - migration not needed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Populate config table from the event
|
||||
int result = populate_config_table_from_event(config_event);
|
||||
|
||||
// Clean up
|
||||
cJSON_Delete(config_event);
|
||||
|
||||
if (result == 0) {
|
||||
log_success("Configuration migration from events to table completed successfully");
|
||||
} else {
|
||||
log_error("Configuration migration from events to table failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// STARTUP CONFIGURATION PROCESSING
|
||||
// ================================
|
||||
|
||||
// Process startup configuration event - bypasses auth and updates config table
|
||||
int process_startup_config_event(const cJSON* event) {
|
||||
if (!event || !g_db) {
|
||||
log_error("Invalid parameters for startup config processing");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Processing startup configuration event through admin API...");
|
||||
|
||||
// Validate event structure first
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) {
|
||||
log_error("Invalid event kind for startup configuration");
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
||||
log_error("Startup configuration event missing tags");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Begin transaction for atomic config updates
|
||||
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("Failed to begin startup config transaction");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int updates_applied = 0;
|
||||
|
||||
// Process each tag as a configuration parameter (same logic as process_admin_config_event)
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags_obj) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* key = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
// Skip relay identifier tag and relay_pubkey (already in table)
|
||||
if (strcmp(key, "d") == 0 || strcmp(key, "relay_pubkey") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update configuration in table
|
||||
if (update_config_in_table(key, value) == 0) {
|
||||
updates_applied++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updates_applied > 0) {
|
||||
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||
invalidate_config_cache();
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg),
|
||||
"Processed startup configuration: %d values updated in config table", updates_applied);
|
||||
log_success(success_msg);
|
||||
return 0;
|
||||
} else {
|
||||
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
||||
log_error("No valid configuration parameters found in startup event");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Process startup configuration event with fallback - for retry scenarios
|
||||
int process_startup_config_event_with_fallback(const cJSON* event) {
|
||||
if (!event) {
|
||||
log_error("Invalid event for startup config processing with fallback");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Try to process through admin API first
|
||||
if (process_startup_config_event(event) == 0) {
|
||||
log_success("Startup configuration processed successfully through admin API");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If that fails, populate defaults and try again
|
||||
log_warning("Startup config processing failed - ensuring defaults are populated");
|
||||
if (populate_default_config_values() != 0) {
|
||||
log_error("Failed to populate default config values");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Retry processing
|
||||
if (process_startup_config_event(event) == 0) {
|
||||
log_success("Startup configuration processed successfully after populating defaults");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_error("Startup configuration processing failed even after populating defaults");
|
||||
return -1;
|
||||
}
|
||||
39
src/config.h
39
src/config.h
@@ -90,4 +90,43 @@ int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_k
|
||||
int is_nip42_auth_required_for_kind(int event_kind);
|
||||
int is_nip42_auth_globally_required(void);
|
||||
|
||||
// ================================
|
||||
// NEW ADMIN API FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Config table management functions (config table created via embedded schema)
|
||||
const char* get_config_value_from_table(const char* key);
|
||||
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
|
||||
const char* description, const char* category, int requires_restart);
|
||||
int update_config_in_table(const char* key, const char* value);
|
||||
int populate_default_config_values(void);
|
||||
|
||||
// Admin event processing functions
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
|
||||
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Auth rules management functions
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value, const char* action);
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value);
|
||||
|
||||
// Configuration cache management
|
||||
void invalidate_config_cache(void);
|
||||
int reload_config_from_table(void);
|
||||
|
||||
// Hybrid config access functions
|
||||
const char* get_config_value_hybrid(const char* key);
|
||||
int is_config_table_ready(void);
|
||||
|
||||
// Migration support functions
|
||||
int initialize_config_system_with_migration(void);
|
||||
int migrate_config_from_events_to_table(void);
|
||||
int populate_config_table_from_event(const cJSON* event);
|
||||
|
||||
// Startup configuration processing functions
|
||||
int process_startup_config_event(const cJSON* event);
|
||||
int process_startup_config_event_with_fallback(const cJSON* event);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
51
src/main.c
51
src/main.c
@@ -227,6 +227,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
||||
// Forward declaration for configuration event handling (kind 33334)
|
||||
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declaration for admin event processing (kinds 33334 and 33335)
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declaration for NOTICE message support
|
||||
void send_notice_message(struct lws* wsi, const char* message);
|
||||
|
||||
@@ -3092,17 +3095,47 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// Cleanup event JSON string
|
||||
free(event_json_str);
|
||||
|
||||
// Store event in database and broadcast to subscriptions
|
||||
// Check for admin events (kinds 33334 and 33335) and intercept them
|
||||
if (result == 0) {
|
||||
// Store the event in the database first
|
||||
if (store_event(event) != 0) {
|
||||
log_error("Failed to store event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
if (event_kind == 33334 || event_kind == 33335) {
|
||||
// This is an admin event - process it through the admin API instead of normal storage
|
||||
log_info("Admin event detected, processing through admin API");
|
||||
|
||||
char admin_error[512] = {0};
|
||||
if (process_admin_event_in_config(event, admin_error, sizeof(admin_error)) != 0) {
|
||||
log_error("Failed to process admin event through admin API");
|
||||
result = -1;
|
||||
strncpy(error_message, admin_error, sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_success("Admin event processed successfully through admin API");
|
||||
// Admin events are processed by the admin API, not broadcast to subscriptions
|
||||
}
|
||||
} else {
|
||||
// Regular event - store in database and broadcast
|
||||
if (store_event(event) != 0) {
|
||||
log_error("Failed to store event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("Event stored successfully in database");
|
||||
// Broadcast event to matching persistent subscriptions
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_info("Event stored successfully in database");
|
||||
// Broadcast event to matching persistent subscriptions
|
||||
broadcast_event_to_subscriptions(event);
|
||||
// Event without valid kind - try normal storage
|
||||
if (store_event(event) != 0) {
|
||||
log_error("Failed to store event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("Event stored successfully in database");
|
||||
broadcast_event_to_subscriptions(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* Embedded SQL Schema for C Nostr Relay
|
||||
* Generated from db/schema.sql - Do not edit manually
|
||||
* Schema Version: 6
|
||||
* Schema Version: 7
|
||||
*/
|
||||
#ifndef SQL_SCHEMA_H
|
||||
#define SQL_SCHEMA_H
|
||||
|
||||
/* Schema version constant */
|
||||
#define EMBEDDED_SCHEMA_VERSION "6"
|
||||
#define EMBEDDED_SCHEMA_VERSION "7"
|
||||
|
||||
/* 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 = 6;\n\
|
||||
PRAGMA user_version = 7;\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', '6'),\n\
|
||||
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
|
||||
('version', '7'),\n\
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
||||
('created_at', strftime('%s', 'now'));\n\
|
||||
\n\
|
||||
-- Helper views for common queries\n\
|
||||
@@ -154,6 +154,60 @@ CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\
|
||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
|
||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
|
||||
\n\
|
||||
-- Configuration Table for Table-Based Config Management\n\
|
||||
-- Hybrid system supporting both event-based and table-based configuration\n\
|
||||
CREATE TABLE config (\n\
|
||||
key TEXT PRIMARY KEY,\n\
|
||||
value TEXT NOT NULL,\n\
|
||||
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\
|
||||
description TEXT,\n\
|
||||
category TEXT DEFAULT 'general',\n\
|
||||
requires_restart INTEGER DEFAULT 0,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for config table performance\n\
|
||||
CREATE INDEX idx_config_category ON config(category);\n\
|
||||
CREATE INDEX idx_config_restart ON config(requires_restart);\n\
|
||||
CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\
|
||||
\n\
|
||||
-- Trigger to update config timestamp on changes\n\
|
||||
CREATE TRIGGER update_config_timestamp\n\
|
||||
AFTER UPDATE ON config\n\
|
||||
FOR EACH ROW\n\
|
||||
BEGIN\n\
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Insert default configuration values\n\
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
|
||||
\n\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||
-- Optional database logging for subscription analytics and debugging\n\
|
||||
\n\
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"kind":1,"id":"6ed088c045874d91eabd02127d613e8babf6240a10532eb25f4c61437cabe710","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1757711333,"tags":[],"content":"Testing unified validation system","sig":"9f96975a831317d9948a097a9c4ae73063f4f0414a463b37a21e733f16d7788a51e72e8e48144974d82c217c31c45b987589219a5d5e2f8d7ec81448b523a474"}
|
||||
@@ -1 +0,0 @@
|
||||
5e01b634b759df55fe19be40e8ce632fe0717506c5bc0e0558a4d7aed2232380
|
||||
Reference in New Issue
Block a user