Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3bab033ed | ||
|
|
524f9bd84f | ||
|
|
4658ede9d6 | ||
|
|
f7b463aca1 | ||
|
|
c1a6e92b1d | ||
|
|
eefb0e427e |
32
07.md
32
07.md
@@ -1,32 +0,0 @@
|
|||||||
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.
|
|
||||||
94
FIX_ME.md
Normal file
94
FIX_ME.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
Inconsistency audit with exact fixes (treating README.md as authoritative)
|
||||||
|
|
||||||
|
Backend auth_rules schema mismatch
|
||||||
|
Evidence:
|
||||||
|
Migration (creates mismatched columns):
|
||||||
|
See "CREATE TABLE IF NOT EXISTS auth_rules ... UNIQUE(rule_type, operation, rule_target)".
|
||||||
|
Active code uses rule_type, pattern_type, pattern_value, action:
|
||||||
|
Insert: "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action)"
|
||||||
|
Delete: "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"
|
||||||
|
Query mapping: map_auth_query_type_to_response()
|
||||||
|
Queries: "... WHERE rule_type LIKE '%blacklist%'"
|
||||||
|
Validator checks:
|
||||||
|
"... WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ?"
|
||||||
|
"... WHERE rule_type = 'blacklist' AND pattern_type = 'hash' AND pattern_value = ?"
|
||||||
|
"... WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ?"
|
||||||
|
Embedded schema expects pattern columns and active/indexes:
|
||||||
|
"CREATE TABLE auth_rules ( ... )"
|
||||||
|
"CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value)"
|
||||||
|
"CREATE INDEX idx_auth_rules_active ON auth_rules(active)"
|
||||||
|
Fix (update migration to align with sql_schema.h/config.c):
|
||||||
|
Replace the DDL at "create_auth_rules_sql" with: CREATE TABLE IF NOT EXISTS auth_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_type TEXT NOT NULL, -- 'whitelist' | 'blacklist' pattern_type TEXT NOT NULL, -- 'pubkey' | 'hash' | future pattern_value TEXT NOT NULL, -- hex pubkey/hash action TEXT NOT NULL, -- 'allow' | 'deny' active INTEGER DEFAULT 1, created_at INTEGER DEFAULT (strftime('%s','now')), UNIQUE(rule_type, pattern_type, pattern_value) );
|
||||||
|
After creation, also create indexes as in "sql_schema.h":
|
||||||
|
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);
|
||||||
|
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);
|
||||||
|
CREATE INDEX idx_auth_rules_active ON auth_rules(active);
|
||||||
|
Duplicate UI function + stale DOM id usage
|
||||||
|
Evidence:
|
||||||
|
Duplicate definition of disconnectFromRelay() and disconnectFromRelay(); the second overwrites the first and uses legacy element access paths.
|
||||||
|
Stale variable: "const relayUrl = document.getElementById('relay-url');" — no element with id="relay-url" exists; the real input is "relay-connection-url" and is referenced as "relayConnectionUrl".
|
||||||
|
Calls using relayUrl.value.trim() (must use relayConnectionUrl):
|
||||||
|
"sendConfigUpdateCommand() publish URL"
|
||||||
|
"loadAuthRules() publish URL"
|
||||||
|
"deleteAuthRule() publish URL"
|
||||||
|
Tests:
|
||||||
|
"testGetAuthRules()"
|
||||||
|
"testClearAuthRules()"
|
||||||
|
"testAddBlacklist()"
|
||||||
|
"testAddWhitelist()"
|
||||||
|
"testConfigQuery()"
|
||||||
|
"testPostEvent()"
|
||||||
|
Fix:
|
||||||
|
Remove the duplicate legacy function entirely: delete the second disconnectFromRelay().
|
||||||
|
Remove stale variable: delete "const relayUrl = document.getElementById('relay-url');".
|
||||||
|
Replace every relayUrl.value.trim() occurrence with relayConnectionUrl.value.trim() at the lines listed above.
|
||||||
|
Supported NIPs inconsistency (README vs UI fallback)
|
||||||
|
Evidence:
|
||||||
|
README implemented NIPs checklist (authoritative): "NIPs list" shows: 1, 9, 11, 13, 15, 20, 33, 40, 42 implemented.
|
||||||
|
UI fallback for manual relay info includes unsupported/undocumented NIPs and misses implemented ones:
|
||||||
|
"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]"
|
||||||
|
"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]"
|
||||||
|
Fix:
|
||||||
|
Replace both arrays with: [1, 9, 11, 13, 15, 20, 33, 40, 42]
|
||||||
|
Config key mismatches (README vs UI edit form)
|
||||||
|
Evidence:
|
||||||
|
README keys (authoritative): "Available Configuration Keys"–(README.md:110)
|
||||||
|
relay_description, relay_contact, max_connections, max_subscriptions_per_client, max_event_tags, max_content_length, auth_enabled, nip42_auth_required, nip42_auth_required_kinds, nip42_challenge_timeout, pow_min_difficulty, nip40_expiration_enabled
|
||||||
|
UI currently declares/uses many non-README keys:
|
||||||
|
Field types: "fieldTypes" include nip42_auth_required_events, nip42_auth_required_subscriptions, relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit.
|
||||||
|
Descriptions: "descriptions" reflect the same non-README keys.
|
||||||
|
Fix:
|
||||||
|
Restrict UI form generation to README keys and rename mismatches:
|
||||||
|
Combine nip42_auth_required_events/subscriptions into README’s "nip42_auth_required" (boolean).
|
||||||
|
Rename nip42_challenge_expiration to "nip42_challenge_timeout".
|
||||||
|
Remove or hide (advanced section) non-README keys: relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit.
|
||||||
|
Update both "fieldTypes" and "descriptions" to reflect only README keys (data types and labels consistent).
|
||||||
|
First-time startup port override (-p) ignored when -a and -r are also provided
|
||||||
|
Observation:
|
||||||
|
You confirmed: first run with -p 7777 works, but with -p plus -a and -r the override isn’t honored.
|
||||||
|
Likely cause:
|
||||||
|
The code path that handles admin/relay key overrides on first-time setup bypasses persisting the CLI port override to config/unified cache before server start, so "start_websocket_relay(-1, ...)" falls back to default.
|
||||||
|
Fix:
|
||||||
|
Ensure first_time_startup_sequence applies cli_options.port_override to persistent config and cache BEFORE default config insertion and before starting the server. Specifically:
|
||||||
|
In the first-time path (main):
|
||||||
|
After "first_time_startup_sequence(&cli_options)" and before creating defaults on the -a/-r path at "populate_default_config_values()", write the port override:
|
||||||
|
set_config_value_in_table("relay_port", "<port>", "integer", "WebSocket port", "relay", 0);
|
||||||
|
and update unified cache if required by the port resolution code.
|
||||||
|
Verify the code path where -a/-r trigger direct table population also applies/overwrites the port with the CLI-provided value.
|
||||||
|
Add a regression test to assert that -p is honored with and without -a/-r on first run.
|
||||||
|
Minor consistency recommendations
|
||||||
|
UI NIP-11 fallback version string:
|
||||||
|
Consider aligning with backend version source (e.g., src/version.h). The UI currently hardcodes "1.0.0" at "version: '1.0.0'".
|
||||||
|
UI hardcoded relay pubkey fallback:
|
||||||
|
"getRelayPubkey()" returns a constant when not connected. Safe for dev, but should not leak into production paths.
|
||||||
|
Added TODO items (as requested)
|
||||||
|
|
||||||
|
The following todos were added/organized:
|
||||||
|
Remove duplicate disconnectFromRelay() and standardize to relay-connection-url
|
||||||
|
Replace all relayUrl.value references with relayConnectionUrl.value in api/index.html
|
||||||
|
Align Supported NIPs fallback arrays in api/index.html with README (1,9,11,13,15,20,33,40,42)
|
||||||
|
Update config form keys/descriptions in api/index.html to match README keys
|
||||||
|
Fix backend auth_rules migration in src/main.c to match src/sql_schema.h/src/config.c
|
||||||
|
Investigate and fix first-time startup port override ignored when -a and -r are provided
|
||||||
|
Add tests for port override and auth_rules flows
|
||||||
|
Rebuild via ./make_and_restart_relay.sh and validate against README
|
||||||
2
Makefile
2
Makefile
@@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k
|
|||||||
BUILD_DIR = build
|
BUILD_DIR = build
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
MAIN_SRC = src/main.c src/config.c src/request_validator.c
|
MAIN_SRC = src/main.c src/config.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c
|
||||||
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||||
|
|
||||||
# Architecture detection
|
# Architecture detection
|
||||||
|
|||||||
134
api/button.html
134
api/button.html
@@ -1,134 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Embedded NOSTR_LOGIN_LITE</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 40px;
|
|
||||||
background: white;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 90vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-button {
|
|
||||||
background: #0066cc;
|
|
||||||
color: white;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
text-align: center;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-button:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div id="login-button">Login</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../lite/nostr.bundle.js"></script>
|
|
||||||
<script src="../lite/nostr-lite.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let isAuthenticated = false;
|
|
||||||
let currentUser = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
await window.NOSTR_LOGIN_LITE.init({
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
extension: true,
|
|
||||||
local: true,
|
|
||||||
readonly: true,
|
|
||||||
connect: true,
|
|
||||||
remote: true,
|
|
||||||
otp: true
|
|
||||||
},
|
|
||||||
floatingTab: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for authentication events
|
|
||||||
window.addEventListener('nlMethodSelected', handleAuthEvent);
|
|
||||||
window.addEventListener('nlLogout', handleLogoutEvent);
|
|
||||||
|
|
||||||
// Check for existing authentication state
|
|
||||||
checkAuthState();
|
|
||||||
|
|
||||||
// Initialize button
|
|
||||||
updateButtonState();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleAuthEvent(event) {
|
|
||||||
const { pubkey, method } = event.detail;
|
|
||||||
console.log(`Authenticated with ${method}, pubkey: ${pubkey}`);
|
|
||||||
|
|
||||||
isAuthenticated = true;
|
|
||||||
currentUser = event.detail;
|
|
||||||
updateButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLogoutEvent() {
|
|
||||||
console.log('Logout event received');
|
|
||||||
|
|
||||||
isAuthenticated = false;
|
|
||||||
currentUser = null;
|
|
||||||
updateButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkAuthState() {
|
|
||||||
// Check if user is already authenticated (from persistent storage)
|
|
||||||
try {
|
|
||||||
// Try to get public key - this will work if already authenticated
|
|
||||||
window.nostr.getPublicKey().then(pubkey => {
|
|
||||||
console.log('Found existing authentication, pubkey:', pubkey);
|
|
||||||
isAuthenticated = true;
|
|
||||||
currentUser = { pubkey, method: 'persistent' };
|
|
||||||
updateButtonState();
|
|
||||||
}).catch(error => {
|
|
||||||
console.log('No existing authentication found:', error.message);
|
|
||||||
// User is not authenticated, button stays in login state
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('No existing authentication found');
|
|
||||||
// User is not authenticated, button stays in login state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateButtonState() {
|
|
||||||
const button = document.getElementById('login-button');
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
button.textContent = 'Logout';
|
|
||||||
button.onclick = () => window.NOSTR_LOGIN_LITE.logout();
|
|
||||||
button.style.background = '#dc3545'; // Red for logout
|
|
||||||
} else {
|
|
||||||
button.textContent = 'Login';
|
|
||||||
button.onclick = () => window.NOSTR_LOGIN_LITE.launch('login');
|
|
||||||
button.style.background = '#0066cc'; // Blue for login
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
381
api/index.html
381
api/index.html
@@ -169,10 +169,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-table {
|
.config-table {
|
||||||
border: var(--border-width) solid var(--primary-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -183,6 +184,12 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-table th {
|
.config-table th {
|
||||||
@@ -425,16 +432,17 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two-Column Layout */
|
/* Main Sections Wrapper */
|
||||||
.two-column-layout {
|
.main-sections-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--border-width);
|
gap: var(--border-width);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.flex-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 300px; /* Ensure columns are at least 300px wide */
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
@@ -453,12 +461,6 @@
|
|||||||
h2 {
|
h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stack columns vertically on screens smaller than 700px */
|
|
||||||
.two-column-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -466,8 +468,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>C-RELAY ADMIN API</h1>
|
<h1>C-RELAY ADMIN API</h1>
|
||||||
|
|
||||||
|
<!-- Main Sections Wrapper -->
|
||||||
|
<div class="main-sections-wrapper">
|
||||||
|
|
||||||
<!-- Persistent Authentication Header - Always Visible -->
|
<!-- Persistent Authentication Header - Always Visible -->
|
||||||
<div id="persistent-auth-container" class="section">
|
<div id="persistent-auth-container" class="section flex-section">
|
||||||
<div class="user-info-container">
|
<div class="user-info-container">
|
||||||
<button type="button" id="login-logout-btn" class="login-logout-btn">LOGIN</button>
|
<button type="button" id="login-logout-btn" class="login-logout-btn">LOGIN</button>
|
||||||
<div class="user-details" id="persistent-user-details" style="display: none;">
|
<div class="user-details" id="persistent-user-details" style="display: none;">
|
||||||
@@ -480,10 +485,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Two-Column Layout: Authentication + Relay Connection -->
|
<!-- Login Section -->
|
||||||
<div class="two-column-layout">
|
<div id="login-section" class="flex-section">
|
||||||
<!-- Login Section - Left Column -->
|
|
||||||
<div id="login-section" class="column">
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>NOSTR AUTHENTICATION</h2>
|
<h2>NOSTR AUTHENTICATION</h2>
|
||||||
<p id="login-instructions">Please login with your Nostr identity to access the admin interface.</p>
|
<p id="login-instructions">Please login with your Nostr identity to access the admin interface.</p>
|
||||||
@@ -491,20 +494,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Relay Connection Section - Right Column -->
|
<!-- Relay Connection Section -->
|
||||||
<div id="relay-connection-section" class="column">
|
<div id="relay-connection-section" class="flex-section">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>RELAY CONNECTION</h2>
|
<h2>RELAY CONNECTION</h2>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="relay-connection-url">Relay URL:</label>
|
<label for="relay-connection-url">Relay URL:</label>
|
||||||
<input type="text" id="relay-connection-url" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
|
<input type="text" id="relay-connection-url" value="ws://localhost:8888"
|
||||||
|
placeholder="ws://localhost:8888 or wss://relay.example.com">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="relay-pubkey-manual">Relay Pubkey (if not available via NIP-11):</label>
|
<label for="relay-pubkey-manual">Relay Pubkey (if not available via NIP-11):</label>
|
||||||
<input type="text" id="relay-pubkey-manual" placeholder="64-character hex pubkey" pattern="[0-9a-fA-F]{64}" title="64-character hexadecimal public key">
|
<input type="text" id="relay-pubkey-manual" placeholder="64-character hex pubkey"
|
||||||
<small>If the relay hasn't been configured yet, enter the relay pubkey shown during startup</small>
|
pattern="[0-9a-fA-F]{64}" title="64-character hexadecimal public key">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
@@ -532,24 +537,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Interface (hidden until logged in) -->
|
|
||||||
<div id="main-interface" class="hidden">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Testing Section - Always Visible -->
|
|
||||||
<div class="section">
|
|
||||||
|
|
||||||
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
|
</div> <!-- End Main Sections Wrapper -->
|
||||||
<div class="inline-buttons">
|
|
||||||
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (REQUIRES LOGIN + RELAY CONNECTION)</button>
|
|
||||||
</div>
|
|
||||||
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Testing Section -->
|
||||||
|
<div id="div_config" class="section flex-section" style="display: none;">
|
||||||
|
<h2>RELAY CONFIGURATION</h2>
|
||||||
<div id="config-display" class="hidden">
|
<div id="config-display" class="hidden">
|
||||||
<div id="config-view-mode">
|
<div id="config-view-mode">
|
||||||
|
<div class="config-table-container">
|
||||||
<table class="config-table" id="config-table">
|
<table class="config-table" id="config-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -561,10 +563,13 @@
|
|||||||
<tbody id="config-table-body">
|
<tbody id="config-table-body">
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
|
|
||||||
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
||||||
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
||||||
|
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -584,17 +589,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth Rules Management - Moved after configuration -->
|
<!-- Auth Rules Management - Moved after configuration -->
|
||||||
<div class="section" id="authRulesSection" style="display: none;">
|
<div class="section flex-section" id="authRulesSection" style="display: none;">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>AUTH RULES MANAGEMENT</h2>
|
<h2>AUTH RULES MANAGEMENT</h2>
|
||||||
<div class="status" id="authRulesStatus">●</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-rules-controls">
|
|
||||||
<div class="inline-buttons">
|
|
||||||
<button id="viewAuthRulesBtn" class="btn">VIEW RULES</button>
|
|
||||||
<button id="refreshAuthRulesBtn" class="btn">REFRESH</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth Rules Table -->
|
<!-- Auth Rules Table -->
|
||||||
@@ -619,36 +616,35 @@
|
|||||||
<div id="authRuleInputSections" style="display: block;">
|
<div id="authRuleInputSections" style="display: block;">
|
||||||
|
|
||||||
<!-- Combined Pubkey Auth Rule Section -->
|
<!-- Combined Pubkey Auth Rule Section -->
|
||||||
<div class="auth-rule-section">
|
|
||||||
<h3>MANAGE PUBKEY ACCESS</h3>
|
|
||||||
<p>Add pubkeys to whitelist (allow) or blacklist (deny) access</p>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
||||||
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
||||||
<small id="authRuleHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
<div id="whitelistWarning" class="warning-box" style="display: none;">
|
||||||
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only mode.
|
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
|
||||||
|
mode.
|
||||||
Only whitelisted users will be able to interact with the relay.
|
Only whitelisted users will be able to interact with the relay.
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO WHITELIST</button>
|
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
|
||||||
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO BLACKLIST</button>
|
WHITELIST</button>
|
||||||
|
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
|
||||||
|
BLACKLIST</button>
|
||||||
|
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="authRuleStatus" class="rule-status"></div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auth Rules Status Display -->
|
|
||||||
<div id="authRulesStatusDisplay" style="display: none;">
|
|
||||||
<h3>Auth System Status</h3>
|
|
||||||
<div class="status" id="authSystemStatus">CHECKING...</div>
|
|
||||||
<div class="json-display" id="authRulesCount">
|
|
||||||
Rules: Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TESTS Section -->
|
<!-- TESTS Section -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -660,7 +656,8 @@
|
|||||||
<label for="test-event-log">Event Log (Sent/Received):</label>
|
<label for="test-event-log">Event Log (Sent/Received):</label>
|
||||||
<div class="log-panel" id="test-event-log" style="height: 300px;">
|
<div class="log-panel" id="test-event-log" style="height: 300px;">
|
||||||
<div class="log-entry">
|
<div class="log-entry">
|
||||||
<span class="log-timestamp">SYSTEM:</span> Test interface ready. Click buttons below to test admin API functions.
|
<span class="log-timestamp">SYSTEM:</span> Test interface ready. Click buttons below to test admin
|
||||||
|
API functions.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="clear-test-log-btn">CLEAR TEST LOG</button>
|
<button type="button" id="clear-test-log-btn">CLEAR TEST LOG</button>
|
||||||
@@ -688,8 +685,10 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="test-pubkey-input">Test Pubkey (for blacklist/whitelist):</label>
|
<label for="test-pubkey-input">Test Pubkey (for blacklist/whitelist):</label>
|
||||||
<div style="display: flex; gap: 10px; align-items: flex-start;">
|
<div style="display: flex; gap: 10px; align-items: flex-start;">
|
||||||
<input type="text" id="test-pubkey-input" placeholder="Enter pubkey or nsec1... for testing" style="flex: 1;">
|
<input type="text" id="test-pubkey-input" placeholder="Enter pubkey or nsec1... for testing"
|
||||||
<button type="button" id="generate-test-key-btn" style="width: auto; padding: 8px 16px; white-space: nowrap;">GENERATE KEY</button>
|
style="flex: 1;">
|
||||||
|
<button type="button" id="generate-test-key-btn"
|
||||||
|
style="width: auto; padding: 8px 16px; white-space: nowrap;">GENERATE KEY</button>
|
||||||
</div>
|
</div>
|
||||||
<small>This pubkey will be used for blacklist/whitelist tests</small>
|
<small>This pubkey will be used for blacklist/whitelist tests</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -746,13 +745,11 @@
|
|||||||
|
|
||||||
// DOM elements
|
// DOM elements
|
||||||
const loginSection = document.getElementById('login-section');
|
const loginSection = document.getElementById('login-section');
|
||||||
const mainInterface = document.getElementById('main-interface');
|
// const mainInterface = document.getElementById('main-interface');
|
||||||
const persistentUserName = document.getElementById('persistent-user-name');
|
const persistentUserName = document.getElementById('persistent-user-name');
|
||||||
const persistentUserPubkey = document.getElementById('persistent-user-pubkey');
|
const persistentUserPubkey = document.getElementById('persistent-user-pubkey');
|
||||||
const persistentUserAbout = document.getElementById('persistent-user-about');
|
const persistentUserAbout = document.getElementById('persistent-user-about');
|
||||||
const persistentUserDetails = document.getElementById('persistent-user-details');
|
const persistentUserDetails = document.getElementById('persistent-user-details');
|
||||||
const relayUrl = document.getElementById('relay-url');
|
|
||||||
const relayStatus = document.getElementById('relay-status');
|
|
||||||
const fetchConfigBtn = document.getElementById('fetch-config-btn');
|
const fetchConfigBtn = document.getElementById('fetch-config-btn');
|
||||||
// Relay connection elements
|
// Relay connection elements
|
||||||
const relayConnectionUrl = document.getElementById('relay-connection-url');
|
const relayConnectionUrl = document.getElementById('relay-connection-url');
|
||||||
@@ -761,7 +758,6 @@
|
|||||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||||
const testWebSocketBtn = document.getElementById('test-websocket-btn');
|
const testWebSocketBtn = document.getElementById('test-websocket-btn');
|
||||||
const configStatus = document.getElementById('config-status');
|
|
||||||
const configDisplay = document.getElementById('config-display');
|
const configDisplay = document.getElementById('config-display');
|
||||||
const configViewMode = document.getElementById('config-view-mode');
|
const configViewMode = document.getElementById('config-view-mode');
|
||||||
const configEditMode = document.getElementById('config-edit-mode');
|
const configEditMode = document.getElementById('config-edit-mode');
|
||||||
@@ -936,7 +932,7 @@
|
|||||||
description: 'C-Relay instance - pubkey provided manually',
|
description: 'C-Relay instance - pubkey provided manually',
|
||||||
pubkey: manualPubkey,
|
pubkey: manualPubkey,
|
||||||
contact: 'admin@manual.config.relay',
|
contact: 'admin@manual.config.relay',
|
||||||
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
|
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
|
||||||
software: 'https://github.com/0xtrr/c-relay',
|
software: 'https://github.com/0xtrr/c-relay',
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
};
|
};
|
||||||
@@ -962,7 +958,7 @@
|
|||||||
description: 'C-Relay instance - pubkey provided manually',
|
description: 'C-Relay instance - pubkey provided manually',
|
||||||
pubkey: manualPubkey,
|
pubkey: manualPubkey,
|
||||||
contact: 'admin@manual.config.relay',
|
contact: 'admin@manual.config.relay',
|
||||||
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
|
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
|
||||||
software: 'https://github.com/0xtrr/c-relay',
|
software: 'https://github.com/0xtrr/c-relay',
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
};
|
};
|
||||||
@@ -978,11 +974,9 @@
|
|||||||
|
|
||||||
// Step 4: Update UI
|
// Step 4: Update UI
|
||||||
updateRelayConnectionStatus('connected');
|
updateRelayConnectionStatus('connected');
|
||||||
|
updateAdminSectionsVisibility();
|
||||||
|
|
||||||
// Step 5: Update the old relay URL field for backward compatibility
|
// Step 5: Relay URL updated
|
||||||
if (relayUrl) {
|
|
||||||
relayUrl.value = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 6: Automatically load configuration and auth rules
|
// Step 6: Automatically load configuration and auth rules
|
||||||
log('Relay connected successfully. Auto-loading configuration and auth rules...', 'INFO');
|
log('Relay connected successfully. Auto-loading configuration and auth rules...', 'INFO');
|
||||||
@@ -1040,6 +1034,7 @@
|
|||||||
// Update UI
|
// Update UI
|
||||||
updateRelayConnectionStatus('disconnected');
|
updateRelayConnectionStatus('disconnected');
|
||||||
hideRelayInfo();
|
hideRelayInfo();
|
||||||
|
updateAdminSectionsVisibility();
|
||||||
|
|
||||||
log('Disconnected from relay', 'INFO');
|
log('Disconnected from relay', 'INFO');
|
||||||
|
|
||||||
@@ -1282,35 +1277,32 @@
|
|||||||
disconnectFromRelay();
|
disconnectFromRelay();
|
||||||
|
|
||||||
// Reset UI
|
// Reset UI
|
||||||
mainInterface.classList.add('hidden');
|
// mainInterface.classList.add('hidden');
|
||||||
loginSection.classList.remove('hidden');
|
loginSection.classList.remove('hidden');
|
||||||
updateConfigStatus(false);
|
updateConfigStatus(false);
|
||||||
relayStatus.textContent = 'READY TO FETCH';
|
|
||||||
relayStatus.className = 'status disconnected';
|
|
||||||
updateLoginLogoutButton();
|
updateLoginLogoutButton();
|
||||||
hideAuthRulesSection();
|
updateAdminSectionsVisibility();
|
||||||
|
|
||||||
console.log('Logout event handled successfully');
|
console.log('Logout event handled successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect from relay and clean up connections
|
|
||||||
function disconnectFromRelay() {
|
// Update visibility of admin sections based on login and relay connection status
|
||||||
if (relayPool) {
|
function updateAdminSectionsVisibility() {
|
||||||
console.log('Cleaning up relay pool connection...');
|
const divConfig = document.getElementById('div_config');
|
||||||
const url = relayUrl.value.trim();
|
const authRulesSection = document.getElementById('authRulesSection');
|
||||||
if (url) {
|
const shouldShow = isLoggedIn && isRelayConnected;
|
||||||
relayPool.close([url]);
|
|
||||||
}
|
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
|
||||||
relayPool = null;
|
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
|
||||||
subscriptionId = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show main interface after login
|
// Show main interface after login
|
||||||
function showMainInterface() {
|
function showMainInterface() {
|
||||||
loginSection.classList.add('hidden');
|
loginSection.classList.add('hidden');
|
||||||
mainInterface.classList.remove('hidden');
|
// mainInterface.classList.remove('hidden');
|
||||||
updateLoginLogoutButton();
|
updateLoginLogoutButton();
|
||||||
|
updateAdminSectionsVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load user profile using nostr-tools pool
|
// Load user profile using nostr-tools pool
|
||||||
@@ -1381,7 +1373,7 @@
|
|||||||
// Clean up configuration pool
|
// Clean up configuration pool
|
||||||
if (relayPool) {
|
if (relayPool) {
|
||||||
console.log('Closing configuration pool...');
|
console.log('Closing configuration pool...');
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
if (url) {
|
if (url) {
|
||||||
relayPool.close([url]);
|
relayPool.close([url]);
|
||||||
}
|
}
|
||||||
@@ -1396,11 +1388,9 @@
|
|||||||
currentConfig = null;
|
currentConfig = null;
|
||||||
|
|
||||||
// Reset UI - keep persistent auth container visible
|
// Reset UI - keep persistent auth container visible
|
||||||
mainInterface.classList.add('hidden');
|
// mainInterface.classList.add('hidden');
|
||||||
loginSection.classList.remove('hidden');
|
loginSection.classList.remove('hidden');
|
||||||
updateConfigStatus(false);
|
updateConfigStatus(false);
|
||||||
relayStatus.textContent = 'READY TO FETCH';
|
|
||||||
relayStatus.className = 'status disconnected';
|
|
||||||
updateLoginLogoutButton();
|
updateLoginLogoutButton();
|
||||||
|
|
||||||
console.log('Logged out successfully');
|
console.log('Logged out successfully');
|
||||||
@@ -1412,12 +1402,8 @@
|
|||||||
|
|
||||||
function updateConfigStatus(loaded) {
|
function updateConfigStatus(loaded) {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
configStatus.textContent = 'CONFIGURATION LOADED';
|
|
||||||
configStatus.className = 'status connected';
|
|
||||||
configDisplay.classList.remove('hidden');
|
configDisplay.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
configStatus.textContent = 'NO CONFIGURATION LOADED';
|
|
||||||
configStatus.className = 'status disconnected';
|
|
||||||
configDisplay.classList.add('hidden');
|
configDisplay.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1445,8 +1431,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Connecting to relay via SimplePool: ${url}`);
|
console.log(`Connecting to relay via SimplePool: ${url}`);
|
||||||
relayStatus.textContent = 'CONNECTING...';
|
|
||||||
relayStatus.className = 'status connected';
|
|
||||||
|
|
||||||
// Clean up existing pool
|
// Clean up existing pool
|
||||||
if (relayPool) {
|
if (relayPool) {
|
||||||
@@ -1462,9 +1446,6 @@
|
|||||||
|
|
||||||
console.log(`Generated subscription ID: ${subscriptionId}`);
|
console.log(`Generated subscription ID: ${subscriptionId}`);
|
||||||
|
|
||||||
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
|
|
||||||
relayStatus.className = 'status connected';
|
|
||||||
|
|
||||||
// Subscribe to kind 23457 events (admin response events)
|
// Subscribe to kind 23457 events (admin response events)
|
||||||
const subscription = relayPool.subscribeMany([url], [{
|
const subscription = relayPool.subscribeMany([url], [{
|
||||||
since: Math.floor(Date.now() / 1000),
|
since: Math.floor(Date.now() / 1000),
|
||||||
@@ -1488,9 +1469,6 @@
|
|||||||
|
|
||||||
// Process admin response event
|
// Process admin response event
|
||||||
processAdminResponse(event);
|
processAdminResponse(event);
|
||||||
|
|
||||||
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
|
||||||
relayStatus.className = 'status connected';
|
|
||||||
},
|
},
|
||||||
oneose() {
|
oneose() {
|
||||||
console.log('EOSE received - End of stored events');
|
console.log('EOSE received - End of stored events');
|
||||||
@@ -1498,19 +1476,10 @@
|
|||||||
|
|
||||||
if (!currentConfig) {
|
if (!currentConfig) {
|
||||||
console.log('No configuration events were received');
|
console.log('No configuration events were received');
|
||||||
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
|
|
||||||
configStatus.className = 'status error';
|
|
||||||
relayStatus.textContent = 'SUBSCRIBED - NO EVENTS FOUND';
|
|
||||||
relayStatus.className = 'status error';
|
|
||||||
} else {
|
|
||||||
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
|
|
||||||
relayStatus.className = 'status connected';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclose(reason) {
|
onclose(reason) {
|
||||||
console.log('Subscription closed:', reason);
|
console.log('Subscription closed:', reason);
|
||||||
relayStatus.textContent = 'SUBSCRIPTION CLOSED';
|
|
||||||
relayStatus.className = 'status error';
|
|
||||||
updateConfigStatus(false);
|
updateConfigStatus(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1525,9 +1494,6 @@
|
|||||||
console.error('Configuration subscription failed:', error.message);
|
console.error('Configuration subscription failed:', error.message);
|
||||||
console.error('Configuration subscription failed:', error);
|
console.error('Configuration subscription failed:', error);
|
||||||
console.error('Error stack:', error.stack);
|
console.error('Error stack:', error.stack);
|
||||||
|
|
||||||
relayStatus.textContent = 'SUBSCRIPTION FAILED';
|
|
||||||
relayStatus.className = 'status error';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1679,8 +1645,6 @@
|
|||||||
} else {
|
} else {
|
||||||
console.log('No configuration data received');
|
console.log('No configuration data received');
|
||||||
updateConfigStatus(false);
|
updateConfigStatus(false);
|
||||||
configStatus.textContent = 'NO CONFIGURATION DATA RECEIVED';
|
|
||||||
configStatus.className = 'status error';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also log to test interface for debugging
|
// Also log to test interface for debugging
|
||||||
@@ -1777,7 +1741,7 @@
|
|||||||
currentAuthRules = responseData.data;
|
currentAuthRules = responseData.data;
|
||||||
console.log('Updated currentAuthRules with', currentAuthRules.length, 'rules');
|
console.log('Updated currentAuthRules with', currentAuthRules.length, 'rules');
|
||||||
|
|
||||||
// Always show the auth rules table when we receive data
|
// Always show the auth rules table when we receive data (no VIEW RULES button anymore)
|
||||||
console.log('Auto-showing auth rules table since we received data...');
|
console.log('Auto-showing auth rules table since we received data...');
|
||||||
showAuthRulesTable();
|
showAuthRulesTable();
|
||||||
|
|
||||||
@@ -1787,7 +1751,7 @@
|
|||||||
currentAuthRules = [];
|
currentAuthRules = [];
|
||||||
console.log('No auth rules data received, cleared currentAuthRules');
|
console.log('No auth rules data received, cleared currentAuthRules');
|
||||||
|
|
||||||
// Show empty table
|
// Show empty table (no VIEW RULES button anymore)
|
||||||
console.log('Auto-showing auth rules table with empty data...');
|
console.log('Auto-showing auth rules table with empty data...');
|
||||||
showAuthRulesTable();
|
showAuthRulesTable();
|
||||||
|
|
||||||
@@ -1987,21 +1951,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Config query command sent successfully - waiting for response...');
|
console.log('Config query command sent successfully - waiting for response...');
|
||||||
configStatus.textContent = 'CONFIGURATION QUERY SENT - WAITING FOR RESPONSE';
|
|
||||||
configStatus.className = 'status connected';
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Not logged in - only subscription established for testing');
|
console.log('Not logged in - only subscription established for testing');
|
||||||
configStatus.textContent = 'SUBSCRIPTION ESTABLISHED - LOGIN REQUIRED FOR CONFIG QUERY';
|
|
||||||
configStatus.className = 'status disconnected';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch configuration:', error);
|
console.error('Failed to fetch configuration:', error);
|
||||||
configStatus.textContent = 'CONFIGURATION FETCH FAILED';
|
|
||||||
configStatus.className = 'status error';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2060,56 +2018,33 @@
|
|||||||
|
|
||||||
configForm.innerHTML = '';
|
configForm.innerHTML = '';
|
||||||
|
|
||||||
// Define field types and validation for different config parameters
|
// Define field types and validation for different config parameters (aligned with README.md)
|
||||||
const fieldTypes = {
|
const fieldTypes = {
|
||||||
'auth_enabled': 'boolean',
|
'auth_enabled': 'boolean',
|
||||||
'nip42_auth_required_events': 'boolean',
|
'nip42_auth_required': 'boolean',
|
||||||
'nip42_auth_required_subscriptions': 'boolean',
|
|
||||||
'nip40_expiration_enabled': 'boolean',
|
'nip40_expiration_enabled': 'boolean',
|
||||||
'nip40_expiration_strict': 'boolean',
|
|
||||||
'nip40_expiration_filter': 'boolean',
|
|
||||||
'relay_port': 'number',
|
|
||||||
'max_connections': 'number',
|
'max_connections': 'number',
|
||||||
'pow_min_difficulty': 'number',
|
'pow_min_difficulty': 'number',
|
||||||
'nip42_challenge_expiration': 'number',
|
'nip42_challenge_timeout': 'number',
|
||||||
'nip40_expiration_grace_period': 'number',
|
|
||||||
'max_subscriptions_per_client': 'number',
|
'max_subscriptions_per_client': 'number',
|
||||||
'max_total_subscriptions': 'number',
|
|
||||||
'max_filters_per_subscription': 'number',
|
|
||||||
'max_event_tags': 'number',
|
'max_event_tags': 'number',
|
||||||
'max_content_length': 'number',
|
'max_content_length': 'number'
|
||||||
'max_message_length': 'number',
|
|
||||||
'default_limit': 'number',
|
|
||||||
'max_limit': 'number'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const descriptions = {
|
const descriptions = {
|
||||||
'relay_pubkey': 'Relay Public Key (Read-only)',
|
'relay_pubkey': 'Relay Public Key (Read-only)',
|
||||||
'auth_enabled': 'Enable Authentication',
|
'auth_enabled': 'Enable Authentication',
|
||||||
'nip42_auth_required_events': 'Require Auth for Events',
|
'nip42_auth_required': 'Enable NIP-42 Cryptographic Authentication',
|
||||||
'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions',
|
'nip42_auth_required_kinds': 'Event Kinds Requiring NIP-42 Auth',
|
||||||
'nip42_auth_required_kinds': 'Auth Required Event Kinds',
|
'nip42_challenge_timeout': 'NIP-42 Challenge Expiration Seconds',
|
||||||
'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)',
|
|
||||||
'relay_port': 'Relay Port',
|
|
||||||
'max_connections': 'Maximum Connections',
|
'max_connections': 'Maximum Connections',
|
||||||
'relay_description': 'Relay Description',
|
'relay_description': 'Relay Description',
|
||||||
'relay_contact': 'Relay Contact',
|
'relay_contact': 'Relay Contact',
|
||||||
'relay_software': 'Relay Software URL',
|
'pow_min_difficulty': 'Minimum Proof-of-Work Difficulty',
|
||||||
'relay_version': 'Relay Version',
|
|
||||||
'pow_min_difficulty': 'Minimum PoW Difficulty',
|
|
||||||
'pow_mode': 'PoW Mode',
|
|
||||||
'nip40_expiration_enabled': 'Enable Event Expiration',
|
'nip40_expiration_enabled': 'Enable Event Expiration',
|
||||||
'nip40_expiration_strict': 'Strict Expiration Mode',
|
|
||||||
'nip40_expiration_filter': 'Filter Expired Events',
|
|
||||||
'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)',
|
|
||||||
'max_subscriptions_per_client': 'Max Subscriptions per Client',
|
'max_subscriptions_per_client': 'Max Subscriptions per Client',
|
||||||
'max_total_subscriptions': 'Max Total Subscriptions',
|
'max_event_tags': 'Maximum Tags per Event',
|
||||||
'max_filters_per_subscription': 'Max Filters per Subscription',
|
'max_content_length': 'Maximum Event Content Length'
|
||||||
'max_event_tags': 'Max Event Tags',
|
|
||||||
'max_content_length': 'Max Content Length',
|
|
||||||
'max_message_length': 'Max Message Length',
|
|
||||||
'default_limit': 'Default Query Limit',
|
|
||||||
'max_limit': 'Maximum Query Limit'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process configuration tags (no d tag filtering for ephemeral events)
|
// Process configuration tags (no d tag filtering for ephemeral events)
|
||||||
@@ -2298,7 +2233,7 @@
|
|||||||
console.log(`Config update event signed with ${configObjects.length} objects`);
|
console.log(`Config update event signed with ${configObjects.length} objects`);
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -2460,9 +2395,6 @@
|
|||||||
|
|
||||||
// DOM elements for auth rules
|
// DOM elements for auth rules
|
||||||
const authRulesSection = document.getElementById('authRulesSection');
|
const authRulesSection = document.getElementById('authRulesSection');
|
||||||
const authRulesStatus = document.getElementById('authRulesStatus');
|
|
||||||
const viewAuthRulesBtn = document.getElementById('viewAuthRulesBtn');
|
|
||||||
const addAuthRuleBtn = document.getElementById('addAuthRuleBtn');
|
|
||||||
const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn');
|
const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn');
|
||||||
const authRulesTableContainer = document.getElementById('authRulesTableContainer');
|
const authRulesTableContainer = document.getElementById('authRulesTableContainer');
|
||||||
const authRulesTableBody = document.getElementById('authRulesTableBody');
|
const authRulesTableBody = document.getElementById('authRulesTableBody');
|
||||||
@@ -2471,9 +2403,6 @@
|
|||||||
const authRuleFormTitle = document.getElementById('authRuleFormTitle');
|
const authRuleFormTitle = document.getElementById('authRuleFormTitle');
|
||||||
const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn');
|
const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn');
|
||||||
const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn');
|
const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn');
|
||||||
const authRulesStatusDisplay = document.getElementById('authRulesStatusDisplay');
|
|
||||||
const authSystemStatus = document.getElementById('authSystemStatus');
|
|
||||||
const authRulesCount = document.getElementById('authRulesCount');
|
|
||||||
|
|
||||||
// Show auth rules section after login
|
// Show auth rules section after login
|
||||||
function showAuthRulesSection() {
|
function showAuthRulesSection() {
|
||||||
@@ -2496,9 +2425,6 @@
|
|||||||
if (authRuleFormContainer) {
|
if (authRuleFormContainer) {
|
||||||
authRuleFormContainer.style.display = 'none';
|
authRuleFormContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
if (authRulesStatusDisplay) {
|
|
||||||
authRulesStatusDisplay.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
currentAuthRules = [];
|
currentAuthRules = [];
|
||||||
editingAuthRule = null;
|
editingAuthRule = null;
|
||||||
@@ -2506,28 +2432,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update auth rules status indicator
|
// Update auth rules status indicator (removed - no status element)
|
||||||
function updateAuthRulesStatus(status) {
|
function updateAuthRulesStatus(status) {
|
||||||
if (!authRulesStatus) return;
|
// Status element removed - no-op
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'ready':
|
|
||||||
authRulesStatus.textContent = 'READY';
|
|
||||||
authRulesStatus.className = 'status disconnected';
|
|
||||||
break;
|
|
||||||
case 'loading':
|
|
||||||
authRulesStatus.textContent = 'LOADING';
|
|
||||||
authRulesStatus.className = 'status connected';
|
|
||||||
break;
|
|
||||||
case 'loaded':
|
|
||||||
authRulesStatus.textContent = 'LOADED';
|
|
||||||
authRulesStatus.className = 'status connected';
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
authRulesStatus.textContent = 'ERROR';
|
|
||||||
authRulesStatus.className = 'status error';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load auth rules from relay using admin API
|
// Load auth rules from relay using admin API
|
||||||
@@ -2573,7 +2480,7 @@
|
|||||||
log('Sending auth rules query to relay...', 'INFO');
|
log('Sending auth rules query to relay...', 'INFO');
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -2659,31 +2566,21 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update status display
|
// Update status display
|
||||||
if (authRulesCount) {
|
console.log(`Total Rules: ${rules.length}, Active Rules: ${rules.filter(r => r.enabled !== false).length}`);
|
||||||
const activeRules = rules.filter(r => r.enabled !== false).length;
|
|
||||||
authRulesCount.textContent = `Total Rules: ${rules.length} (${activeRules} active)`;
|
|
||||||
console.log(`Updated status display: ${rules.length} total, ${activeRules} active`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('=== END DISPLAY AUTH RULES DEBUG ===');
|
console.log('=== END DISPLAY AUTH RULES DEBUG ===');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show auth rules table
|
// Show auth rules table (automatically called when auth rules are loaded)
|
||||||
function showAuthRulesTable() {
|
function showAuthRulesTable() {
|
||||||
console.log('=== SHOW AUTH RULES TABLE DEBUG ===');
|
console.log('=== SHOW AUTH RULES TABLE DEBUG ===');
|
||||||
console.log('authRulesTableContainer element:', authRulesTableContainer);
|
console.log('authRulesTableContainer element:', authRulesTableContainer);
|
||||||
console.log('authRulesStatusDisplay element:', authRulesStatusDisplay);
|
|
||||||
console.log('Current display style:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found');
|
console.log('Current display style:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found');
|
||||||
|
|
||||||
if (authRulesTableContainer) {
|
if (authRulesTableContainer) {
|
||||||
authRulesTableContainer.style.display = 'block';
|
authRulesTableContainer.style.display = 'block';
|
||||||
console.log('Set authRulesTableContainer display to block');
|
console.log('Set authRulesTableContainer display to block');
|
||||||
|
|
||||||
if (authRulesStatusDisplay) {
|
|
||||||
authRulesStatusDisplay.style.display = 'block';
|
|
||||||
console.log('Set authRulesStatusDisplay display to block');
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we already have cached auth rules, display them immediately
|
// If we already have cached auth rules, display them immediately
|
||||||
if (currentAuthRules && currentAuthRules.length >= 0) {
|
if (currentAuthRules && currentAuthRules.length >= 0) {
|
||||||
console.log('Displaying cached auth rules:', currentAuthRules.length, 'rules');
|
console.log('Displaying cached auth rules:', currentAuthRules.length, 'rules');
|
||||||
@@ -2789,7 +2686,7 @@
|
|||||||
log('Sending delete auth rule command to relay...', 'INFO');
|
log('Sending delete auth rule command to relay...', 'INFO');
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -2928,34 +2825,20 @@
|
|||||||
|
|
||||||
// Update existing logout and showMainInterface functions to handle auth rules
|
// Update existing logout and showMainInterface functions to handle auth rules
|
||||||
const originalLogout = logout;
|
const originalLogout = logout;
|
||||||
logout = async function() {
|
logout = async function () {
|
||||||
hideAuthRulesSection();
|
hideAuthRulesSection();
|
||||||
await originalLogout();
|
await originalLogout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalShowMainInterface = showMainInterface;
|
const originalShowMainInterface = showMainInterface;
|
||||||
showMainInterface = function() {
|
showMainInterface = function () {
|
||||||
originalShowMainInterface();
|
originalShowMainInterface();
|
||||||
showAuthRulesSection();
|
showAuthRulesSection();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auth rules event handlers
|
// Auth rules event handlers
|
||||||
if (viewAuthRulesBtn) {
|
|
||||||
viewAuthRulesBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
showAuthRulesTable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addAuthRuleBtn) {
|
|
||||||
addAuthRuleBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
showAddAuthRuleForm();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refreshAuthRulesBtn) {
|
if (refreshAuthRulesBtn) {
|
||||||
refreshAuthRulesBtn.addEventListener('click', function(e) {
|
refreshAuthRulesBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
loadAuthRules();
|
loadAuthRules();
|
||||||
});
|
});
|
||||||
@@ -2966,7 +2849,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cancelAuthRuleBtn) {
|
if (cancelAuthRuleBtn) {
|
||||||
cancelAuthRuleBtn.addEventListener('click', function(e) {
|
cancelAuthRuleBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hideAuthRuleForm();
|
hideAuthRuleForm();
|
||||||
});
|
});
|
||||||
@@ -3014,34 +2897,29 @@
|
|||||||
// Add blacklist rule (updated to use combined input)
|
// Add blacklist rule (updated to use combined input)
|
||||||
function addBlacklistRule() {
|
function addBlacklistRule() {
|
||||||
const input = document.getElementById('authRulePubkey');
|
const input = document.getElementById('authRulePubkey');
|
||||||
const statusDiv = document.getElementById('authRuleStatus');
|
|
||||||
|
|
||||||
if (!input || !statusDiv) return;
|
if (!input) return;
|
||||||
|
|
||||||
const inputValue = input.value.trim();
|
const inputValue = input.value.trim();
|
||||||
if (!inputValue) {
|
if (!inputValue) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Please enter a pubkey or nsec', 'ERROR');
|
||||||
statusDiv.textContent = 'Please enter a pubkey or nsec';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert nsec to hex if needed
|
// Convert nsec to hex if needed
|
||||||
const hexPubkey = nsecToHex(inputValue);
|
const hexPubkey = nsecToHex(inputValue);
|
||||||
if (!hexPubkey) {
|
if (!hexPubkey) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
|
||||||
statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate hex length
|
// Validate hex length
|
||||||
if (hexPubkey.length !== 64) {
|
if (hexPubkey.length !== 64) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR');
|
||||||
statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
statusDiv.className = 'rule-status';
|
log('Adding to blacklist...', 'INFO');
|
||||||
statusDiv.textContent = 'Adding to blacklist...';
|
|
||||||
|
|
||||||
// Create auth rule data
|
// Create auth rule data
|
||||||
const ruleData = {
|
const ruleData = {
|
||||||
@@ -3054,8 +2932,7 @@
|
|||||||
// Add to WebSocket queue for processing
|
// Add to WebSocket queue for processing
|
||||||
addAuthRuleViaWebSocket(ruleData)
|
addAuthRuleViaWebSocket(ruleData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
statusDiv.className = 'rule-status success';
|
log(`Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`, 'INFO');
|
||||||
statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`;
|
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
|
||||||
// Refresh auth rules display if visible
|
// Refresh auth rules display if visible
|
||||||
@@ -3064,38 +2941,33 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
statusDiv.className = 'rule-status error';
|
log(`Failed to add rule: ${error.message}`, 'ERROR');
|
||||||
statusDiv.textContent = `Failed to add rule: ${error.message}`;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add whitelist rule (updated to use combined input)
|
// Add whitelist rule (updated to use combined input)
|
||||||
function addWhitelistRule() {
|
function addWhitelistRule() {
|
||||||
const input = document.getElementById('authRulePubkey');
|
const input = document.getElementById('authRulePubkey');
|
||||||
const statusDiv = document.getElementById('authRuleStatus');
|
|
||||||
const warningDiv = document.getElementById('whitelistWarning');
|
const warningDiv = document.getElementById('whitelistWarning');
|
||||||
|
|
||||||
if (!input || !statusDiv) return;
|
if (!input) return;
|
||||||
|
|
||||||
const inputValue = input.value.trim();
|
const inputValue = input.value.trim();
|
||||||
if (!inputValue) {
|
if (!inputValue) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Please enter a pubkey or nsec', 'ERROR');
|
||||||
statusDiv.textContent = 'Please enter a pubkey or nsec';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert nsec to hex if needed
|
// Convert nsec to hex if needed
|
||||||
const hexPubkey = nsecToHex(inputValue);
|
const hexPubkey = nsecToHex(inputValue);
|
||||||
if (!hexPubkey) {
|
if (!hexPubkey) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
|
||||||
statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate hex length
|
// Validate hex length
|
||||||
if (hexPubkey.length !== 64) {
|
if (hexPubkey.length !== 64) {
|
||||||
statusDiv.className = 'rule-status error';
|
log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR');
|
||||||
statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3104,8 +2976,7 @@
|
|||||||
warningDiv.style.display = 'block';
|
warningDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
statusDiv.className = 'rule-status';
|
log('Adding to whitelist...', 'INFO');
|
||||||
statusDiv.textContent = 'Adding to whitelist...';
|
|
||||||
|
|
||||||
// Create auth rule data
|
// Create auth rule data
|
||||||
const ruleData = {
|
const ruleData = {
|
||||||
@@ -3118,8 +2989,7 @@
|
|||||||
// Add to WebSocket queue for processing
|
// Add to WebSocket queue for processing
|
||||||
addAuthRuleViaWebSocket(ruleData)
|
addAuthRuleViaWebSocket(ruleData)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
statusDiv.className = 'rule-status success';
|
log(`Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`, 'INFO');
|
||||||
statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`;
|
|
||||||
input.value = '';
|
input.value = '';
|
||||||
|
|
||||||
// Refresh auth rules display if visible
|
// Refresh auth rules display if visible
|
||||||
@@ -3128,8 +2998,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
statusDiv.className = 'rule-status error';
|
log(`Failed to add rule: ${error.message}`, 'ERROR');
|
||||||
statusDiv.textContent = `Failed to add rule: ${error.message}`;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3202,7 +3071,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -3308,7 +3177,7 @@
|
|||||||
logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -3382,7 +3251,7 @@
|
|||||||
logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -3465,7 +3334,7 @@
|
|||||||
logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
||||||
|
|
||||||
// Publish via SimplePool with detailed error diagnostics
|
// Publish via SimplePool with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -3548,7 +3417,7 @@
|
|||||||
logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
||||||
|
|
||||||
// Publish via SimplePool
|
// Publish via SimplePool
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|
||||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||||
@@ -3690,7 +3559,7 @@
|
|||||||
logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT');
|
||||||
|
|
||||||
// Publish via SimplePool to the same relay with detailed error diagnostics
|
// Publish via SimplePool to the same relay with detailed error diagnostics
|
||||||
const url = relayUrl.value.trim();
|
const url = relayConnectionUrl.value.trim();
|
||||||
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
|
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
|
||||||
|
|
||||||
const publishPromises = relayPool.publish([url], signedEvent);
|
const publishPromises = relayPool.publish([url], signedEvent);
|
||||||
|
|||||||
Binary file not shown.
@@ -63,7 +63,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift 2
|
shift 2
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
--preserve-database)
|
-d|--preserve-database)
|
||||||
PRESERVE_DATABASE=true
|
PRESERVE_DATABASE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
@@ -282,7 +282,7 @@ cd build
|
|||||||
# Start relay in background and capture its PID
|
# Start relay in background and capture its PID
|
||||||
if [ "$USE_TEST_KEYS" = true ]; then
|
if [ "$USE_TEST_KEYS" = true ]; then
|
||||||
echo "Using deterministic test keys for development..."
|
echo "Using deterministic test keys for development..."
|
||||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
|
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
|
||||||
elif [ -n "$RELAY_ARGS" ]; then
|
elif [ -n "$RELAY_ARGS" ]; then
|
||||||
echo "Starting relay with custom configuration..."
|
echo "Starting relay with custom configuration..."
|
||||||
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
|
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
|
||||||
|
|||||||
@@ -1,455 +0,0 @@
|
|||||||
# NIP-11 Relay Connection Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Implement NIP-11 relay information fetching in the web admin interface to replace hardcoded relay pubkey and provide proper relay connection flow.
|
|
||||||
|
|
||||||
## Current Issues
|
|
||||||
1. **Hardcoded Relay Pubkey**: `getRelayPubkey()` returns hardcoded value `'4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'`
|
|
||||||
2. **Relay URL in Debug Section**: Currently in "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 336-385)
|
|
||||||
3. **No Relay Verification**: Users can attempt admin operations without verifying relay identity
|
|
||||||
4. **Missing NIP-11 Support**: No fetching of relay information document
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### 1. New Relay Connection Section (HTML Structure)
|
|
||||||
|
|
||||||
Add after User Info section (around line 332):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Relay Connection Section -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>RELAY CONNECTION</h2>
|
|
||||||
<div class="input-group">
|
|
||||||
<label for="relay-url-input">Relay URL:</label>
|
|
||||||
<input type="text" id="relay-url-input" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
|
|
||||||
</div>
|
|
||||||
<div class="inline-buttons">
|
|
||||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
|
||||||
<button type="button" id="disconnect-relay-btn" style="display: none;">DISCONNECT</button>
|
|
||||||
</div>
|
|
||||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
|
||||||
|
|
||||||
<!-- Relay Information Display -->
|
|
||||||
<div id="relay-info-display" class="hidden">
|
|
||||||
<h3>Relay Information</h3>
|
|
||||||
<div class="user-info">
|
|
||||||
<div><strong>Name:</strong> <span id="relay-name">-</span></div>
|
|
||||||
<div><strong>Description:</strong> <span id="relay-description">-</span></div>
|
|
||||||
<div><strong>Public Key:</strong>
|
|
||||||
<div class="user-pubkey" id="relay-pubkey-display">-</div>
|
|
||||||
</div>
|
|
||||||
<div><strong>Software:</strong> <span id="relay-software">-</span></div>
|
|
||||||
<div><strong>Version:</strong> <span id="relay-version">-</span></div>
|
|
||||||
<div><strong>Contact:</strong> <span id="relay-contact">-</span></div>
|
|
||||||
<div><strong>Supported NIPs:</strong> <span id="relay-nips">-</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. JavaScript Implementation
|
|
||||||
|
|
||||||
#### Global State Variables
|
|
||||||
Add to global state section (around line 535):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Relay connection state
|
|
||||||
let relayInfo = null;
|
|
||||||
let isRelayConnected = false;
|
|
||||||
let relayWebSocket = null;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### NIP-11 Fetching Function
|
|
||||||
Add new function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Fetch relay information using NIP-11
|
|
||||||
async function fetchRelayInfo(relayUrl) {
|
|
||||||
try {
|
|
||||||
console.log('=== FETCHING RELAY INFO VIA NIP-11 ===');
|
|
||||||
console.log('Relay URL:', relayUrl);
|
|
||||||
|
|
||||||
// Convert WebSocket URL to HTTP URL for NIP-11
|
|
||||||
let httpUrl = relayUrl;
|
|
||||||
if (relayUrl.startsWith('ws://')) {
|
|
||||||
httpUrl = relayUrl.replace('ws://', 'http://');
|
|
||||||
} else if (relayUrl.startsWith('wss://')) {
|
|
||||||
httpUrl = relayUrl.replace('wss://', 'https://');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('HTTP URL for NIP-11:', httpUrl);
|
|
||||||
|
|
||||||
// Fetch relay information document
|
|
||||||
const response = await fetch(httpUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/nostr+json'
|
|
||||||
},
|
|
||||||
// Add timeout
|
|
||||||
signal: AbortSignal.timeout(10000) // 10 second timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if (!contentType || !contentType.includes('application/json')) {
|
|
||||||
throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const relayInfoData = await response.json();
|
|
||||||
console.log('Fetched relay info:', relayInfoData);
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!relayInfoData.pubkey) {
|
|
||||||
throw new Error('Relay information missing required pubkey field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate pubkey format (64 hex characters)
|
|
||||||
if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) {
|
|
||||||
throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return relayInfoData;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch relay info:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Relay Connection Function
|
|
||||||
Add new function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Connect to relay and fetch information
|
|
||||||
async function connectToRelay() {
|
|
||||||
try {
|
|
||||||
const relayUrlInput = document.getElementById('relay-url-input');
|
|
||||||
const connectBtn = document.getElementById('connect-relay-btn');
|
|
||||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
|
||||||
const statusDiv = document.getElementById('relay-connection-status');
|
|
||||||
const infoDisplay = document.getElementById('relay-info-display');
|
|
||||||
|
|
||||||
const url = relayUrlInput.value.trim();
|
|
||||||
if (!url) {
|
|
||||||
throw new Error('Please enter a relay URL');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update UI to show connecting state
|
|
||||||
connectBtn.disabled = true;
|
|
||||||
statusDiv.textContent = 'CONNECTING...';
|
|
||||||
statusDiv.className = 'status connected';
|
|
||||||
|
|
||||||
console.log('Connecting to relay:', url);
|
|
||||||
|
|
||||||
// Fetch relay information via NIP-11
|
|
||||||
console.log('Fetching relay information...');
|
|
||||||
const fetchedRelayInfo = await fetchRelayInfo(url);
|
|
||||||
|
|
||||||
// Test WebSocket connection
|
|
||||||
console.log('Testing WebSocket connection...');
|
|
||||||
await testWebSocketConnection(url);
|
|
||||||
|
|
||||||
// Store relay information
|
|
||||||
relayInfo = fetchedRelayInfo;
|
|
||||||
isRelayConnected = true;
|
|
||||||
|
|
||||||
// Update UI with relay information
|
|
||||||
displayRelayInfo(relayInfo);
|
|
||||||
|
|
||||||
// Update connection status
|
|
||||||
statusDiv.textContent = 'CONNECTED';
|
|
||||||
statusDiv.className = 'status connected';
|
|
||||||
|
|
||||||
// Update button states
|
|
||||||
connectBtn.style.display = 'none';
|
|
||||||
disconnectBtn.style.display = 'inline-block';
|
|
||||||
relayUrlInput.disabled = true;
|
|
||||||
|
|
||||||
// Show relay info
|
|
||||||
infoDisplay.classList.remove('hidden');
|
|
||||||
|
|
||||||
console.log('Successfully connected to relay:', relayInfo.name || url);
|
|
||||||
log(`Connected to relay: ${relayInfo.name || url}`, 'INFO');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to relay:', error);
|
|
||||||
|
|
||||||
// Reset UI state
|
|
||||||
const connectBtn = document.getElementById('connect-relay-btn');
|
|
||||||
const statusDiv = document.getElementById('relay-connection-status');
|
|
||||||
|
|
||||||
connectBtn.disabled = false;
|
|
||||||
statusDiv.textContent = `CONNECTION FAILED: ${error.message}`;
|
|
||||||
statusDiv.className = 'status error';
|
|
||||||
|
|
||||||
// Clear any partial state
|
|
||||||
relayInfo = null;
|
|
||||||
isRelayConnected = false;
|
|
||||||
|
|
||||||
log(`Failed to connect to relay: ${error.message}`, 'ERROR');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### WebSocket Connection Test
|
|
||||||
Add new function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Test WebSocket connection to relay
|
|
||||||
async function testWebSocketConnection(url) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
ws.close();
|
|
||||||
reject(new Error('WebSocket connection timeout'));
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
const ws = new WebSocket(url);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
console.log('WebSocket connection successful');
|
|
||||||
ws.close();
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
console.error('WebSocket connection failed:', error);
|
|
||||||
reject(new Error('WebSocket connection failed'));
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
|
||||||
if (event.code !== 1000) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Display Relay Information
|
|
||||||
Add new function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Display relay information in the UI
|
|
||||||
function displayRelayInfo(info) {
|
|
||||||
document.getElementById('relay-name').textContent = info.name || 'Unknown';
|
|
||||||
document.getElementById('relay-description').textContent = info.description || 'No description';
|
|
||||||
document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown';
|
|
||||||
document.getElementById('relay-software').textContent = info.software || 'Unknown';
|
|
||||||
document.getElementById('relay-version').textContent = info.version || 'Unknown';
|
|
||||||
document.getElementById('relay-contact').textContent = info.contact || 'No contact info';
|
|
||||||
|
|
||||||
// Format supported NIPs
|
|
||||||
let nipsText = 'None specified';
|
|
||||||
if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) {
|
|
||||||
nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', ');
|
|
||||||
}
|
|
||||||
document.getElementById('relay-nips').textContent = nipsText;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Disconnect Function
|
|
||||||
Add new function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Disconnect from relay
|
|
||||||
function disconnectFromRelay() {
|
|
||||||
console.log('Disconnecting from relay...');
|
|
||||||
|
|
||||||
// Clear relay state
|
|
||||||
relayInfo = null;
|
|
||||||
isRelayConnected = false;
|
|
||||||
|
|
||||||
// Close any existing connections
|
|
||||||
if (relayPool) {
|
|
||||||
const url = document.getElementById('relay-url-input').value.trim();
|
|
||||||
if (url) {
|
|
||||||
relayPool.close([url]);
|
|
||||||
}
|
|
||||||
relayPool = null;
|
|
||||||
subscriptionId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset UI
|
|
||||||
const connectBtn = document.getElementById('connect-relay-btn');
|
|
||||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
|
||||||
const statusDiv = document.getElementById('relay-connection-status');
|
|
||||||
const infoDisplay = document.getElementById('relay-info-display');
|
|
||||||
const relayUrlInput = document.getElementById('relay-url-input');
|
|
||||||
|
|
||||||
connectBtn.style.display = 'inline-block';
|
|
||||||
disconnectBtn.style.display = 'none';
|
|
||||||
connectBtn.disabled = false;
|
|
||||||
relayUrlInput.disabled = false;
|
|
||||||
|
|
||||||
statusDiv.textContent = 'NOT CONNECTED';
|
|
||||||
statusDiv.className = 'status disconnected';
|
|
||||||
|
|
||||||
infoDisplay.classList.add('hidden');
|
|
||||||
|
|
||||||
// Reset configuration status
|
|
||||||
updateConfigStatus(false);
|
|
||||||
|
|
||||||
log('Disconnected from relay', 'INFO');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Update getRelayPubkey Function
|
|
||||||
Replace existing function (around line 3142):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Helper function to get relay pubkey from connected relay info
|
|
||||||
function getRelayPubkey() {
|
|
||||||
if (relayInfo && relayInfo.pubkey) {
|
|
||||||
return relayInfo.pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to hardcoded value if no relay connected (for testing)
|
|
||||||
console.warn('No relay connected, using fallback pubkey');
|
|
||||||
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Event Handlers
|
|
||||||
|
|
||||||
Add event handlers in the DOMContentLoaded section:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Relay connection event handlers
|
|
||||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
|
||||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
|
||||||
|
|
||||||
if (connectRelayBtn) {
|
|
||||||
connectRelayBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
connectToRelay().catch(error => {
|
|
||||||
console.error('Connect to relay failed:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disconnectRelayBtn) {
|
|
||||||
disconnectRelayBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
disconnectFromRelay();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Update Existing Functions
|
|
||||||
|
|
||||||
#### Update fetchConfiguration Function
|
|
||||||
Add relay connection check at the beginning:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async function fetchConfiguration() {
|
|
||||||
try {
|
|
||||||
console.log('=== FETCHING CONFIGURATION VIA ADMIN API ===');
|
|
||||||
|
|
||||||
// Check if relay is connected
|
|
||||||
if (!isRelayConnected || !relayInfo) {
|
|
||||||
throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... rest of existing function
|
|
||||||
} catch (error) {
|
|
||||||
// ... existing error handling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Update subscribeToConfiguration Function
|
|
||||||
Add relay connection check:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async function subscribeToConfiguration() {
|
|
||||||
try {
|
|
||||||
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
|
|
||||||
|
|
||||||
if (!isRelayConnected || !relayInfo) {
|
|
||||||
console.error('Must be connected to relay first');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the relay URL from the connection section instead of the debug section
|
|
||||||
const url = document.getElementById('relay-url-input').value.trim();
|
|
||||||
|
|
||||||
// ... rest of existing function
|
|
||||||
} catch (error) {
|
|
||||||
// ... existing error handling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Update UI Flow
|
|
||||||
|
|
||||||
#### Modify showMainInterface Function
|
|
||||||
Update to show relay connection requirement:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function showMainInterface() {
|
|
||||||
loginSection.classList.add('hidden');
|
|
||||||
mainInterface.classList.remove('hidden');
|
|
||||||
userPubkeyDisplay.textContent = userPubkey;
|
|
||||||
|
|
||||||
// Show message about relay connection requirement
|
|
||||||
if (!isRelayConnected) {
|
|
||||||
log('Please connect to a relay to access admin functions', 'INFO');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Remove/Update Debug Section
|
|
||||||
|
|
||||||
#### Option 1: Remove Debug Section Entirely
|
|
||||||
Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section.
|
|
||||||
|
|
||||||
#### Option 2: Keep Debug Section for Testing
|
|
||||||
Update the debug section to use the connected relay URL and add a note that it's for testing purposes.
|
|
||||||
|
|
||||||
### 7. Error Handling
|
|
||||||
|
|
||||||
Add comprehensive error handling for:
|
|
||||||
- Network timeouts
|
|
||||||
- Invalid relay URLs
|
|
||||||
- Missing NIP-11 support
|
|
||||||
- Invalid relay pubkey format
|
|
||||||
- WebSocket connection failures
|
|
||||||
- CORS issues
|
|
||||||
|
|
||||||
### 8. Security Considerations
|
|
||||||
|
|
||||||
- Validate relay pubkey format (64 hex characters)
|
|
||||||
- Verify relay identity before admin operations
|
|
||||||
- Handle CORS properly for NIP-11 requests
|
|
||||||
- Sanitize relay information display
|
|
||||||
- Warn users about connecting to untrusted relays
|
|
||||||
|
|
||||||
## Testing Plan
|
|
||||||
|
|
||||||
1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays)
|
|
||||||
2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures
|
|
||||||
3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch
|
|
||||||
4. **Admin API Integration**: Ensure admin commands use correct relay pubkey
|
|
||||||
5. **UI Flow**: Test complete user journey from login → relay connection → admin operations
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value
|
|
||||||
2. **Better UX**: Clear connection flow and relay information display
|
|
||||||
3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery
|
|
||||||
4. **Security**: Verifies relay identity before admin operations
|
|
||||||
5. **Flexibility**: Works with any NIP-11 compliant relay
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
- Existing users will need to connect to relay after this update
|
|
||||||
- Debug section can be kept for development/testing purposes
|
|
||||||
- All admin functions will require relay connection
|
|
||||||
- Relay pubkey will be dynamically fetched instead of hardcoded
|
|
||||||
81
src/config.c
81
src/config.c
@@ -921,39 +921,48 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
|||||||
// 1. Generate or use provided admin keypair
|
// 1. Generate or use provided admin keypair
|
||||||
unsigned char admin_privkey_bytes[32];
|
unsigned char admin_privkey_bytes[32];
|
||||||
char admin_privkey[65], admin_pubkey[65];
|
char admin_privkey[65], admin_pubkey[65];
|
||||||
|
int generated_admin_key = 0; // Track if we generated a new admin key
|
||||||
|
|
||||||
if (cli_options && strlen(cli_options->admin_privkey_override) == 64) {
|
if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) {
|
||||||
// Use provided admin private key
|
// Use provided admin public key directly - skip private key generation entirely
|
||||||
log_info("Using provided admin private key override");
|
log_info("Using provided admin public key override - skipping private key generation");
|
||||||
strncpy(admin_privkey, cli_options->admin_privkey_override, sizeof(admin_privkey) - 1);
|
strncpy(admin_pubkey, cli_options->admin_pubkey_override, sizeof(admin_pubkey) - 1);
|
||||||
admin_privkey[sizeof(admin_privkey) - 1] = '\0';
|
admin_pubkey[sizeof(admin_pubkey) - 1] = '\0';
|
||||||
|
|
||||||
// Convert hex string to bytes
|
// Validate the public key format (must be 64 hex characters)
|
||||||
if (nostr_hex_to_bytes(admin_privkey, admin_privkey_bytes, 32) != NOSTR_SUCCESS) {
|
for (int i = 0; i < 64; i++) {
|
||||||
log_error("Failed to convert admin private key hex to bytes");
|
char c = admin_pubkey[i];
|
||||||
|
if (!((c >= '0' && c <= '9') ||
|
||||||
|
(c >= 'a' && c <= 'f') ||
|
||||||
|
(c >= 'A' && c <= 'F'))) {
|
||||||
|
log_error("Invalid admin public key format - must contain only hex characters");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the private key
|
|
||||||
if (nostr_ec_private_key_verify(admin_privkey_bytes) != NOSTR_SUCCESS) {
|
|
||||||
log_error("Provided admin private key is invalid");
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip private key generation - we only need the pubkey for admin verification
|
||||||
|
// Set a dummy private key that will never be used (not displayed or stored)
|
||||||
|
memset(admin_privkey_bytes, 0, 32); // Zero out for security
|
||||||
|
memset(admin_privkey, 0, sizeof(admin_privkey)); // Zero out the hex string
|
||||||
|
generated_admin_key = 0; // Did not generate a new key
|
||||||
} else {
|
} else {
|
||||||
// Generate random admin keypair using /dev/urandom + nostr_core_lib
|
// Generate random admin keypair using /dev/urandom + nostr_core_lib
|
||||||
|
log_info("Generating random admin keypair");
|
||||||
if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) {
|
if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) {
|
||||||
log_error("Failed to generate admin private key");
|
log_error("Failed to generate admin private key");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
||||||
}
|
|
||||||
|
|
||||||
|
// Derive public key from private key
|
||||||
unsigned char admin_pubkey_bytes[32];
|
unsigned char admin_pubkey_bytes[32];
|
||||||
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) {
|
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) {
|
||||||
log_error("Failed to derive admin public key");
|
log_error("Failed to derive admin public key");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
|
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
|
||||||
|
generated_admin_key = 1; // Generated a new key
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Generate or use provided relay keypair
|
// 2. Generate or use provided relay keypair
|
||||||
unsigned char relay_privkey_bytes[32];
|
unsigned char relay_privkey_bytes[32];
|
||||||
@@ -1011,35 +1020,15 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
|||||||
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
|
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
|
||||||
log_info("Relay private key cached for secure storage after database initialization");
|
log_info("Relay private key cached for secure storage after database initialization");
|
||||||
|
|
||||||
// 6. Create initial configuration event using defaults (without private key)
|
// 6. Handle configuration setup - defaults will be populated after database initialization
|
||||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
|
log_info("Configuration setup prepared - defaults will be populated after database initialization");
|
||||||
if (!config_event) {
|
|
||||||
log_error("Failed to create default configuration event");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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);
|
|
||||||
}
|
|
||||||
g_pending_config_event = cJSON_Duplicate(config_event, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Cache the current config
|
// CLI overrides will be applied after database initialization in main.c
|
||||||
if (g_current_config) {
|
// This prevents "g_db is NULL" errors during first-time startup
|
||||||
cJSON_Delete(g_current_config);
|
|
||||||
}
|
|
||||||
g_current_config = cJSON_Duplicate(config_event, 1);
|
|
||||||
|
|
||||||
// 9. Clean up
|
// 10. Print admin private key for user to save (only if we generated a new key)
|
||||||
cJSON_Delete(config_event);
|
if (generated_admin_key) {
|
||||||
|
|
||||||
// 10. Print admin private key for user to save
|
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("=================================================================\n");
|
printf("=================================================================\n");
|
||||||
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
|
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
|
||||||
@@ -1052,6 +1041,18 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
|
|||||||
printf("Store it safely - it will not be displayed again.\n");
|
printf("Store it safely - it will not be displayed again.\n");
|
||||||
printf("=================================================================\n");
|
printf("=================================================================\n");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
} else {
|
||||||
|
printf("\n");
|
||||||
|
printf("=================================================================\n");
|
||||||
|
printf("RELAY STARTUP COMPLETE\n");
|
||||||
|
printf("=================================================================\n");
|
||||||
|
printf("Using provided admin public key for authentication\n");
|
||||||
|
printf("Admin Public Key: %s\n", admin_pubkey);
|
||||||
|
printf("Relay Public Key: %s\n", relay_pubkey);
|
||||||
|
printf("\nDatabase: %s\n", g_database_path);
|
||||||
|
printf("=================================================================\n");
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
log_success("First-time startup sequence completed");
|
log_success("First-time startup sequence completed");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ typedef struct {
|
|||||||
// Command line options structure for first-time startup
|
// Command line options structure for first-time startup
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int port_override; // -1 = not set, >0 = port value
|
int port_override; // -1 = not set, >0 = port value
|
||||||
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
char admin_pubkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||||
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||||
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
|
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
|
||||||
} cli_options_t;
|
} cli_options_t;
|
||||||
|
|||||||
2857
src/main.c
2857
src/main.c
File diff suppressed because it is too large
Load Diff
313
src/nip009.c
Normal file
313
src/nip009.c
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// NIP-09 EVENT DELETION REQUEST HANDLING
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <printf.h>
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_warning(const char* message);
|
||||||
|
void log_info(const char* message);
|
||||||
|
|
||||||
|
// Forward declaration for database functions
|
||||||
|
int store_event(cJSON* event);
|
||||||
|
|
||||||
|
// Forward declarations for deletion functions
|
||||||
|
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids);
|
||||||
|
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp);
|
||||||
|
|
||||||
|
// Global database variable
|
||||||
|
extern sqlite3* g_db;
|
||||||
|
|
||||||
|
|
||||||
|
// Handle NIP-09 deletion request event (kind 5)
|
||||||
|
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
if (!event) {
|
||||||
|
snprintf(error_message, error_size, "invalid: null deletion request");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract event details
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||||
|
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||||
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
|
|
||||||
|
if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) {
|
||||||
|
snprintf(error_message, error_size, "invalid: incomplete deletion request");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
|
if (kind != 5) {
|
||||||
|
snprintf(error_message, error_size, "invalid: not a deletion request");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||||
|
// Extract deletion event ID and reason (for potential logging)
|
||||||
|
const char* deletion_event_id = cJSON_GetStringValue(event_id_obj);
|
||||||
|
const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : "";
|
||||||
|
(void)deletion_event_id; // Mark as intentionally unused for now
|
||||||
|
(void)reason; // Mark as intentionally unused for now
|
||||||
|
long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj);
|
||||||
|
|
||||||
|
if (!cJSON_IsArray(tags_obj)) {
|
||||||
|
snprintf(error_message, error_size, "invalid: deletion request tags must be an array");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect event IDs and addresses from tags
|
||||||
|
cJSON* event_ids = cJSON_CreateArray();
|
||||||
|
cJSON* addresses = cJSON_CreateArray();
|
||||||
|
cJSON* kinds_to_delete = cJSON_CreateArray();
|
||||||
|
|
||||||
|
int deletion_targets_found = 0;
|
||||||
|
|
||||||
|
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* name = cJSON_GetStringValue(tag_name);
|
||||||
|
const char* value = cJSON_GetStringValue(tag_value);
|
||||||
|
|
||||||
|
if (strcmp(name, "e") == 0) {
|
||||||
|
// Event ID reference
|
||||||
|
cJSON_AddItemToArray(event_ids, cJSON_CreateString(value));
|
||||||
|
deletion_targets_found++;
|
||||||
|
} else if (strcmp(name, "a") == 0) {
|
||||||
|
// Addressable event reference (kind:pubkey:d-identifier)
|
||||||
|
cJSON_AddItemToArray(addresses, cJSON_CreateString(value));
|
||||||
|
deletion_targets_found++;
|
||||||
|
} else if (strcmp(name, "k") == 0) {
|
||||||
|
// Kind hint - store for validation but not required
|
||||||
|
int kind_hint = atoi(value);
|
||||||
|
if (kind_hint > 0) {
|
||||||
|
cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletion_targets_found == 0) {
|
||||||
|
cJSON_Delete(event_ids);
|
||||||
|
cJSON_Delete(addresses);
|
||||||
|
cJSON_Delete(kinds_to_delete);
|
||||||
|
snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deleted_count = 0;
|
||||||
|
|
||||||
|
// Process event ID deletions
|
||||||
|
if (cJSON_GetArraySize(event_ids) > 0) {
|
||||||
|
int result = delete_events_by_id(requester_pubkey, event_ids);
|
||||||
|
if (result > 0) {
|
||||||
|
deleted_count += result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process addressable event deletions
|
||||||
|
if (cJSON_GetArraySize(addresses) > 0) {
|
||||||
|
int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp);
|
||||||
|
if (result > 0) {
|
||||||
|
deleted_count += result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
cJSON_Delete(event_ids);
|
||||||
|
cJSON_Delete(addresses);
|
||||||
|
cJSON_Delete(kinds_to_delete);
|
||||||
|
|
||||||
|
// Store the deletion request itself (it should be kept according to NIP-09)
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
log_warning("Failed to store deletion request event");
|
||||||
|
}
|
||||||
|
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count);
|
||||||
|
log_info(debug_msg);
|
||||||
|
|
||||||
|
error_message[0] = '\0'; // Success - empty error message
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete events by ID (with pubkey authorization)
|
||||||
|
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
|
||||||
|
if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deleted_count = 0;
|
||||||
|
|
||||||
|
cJSON* event_id = NULL;
|
||||||
|
cJSON_ArrayForEach(event_id, event_ids) {
|
||||||
|
if (!cJSON_IsString(event_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* id = cJSON_GetStringValue(event_id);
|
||||||
|
|
||||||
|
// First check if event exists and if requester is authorized
|
||||||
|
const char* check_sql = "SELECT pubkey FROM events WHERE id = ?";
|
||||||
|
sqlite3_stmt* check_stmt;
|
||||||
|
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(check_stmt) == SQLITE_ROW) {
|
||||||
|
const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0);
|
||||||
|
|
||||||
|
// Only delete if the requester is the author
|
||||||
|
if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) {
|
||||||
|
sqlite3_finalize(check_stmt);
|
||||||
|
|
||||||
|
// Delete the event
|
||||||
|
const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
|
||||||
|
sqlite3_stmt* delete_stmt;
|
||||||
|
|
||||||
|
rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) {
|
||||||
|
deleted_count++;
|
||||||
|
|
||||||
|
char debug_msg[128];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(delete_stmt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sqlite3_finalize(check_stmt);
|
||||||
|
char warning_msg[128];
|
||||||
|
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
|
||||||
|
log_warning(warning_msg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sqlite3_finalize(check_stmt);
|
||||||
|
char debug_msg[128];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete events by addressable reference (kind:pubkey:d-identifier)
|
||||||
|
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) {
|
||||||
|
if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deleted_count = 0;
|
||||||
|
|
||||||
|
cJSON* address = NULL;
|
||||||
|
cJSON_ArrayForEach(address, addresses) {
|
||||||
|
if (!cJSON_IsString(address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* addr = cJSON_GetStringValue(address);
|
||||||
|
|
||||||
|
// Parse address format: kind:pubkey:d-identifier
|
||||||
|
char* addr_copy = strdup(addr);
|
||||||
|
if (!addr_copy) continue;
|
||||||
|
|
||||||
|
char* kind_str = strtok(addr_copy, ":");
|
||||||
|
char* pubkey_str = strtok(NULL, ":");
|
||||||
|
char* d_identifier = strtok(NULL, ":");
|
||||||
|
|
||||||
|
if (!kind_str || !pubkey_str) {
|
||||||
|
free(addr_copy);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int kind = atoi(kind_str);
|
||||||
|
|
||||||
|
// Only delete if the requester is the author
|
||||||
|
if (strcmp(pubkey_str, requester_pubkey) != 0) {
|
||||||
|
free(addr_copy);
|
||||||
|
char warning_msg[128];
|
||||||
|
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
|
||||||
|
log_warning(warning_msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build deletion query based on whether we have d-identifier
|
||||||
|
const char* delete_sql;
|
||||||
|
sqlite3_stmt* delete_stmt;
|
||||||
|
|
||||||
|
if (d_identifier && strlen(d_identifier) > 0) {
|
||||||
|
// Delete specific addressable event with d-tag
|
||||||
|
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
|
||||||
|
"AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
|
||||||
|
} else {
|
||||||
|
// Delete all events of this kind by this author up to deletion timestamp
|
||||||
|
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_int(delete_stmt, 1, kind);
|
||||||
|
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp);
|
||||||
|
|
||||||
|
if (d_identifier && strlen(d_identifier) > 0) {
|
||||||
|
sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sqlite3_step(delete_stmt) == SQLITE_DONE) {
|
||||||
|
int changes = sqlite3_changes(g_db);
|
||||||
|
if (changes > 0) {
|
||||||
|
deleted_count += changes;
|
||||||
|
|
||||||
|
char debug_msg[128];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(delete_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(addr_copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark event as deleted (alternative to hard deletion - not used in current implementation)
|
||||||
|
int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) {
|
||||||
|
(void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings
|
||||||
|
|
||||||
|
// This function could be used if we wanted to implement soft deletion
|
||||||
|
// For now, NIP-09 implementation uses hard deletion as specified
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
454
src/nip011.c
Normal file
454
src/nip011.c
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
// NIP-11 Relay Information Document module
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <libwebsockets.h>
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_success(const char* message);
|
||||||
|
void log_error(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for configuration functions
|
||||||
|
const char* get_config_value(const char* key);
|
||||||
|
int get_config_int(const char* key, int default_value);
|
||||||
|
int get_config_bool(const char* key, int default_value);
|
||||||
|
|
||||||
|
// Forward declarations for global cache access
|
||||||
|
extern unified_config_cache_t g_unified_cache;
|
||||||
|
|
||||||
|
// Forward declarations for constants (defined in config.h and other headers)
|
||||||
|
#define HTTP_STATUS_OK 200
|
||||||
|
#define HTTP_STATUS_NOT_ACCEPTABLE 406
|
||||||
|
#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// NIP-11 RELAY INFORMATION DOCUMENT
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Initialize relay information using configuration system
|
||||||
|
void init_relay_info() {
|
||||||
|
// Get all config values first (without holding mutex to avoid deadlock)
|
||||||
|
const char* relay_name = get_config_value("relay_name");
|
||||||
|
const char* relay_description = get_config_value("relay_description");
|
||||||
|
const char* relay_software = get_config_value("relay_software");
|
||||||
|
const char* relay_version = get_config_value("relay_version");
|
||||||
|
const char* relay_contact = get_config_value("relay_contact");
|
||||||
|
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||||||
|
|
||||||
|
// Get config values for limitations
|
||||||
|
int max_message_length = get_config_int("max_message_length", 16384);
|
||||||
|
int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20);
|
||||||
|
int max_limit = get_config_int("max_limit", 5000);
|
||||||
|
int max_event_tags = get_config_int("max_event_tags", 100);
|
||||||
|
int max_content_length = get_config_int("max_content_length", 8196);
|
||||||
|
int default_limit = get_config_int("default_limit", 500);
|
||||||
|
int admin_enabled = get_config_bool("admin_enabled", 0);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
// Update relay information fields
|
||||||
|
if (relay_name) {
|
||||||
|
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_description) {
|
||||||
|
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_software) {
|
||||||
|
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_version) {
|
||||||
|
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_contact) {
|
||||||
|
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relay_pubkey) {
|
||||||
|
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize supported NIPs array
|
||||||
|
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
|
||||||
|
if (g_unified_cache.relay_info.supported_nips) {
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize server limitations using configuration
|
||||||
|
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
|
||||||
|
if (g_unified_cache.relay_info.limitation) {
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
|
||||||
|
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
||||||
|
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
|
||||||
|
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
|
||||||
|
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize empty retention policies (can be configured later)
|
||||||
|
g_unified_cache.relay_info.retention = cJSON_CreateArray();
|
||||||
|
|
||||||
|
// Initialize language tags - set to global for now
|
||||||
|
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
|
||||||
|
if (g_unified_cache.relay_info.language_tags) {
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize relay countries - set to global for now
|
||||||
|
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
|
||||||
|
if (g_unified_cache.relay_info.relay_countries) {
|
||||||
|
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize content tags as empty array
|
||||||
|
g_unified_cache.relay_info.tags = cJSON_CreateArray();
|
||||||
|
|
||||||
|
// Initialize fees as empty object (no payment required by default)
|
||||||
|
g_unified_cache.relay_info.fees = cJSON_CreateObject();
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
log_success("Relay information initialized with default values");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up relay information JSON objects
|
||||||
|
void cleanup_relay_info() {
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
if (g_unified_cache.relay_info.supported_nips) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.supported_nips);
|
||||||
|
g_unified_cache.relay_info.supported_nips = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.limitation) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.limitation);
|
||||||
|
g_unified_cache.relay_info.limitation = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.retention) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.retention);
|
||||||
|
g_unified_cache.relay_info.retention = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.language_tags) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.language_tags);
|
||||||
|
g_unified_cache.relay_info.language_tags = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.relay_countries) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.relay_countries);
|
||||||
|
g_unified_cache.relay_info.relay_countries = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.tags) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.tags);
|
||||||
|
g_unified_cache.relay_info.tags = NULL;
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.fees) {
|
||||||
|
cJSON_Delete(g_unified_cache.relay_info.fees);
|
||||||
|
g_unified_cache.relay_info.fees = NULL;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate NIP-11 compliant JSON document
|
||||||
|
cJSON* generate_relay_info_json() {
|
||||||
|
cJSON* info = cJSON_CreateObject();
|
||||||
|
if (!info) {
|
||||||
|
log_error("Failed to create relay info JSON object");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
// Add basic relay information
|
||||||
|
if (strlen(g_unified_cache.relay_info.name) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.description) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.banner) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.icon) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.contact) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add supported NIPs
|
||||||
|
if (g_unified_cache.relay_info.supported_nips) {
|
||||||
|
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add software information
|
||||||
|
if (strlen(g_unified_cache.relay_info.software) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.version) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add policies
|
||||||
|
if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service);
|
||||||
|
}
|
||||||
|
if (strlen(g_unified_cache.relay_info.posting_policy) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add server limitations
|
||||||
|
if (g_unified_cache.relay_info.limitation) {
|
||||||
|
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add retention policies if configured
|
||||||
|
if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) {
|
||||||
|
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add geographical and language information
|
||||||
|
if (g_unified_cache.relay_info.relay_countries) {
|
||||||
|
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1));
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.language_tags) {
|
||||||
|
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1));
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) {
|
||||||
|
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add payment information if configured
|
||||||
|
if (strlen(g_unified_cache.relay_info.payments_url) > 0) {
|
||||||
|
cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
|
||||||
|
}
|
||||||
|
if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) {
|
||||||
|
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||||
|
struct nip11_session_data {
|
||||||
|
char* json_buffer;
|
||||||
|
size_t json_length;
|
||||||
|
int headers_sent;
|
||||||
|
int body_sent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle NIP-11 HTTP request with proper asynchronous buffer management
|
||||||
|
int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
||||||
|
log_info("Handling NIP-11 relay information request");
|
||||||
|
|
||||||
|
// Check if client accepts application/nostr+json
|
||||||
|
int accepts_nostr_json = 0;
|
||||||
|
if (accept_header) {
|
||||||
|
if (strstr(accept_header, "application/nostr+json") != NULL) {
|
||||||
|
accepts_nostr_json = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accepts_nostr_json) {
|
||||||
|
log_warning("HTTP request without proper Accept header for NIP-11");
|
||||||
|
// Return 406 Not Acceptable
|
||||||
|
unsigned char buf[LWS_PRE + 256];
|
||||||
|
unsigned char *p = &buf[LWS_PRE];
|
||||||
|
unsigned char *start = p;
|
||||||
|
unsigned char *end = &buf[sizeof(buf) - 1];
|
||||||
|
|
||||||
|
if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_finalize_http_header(wsi, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
|
||||||
|
return -1; // Close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate relay information JSON
|
||||||
|
cJSON* info_json = generate_relay_info_json();
|
||||||
|
if (!info_json) {
|
||||||
|
log_error("Failed to generate relay info JSON");
|
||||||
|
unsigned char buf[LWS_PRE + 256];
|
||||||
|
unsigned char *p = &buf[LWS_PRE];
|
||||||
|
unsigned char *start = p;
|
||||||
|
unsigned char *end = &buf[sizeof(buf) - 1];
|
||||||
|
|
||||||
|
if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_finalize_http_header(wsi, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* json_string = cJSON_Print(info_json);
|
||||||
|
cJSON_Delete(info_json);
|
||||||
|
|
||||||
|
if (!json_string) {
|
||||||
|
log_error("Failed to serialize relay info JSON");
|
||||||
|
unsigned char buf[LWS_PRE + 256];
|
||||||
|
unsigned char *p = &buf[LWS_PRE];
|
||||||
|
unsigned char *start = p;
|
||||||
|
unsigned char *end = &buf[sizeof(buf) - 1];
|
||||||
|
|
||||||
|
if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_finalize_http_header(wsi, &p, end)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t json_len = strlen(json_string);
|
||||||
|
|
||||||
|
// Allocate session data to manage buffer lifetime across callbacks
|
||||||
|
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
|
||||||
|
if (!session_data) {
|
||||||
|
log_error("Failed to allocate NIP-11 session data");
|
||||||
|
free(json_string);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store JSON buffer in session data for asynchronous handling
|
||||||
|
session_data->json_buffer = json_string;
|
||||||
|
session_data->json_length = json_len;
|
||||||
|
session_data->headers_sent = 0;
|
||||||
|
session_data->body_sent = 0;
|
||||||
|
|
||||||
|
// Store session data in WSI user data for callback access
|
||||||
|
lws_set_wsi_user(wsi, session_data);
|
||||||
|
|
||||||
|
// Prepare HTTP response with CORS headers
|
||||||
|
unsigned char buf[LWS_PRE + 1024];
|
||||||
|
unsigned char *p = &buf[LWS_PRE];
|
||||||
|
unsigned char *start = p;
|
||||||
|
unsigned char *end = &buf[sizeof(buf) - 1];
|
||||||
|
|
||||||
|
// Add status
|
||||||
|
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content type
|
||||||
|
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
|
||||||
|
(unsigned char*)"application/nostr+json", 22, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content length
|
||||||
|
if (lws_add_http_header_content_length(wsi, json_len, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CORS headers as required by NIP-11
|
||||||
|
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:",
|
||||||
|
(unsigned char*)"*", 1, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:",
|
||||||
|
(unsigned char*)"content-type, accept", 20, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:",
|
||||||
|
(unsigned char*)"GET, OPTIONS", 12, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize headers
|
||||||
|
if (lws_finalize_http_header(wsi, &p, end)) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write headers
|
||||||
|
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
session_data->headers_sent = 1;
|
||||||
|
|
||||||
|
// Request callback for body transmission
|
||||||
|
lws_callback_on_writable(wsi);
|
||||||
|
|
||||||
|
log_success("NIP-11 headers sent, body transmission scheduled");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
190
src/nip013.c
Normal file
190
src/nip013.c
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// NIP-13 Proof of Work validation module
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nip013.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_success(const char* message);
|
||||||
|
void log_error(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// NIP-13 PoW configuration structure
|
||||||
|
struct pow_config {
|
||||||
|
int enabled; // 0 = disabled, 1 = enabled
|
||||||
|
int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
|
||||||
|
int validation_flags; // Bitflags for validation options
|
||||||
|
int require_nonce_tag; // 1 = require nonce tag presence
|
||||||
|
int reject_lower_targets; // 1 = reject if committed < actual difficulty
|
||||||
|
int strict_format; // 1 = enforce strict nonce tag format
|
||||||
|
int anti_spam_mode; // 1 = full anti-spam validation
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize PoW configuration using configuration system
|
||||||
|
void init_pow_config() {
|
||||||
|
log_info("Initializing NIP-13 Proof of Work configuration");
|
||||||
|
|
||||||
|
// Get all config values first (without holding mutex to avoid deadlock)
|
||||||
|
int pow_enabled = get_config_bool("pow_enabled", 1);
|
||||||
|
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||||
|
const char* pow_mode = get_config_value("pow_mode");
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
// Load PoW settings from configuration system
|
||||||
|
g_unified_cache.pow_config.enabled = pow_enabled;
|
||||||
|
g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty;
|
||||||
|
|
||||||
|
// Configure PoW mode
|
||||||
|
if (pow_mode) {
|
||||||
|
if (strcmp(pow_mode, "strict") == 0) {
|
||||||
|
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
|
||||||
|
g_unified_cache.pow_config.require_nonce_tag = 1;
|
||||||
|
g_unified_cache.pow_config.reject_lower_targets = 1;
|
||||||
|
g_unified_cache.pow_config.strict_format = 1;
|
||||||
|
g_unified_cache.pow_config.anti_spam_mode = 1;
|
||||||
|
log_info("PoW configured in strict anti-spam mode");
|
||||||
|
} else if (strcmp(pow_mode, "full") == 0) {
|
||||||
|
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
|
||||||
|
g_unified_cache.pow_config.require_nonce_tag = 1;
|
||||||
|
log_info("PoW configured in full validation mode");
|
||||||
|
} else if (strcmp(pow_mode, "basic") == 0) {
|
||||||
|
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||||
|
log_info("PoW configured in basic validation mode");
|
||||||
|
} else if (strcmp(pow_mode, "disabled") == 0) {
|
||||||
|
g_unified_cache.pow_config.enabled = 0;
|
||||||
|
log_info("PoW validation disabled via configuration");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default to basic mode
|
||||||
|
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||||
|
log_info("PoW configured in basic validation mode (default)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final configuration
|
||||||
|
char config_msg[512];
|
||||||
|
snprintf(config_msg, sizeof(config_msg),
|
||||||
|
"PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s",
|
||||||
|
g_unified_cache.pow_config.enabled ? "true" : "false",
|
||||||
|
g_unified_cache.pow_config.min_pow_difficulty,
|
||||||
|
g_unified_cache.pow_config.validation_flags,
|
||||||
|
g_unified_cache.pow_config.anti_spam_mode ? "anti-spam" :
|
||||||
|
(g_unified_cache.pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic");
|
||||||
|
log_info(config_msg);
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate event Proof of Work according to NIP-13
|
||||||
|
int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
int enabled = g_unified_cache.pow_config.enabled;
|
||||||
|
int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
|
||||||
|
int validation_flags = g_unified_cache.pow_config.validation_flags;
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return 0; // PoW validation disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
snprintf(error_message, error_size, "pow: null event");
|
||||||
|
return NOSTR_ERROR_INVALID_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If min_pow_difficulty is 0, only validate events that have nonce tags
|
||||||
|
// This allows events without PoW when difficulty requirement is 0
|
||||||
|
if (min_pow_difficulty == 0) {
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
int has_nonce_tag = 0;
|
||||||
|
|
||||||
|
if (tags && cJSON_IsArray(tags)) {
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
if (cJSON_IsString(tag_name)) {
|
||||||
|
const char* name = cJSON_GetStringValue(tag_name);
|
||||||
|
if (name && strcmp(name, "nonce") == 0) {
|
||||||
|
has_nonce_tag = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no minimum difficulty required and no nonce tag, skip PoW validation
|
||||||
|
if (!has_nonce_tag) {
|
||||||
|
return 0; // Accept event without PoW when min_difficulty=0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform PoW validation using nostr_core_lib
|
||||||
|
nostr_pow_result_t pow_result;
|
||||||
|
int validation_result = nostr_validate_pow(event, min_pow_difficulty,
|
||||||
|
validation_flags, &pow_result);
|
||||||
|
|
||||||
|
if (validation_result != NOSTR_SUCCESS) {
|
||||||
|
// Handle specific error cases with appropriate messages
|
||||||
|
switch (validation_result) {
|
||||||
|
case NOSTR_ERROR_NIP13_INSUFFICIENT:
|
||||||
|
snprintf(error_message, error_size,
|
||||||
|
"pow: insufficient difficulty: %d < %d",
|
||||||
|
pow_result.actual_difficulty, min_pow_difficulty);
|
||||||
|
log_warning("Event rejected: insufficient PoW difficulty");
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_NIP13_NO_NONCE_TAG:
|
||||||
|
// This should not happen with min_difficulty=0 after our check above
|
||||||
|
if (min_pow_difficulty > 0) {
|
||||||
|
snprintf(error_message, error_size, "pow: missing required nonce tag");
|
||||||
|
log_warning("Event rejected: missing nonce tag");
|
||||||
|
} else {
|
||||||
|
return 0; // Allow when min_difficulty=0
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
|
||||||
|
snprintf(error_message, error_size, "pow: invalid nonce tag format");
|
||||||
|
log_warning("Event rejected: invalid nonce tag format");
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
|
||||||
|
snprintf(error_message, error_size,
|
||||||
|
"pow: committed target (%d) lower than minimum (%d)",
|
||||||
|
pow_result.committed_target, min_pow_difficulty);
|
||||||
|
log_warning("Event rejected: committed target too low (anti-spam protection)");
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_NIP13_CALCULATION:
|
||||||
|
snprintf(error_message, error_size, "pow: difficulty calculation failed");
|
||||||
|
log_error("PoW difficulty calculation error");
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||||
|
snprintf(error_message, error_size, "pow: invalid event ID format");
|
||||||
|
log_warning("Event rejected: invalid event ID for PoW calculation");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
snprintf(error_message, error_size, "pow: validation failed - %s",
|
||||||
|
strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
|
||||||
|
log_warning("Event rejected: PoW validation failed");
|
||||||
|
}
|
||||||
|
return validation_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful PoW validation (only if minimum difficulty is required)
|
||||||
|
if (min_pow_difficulty > 0 || pow_result.has_nonce_tag) {
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg),
|
||||||
|
"PoW validated: difficulty=%d, target=%d, nonce=%llu%s",
|
||||||
|
pow_result.actual_difficulty,
|
||||||
|
pow_result.committed_target,
|
||||||
|
(unsigned long long)pow_result.nonce_value,
|
||||||
|
pow_result.has_nonce_tag ? "" : " (no nonce tag)");
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Success
|
||||||
|
}
|
||||||
173
src/nip040.c
Normal file
173
src/nip040.c
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Include nostr_core_lib for cJSON
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
|
||||||
|
// Configuration management system
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// NIP-40 Expiration configuration structure
|
||||||
|
struct expiration_config {
|
||||||
|
int enabled; // 0 = disabled, 1 = enabled
|
||||||
|
int strict_mode; // 1 = reject expired events on submission
|
||||||
|
int filter_responses; // 1 = filter expired events from responses
|
||||||
|
int delete_expired; // 1 = delete expired events from DB (future feature)
|
||||||
|
long grace_period; // Grace period in seconds for clock skew
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global expiration configuration instance
|
||||||
|
struct expiration_config g_expiration_config = {
|
||||||
|
.enabled = 1, // Enable expiration handling by default
|
||||||
|
.strict_mode = 1, // Reject expired events on submission by default
|
||||||
|
.filter_responses = 1, // Filter expired events from responses by default
|
||||||
|
.delete_expired = 0, // Don't delete by default (keep for audit)
|
||||||
|
.grace_period = 1 // 1 second grace period for testing (was 300)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// Initialize expiration configuration using configuration system
|
||||||
|
void init_expiration_config() {
|
||||||
|
log_info("Initializing NIP-40 Expiration Timestamp configuration");
|
||||||
|
|
||||||
|
// Get all config values first (without holding mutex to avoid deadlock)
|
||||||
|
int expiration_enabled = get_config_bool("expiration_enabled", 1);
|
||||||
|
int expiration_strict = get_config_bool("expiration_strict", 1);
|
||||||
|
int expiration_filter = get_config_bool("expiration_filter", 1);
|
||||||
|
int expiration_delete = get_config_bool("expiration_delete", 0);
|
||||||
|
long expiration_grace_period = get_config_int("expiration_grace_period", 1);
|
||||||
|
|
||||||
|
// Load expiration settings from configuration system
|
||||||
|
g_expiration_config.enabled = expiration_enabled;
|
||||||
|
g_expiration_config.strict_mode = expiration_strict;
|
||||||
|
g_expiration_config.filter_responses = expiration_filter;
|
||||||
|
g_expiration_config.delete_expired = expiration_delete;
|
||||||
|
g_expiration_config.grace_period = expiration_grace_period;
|
||||||
|
|
||||||
|
// Validate grace period bounds
|
||||||
|
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
|
||||||
|
log_warning("Invalid grace period, using default of 300 seconds");
|
||||||
|
g_expiration_config.grace_period = 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final configuration
|
||||||
|
char config_msg[512];
|
||||||
|
snprintf(config_msg, sizeof(config_msg),
|
||||||
|
"Expiration Configuration: enabled=%s, strict_mode=%s, filter_responses=%s, grace_period=%ld seconds",
|
||||||
|
g_expiration_config.enabled ? "true" : "false",
|
||||||
|
g_expiration_config.strict_mode ? "true" : "false",
|
||||||
|
g_expiration_config.filter_responses ? "true" : "false",
|
||||||
|
g_expiration_config.grace_period);
|
||||||
|
log_info(config_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract expiration timestamp from event tags
|
||||||
|
long extract_expiration_timestamp(cJSON* tags) {
|
||||||
|
if (!tags || !cJSON_IsArray(tags)) {
|
||||||
|
return 0; // No expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||||
|
const char* name = cJSON_GetStringValue(tag_name);
|
||||||
|
const char* value = cJSON_GetStringValue(tag_value);
|
||||||
|
|
||||||
|
if (name && value && strcmp(name, "expiration") == 0) {
|
||||||
|
// Validate that the string contains only digits (and optional leading whitespace)
|
||||||
|
const char* p = value;
|
||||||
|
|
||||||
|
// Skip leading whitespace
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
|
||||||
|
// Check if we have at least one digit
|
||||||
|
if (*p == '\0') {
|
||||||
|
continue; // Empty or whitespace-only string, ignore this tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that all remaining characters are digits
|
||||||
|
const char* digit_start = p;
|
||||||
|
while (*p >= '0' && *p <= '9') p++;
|
||||||
|
|
||||||
|
// If we didn't consume the entire string or found no digits, it's malformed
|
||||||
|
if (*p != '\0' || p == digit_start) {
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg),
|
||||||
|
"Ignoring malformed expiration tag value: '%.32s'", value);
|
||||||
|
log_warning(debug_msg);
|
||||||
|
continue; // Ignore malformed expiration tag
|
||||||
|
}
|
||||||
|
|
||||||
|
long expiration_ts = atol(value);
|
||||||
|
if (expiration_ts > 0) {
|
||||||
|
return expiration_ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // No valid expiration tag found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event is currently expired
|
||||||
|
int is_event_expired(cJSON* event, time_t current_time) {
|
||||||
|
if (!event) {
|
||||||
|
return 0; // Invalid event, not expired
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
long expiration_ts = extract_expiration_timestamp(tags);
|
||||||
|
|
||||||
|
if (expiration_ts == 0) {
|
||||||
|
return 0; // No expiration timestamp, not expired
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current time exceeds expiration + grace period
|
||||||
|
return (current_time > (expiration_ts + g_expiration_config.grace_period));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate event expiration according to NIP-40
|
||||||
|
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size) {
|
||||||
|
if (!g_expiration_config.enabled) {
|
||||||
|
return 0; // Expiration validation disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
snprintf(error_message, error_size, "expiration: null event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event is expired
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
if (is_event_expired(event, current_time)) {
|
||||||
|
if (g_expiration_config.strict_mode) {
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
long expiration_ts = extract_expiration_timestamp(tags);
|
||||||
|
|
||||||
|
snprintf(error_message, error_size,
|
||||||
|
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
|
||||||
|
expiration_ts, (long)current_time, g_expiration_config.grace_period);
|
||||||
|
log_warning("Event rejected: expired timestamp");
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
// In non-strict mode, log but allow expired events
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg),
|
||||||
|
"Accepting expired event (strict_mode disabled)");
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Success
|
||||||
|
}
|
||||||
180
src/nip042.c
Normal file
180
src/nip042.c
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// NIP-42 AUTHENTICATION FUNCTIONS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <libwebsockets.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_error(const char* message);
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
void log_success(const char* message);
|
||||||
|
|
||||||
|
// Forward declaration for notice message function
|
||||||
|
void send_notice_message(struct lws* wsi, const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-42 functions from request_validator.c
|
||||||
|
int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
|
||||||
|
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
|
||||||
|
const char *relay_url, int time_tolerance_seconds);
|
||||||
|
|
||||||
|
// Forward declaration for per_session_data struct (defined in main.c)
|
||||||
|
struct per_session_data {
|
||||||
|
int authenticated;
|
||||||
|
void* subscriptions; // Head of this session's subscription list
|
||||||
|
pthread_mutex_t session_lock; // Per-session thread safety
|
||||||
|
char client_ip[41]; // Client IP for logging
|
||||||
|
int subscription_count; // Number of subscriptions for this session
|
||||||
|
|
||||||
|
// NIP-42 Authentication State
|
||||||
|
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
|
||||||
|
char active_challenge[65]; // Current challenge for this session (64 hex + null)
|
||||||
|
time_t challenge_created; // When challenge was created
|
||||||
|
time_t challenge_expires; // Challenge expiration time
|
||||||
|
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
|
||||||
|
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
|
||||||
|
int auth_challenge_sent; // Whether challenge has been sent (0/1)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Send NIP-42 authentication challenge to client
|
||||||
|
void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
|
||||||
|
if (!wsi || !pss) return;
|
||||||
|
|
||||||
|
// Generate challenge using existing request_validator function
|
||||||
|
char challenge[65];
|
||||||
|
if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
|
||||||
|
log_error("Failed to generate NIP-42 challenge");
|
||||||
|
send_notice_message(wsi, "Authentication temporarily unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store challenge in session
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
strncpy(pss->active_challenge, challenge, sizeof(pss->active_challenge) - 1);
|
||||||
|
pss->active_challenge[sizeof(pss->active_challenge) - 1] = '\0';
|
||||||
|
pss->challenge_created = time(NULL);
|
||||||
|
pss->challenge_expires = pss->challenge_created + 600; // 10 minutes
|
||||||
|
pss->auth_challenge_sent = 1;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Send AUTH challenge message: ["AUTH", <challenge>]
|
||||||
|
cJSON* auth_msg = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(auth_msg, cJSON_CreateString("AUTH"));
|
||||||
|
cJSON_AddItemToArray(auth_msg, cJSON_CreateString(challenge));
|
||||||
|
|
||||||
|
char* msg_str = cJSON_Print(auth_msg);
|
||||||
|
if (msg_str) {
|
||||||
|
size_t msg_len = strlen(msg_str);
|
||||||
|
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(msg_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(auth_msg);
|
||||||
|
|
||||||
|
char debug_msg[128];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "NIP-42 auth challenge sent: %.16s...", challenge);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle NIP-42 signed authentication event from client
|
||||||
|
void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event) {
|
||||||
|
if (!wsi || !pss || !auth_event) return;
|
||||||
|
|
||||||
|
// Serialize event for validation
|
||||||
|
char* event_json = cJSON_Print(auth_event);
|
||||||
|
if (!event_json) {
|
||||||
|
send_notice_message(wsi, "Invalid authentication event format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
char challenge_copy[65];
|
||||||
|
strncpy(challenge_copy, pss->active_challenge, sizeof(challenge_copy) - 1);
|
||||||
|
challenge_copy[sizeof(challenge_copy) - 1] = '\0';
|
||||||
|
time_t challenge_expires = pss->challenge_expires;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Check if challenge has expired
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
if (current_time > challenge_expires) {
|
||||||
|
free(event_json);
|
||||||
|
send_notice_message(wsi, "Authentication challenge expired, please retry");
|
||||||
|
log_warning("NIP-42 authentication failed: challenge expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify authentication using existing request_validator function
|
||||||
|
// Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately
|
||||||
|
int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy,
|
||||||
|
"ws://localhost:8888", 600); // 10 minutes tolerance
|
||||||
|
|
||||||
|
char authenticated_pubkey[65] = {0};
|
||||||
|
if (result == 0) {
|
||||||
|
// Extract pubkey from the auth event
|
||||||
|
cJSON* pubkey_json = cJSON_GetObjectItem(auth_event, "pubkey");
|
||||||
|
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||||||
|
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||||||
|
if (pubkey_str && strlen(pubkey_str) == 64) {
|
||||||
|
strncpy(authenticated_pubkey, pubkey_str, sizeof(authenticated_pubkey) - 1);
|
||||||
|
authenticated_pubkey[sizeof(authenticated_pubkey) - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
result = -1; // Invalid pubkey format
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = -1; // Missing pubkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(event_json);
|
||||||
|
|
||||||
|
if (result == 0) {
|
||||||
|
// Authentication successful
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
pss->authenticated = 1;
|
||||||
|
strncpy(pss->authenticated_pubkey, authenticated_pubkey, sizeof(pss->authenticated_pubkey) - 1);
|
||||||
|
pss->authenticated_pubkey[sizeof(pss->authenticated_pubkey) - 1] = '\0';
|
||||||
|
// Clear challenge
|
||||||
|
memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
|
||||||
|
pss->challenge_expires = 0;
|
||||||
|
pss->auth_challenge_sent = 0;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg),
|
||||||
|
"NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey);
|
||||||
|
log_success(success_msg);
|
||||||
|
|
||||||
|
send_notice_message(wsi, "NIP-42 authentication successful");
|
||||||
|
} else {
|
||||||
|
// Authentication failed
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg),
|
||||||
|
"NIP-42 authentication failed (error code: %d)", result);
|
||||||
|
log_warning(error_msg);
|
||||||
|
|
||||||
|
send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle challenge response (not typically used in NIP-42, but included for completeness)
|
||||||
|
void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge) {
|
||||||
|
(void)wsi; (void)pss; (void)challenge; // Mark as intentionally unused
|
||||||
|
|
||||||
|
// NIP-42 doesn't typically use challenge responses from client to server
|
||||||
|
// This is reserved for potential future use or protocol extensions
|
||||||
|
log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
|
||||||
|
send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
|
||||||
|
}
|
||||||
@@ -169,7 +169,7 @@ static void validator_debug_log(const char *message) {
|
|||||||
|
|
||||||
static int reload_auth_config(void);
|
static int reload_auth_config(void);
|
||||||
// Removed unused forward declarations for functions that are no longer called
|
// Removed unused forward declarations for functions that are no longer called
|
||||||
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||||
const char *resource_hash);
|
const char *resource_hash);
|
||||||
void nostr_request_validator_clear_violation(void);
|
void nostr_request_validator_clear_violation(void);
|
||||||
|
|
||||||
@@ -595,7 +595,7 @@ static int reload_auth_config(void) {
|
|||||||
* Check database authentication rules for the request
|
* Check database authentication rules for the request
|
||||||
* Implements the 6-step rule evaluation engine from AUTH_API.md
|
* Implements the 6-step rule evaluation engine from AUTH_API.md
|
||||||
*/
|
*/
|
||||||
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||||
const char *resource_hash) {
|
const char *resource_hash) {
|
||||||
sqlite3 *db = NULL;
|
sqlite3 *db = NULL;
|
||||||
sqlite3_stmt *stmt = NULL;
|
sqlite3_stmt *stmt = NULL;
|
||||||
|
|||||||
723
src/subscriptions.c
Normal file
723
src/subscriptions.c
Normal file
@@ -0,0 +1,723 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <printf.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <libwebsockets.h>
|
||||||
|
#include "subscriptions.h"
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_error(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for configuration functions
|
||||||
|
const char* get_config_value(const char* key);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-40 expiration functions
|
||||||
|
int is_event_expired(cJSON* event, time_t current_time);
|
||||||
|
|
||||||
|
// Global database variable
|
||||||
|
extern sqlite3* g_db;
|
||||||
|
|
||||||
|
// Global unified cache
|
||||||
|
extern unified_config_cache_t g_unified_cache;
|
||||||
|
|
||||||
|
// Global subscription manager
|
||||||
|
extern subscription_manager_t g_subscription_manager;
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PERSISTENT SUBSCRIPTIONS SYSTEM
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Create a subscription filter from cJSON filter object
|
||||||
|
subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
|
||||||
|
if (!filter_json || !cJSON_IsObject(filter_json)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
|
||||||
|
if (!filter) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy filter criteria
|
||||||
|
cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
|
||||||
|
if (kinds && cJSON_IsArray(kinds)) {
|
||||||
|
filter->kinds = cJSON_Duplicate(kinds, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
|
||||||
|
if (authors && cJSON_IsArray(authors)) {
|
||||||
|
filter->authors = cJSON_Duplicate(authors, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
|
||||||
|
if (ids && cJSON_IsArray(ids)) {
|
||||||
|
filter->ids = cJSON_Duplicate(ids, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* since = cJSON_GetObjectItem(filter_json, "since");
|
||||||
|
if (since && cJSON_IsNumber(since)) {
|
||||||
|
filter->since = (long)cJSON_GetNumberValue(since);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* until = cJSON_GetObjectItem(filter_json, "until");
|
||||||
|
if (until && cJSON_IsNumber(until)) {
|
||||||
|
filter->until = (long)cJSON_GetNumberValue(until);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
|
||||||
|
if (limit && cJSON_IsNumber(limit)) {
|
||||||
|
filter->limit = (int)cJSON_GetNumberValue(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tag filters (e.g., {"#e": ["id1"], "#p": ["pubkey1"]})
|
||||||
|
cJSON* item = NULL;
|
||||||
|
cJSON_ArrayForEach(item, filter_json) {
|
||||||
|
if (item->string && strlen(item->string) >= 2 && item->string[0] == '#') {
|
||||||
|
if (!filter->tag_filters) {
|
||||||
|
filter->tag_filters = cJSON_CreateObject();
|
||||||
|
}
|
||||||
|
if (filter->tag_filters) {
|
||||||
|
cJSON_AddItemToObject(filter->tag_filters, item->string, cJSON_Duplicate(item, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free a subscription filter
|
||||||
|
void free_subscription_filter(subscription_filter_t* filter) {
|
||||||
|
if (!filter) return;
|
||||||
|
|
||||||
|
if (filter->kinds) cJSON_Delete(filter->kinds);
|
||||||
|
if (filter->authors) cJSON_Delete(filter->authors);
|
||||||
|
if (filter->ids) cJSON_Delete(filter->ids);
|
||||||
|
if (filter->tag_filters) cJSON_Delete(filter->tag_filters);
|
||||||
|
|
||||||
|
if (filter->next) {
|
||||||
|
free_subscription_filter(filter->next);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new subscription
|
||||||
|
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
|
||||||
|
if (!sub_id || !wsi || !filters_array) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription_t* sub = calloc(1, sizeof(subscription_t));
|
||||||
|
if (!sub) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy subscription ID (truncate if too long)
|
||||||
|
strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
|
||||||
|
sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
||||||
|
|
||||||
|
// Set WebSocket connection
|
||||||
|
sub->wsi = wsi;
|
||||||
|
|
||||||
|
// Set client IP
|
||||||
|
if (client_ip) {
|
||||||
|
strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
|
||||||
|
sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timestamps and state
|
||||||
|
sub->created_at = time(NULL);
|
||||||
|
sub->events_sent = 0;
|
||||||
|
sub->active = 1;
|
||||||
|
|
||||||
|
// Convert filters array to linked list
|
||||||
|
subscription_filter_t* filter_tail = NULL;
|
||||||
|
int filter_count = 0;
|
||||||
|
|
||||||
|
if (cJSON_IsArray(filters_array)) {
|
||||||
|
cJSON* filter_json = NULL;
|
||||||
|
cJSON_ArrayForEach(filter_json, filters_array) {
|
||||||
|
if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
|
||||||
|
log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription_filter_t* filter = create_subscription_filter(filter_json);
|
||||||
|
if (filter) {
|
||||||
|
if (!sub->filters) {
|
||||||
|
sub->filters = filter;
|
||||||
|
filter_tail = filter;
|
||||||
|
} else {
|
||||||
|
filter_tail->next = filter;
|
||||||
|
filter_tail = filter;
|
||||||
|
}
|
||||||
|
filter_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_count == 0) {
|
||||||
|
log_error("No valid filters found for subscription");
|
||||||
|
free(sub);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free a subscription
|
||||||
|
void free_subscription(subscription_t* sub) {
|
||||||
|
if (!sub) return;
|
||||||
|
|
||||||
|
if (sub->filters) {
|
||||||
|
free_subscription_filter(sub->filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subscription to global manager (thread-safe)
|
||||||
|
int add_subscription_to_manager(subscription_t* sub) {
|
||||||
|
if (!sub) return -1;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
// Check global limits
|
||||||
|
if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
log_error("Maximum total subscriptions reached");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to global list
|
||||||
|
sub->next = g_subscription_manager.active_subscriptions;
|
||||||
|
g_subscription_manager.active_subscriptions = sub;
|
||||||
|
g_subscription_manager.total_subscriptions++;
|
||||||
|
g_subscription_manager.total_created++;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
// Log subscription creation to database
|
||||||
|
log_subscription_created(sub);
|
||||||
|
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Added subscription '%s' (total: %d)",
|
||||||
|
sub->id, g_subscription_manager.total_subscriptions);
|
||||||
|
log_info(debug_msg);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove subscription from global manager (thread-safe)
|
||||||
|
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
||||||
|
if (!sub_id) return -1;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
subscription_t** current = &g_subscription_manager.active_subscriptions;
|
||||||
|
|
||||||
|
while (*current) {
|
||||||
|
subscription_t* sub = *current;
|
||||||
|
|
||||||
|
// Match by ID and WebSocket connection
|
||||||
|
if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
|
||||||
|
// Remove from list
|
||||||
|
*current = sub->next;
|
||||||
|
g_subscription_manager.total_subscriptions--;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
// Log subscription closure to database
|
||||||
|
log_subscription_closed(sub_id, sub->client_ip, "closed");
|
||||||
|
|
||||||
|
// Update events sent counter before freeing
|
||||||
|
update_subscription_events_sent(sub_id, sub->events_sent);
|
||||||
|
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Removed subscription '%s' (total: %d)",
|
||||||
|
sub_id, g_subscription_manager.total_subscriptions);
|
||||||
|
log_info(debug_msg);
|
||||||
|
|
||||||
|
free_subscription(sub);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = &(sub->next);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
|
||||||
|
log_warning(debug_msg);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an event matches a subscription filter
|
||||||
|
int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||||
|
if (!event || !filter) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check kinds filter
|
||||||
|
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||||
|
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||||
|
int kind_match = 0;
|
||||||
|
|
||||||
|
cJSON* kind_item = NULL;
|
||||||
|
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||||
|
if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) {
|
||||||
|
kind_match = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kind_match) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authors filter
|
||||||
|
if (filter->authors && cJSON_IsArray(filter->authors)) {
|
||||||
|
cJSON* event_pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
if (!event_pubkey || !cJSON_IsString(event_pubkey)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* event_pubkey_str = cJSON_GetStringValue(event_pubkey);
|
||||||
|
int author_match = 0;
|
||||||
|
|
||||||
|
cJSON* author_item = NULL;
|
||||||
|
cJSON_ArrayForEach(author_item, filter->authors) {
|
||||||
|
if (cJSON_IsString(author_item)) {
|
||||||
|
const char* author_str = cJSON_GetStringValue(author_item);
|
||||||
|
// Support prefix matching (partial pubkeys)
|
||||||
|
if (strncmp(event_pubkey_str, author_str, strlen(author_str)) == 0) {
|
||||||
|
author_match = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!author_match) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IDs filter
|
||||||
|
if (filter->ids && cJSON_IsArray(filter->ids)) {
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (!event_id || !cJSON_IsString(event_id)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* event_id_str = cJSON_GetStringValue(event_id);
|
||||||
|
int id_match = 0;
|
||||||
|
|
||||||
|
cJSON* id_item = NULL;
|
||||||
|
cJSON_ArrayForEach(id_item, filter->ids) {
|
||||||
|
if (cJSON_IsString(id_item)) {
|
||||||
|
const char* id_str = cJSON_GetStringValue(id_item);
|
||||||
|
// Support prefix matching (partial IDs)
|
||||||
|
if (strncmp(event_id_str, id_str, strlen(id_str)) == 0) {
|
||||||
|
id_match = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id_match) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check since filter
|
||||||
|
if (filter->since > 0) {
|
||||||
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||||
|
if (event_timestamp < filter->since) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check until filter
|
||||||
|
if (filter->until > 0) {
|
||||||
|
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
|
||||||
|
if (event_timestamp > filter->until) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tag filters (e.g., #e, #p tags)
|
||||||
|
if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) {
|
||||||
|
cJSON* event_tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!event_tags || !cJSON_IsArray(event_tags)) {
|
||||||
|
return 0; // Event has no tags but filter requires tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each tag filter
|
||||||
|
cJSON* tag_filter = NULL;
|
||||||
|
cJSON_ArrayForEach(tag_filter, filter->tag_filters) {
|
||||||
|
if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') {
|
||||||
|
continue; // Invalid tag filter
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* tag_name = tag_filter->string + 1; // Skip the '#'
|
||||||
|
|
||||||
|
if (!cJSON_IsArray(tag_filter)) {
|
||||||
|
continue; // Tag filter must be an array
|
||||||
|
}
|
||||||
|
|
||||||
|
int tag_match = 0;
|
||||||
|
|
||||||
|
// Search through event tags for matching tag name and value
|
||||||
|
cJSON* event_tag = NULL;
|
||||||
|
cJSON_ArrayForEach(event_tag, event_tags) {
|
||||||
|
if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) {
|
||||||
|
continue; // Invalid tag format
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0);
|
||||||
|
cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1);
|
||||||
|
|
||||||
|
if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tag name matches
|
||||||
|
if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) {
|
||||||
|
const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
|
||||||
|
|
||||||
|
// Check if any of the filter values match this tag value
|
||||||
|
cJSON* filter_value = NULL;
|
||||||
|
cJSON_ArrayForEach(filter_value, tag_filter) {
|
||||||
|
if (cJSON_IsString(filter_value)) {
|
||||||
|
const char* filter_value_str = cJSON_GetStringValue(filter_value);
|
||||||
|
// Support prefix matching for tag values
|
||||||
|
if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) {
|
||||||
|
tag_match = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag_match) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tag_match) {
|
||||||
|
return 0; // This tag filter didn't match, so the event doesn't match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1; // All filters passed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an event matches any filter in a subscription (filters are OR'd together)
|
||||||
|
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
||||||
|
if (!event || !subscription || !subscription->filters) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription_filter_t* filter = subscription->filters;
|
||||||
|
while (filter) {
|
||||||
|
if (event_matches_filter(event, filter)) {
|
||||||
|
return 1; // Match found (OR logic)
|
||||||
|
}
|
||||||
|
filter = filter->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // No filters matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast event to all matching subscriptions (thread-safe)
|
||||||
|
int broadcast_event_to_subscriptions(cJSON* event) {
|
||||||
|
if (!event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event is expired and should not be broadcast (NIP-40)
|
||||||
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||||
|
int expiration_enabled = g_unified_cache.expiration_config.enabled;
|
||||||
|
int filter_responses = g_unified_cache.expiration_config.filter_responses;
|
||||||
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||||
|
|
||||||
|
if (expiration_enabled && filter_responses) {
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
if (is_event_expired(event, current_time)) {
|
||||||
|
char debug_msg[256];
|
||||||
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
|
const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown";
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id);
|
||||||
|
log_info(debug_msg);
|
||||||
|
return 0; // Don't broadcast expired events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int broadcasts = 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (sub) {
|
||||||
|
if (sub->active && event_matches_subscription(event, sub)) {
|
||||||
|
// Create EVENT message for this subscription
|
||||||
|
cJSON* event_msg = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
||||||
|
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id));
|
||||||
|
cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
|
||||||
|
|
||||||
|
char* msg_str = cJSON_Print(event_msg);
|
||||||
|
if (msg_str) {
|
||||||
|
size_t msg_len = strlen(msg_str);
|
||||||
|
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||||
|
|
||||||
|
// Send to WebSocket connection
|
||||||
|
int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||||
|
if (write_result >= 0) {
|
||||||
|
sub->events_sent++;
|
||||||
|
broadcasts++;
|
||||||
|
|
||||||
|
// Log event broadcast to database (optional - can be disabled for performance)
|
||||||
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||||
|
log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(msg_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(event_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub = sub->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global statistics
|
||||||
|
g_subscription_manager.total_events_broadcast += broadcasts;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
if (broadcasts > 0) {
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Broadcasted event to %d subscriptions", broadcasts);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return broadcasts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// SUBSCRIPTION DATABASE LOGGING
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Log subscription creation to database
|
||||||
|
void log_subscription_created(const subscription_t* sub) {
|
||||||
|
if (!g_db || !sub) return;
|
||||||
|
|
||||||
|
// Create filter JSON for logging
|
||||||
|
char* filter_json = NULL;
|
||||||
|
if (sub->filters) {
|
||||||
|
cJSON* filters_array = cJSON_CreateArray();
|
||||||
|
subscription_filter_t* filter = sub->filters;
|
||||||
|
|
||||||
|
while (filter) {
|
||||||
|
cJSON* filter_obj = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (filter->kinds) {
|
||||||
|
cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
|
||||||
|
}
|
||||||
|
if (filter->authors) {
|
||||||
|
cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
|
||||||
|
}
|
||||||
|
if (filter->ids) {
|
||||||
|
cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
|
||||||
|
}
|
||||||
|
if (filter->since > 0) {
|
||||||
|
cJSON_AddNumberToObject(filter_obj, "since", filter->since);
|
||||||
|
}
|
||||||
|
if (filter->until > 0) {
|
||||||
|
cJSON_AddNumberToObject(filter_obj, "until", filter->until);
|
||||||
|
}
|
||||||
|
if (filter->limit > 0) {
|
||||||
|
cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
|
||||||
|
}
|
||||||
|
if (filter->tag_filters) {
|
||||||
|
cJSON* tags_obj = cJSON_Duplicate(filter->tag_filters, 1);
|
||||||
|
cJSON* item = NULL;
|
||||||
|
cJSON_ArrayForEach(item, tags_obj) {
|
||||||
|
if (item->string) {
|
||||||
|
cJSON_AddItemToObject(filter_obj, item->string, cJSON_Duplicate(item, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON_Delete(tags_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(filters_array, filter_obj);
|
||||||
|
filter = filter->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_json = cJSON_Print(filters_array);
|
||||||
|
cJSON_Delete(filters_array);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
|
||||||
|
"VALUES (?, ?, 'created', ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_json) free(filter_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log subscription closure to database
|
||||||
|
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason) {
|
||||||
|
(void)reason; // Mark as intentionally unused
|
||||||
|
if (!g_db || !sub_id) return;
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
||||||
|
"VALUES (?, ?, 'closed')";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the corresponding 'created' entry with end time and events sent
|
||||||
|
const char* update_sql =
|
||||||
|
"UPDATE subscription_events "
|
||||||
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
|
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
|
rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log subscription disconnection to database
|
||||||
|
void log_subscription_disconnected(const char* client_ip) {
|
||||||
|
if (!g_db || !client_ip) return;
|
||||||
|
|
||||||
|
// Mark all active subscriptions for this client as disconnected
|
||||||
|
const char* sql =
|
||||||
|
"UPDATE subscription_events "
|
||||||
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
|
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
|
||||||
|
int changes = sqlite3_changes(g_db);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (changes > 0) {
|
||||||
|
// Log a disconnection event
|
||||||
|
const char* insert_sql =
|
||||||
|
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
||||||
|
"VALUES ('disconnect', ?, 'disconnected')";
|
||||||
|
|
||||||
|
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log event broadcast to database (optional, can be resource intensive)
|
||||||
|
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
|
||||||
|
if (!g_db || !event_id || !sub_id || !client_ip) return;
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
|
||||||
|
"VALUES (?, ?, ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update events sent counter for a subscription
|
||||||
|
void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||||
|
if (!g_db || !sub_id) return;
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"UPDATE subscription_events "
|
||||||
|
"SET events_sent = ? "
|
||||||
|
"WHERE subscription_id = ? AND event_type = 'created'";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_int(stmt, 1, events_sent);
|
||||||
|
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/subscriptions.h
Normal file
91
src/subscriptions.h
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// Subscription system structures and functions for C-Relay
|
||||||
|
// This header defines subscription management functionality
|
||||||
|
|
||||||
|
#ifndef SUBSCRIPTIONS_H
|
||||||
|
#define SUBSCRIPTIONS_H
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
#include "config.h" // For CLIENT_IP_MAX_LENGTH
|
||||||
|
|
||||||
|
// Forward declaration for libwebsockets struct
|
||||||
|
struct lws;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
#define SUBSCRIPTION_ID_MAX_LENGTH 64
|
||||||
|
#define MAX_FILTERS_PER_SUBSCRIPTION 10
|
||||||
|
#define MAX_TOTAL_SUBSCRIPTIONS 5000
|
||||||
|
|
||||||
|
// Forward declarations for typedefs
|
||||||
|
typedef struct subscription_filter subscription_filter_t;
|
||||||
|
typedef struct subscription subscription_t;
|
||||||
|
typedef struct subscription_manager subscription_manager_t;
|
||||||
|
|
||||||
|
// Subscription filter structure
|
||||||
|
struct subscription_filter {
|
||||||
|
// Filter criteria (all optional)
|
||||||
|
cJSON* kinds; // Array of event kinds [1,2,3]
|
||||||
|
cJSON* authors; // Array of author pubkeys
|
||||||
|
cJSON* ids; // Array of event IDs
|
||||||
|
long since; // Unix timestamp (0 = not set)
|
||||||
|
long until; // Unix timestamp (0 = not set)
|
||||||
|
int limit; // Result limit (0 = no limit)
|
||||||
|
cJSON* tag_filters; // Object with tag filters: {"#e": ["id1"], "#p": ["pubkey1"]}
|
||||||
|
|
||||||
|
// Linked list for multiple filters per subscription
|
||||||
|
struct subscription_filter* next;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Active subscription structure
|
||||||
|
struct subscription {
|
||||||
|
char id[SUBSCRIPTION_ID_MAX_LENGTH]; // Subscription ID
|
||||||
|
struct lws* wsi; // WebSocket connection handle
|
||||||
|
subscription_filter_t* filters; // Linked list of filters (OR'd together)
|
||||||
|
time_t created_at; // When subscription was created
|
||||||
|
int events_sent; // Counter for sent events
|
||||||
|
int active; // 1 = active, 0 = closed
|
||||||
|
|
||||||
|
// Client info for logging
|
||||||
|
char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP address
|
||||||
|
|
||||||
|
// Linked list pointers
|
||||||
|
struct subscription* next; // Next subscription globally
|
||||||
|
struct subscription* session_next; // Next subscription for this session
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global subscription manager
|
||||||
|
struct subscription_manager {
|
||||||
|
subscription_t* active_subscriptions; // Head of global subscription list
|
||||||
|
pthread_mutex_t subscriptions_lock; // Global thread safety
|
||||||
|
int total_subscriptions; // Current count
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
int max_subscriptions_per_client; // Default: 20
|
||||||
|
int max_total_subscriptions; // Default: 5000
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
uint64_t total_created; // Lifetime subscription count
|
||||||
|
uint64_t total_events_broadcast; // Lifetime event broadcast count
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function declarations
|
||||||
|
subscription_filter_t* create_subscription_filter(cJSON* filter_json);
|
||||||
|
void free_subscription_filter(subscription_filter_t* filter);
|
||||||
|
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip);
|
||||||
|
void free_subscription(subscription_t* sub);
|
||||||
|
int add_subscription_to_manager(subscription_t* sub);
|
||||||
|
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi);
|
||||||
|
int event_matches_filter(cJSON* event, subscription_filter_t* filter);
|
||||||
|
int event_matches_subscription(cJSON* event, subscription_t* subscription);
|
||||||
|
int broadcast_event_to_subscriptions(cJSON* event);
|
||||||
|
|
||||||
|
// Database logging functions
|
||||||
|
void log_subscription_created(const subscription_t* sub);
|
||||||
|
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
|
||||||
|
void log_subscription_disconnected(const char* client_ip);
|
||||||
|
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
|
||||||
|
void update_subscription_events_sent(const char* sub_id, int events_sent);
|
||||||
|
|
||||||
|
#endif // SUBSCRIPTIONS_H
|
||||||
901
src/websockets.c
Normal file
901
src/websockets.c
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
// Define _GNU_SOURCE to ensure all POSIX features are available
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
// Includes
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
// Include libwebsockets after pthread.h to ensure pthread_rwlock_t is defined
|
||||||
|
#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"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
|
||||||
|
#include "config.h" // Configuration management system
|
||||||
|
#include "sql_schema.h" // Embedded database schema
|
||||||
|
#include "websockets.h" // WebSocket structures and constants
|
||||||
|
#include "subscriptions.h" // Subscription structures and functions
|
||||||
|
|
||||||
|
// Forward declarations for logging functions
|
||||||
|
void log_info(const char* message);
|
||||||
|
void log_success(const char* message);
|
||||||
|
void log_error(const char* message);
|
||||||
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for configuration functions
|
||||||
|
const char* get_config_value(const char* key);
|
||||||
|
int get_config_int(const char* key, int default_value);
|
||||||
|
int get_config_bool(const char* key, int default_value);
|
||||||
|
// Forward declarations for NIP-42 authentication functions
|
||||||
|
int is_nip42_auth_globally_required(void);
|
||||||
|
int is_nip42_auth_required_for_kind(int kind);
|
||||||
|
void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss);
|
||||||
|
void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event);
|
||||||
|
void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-11 relay information handling
|
||||||
|
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
|
||||||
|
|
||||||
|
// Forward declarations for database functions
|
||||||
|
int store_event(cJSON* event);
|
||||||
|
|
||||||
|
// Forward declarations for subscription management
|
||||||
|
int broadcast_event_to_subscriptions(cJSON* event);
|
||||||
|
int add_subscription_to_manager(struct subscription* sub);
|
||||||
|
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi);
|
||||||
|
|
||||||
|
// Forward declarations for event handling
|
||||||
|
int handle_event_message(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
||||||
|
|
||||||
|
// Forward declarations for admin event processing
|
||||||
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||||
|
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-09 deletion request handling
|
||||||
|
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-13 PoW handling
|
||||||
|
int validate_event_pow(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declarations for NIP-40 expiration handling
|
||||||
|
int is_event_expired(cJSON* event, time_t current_time);
|
||||||
|
|
||||||
|
// Forward declarations for subscription handling
|
||||||
|
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
||||||
|
|
||||||
|
// Forward declarations for NOTICE message support
|
||||||
|
void send_notice_message(struct lws* wsi, const char* message);
|
||||||
|
|
||||||
|
// Forward declarations for unified cache access
|
||||||
|
extern unified_config_cache_t g_unified_cache;
|
||||||
|
|
||||||
|
// Forward declarations for global state
|
||||||
|
extern sqlite3* g_db;
|
||||||
|
extern int g_server_running;
|
||||||
|
extern struct lws_context *ws_context;
|
||||||
|
|
||||||
|
// Global subscription manager
|
||||||
|
struct subscription_manager g_subscription_manager;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// WEBSOCKET PROTOCOL
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// WebSocket callback function for Nostr relay protocol
|
||||||
|
static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reason,
|
||||||
|
void *user, void *in, size_t len) {
|
||||||
|
struct per_session_data *pss = (struct per_session_data *)user;
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case LWS_CALLBACK_HTTP:
|
||||||
|
// Handle NIP-11 relay information requests (HTTP GET to root path)
|
||||||
|
{
|
||||||
|
char *requested_uri = (char *)in;
|
||||||
|
log_info("HTTP request received");
|
||||||
|
|
||||||
|
// Check if this is a GET request to the root path
|
||||||
|
if (strcmp(requested_uri, "/") == 0) {
|
||||||
|
// Get Accept header
|
||||||
|
char accept_header[256] = {0};
|
||||||
|
int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
|
||||||
|
|
||||||
|
if (header_len > 0) {
|
||||||
|
accept_header[header_len] = '\0';
|
||||||
|
|
||||||
|
// Handle NIP-11 request
|
||||||
|
if (handle_nip11_http_request(wsi, accept_header) == 0) {
|
||||||
|
return 0; // Successfully handled
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_warning("HTTP request without Accept header");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 404 for other requests
|
||||||
|
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 404 for non-root paths
|
||||||
|
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LWS_CALLBACK_HTTP_WRITEABLE:
|
||||||
|
// Handle NIP-11 HTTP body transmission with proper buffer management
|
||||||
|
{
|
||||||
|
struct nip11_session_data* session_data = (struct nip11_session_data*)lws_wsi_user(wsi);
|
||||||
|
if (session_data && session_data->headers_sent && !session_data->body_sent) {
|
||||||
|
// Allocate buffer for JSON body transmission
|
||||||
|
unsigned char *json_buf = malloc(LWS_PRE + session_data->json_length);
|
||||||
|
if (!json_buf) {
|
||||||
|
log_error("Failed to allocate buffer for NIP-11 body transmission");
|
||||||
|
// Clean up session data
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
lws_set_wsi_user(wsi, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy JSON data to buffer
|
||||||
|
memcpy(json_buf + LWS_PRE, session_data->json_buffer, session_data->json_length);
|
||||||
|
|
||||||
|
// Write JSON body
|
||||||
|
int write_result = lws_write(wsi, json_buf + LWS_PRE, session_data->json_length, LWS_WRITE_HTTP);
|
||||||
|
|
||||||
|
// Free the transmission buffer immediately (it's been copied by libwebsockets)
|
||||||
|
free(json_buf);
|
||||||
|
|
||||||
|
if (write_result < 0) {
|
||||||
|
log_error("Failed to write NIP-11 JSON body");
|
||||||
|
// Clean up session data
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
lws_set_wsi_user(wsi, NULL);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark body as sent and clean up session data
|
||||||
|
session_data->body_sent = 1;
|
||||||
|
free(session_data->json_buffer);
|
||||||
|
free(session_data);
|
||||||
|
lws_set_wsi_user(wsi, NULL);
|
||||||
|
|
||||||
|
log_success("NIP-11 relay information served successfully");
|
||||||
|
return 0; // Close connection after successful transmission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_ESTABLISHED:
|
||||||
|
log_info("WebSocket connection established");
|
||||||
|
memset(pss, 0, sizeof(*pss));
|
||||||
|
pthread_mutex_init(&pss->session_lock, NULL);
|
||||||
|
|
||||||
|
// Get real client IP address
|
||||||
|
char client_ip[CLIENT_IP_MAX_LENGTH];
|
||||||
|
lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
|
||||||
|
|
||||||
|
// Ensure client_ip is null-terminated and copy safely
|
||||||
|
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||||
|
size_t ip_len = strlen(client_ip);
|
||||||
|
size_t copy_len = (ip_len < CLIENT_IP_MAX_LENGTH - 1) ? ip_len : CLIENT_IP_MAX_LENGTH - 1;
|
||||||
|
memcpy(pss->client_ip, client_ip, copy_len);
|
||||||
|
pss->client_ip[copy_len] = '\0';
|
||||||
|
|
||||||
|
// Initialize NIP-42 authentication state
|
||||||
|
pss->authenticated = 0;
|
||||||
|
pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0);
|
||||||
|
pss->nip42_auth_required_subscriptions = get_config_bool("nip42_auth_required_subscriptions", 0);
|
||||||
|
pss->auth_challenge_sent = 0;
|
||||||
|
memset(pss->authenticated_pubkey, 0, sizeof(pss->authenticated_pubkey));
|
||||||
|
memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
|
||||||
|
pss->challenge_created = 0;
|
||||||
|
pss->challenge_expires = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_RECEIVE:
|
||||||
|
if (len > 0) {
|
||||||
|
char *message = malloc(len + 1);
|
||||||
|
if (message) {
|
||||||
|
memcpy(message, in, len);
|
||||||
|
message[len] = '\0';
|
||||||
|
|
||||||
|
// Parse JSON message (this is the normal program flow)
|
||||||
|
cJSON* json = cJSON_Parse(message);
|
||||||
|
if (json && cJSON_IsArray(json)) {
|
||||||
|
// Log the complete parsed JSON message once
|
||||||
|
char* complete_message = cJSON_Print(json);
|
||||||
|
if (complete_message) {
|
||||||
|
char debug_msg[2048];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg),
|
||||||
|
"Received complete WebSocket message: %s", complete_message);
|
||||||
|
log_info(debug_msg);
|
||||||
|
free(complete_message);
|
||||||
|
}
|
||||||
|
// Get message type
|
||||||
|
cJSON* type = cJSON_GetArrayItem(json, 0);
|
||||||
|
if (type && cJSON_IsString(type)) {
|
||||||
|
const char* msg_type = cJSON_GetStringValue(type);
|
||||||
|
|
||||||
|
if (strcmp(msg_type, "EVENT") == 0) {
|
||||||
|
// Extract event for kind-specific NIP-42 authentication check
|
||||||
|
cJSON* event_obj = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (event_obj && cJSON_IsObject(event_obj)) {
|
||||||
|
// Extract event kind for kind-specific NIP-42 authentication check
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event_obj, "kind");
|
||||||
|
int event_kind = kind_obj && cJSON_IsNumber(kind_obj) ? (int)cJSON_GetNumberValue(kind_obj) : -1;
|
||||||
|
|
||||||
|
// Extract pubkey and event ID for debugging
|
||||||
|
cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey");
|
||||||
|
cJSON* id_obj = cJSON_GetObjectItem(event_obj, "id");
|
||||||
|
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown";
|
||||||
|
const char* event_id = id_obj ? cJSON_GetStringValue(id_obj) : "unknown";
|
||||||
|
|
||||||
|
char debug_event_msg[512];
|
||||||
|
snprintf(debug_event_msg, sizeof(debug_event_msg),
|
||||||
|
"DEBUG EVENT: Processing kind %d event from pubkey %.16s... ID %.16s...",
|
||||||
|
event_kind, event_pubkey, event_id);
|
||||||
|
log_info(debug_event_msg);
|
||||||
|
|
||||||
|
// Check if NIP-42 authentication is required for this event kind or globally
|
||||||
|
int auth_required = is_nip42_auth_globally_required() || is_nip42_auth_required_for_kind(event_kind);
|
||||||
|
|
||||||
|
char debug_auth_msg[256];
|
||||||
|
snprintf(debug_auth_msg, sizeof(debug_auth_msg),
|
||||||
|
"DEBUG AUTH: auth_required=%d, pss->authenticated=%d, event_kind=%d",
|
||||||
|
auth_required, pss ? pss->authenticated : -1, event_kind);
|
||||||
|
log_info(debug_auth_msg);
|
||||||
|
|
||||||
|
if (pss && auth_required && !pss->authenticated) {
|
||||||
|
if (!pss->auth_challenge_sent) {
|
||||||
|
log_info("DEBUG AUTH: Sending NIP-42 authentication challenge");
|
||||||
|
send_nip42_auth_challenge(wsi, pss);
|
||||||
|
} else {
|
||||||
|
char auth_msg[256];
|
||||||
|
if (event_kind == 4 || event_kind == 14) {
|
||||||
|
snprintf(auth_msg, sizeof(auth_msg),
|
||||||
|
"NIP-42 authentication required for direct message events (kind %d)", event_kind);
|
||||||
|
} else {
|
||||||
|
snprintf(auth_msg, sizeof(auth_msg),
|
||||||
|
"NIP-42 authentication required for event kind %d", event_kind);
|
||||||
|
}
|
||||||
|
send_notice_message(wsi, auth_msg);
|
||||||
|
log_warning("Event rejected: NIP-42 authentication required for kind");
|
||||||
|
char debug_msg[128];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Auth required for kind %d", event_kind);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check blacklist/whitelist rules regardless of NIP-42 auth settings
|
||||||
|
// Blacklist should always be enforced
|
||||||
|
if (event_pubkey) {
|
||||||
|
// Forward declaration for auth rules checking function
|
||||||
|
extern int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
||||||
|
|
||||||
|
int auth_rules_result = check_database_auth_rules(event_pubkey, "event", NULL);
|
||||||
|
if (auth_rules_result != 0) { // 0 = NOSTR_SUCCESS, non-zero = blocked
|
||||||
|
char auth_rules_msg[256];
|
||||||
|
if (auth_rules_result == -101) { // NOSTR_ERROR_AUTH_REQUIRED
|
||||||
|
snprintf(auth_rules_msg, sizeof(auth_rules_msg),
|
||||||
|
"blocked: pubkey not authorized (blacklist/whitelist violation)");
|
||||||
|
} else {
|
||||||
|
snprintf(auth_rules_msg, sizeof(auth_rules_msg),
|
||||||
|
"blocked: authorization check failed (error %d)", auth_rules_result);
|
||||||
|
}
|
||||||
|
send_notice_message(wsi, auth_rules_msg);
|
||||||
|
log_warning("Event rejected: blacklist/whitelist violation");
|
||||||
|
|
||||||
|
// Send OK response with false status
|
||||||
|
cJSON* response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false = rejected
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(auth_rules_msg));
|
||||||
|
|
||||||
|
char *response_str = cJSON_Print(response);
|
||||||
|
if (response_str) {
|
||||||
|
size_t response_len = strlen(response_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + response_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, response_str, response_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(response_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(response);
|
||||||
|
|
||||||
|
cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle EVENT message
|
||||||
|
cJSON* event = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (event && cJSON_IsObject(event)) {
|
||||||
|
// Extract event JSON string for unified validator
|
||||||
|
char *event_json_str = cJSON_Print(event);
|
||||||
|
if (!event_json_str) {
|
||||||
|
log_error("Failed to serialize event JSON for validation");
|
||||||
|
cJSON* error_response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateBool(0));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event"));
|
||||||
|
|
||||||
|
char *error_str = cJSON_Print(error_response);
|
||||||
|
if (error_str) {
|
||||||
|
size_t error_len = strlen(error_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + error_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, error_str, error_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(error_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(error_response);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("DEBUG VALIDATION: Starting unified validator");
|
||||||
|
|
||||||
|
// Call unified validator with JSON string
|
||||||
|
size_t event_json_len = strlen(event_json_str);
|
||||||
|
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
|
||||||
|
|
||||||
|
// Map validation result to old result format (0 = success, -1 = failure)
|
||||||
|
int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
|
||||||
|
|
||||||
|
char debug_validation_msg[256];
|
||||||
|
snprintf(debug_validation_msg, sizeof(debug_validation_msg),
|
||||||
|
"DEBUG VALIDATION: validation_result=%d, result=%d", validation_result, result);
|
||||||
|
log_info(debug_validation_msg);
|
||||||
|
|
||||||
|
// Generate error message based on validation result
|
||||||
|
char error_message[512] = {0};
|
||||||
|
if (result != 0) {
|
||||||
|
switch (validation_result) {
|
||||||
|
case NOSTR_ERROR_INVALID_INPUT:
|
||||||
|
strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
|
||||||
|
strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||||
|
strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
|
||||||
|
strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -103: // NOSTR_ERROR_EVENT_EXPIRED
|
||||||
|
strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -102: // NOSTR_ERROR_NIP42_DISABLED
|
||||||
|
strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -101: // NOSTR_ERROR_AUTH_REQUIRED
|
||||||
|
strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char debug_error_msg[256];
|
||||||
|
snprintf(debug_error_msg, sizeof(debug_error_msg),
|
||||||
|
"DEBUG VALIDATION ERROR: %s", error_message);
|
||||||
|
log_warning(debug_error_msg);
|
||||||
|
} else {
|
||||||
|
log_info("DEBUG VALIDATION: Event validated successfully using unified validator");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup event JSON string
|
||||||
|
free(event_json_str);
|
||||||
|
|
||||||
|
// Check for admin events (kind 23456) and intercept them
|
||||||
|
if (result == 0) {
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||||
|
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
|
|
||||||
|
log_info("DEBUG ADMIN: Checking if admin event processing is needed");
|
||||||
|
|
||||||
|
// Log reception of Kind 23456 events
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
char* event_json_debug = cJSON_Print(event);
|
||||||
|
char debug_received_msg[1024];
|
||||||
|
snprintf(debug_received_msg, sizeof(debug_received_msg),
|
||||||
|
"RECEIVED Kind %d event: %s", event_kind,
|
||||||
|
event_json_debug ? event_json_debug : "Failed to serialize");
|
||||||
|
log_info(debug_received_msg);
|
||||||
|
|
||||||
|
if (event_json_debug) {
|
||||||
|
free(event_json_debug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
// Enhanced admin event security - check authorization first
|
||||||
|
log_info("DEBUG ADMIN: Admin event detected, checking authorization");
|
||||||
|
|
||||||
|
char auth_error[512] = {0};
|
||||||
|
int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
|
||||||
|
|
||||||
|
if (auth_result != 0) {
|
||||||
|
// Authorization failed - log and reject
|
||||||
|
log_warning("DEBUG ADMIN: Admin event authorization failed");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(auth_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, auth_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
|
||||||
|
char debug_auth_error_msg[600];
|
||||||
|
snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg),
|
||||||
|
"DEBUG ADMIN AUTH ERROR: %.400s", auth_error);
|
||||||
|
log_warning(debug_auth_error_msg);
|
||||||
|
} else {
|
||||||
|
// Authorization successful - process through admin API
|
||||||
|
log_info("DEBUG ADMIN: Admin event authorized, processing through admin API");
|
||||||
|
|
||||||
|
char admin_error[512] = {0};
|
||||||
|
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
|
||||||
|
|
||||||
|
char debug_admin_msg[256];
|
||||||
|
snprintf(debug_admin_msg, sizeof(debug_admin_msg),
|
||||||
|
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
|
||||||
|
log_info(debug_admin_msg);
|
||||||
|
|
||||||
|
// Log results for Kind 23456 events
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
if (admin_result == 0) {
|
||||||
|
char success_result_msg[256];
|
||||||
|
snprintf(success_result_msg, sizeof(success_result_msg),
|
||||||
|
"SUCCESS: Kind %d event processed successfully", event_kind);
|
||||||
|
log_success(success_result_msg);
|
||||||
|
} else {
|
||||||
|
char error_result_msg[512];
|
||||||
|
snprintf(error_result_msg, sizeof(error_result_msg),
|
||||||
|
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
|
||||||
|
log_error(error_result_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin_result != 0) {
|
||||||
|
log_error("DEBUG ADMIN: Failed to process admin event through admin API");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(admin_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, admin_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
|
||||||
|
char debug_admin_error_msg[600];
|
||||||
|
snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
|
||||||
|
"DEBUG ADMIN ERROR: %.400s", admin_error);
|
||||||
|
log_error(debug_admin_error_msg);
|
||||||
|
} else {
|
||||||
|
log_success("DEBUG ADMIN: 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
|
||||||
|
log_info("DEBUG STORAGE: Regular event - storing in database");
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
log_error("DEBUG STORAGE: Failed to store event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
log_info("DEBUG STORAGE: Event stored successfully in database");
|
||||||
|
// Broadcast event to matching persistent subscriptions
|
||||||
|
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||||
|
char debug_broadcast_msg[128];
|
||||||
|
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||||
|
"DEBUG BROADCAST: Event broadcast to %d subscriptions", broadcast_count);
|
||||||
|
log_info(debug_broadcast_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Event without valid kind - try normal storage
|
||||||
|
log_warning("DEBUG STORAGE: Event without valid kind - trying normal storage");
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
log_error("DEBUG STORAGE: Failed to store event without kind in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
log_info("DEBUG STORAGE: Event without kind stored successfully in database");
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send OK response
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (event_id && cJSON_IsString(event_id)) {
|
||||||
|
cJSON* response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
||||||
|
|
||||||
|
// TODO: REPLACE - Remove wasteful cJSON_Print conversion
|
||||||
|
char *response_str = cJSON_Print(response);
|
||||||
|
if (response_str) {
|
||||||
|
char debug_response_msg[512];
|
||||||
|
snprintf(debug_response_msg, sizeof(debug_response_msg),
|
||||||
|
"DEBUG RESPONSE: Sending OK response: %s", response_str);
|
||||||
|
log_info(debug_response_msg);
|
||||||
|
|
||||||
|
size_t response_len = strlen(response_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + response_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, response_str, response_len);
|
||||||
|
int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||||
|
|
||||||
|
char debug_write_msg[128];
|
||||||
|
snprintf(debug_write_msg, sizeof(debug_write_msg),
|
||||||
|
"DEBUG RESPONSE: lws_write returned %d", write_result);
|
||||||
|
log_info(debug_write_msg);
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(response_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||||
|
// Check NIP-42 authentication for REQ subscriptions if required
|
||||||
|
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||||
|
if (!pss->auth_challenge_sent) {
|
||||||
|
send_nip42_auth_challenge(wsi, pss);
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
|
||||||
|
log_warning("REQ rejected: NIP-42 authentication required");
|
||||||
|
}
|
||||||
|
cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle REQ message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
|
||||||
|
// Create array of filter objects from position 2 onwards
|
||||||
|
cJSON* filters = cJSON_CreateArray();
|
||||||
|
int json_size = cJSON_GetArraySize(json);
|
||||||
|
for (int i = 2; i < json_size; i++) {
|
||||||
|
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||||
|
if (filter) {
|
||||||
|
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_req_message(subscription_id, filters, wsi, pss);
|
||||||
|
|
||||||
|
// Clean up the filters array we created
|
||||||
|
cJSON_Delete(filters);
|
||||||
|
|
||||||
|
// Send EOSE (End of Stored Events)
|
||||||
|
cJSON* eose_response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
|
||||||
|
|
||||||
|
char *eose_str = cJSON_Print(eose_response);
|
||||||
|
if (eose_str) {
|
||||||
|
size_t eose_len = strlen(eose_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + eose_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, eose_str, eose_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(eose_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(eose_response);
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "CLOSE") == 0) {
|
||||||
|
// Handle CLOSE message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
|
||||||
|
// Remove from global manager
|
||||||
|
remove_subscription_from_manager(subscription_id, wsi);
|
||||||
|
|
||||||
|
// Remove from session list if present
|
||||||
|
if (pss) {
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
struct subscription** current = &pss->subscriptions;
|
||||||
|
while (*current) {
|
||||||
|
if (strcmp((*current)->id, subscription_id) == 0) {
|
||||||
|
struct subscription* to_remove = *current;
|
||||||
|
*current = to_remove->session_next;
|
||||||
|
pss->subscription_count--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = &((*current)->session_next);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
char debug_msg[256];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Closed subscription: %s", subscription_id);
|
||||||
|
log_info(debug_msg);
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||||
|
// Handle NIP-42 AUTH message
|
||||||
|
if (cJSON_GetArraySize(json) >= 2) {
|
||||||
|
cJSON* auth_payload = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
|
if (cJSON_IsString(auth_payload)) {
|
||||||
|
// AUTH challenge response: ["AUTH", <challenge>] (unusual)
|
||||||
|
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
|
||||||
|
} else if (cJSON_IsObject(auth_payload)) {
|
||||||
|
// AUTH signed event: ["AUTH", <event>] (standard NIP-42)
|
||||||
|
handle_nip42_auth_signed_event(wsi, pss, auth_payload);
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "Invalid AUTH message format");
|
||||||
|
log_warning("Received AUTH message with invalid payload type");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "AUTH message requires payload");
|
||||||
|
log_warning("Received AUTH message without payload");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown message type
|
||||||
|
char unknown_msg[128];
|
||||||
|
snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
|
||||||
|
log_warning(unknown_msg);
|
||||||
|
send_notice_message(wsi, "Unknown message type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_CLOSED:
|
||||||
|
log_info("WebSocket connection closed");
|
||||||
|
|
||||||
|
// Clean up session subscriptions
|
||||||
|
if (pss) {
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
struct subscription* sub = pss->subscriptions;
|
||||||
|
while (sub) {
|
||||||
|
struct subscription* next = sub->session_next;
|
||||||
|
remove_subscription_from_manager(sub->id, wsi);
|
||||||
|
sub = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pss->subscriptions = NULL;
|
||||||
|
pss->subscription_count = 0;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
pthread_mutex_destroy(&pss->session_lock);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket protocol definition
|
||||||
|
static struct lws_protocols protocols[] = {
|
||||||
|
{
|
||||||
|
"nostr-relay-protocol",
|
||||||
|
nostr_relay_callback,
|
||||||
|
sizeof(struct per_session_data),
|
||||||
|
4096, // rx buffer size
|
||||||
|
0, NULL, 0
|
||||||
|
},
|
||||||
|
{ 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;
|
||||||
|
int reuse = 1;
|
||||||
|
|
||||||
|
// Create a socket
|
||||||
|
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sockfd < 0) {
|
||||||
|
return 0; // Cannot create socket, assume port unavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state
|
||||||
|
// This matches libwebsockets behavior and prevents false unavailability
|
||||||
|
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
|
||||||
|
close(sockfd);
|
||||||
|
return 0; // Failed to set socket option
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, int strict_port) {
|
||||||
|
struct lws_context_creation_info info;
|
||||||
|
|
||||||
|
log_info("Starting libwebsockets-based Nostr relay server...");
|
||||||
|
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
// Use port override if provided, otherwise use configuration
|
||||||
|
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 = 10; // Increased from 5 to 10
|
||||||
|
|
||||||
|
// Minimal libwebsockets configuration
|
||||||
|
info.protocols = protocols;
|
||||||
|
info.gid = -1;
|
||||||
|
info.uid = -1;
|
||||||
|
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
|
||||||
|
|
||||||
|
// Remove interface restrictions - let system choose
|
||||||
|
// info.vhost_name = NULL;
|
||||||
|
// info.iface = NULL;
|
||||||
|
|
||||||
|
// Increase max connections for relay usage
|
||||||
|
info.max_http_header_pool = 16;
|
||||||
|
info.timeout_secs = 10;
|
||||||
|
|
||||||
|
// Max payload size for Nostr events
|
||||||
|
info.max_http_header_data = 4096;
|
||||||
|
|
||||||
|
// Find an available port with pre-checking (or fail immediately in strict mode)
|
||||||
|
while (port_attempts < (strict_port ? 1 : 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 (strict_port) {
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg),
|
||||||
|
"Strict port mode: port %d is not available", actual_port);
|
||||||
|
log_error(error_msg);
|
||||||
|
return -1;
|
||||||
|
} else 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 (strict_port) {
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg),
|
||||||
|
"Strict port mode: failed to bind to port %d", actual_port);
|
||||||
|
log_error(error_msg);
|
||||||
|
break;
|
||||||
|
} else if (port_attempts < max_port_attempts) {
|
||||||
|
actual_port++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we've exhausted attempts
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws_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];
|
||||||
|
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
|
||||||
|
while (g_server_running) {
|
||||||
|
int result = lws_service(ws_context, 1000);
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
log_error("libwebsockets service error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Shutting down WebSocket server...");
|
||||||
|
lws_context_destroy(ws_context);
|
||||||
|
ws_context = NULL;
|
||||||
|
|
||||||
|
log_success("WebSocket relay shut down cleanly");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
49
src/websockets.h
Normal file
49
src/websockets.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// WebSocket protocol structures and constants for C-Relay
|
||||||
|
// This header defines structures shared between main.c and websockets.c
|
||||||
|
|
||||||
|
#ifndef WEBSOCKETS_H
|
||||||
|
#define WEBSOCKETS_H
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <libwebsockets.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
#include "config.h" // For CLIENT_IP_MAX_LENGTH and MAX_SUBSCRIPTIONS_PER_CLIENT
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
#define CHALLENGE_MAX_LENGTH 128
|
||||||
|
#define AUTHENTICATED_PUBKEY_MAX_LENGTH 65 // 64 hex + null
|
||||||
|
|
||||||
|
// Enhanced per-session data with subscription management and NIP-42 authentication
|
||||||
|
struct per_session_data {
|
||||||
|
int authenticated;
|
||||||
|
struct subscription* subscriptions; // Head of this session's subscription list
|
||||||
|
pthread_mutex_t session_lock; // Per-session thread safety
|
||||||
|
char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging
|
||||||
|
int subscription_count; // Number of subscriptions for this session
|
||||||
|
|
||||||
|
// NIP-42 Authentication State
|
||||||
|
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
|
||||||
|
char active_challenge[65]; // Current challenge for this session (64 hex + null)
|
||||||
|
time_t challenge_created; // When challenge was created
|
||||||
|
time_t challenge_expires; // Challenge expiration time
|
||||||
|
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
|
||||||
|
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
|
||||||
|
int auth_challenge_sent; // Whether challenge has been sent (0/1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||||
|
struct nip11_session_data {
|
||||||
|
char* json_buffer;
|
||||||
|
size_t json_length;
|
||||||
|
int headers_sent;
|
||||||
|
int body_sent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function declarations
|
||||||
|
int start_websocket_relay(int port_override, int strict_port);
|
||||||
|
|
||||||
|
// Auth rules checking function from request_validator.c
|
||||||
|
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
||||||
|
|
||||||
|
#endif // WEBSOCKETS_H
|
||||||
@@ -163,8 +163,96 @@ test_subscription() {
|
|||||||
|
|
||||||
# Count EVENT responses (lines containing ["EVENT","sub_id",...])
|
# Count EVENT responses (lines containing ["EVENT","sub_id",...])
|
||||||
local event_count=0
|
local event_count=0
|
||||||
|
local filter_mismatch_count=0
|
||||||
if [[ -n "$response" ]]; then
|
if [[ -n "$response" ]]; then
|
||||||
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
|
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
|
||||||
|
filter_mismatch_count=$(echo "$response" | grep -c "filter does not match" 2>/dev/null || echo "0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up the filter_mismatch_count (remove any extra spaces/newlines)
|
||||||
|
filter_mismatch_count=$(echo "$filter_mismatch_count" | tr -d '[:space:]' | sed 's/[^0-9]//g')
|
||||||
|
if [[ -z "$filter_mismatch_count" ]]; then
|
||||||
|
filter_mismatch_count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debug: Show what we found
|
||||||
|
print_info "Found $event_count events, $filter_mismatch_count filter mismatches"
|
||||||
|
|
||||||
|
# Check for filter mismatches (protocol violation)
|
||||||
|
if [[ "$filter_mismatch_count" -gt 0 ]]; then
|
||||||
|
print_error "$description - PROTOCOL VIOLATION: Relay sent $filter_mismatch_count events that don't match filter!"
|
||||||
|
print_error "Filter: $filter"
|
||||||
|
print_error "This indicates improper server-side filtering - relay should only send matching events"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Additional check: Analyze returned events against filter criteria
|
||||||
|
local filter_violation_count=0
|
||||||
|
if [[ -n "$response" && "$event_count" -gt 0 ]]; then
|
||||||
|
# Parse filter to check for violations
|
||||||
|
if echo "$filter" | grep -q '"kinds":\['; then
|
||||||
|
# Kind filter - check that all returned events have matching kinds
|
||||||
|
local allowed_kinds=$(echo "$filter" | sed 's/.*"kinds":\[\([^]]*\)\].*/\1/' | sed 's/[^0-9,]//g')
|
||||||
|
echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
|
||||||
|
local event_kind=$(echo "$event_line" | jq -r '.[2].kind' 2>/dev/null)
|
||||||
|
if [[ -n "$event_kind" && "$event_kind" =~ ^[0-9]+$ ]]; then
|
||||||
|
local kind_matches=0
|
||||||
|
IFS=',' read -ra KIND_ARRAY <<< "$allowed_kinds"
|
||||||
|
for kind in "${KIND_ARRAY[@]}"; do
|
||||||
|
if [[ "$event_kind" == "$kind" ]]; then
|
||||||
|
kind_matches=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$kind_matches" == "0" ]]; then
|
||||||
|
((filter_violation_count++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
elif echo "$filter" | grep -q '"ids":\['; then
|
||||||
|
# ID filter - check that all returned events have matching IDs
|
||||||
|
local allowed_ids=$(echo "$filter" | sed 's/.*"ids":\[\([^]]*\)\].*/\1/' | sed 's/"//g' | sed 's/[][]//g')
|
||||||
|
echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
|
||||||
|
local event_id=$(echo "$event_line" | jq -r '.[2].id' 2>/dev/null)
|
||||||
|
if [[ -n "$event_id" ]]; then
|
||||||
|
local id_matches=0
|
||||||
|
IFS=',' read -ra ID_ARRAY <<< "$allowed_ids"
|
||||||
|
for id in "${ID_ARRAY[@]}"; do
|
||||||
|
if [[ "$event_id" == "$id" ]]; then
|
||||||
|
id_matches=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$id_matches" == "0" ]]; then
|
||||||
|
((filter_violation_count++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report filter violations
|
||||||
|
if [[ "$filter_violation_count" -gt 0 ]]; then
|
||||||
|
print_error "$description - FILTER VIOLATION: $filter_violation_count events don't match the filter criteria!"
|
||||||
|
print_error "Filter: $filter"
|
||||||
|
print_error "Expected only events matching the filter, but received non-matching events"
|
||||||
|
print_error "This indicates improper server-side filtering"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also fail on count mismatches for strict filters (like specific IDs and kinds with expected counts)
|
||||||
|
if [[ "$expected_count" != "any" && "$event_count" != "$expected_count" ]]; then
|
||||||
|
if echo "$filter" | grep -q '"ids":\['; then
|
||||||
|
print_error "$description - CRITICAL VIOLATION: ID filter should return exactly $expected_count event(s), got $event_count"
|
||||||
|
print_error "Filter: $filter"
|
||||||
|
print_error "ID queries must return exactly the requested event or none"
|
||||||
|
return 1
|
||||||
|
elif echo "$filter" | grep -q '"kinds":\[' && [[ "$expected_count" =~ ^[0-9]+$ ]]; then
|
||||||
|
print_error "$description - FILTER VIOLATION: Kind filter expected $expected_count event(s), got $event_count"
|
||||||
|
print_error "Filter: $filter"
|
||||||
|
print_error "This suggests improper filtering - events of wrong kinds are being returned"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$expected_count" == "any" ]]; then
|
if [[ "$expected_count" == "any" ]]; then
|
||||||
@@ -291,29 +379,63 @@ run_comprehensive_test() {
|
|||||||
# Test subscription filters
|
# Test subscription filters
|
||||||
print_step "Testing various subscription filters..."
|
print_step "Testing various subscription filters..."
|
||||||
|
|
||||||
|
local test_failures=0
|
||||||
|
|
||||||
# Test 1: Get all events
|
# Test 1: Get all events
|
||||||
test_subscription "test_all" '{}' "All events" "any"
|
if ! test_subscription "test_all" '{}' "All events" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 2: Get events by kind
|
# Test 2: Get events by kind
|
||||||
test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "2"
|
if ! test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "any"; then
|
||||||
test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
if ! test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 3: Get events by author (pubkey)
|
# Test 3: Get events by author (pubkey)
|
||||||
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
|
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
|
||||||
test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"
|
if ! test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 4: Get recent events (time-based)
|
# Test 4: Get recent events (time-based)
|
||||||
local recent_timestamp=$(($(date +%s) - 200))
|
local recent_timestamp=$(($(date +%s) - 200))
|
||||||
test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"
|
if ! test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 5: Get events with specific tags
|
# Test 5: Get events with specific tags
|
||||||
test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"
|
if ! test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 6: Multiple kinds
|
# Test 6: Multiple kinds
|
||||||
test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"
|
if ! test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
# Test 7: Limit results
|
# Test 7: Limit results
|
||||||
test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"
|
if ! test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 8: Specific event ID query (tests for "filter does not match" bug)
|
||||||
|
if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then
|
||||||
|
local test_event_id="${REGULAR_EVENT_IDS[0]}"
|
||||||
|
if ! test_subscription "test_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Specific event ID query" "1"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report subscription test results
|
||||||
|
if [[ $test_failures -gt 0 ]]; then
|
||||||
|
print_error "SUBSCRIPTION TESTS FAILED: $test_failures test(s) detected protocol violations"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
print_success "All subscription tests passed"
|
||||||
|
fi
|
||||||
|
|
||||||
print_header "PHASE 4: Database Verification"
|
print_header "PHASE 4: Database Verification"
|
||||||
|
|
||||||
@@ -321,17 +443,28 @@ run_comprehensive_test() {
|
|||||||
print_step "Verifying database contents..."
|
print_step "Verifying database contents..."
|
||||||
|
|
||||||
if command -v sqlite3 &> /dev/null; then
|
if command -v sqlite3 &> /dev/null; then
|
||||||
print_info "Events by type in database:"
|
# Find the database file (should be in build/ directory with relay pubkey as filename)
|
||||||
sqlite3 db/c_nostr_relay.db "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" | while read line; do
|
local db_file=""
|
||||||
|
if [[ -d "../build" ]]; then
|
||||||
|
db_file=$(find ../build -name "*.db" -type f | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$db_file" && -f "$db_file" ]]; then
|
||||||
|
print_info "Events by type in database ($db_file):"
|
||||||
|
sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do
|
||||||
echo " $line"
|
echo " $line"
|
||||||
done
|
done
|
||||||
|
|
||||||
print_info "Recent events in database:"
|
print_info "Recent events in database:"
|
||||||
sqlite3 db/c_nostr_relay.db "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" | while read line; do
|
sqlite3 "$db_file" "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null | while read line; do
|
||||||
echo " $line"
|
echo " $line"
|
||||||
done
|
done
|
||||||
|
|
||||||
print_success "Database verification complete"
|
print_success "Database verification complete"
|
||||||
|
else
|
||||||
|
print_warning "Database file not found in build/ directory"
|
||||||
|
print_info "Expected database files: build/*.db (named after relay pubkey)"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
print_warning "sqlite3 not available for database verification"
|
print_warning "sqlite3 not available for database verification"
|
||||||
fi
|
fi
|
||||||
@@ -352,6 +485,11 @@ if run_comprehensive_test; then
|
|||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo
|
echo
|
||||||
print_error "Some tests failed"
|
print_error "❌ TESTS FAILED: Protocol violations detected!"
|
||||||
|
print_error "The C-Relay has critical issues that need to be fixed:"
|
||||||
|
print_error " - Server-side filtering is not implemented properly"
|
||||||
|
print_error " - Events are sent to clients regardless of subscription filters"
|
||||||
|
print_error " - This violates the Nostr protocol specification"
|
||||||
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -310,8 +310,51 @@ else
|
|||||||
print_failure "Relay failed to start for network test"
|
print_failure "Relay failed to start for network test"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# TEST 10: Multiple Startup Attempts (Port Conflict)
|
# TEST 10: Port Override with Admin/Relay Key Overrides
|
||||||
print_test_header "Test 10: Port Conflict Handling"
|
print_test_header "Test 10: Port Override with -a/-r Flags"
|
||||||
|
|
||||||
|
cleanup_test_files
|
||||||
|
|
||||||
|
# Generate test keys (64 hex chars each)
|
||||||
|
TEST_ADMIN_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||||
|
TEST_RELAY_PRIVKEY="abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
||||||
|
|
||||||
|
print_info "Testing port override with -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY"
|
||||||
|
|
||||||
|
# Start relay with port override and key overrides
|
||||||
|
timeout 15 $RELAY_BINARY -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY > "test_port_override.log" 2>&1 &
|
||||||
|
relay_pid=$!
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
if kill -0 $relay_pid 2>/dev/null; then
|
||||||
|
# Check if relay bound to port 9999 (not default 8888)
|
||||||
|
if netstat -tln 2>/dev/null | grep -q ":9999"; then
|
||||||
|
print_success "Relay successfully bound to overridden port 9999"
|
||||||
|
else
|
||||||
|
print_failure "Relay not bound to overridden port 9999"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that relay started successfully
|
||||||
|
if check_relay_startup "test_port_override.log"; then
|
||||||
|
print_success "Relay startup completed with overrides"
|
||||||
|
else
|
||||||
|
print_failure "Relay failed to complete startup with overrides"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that admin keys were NOT generated (since -a was provided)
|
||||||
|
if ! check_admin_keys "test_port_override.log"; then
|
||||||
|
print_success "Admin keys not generated (correctly using provided -a key)"
|
||||||
|
else
|
||||||
|
print_failure "Admin keys generated despite -a override"
|
||||||
|
fi
|
||||||
|
|
||||||
|
stop_relay_test $relay_pid
|
||||||
|
else
|
||||||
|
print_failure "Relay failed to start with port/key overrides"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TEST 11: Multiple Startup Attempts (Port Conflict)
|
||||||
|
print_test_header "Test 11: Port Conflict Handling"
|
||||||
|
|
||||||
relay_pid1=$(start_relay_test "port_conflict_1" 10)
|
relay_pid1=$(start_relay_test "port_conflict_1" 10)
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|||||||
88
tests/nip42_test.log
Normal file
88
tests/nip42_test.log
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
=== NIP-42 Authentication Test Started ===
|
||||||
|
2025-09-30 11:15:28 - Starting NIP-42 authentication tests
|
||||||
|
[34m[1m[INFO][0m === Starting NIP-42 Authentication Tests ===
|
||||||
|
[34m[1m[INFO][0m Checking dependencies...
|
||||||
|
[32m[1m[SUCCESS][0m Dependencies check complete
|
||||||
|
[34m[1m[INFO][0m Test 1: Checking NIP-42 support in relay info
|
||||||
|
[32m[1m[SUCCESS][0m NIP-42 is advertised in supported NIPs
|
||||||
|
2025-09-30 11:15:28 - Supported NIPs: 1,9,11,13,15,20,40,42
|
||||||
|
[34m[1m[INFO][0m Test 2: Testing AUTH challenge generation
|
||||||
|
[34m[1m[INFO][0m Found admin private key, configuring NIP-42 authentication...
|
||||||
|
[33m[1m[WARNING][0m Failed to create configuration event - proceeding with manual test
|
||||||
|
[34m[1m[INFO][0m Test 3: Testing complete NIP-42 authentication flow
|
||||||
|
[34m[1m[INFO][0m Generated test keypair: test_pubkey
|
||||||
|
[34m[1m[INFO][0m Attempting to publish event without authentication...
|
||||||
|
[34m[1m[INFO][0m Publishing test event to relay...
|
||||||
|
2025-09-30 11:15:30 - Event publish result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"acfc4da1903ce1c065f2c472348b21837a322c79cb4b248c62de5cff9b5b6607","pubkey":"d3e8d83eabac2a28e21039136a897399f4866893dd43bfbf0bdc8391913a4013","created_at":1759245329,"tags":[],"content":"NIP-42 test event - should require auth","sig":"2051b3da705214d5b5e95fb5b4dd9f1c893666965f7c51ccd2a9ccd495b67dd76ed3ce9768f0f2a16a3f9a602368e8102758ca3cc1408280094abf7e92fcc75e"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Relay requested authentication as expected
|
||||||
|
[34m[1m[INFO][0m Test 4: Testing WebSocket AUTH message handling
|
||||||
|
[34m[1m[INFO][0m Testing WebSocket connection and AUTH message...
|
||||||
|
[34m[1m[INFO][0m Sending test message via WebSocket...
|
||||||
|
2025-09-30 11:15:30 - WebSocket response:
|
||||||
|
[34m[1m[INFO][0m No AUTH challenge in WebSocket response
|
||||||
|
[34m[1m[INFO][0m Test 5: Testing NIP-42 configuration options
|
||||||
|
[34m[1m[INFO][0m Retrieving current relay configuration...
|
||||||
|
[33m[1m[WARNING][0m Could not retrieve configuration events
|
||||||
|
[34m[1m[INFO][0m Test 6: Testing NIP-42 performance and stability
|
||||||
|
[34m[1m[INFO][0m Testing multiple authentication attempts...
|
||||||
|
2025-09-30 11:15:31 - Attempt 1: .297874340s - connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"0d742f093b7be0ce811068e7a6171573dd225418c9459f5c7e9580f57d88af7b","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 1","sig":"d4aec950c47fbd4c1da637b84fafbde570adf86e08795236fb6a3f7e12d2dbaa16cb38cbb68d3b9755d186b20800bdb84b0a050f8933d06b10991a9542fe9909"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
2025-09-30 11:15:32 - Attempt 2: .270493759s - connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"b45ae1b0458e284ed89b6de453bab489d506352680f6d37c8a5f0aed9eebc7a5","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 2","sig":"f9702aa537ec1485d151a0115c38c7f6f1bc05a63929be784e33850b46be6a961996eb922b8b337d607312c8e4583590ee35f38330300e19ab921f94926719c5"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
2025-09-30 11:15:32 - Attempt 3: .239220029s - connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"5f70f9cb2a30a12e7d088e62a9295ef2fbea4f40a1d8b07006db03f610c5abce","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 3","sig":"ea2e1611ce3ddea3aa73764f4542bad7d922fc0d2ed40e58dcc2a66cb6e046bfae22d6baef296eb51d965a22b2a07394fc5f8664e3a7777382ae523431c782cd"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
2025-09-30 11:15:33 - Attempt 4: .221429674s - connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"eafcf5f7e0bd0be35267f13ff93eef339faec6a5af13fe451fee2b7443b9de6e","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 4","sig":"976017abe67582af29d46cd54159ce0465c94caf348be35f26b6522cb48c4c9ce5ba9835e92873cf96a906605a032071360fc85beea815a8e4133a4f45d2bf0a"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
2025-09-30 11:15:33 - Attempt 5: .242410067s - connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"c7cf6776000a325b1180240c61ef20b849b84dee3f5d2efed4c1a9e9fbdbd7b1","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245333,"tags":[],"content":"Performance test event 5","sig":"18b4575bd644146451dcf86607d75f358828ce2907e8904bd08b903ff5d79ec5a69ff60168735975cc406dcee788fd22fc7bf7c97fb7ac6dff3580eda56cee2e"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Performance test completed: 5/5 successful responses
|
||||||
|
[34m[1m[INFO][0m Test 7: Testing kind-specific NIP-42 authentication requirements
|
||||||
|
[34m[1m[INFO][0m Generated test keypair for kind-specific tests: test_pubkey
|
||||||
|
[34m[1m[INFO][0m Testing kind 1 event (regular note) - should work without authentication...
|
||||||
|
2025-09-30 11:15:34 - Kind 1 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":1,"id":"012690335e48736fd29769669d2bda15a079183c1d0f27b8400366a54b5b9ddd","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[],"content":"Regular note - should not require auth","sig":"a3a0ce218666d2a374983a343bc24da5a727ce251c23828171021f15a3ab441a0c86f56200321467914ce4bee9a987f1de301151467ae639d7f941bac7fbe68e"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Kind 1 event accepted without authentication (correct behavior)
|
||||||
|
[34m[1m[INFO][0m Testing kind 4 event (direct message) - should require authentication...
|
||||||
|
2025-09-30 11:15:44 - Kind 4 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":4,"id":"e629dd91320d48c1e3103ec16e40c707c2ee8143012c9ad8bb9d32f98610f447","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"7677b3f2932fb4979bab3da6d241217b7ea2010411fc8bf5a51f6987f38696d5634f91a30b13e0f4861479ceabff995b3bb2eb2fc74af5f3d1175235d5448ce2"}
|
||||||
|
publishing to ws://localhost:8888...
|
||||||
|
[32m[1m[SUCCESS][0m Kind 4 event requested authentication (correct behavior for DMs)
|
||||||
|
[34m[1m[INFO][0m Testing kind 14 event (chat message) - should require authentication...
|
||||||
|
2025-09-30 11:15:55 - Kind 14 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":14,"id":"a5398c5851dd72a8980723c91d35345bd0088b800102180dd41af7056f1cad50","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245344,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"62d43f3f81755d4ef81cbfc8aca9abc11f28b0c45640f19d3dd41a09bae746fe7a4e9d8e458c416dcd2cab02deb090ce1e29e8426d9be5445d130eaa00d339f2"}
|
||||||
|
publishing to ws://localhost:8888...
|
||||||
|
[32m[1m[SUCCESS][0m Kind 14 event requested authentication (correct behavior for DMs)
|
||||||
|
[34m[1m[INFO][0m Testing other event kinds - should work without authentication...
|
||||||
|
2025-09-30 11:15:55 - Kind 0 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":0,"id":"069ac4db07da3230681aa37ab9e6a2aa48e2c199245259681e45ffb2f1b21846","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"3c99b97c0ea2d18bc88fc07b2e95e213b6a6af804512d62158f8fd63cc24a3937533b830f59d38ccacccf98ba2fb0ed7467b16271154d4dd37fbc075eba32e49"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Kind 0 event accepted without authentication (correct)
|
||||||
|
2025-09-30 11:15:56 - Kind 3 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":3,"id":"1dd1ccb13ebd0d50b2aa79dbb938b408a24f0a4dd9f872b717ed91ae6729051c","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"c205cc76f687c3957cf8b35cd8346fd8c2e44d9ef82324b95a7eef7f57429fb6f2ab1d0263dd5d00204dd90e626d5918a8710341b0d68a5095b41455f49cf0dd"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Kind 3 event accepted without authentication (correct)
|
||||||
|
2025-09-30 11:15:56 - Kind 7 event result: connecting to ws://localhost:8888... ok.
|
||||||
|
{"kind":7,"id":"b6161b1da8a4d362e3c230df99c4f87b6311ef6e9f67e03a2476f8a6366352c1","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245356,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"ab06c4b00a04d726109acd02d663e30188ff9ee854cf877e854fda90dd776a649ef3fab8ae5b530b4e6b5530490dd536a281a721e471bd3748a0dacc4eac9622"}
|
||||||
|
publishing to ws://localhost:8888... success.
|
||||||
|
[32m[1m[SUCCESS][0m Kind 7 event accepted without authentication (correct)
|
||||||
|
[34m[1m[INFO][0m Kind-specific authentication test completed
|
||||||
|
[34m[1m[INFO][0m === NIP-42 Test Results Summary ===
|
||||||
|
[32m[1m[SUCCESS][0m Dependencies: PASS
|
||||||
|
[32m[1m[SUCCESS][0m NIP-42 Support: PASS
|
||||||
|
[32m[1m[SUCCESS][0m Auth Challenge: PASS
|
||||||
|
[32m[1m[SUCCESS][0m Auth Flow: PASS
|
||||||
|
[32m[1m[SUCCESS][0m WebSocket AUTH: PASS
|
||||||
|
[32m[1m[SUCCESS][0m Configuration: PASS
|
||||||
|
[32m[1m[SUCCESS][0m Performance: PASS
|
||||||
|
[32m[1m[SUCCESS][0m Kind-Specific Auth: PASS
|
||||||
|
[32m[1m[SUCCESS][0m All NIP-42 tests completed successfully!
|
||||||
|
[32m[1m[SUCCESS][0m NIP-42 authentication implementation is working correctly
|
||||||
|
[34m[1m[INFO][0m === NIP-42 Authentication Tests Complete ===
|
||||||
File diff suppressed because it is too large
Load Diff
413
tests/white_black_test.sh
Executable file
413
tests/white_black_test.sh
Executable file
@@ -0,0 +1,413 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# C-Relay Whitelist/Blacklist Test Script
|
||||||
|
# Tests the relay's authentication functionality using nak
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
RELAY_URL="ws://localhost:8888"
|
||||||
|
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||||
|
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if nak is installed
|
||||||
|
check_nak() {
|
||||||
|
if ! command -v nak &> /dev/null; then
|
||||||
|
log_error "nak command not found. Please install nak first."
|
||||||
|
log_error "Visit: https://github.com/fiatjaf/nak"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_success "nak is available"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate test keypair
|
||||||
|
generate_test_keypair() {
|
||||||
|
log_info "Generating test keypair..."
|
||||||
|
|
||||||
|
# Generate private key
|
||||||
|
TEST_PRIVKEY=$(nak key generate 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$TEST_PRIVKEY" ]; then
|
||||||
|
log_error "Failed to generate private key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derive public key from private key
|
||||||
|
TEST_PUBKEY=$(nak key public "$TEST_PRIVKEY" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$TEST_PUBKEY" ]; then
|
||||||
|
log_error "Failed to derive public key from private key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Generated test keypair:"
|
||||||
|
log_info " Private key: $TEST_PRIVKEY"
|
||||||
|
log_info " Public key: $TEST_PUBKEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create test event
|
||||||
|
create_test_event() {
|
||||||
|
local timestamp=$(date +%s)
|
||||||
|
local content="Test event at timestamp $timestamp"
|
||||||
|
|
||||||
|
log_info "Creating test event (kind 1) with content: '$content'"
|
||||||
|
|
||||||
|
# Create event using nak
|
||||||
|
EVENT_JSON=$(nak event \
|
||||||
|
--kind 1 \
|
||||||
|
--content "$content" \
|
||||||
|
--sec "$TEST_PRIVKEY" \
|
||||||
|
--tag 't=test')
|
||||||
|
|
||||||
|
# Extract event ID
|
||||||
|
EVENT_ID=$(echo "$EVENT_JSON" | jq -r '.id')
|
||||||
|
|
||||||
|
if [ -z "$EVENT_ID" ] || [ "$EVENT_ID" = "null" ]; then
|
||||||
|
log_error "Failed to create test event"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Created test event with ID: $EVENT_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 1: Post event and verify retrieval
|
||||||
|
test_post_and_retrieve() {
|
||||||
|
log_info "=== TEST 1: Post event and verify retrieval ==="
|
||||||
|
|
||||||
|
# Post the event
|
||||||
|
log_info "Posting test event to relay..."
|
||||||
|
POST_RESULT=$(echo "$EVENT_JSON" | nak event "$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$POST_RESULT" | grep -q "error\|failed\|denied"; then
|
||||||
|
log_error "Failed to post event: $POST_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Event posted successfully"
|
||||||
|
|
||||||
|
# Wait a moment for processing
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Try to retrieve the event
|
||||||
|
log_info "Retrieving event from relay..."
|
||||||
|
RETRIEVE_RESULT=$(nak req \
|
||||||
|
--id "$EVENT_ID" \
|
||||||
|
"$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$RETRIEVE_RESULT" | grep -q "$EVENT_ID"; then
|
||||||
|
log_success "Event successfully retrieved from relay"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Failed to retrieve event from relay"
|
||||||
|
log_error "Query result: $RETRIEVE_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send admin command to add user to blacklist
|
||||||
|
add_to_blacklist() {
|
||||||
|
log_info "Adding test user to blacklist..."
|
||||||
|
|
||||||
|
# Create the admin command
|
||||||
|
COMMAND="[\"blacklist\", \"pubkey\", \"$TEST_PUBKEY\"]"
|
||||||
|
|
||||||
|
# Encrypt the command using NIP-44
|
||||||
|
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--recipient-pubkey "$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
if [ -z "$ENCRYPTED_COMMAND" ]; then
|
||||||
|
log_error "Failed to encrypt admin command"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin event
|
||||||
|
ADMIN_EVENT=$(nak event \
|
||||||
|
--kind 23456 \
|
||||||
|
--content "$ENCRYPTED_COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--tag "p=$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
# Post admin event
|
||||||
|
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
|
||||||
|
log_error "Failed to send admin command: $ADMIN_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Admin command sent successfully - user added to blacklist"
|
||||||
|
# Wait for the relay to process the admin command
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send admin command to add user to whitelist
|
||||||
|
add_to_whitelist() {
|
||||||
|
local pubkey="$1"
|
||||||
|
log_info "Adding pubkey to whitelist: ${pubkey:0:16}..."
|
||||||
|
|
||||||
|
# Create the admin command
|
||||||
|
COMMAND="[\"whitelist\", \"pubkey\", \"$pubkey\"]"
|
||||||
|
|
||||||
|
# Encrypt the command using NIP-44
|
||||||
|
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--recipient-pubkey "$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
if [ -z "$ENCRYPTED_COMMAND" ]; then
|
||||||
|
log_error "Failed to encrypt admin command"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin event
|
||||||
|
ADMIN_EVENT=$(nak event \
|
||||||
|
--kind 23456 \
|
||||||
|
--content "$ENCRYPTED_COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--tag "p=$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
# Post admin event
|
||||||
|
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
|
||||||
|
log_error "Failed to send admin command: $ADMIN_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Admin command sent successfully - user added to whitelist"
|
||||||
|
# Wait for the relay to process the admin command
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear all auth rules
|
||||||
|
clear_auth_rules() {
|
||||||
|
log_info "Clearing all auth rules..."
|
||||||
|
|
||||||
|
# Create the admin command
|
||||||
|
COMMAND="[\"system_command\", \"clear_all_auth_rules\"]"
|
||||||
|
|
||||||
|
# Encrypt the command using NIP-44
|
||||||
|
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--recipient-pubkey "$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
if [ -z "$ENCRYPTED_COMMAND" ]; then
|
||||||
|
log_error "Failed to encrypt admin command"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin event
|
||||||
|
ADMIN_EVENT=$(nak event \
|
||||||
|
--kind 23456 \
|
||||||
|
--content "$ENCRYPTED_COMMAND" \
|
||||||
|
--sec "$ADMIN_PRIVKEY" \
|
||||||
|
--tag "p=$RELAY_PUBKEY")
|
||||||
|
|
||||||
|
# Post admin event
|
||||||
|
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
|
||||||
|
log_error "Failed to send admin command: $ADMIN_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Admin command sent successfully - all auth rules cleared"
|
||||||
|
# Wait for the relay to process the admin command
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 2: Try to post after blacklisting
|
||||||
|
test_blacklist_post() {
|
||||||
|
log_info "=== TEST 2: Attempt to post event after blacklisting ==="
|
||||||
|
|
||||||
|
# Create a new test event
|
||||||
|
local timestamp=$(date +%s)
|
||||||
|
local content="Blacklisted test event at timestamp $timestamp"
|
||||||
|
|
||||||
|
log_info "Creating new test event for blacklisted user..."
|
||||||
|
|
||||||
|
NEW_EVENT_JSON=$(nak event \
|
||||||
|
--kind 1 \
|
||||||
|
--content "$content" \
|
||||||
|
--sec "$TEST_PRIVKEY" \
|
||||||
|
--tag 't=blacklist-test')
|
||||||
|
|
||||||
|
NEW_EVENT_ID=$(echo "$NEW_EVENT_JSON" | jq -r '.id')
|
||||||
|
|
||||||
|
# Try to post the event
|
||||||
|
log_info "Attempting to post event with blacklisted user..."
|
||||||
|
POST_RESULT=$(echo "$NEW_EVENT_JSON" | nak event "$RELAY_URL" 2>&1)
|
||||||
|
|
||||||
|
# Check if posting failed (should fail for blacklisted user)
|
||||||
|
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
|
||||||
|
log_success "Event posting correctly blocked for blacklisted user"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Event posting was not blocked - blacklist may not be working"
|
||||||
|
log_error "Post result: $POST_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 3: Test whitelist functionality
|
||||||
|
test_whitelist_functionality() {
|
||||||
|
log_info "=== TEST 3: Test whitelist functionality ==="
|
||||||
|
|
||||||
|
# Generate a second test keypair for whitelist testing
|
||||||
|
log_info "Generating second test keypair for whitelist testing..."
|
||||||
|
WHITELIST_PRIVKEY=$(nak key generate 2>/dev/null)
|
||||||
|
WHITELIST_PUBKEY=$(nak key public "$WHITELIST_PRIVKEY" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$WHITELIST_PUBKEY" ]; then
|
||||||
|
log_error "Failed to generate whitelist test keypair"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Generated whitelist test keypair: ${WHITELIST_PUBKEY:0:16}..."
|
||||||
|
|
||||||
|
# Clear all auth rules first
|
||||||
|
if ! clear_auth_rules; then
|
||||||
|
log_error "Failed to clear auth rules for whitelist test"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add the whitelist user to whitelist
|
||||||
|
if ! add_to_whitelist "$WHITELIST_PUBKEY"; then
|
||||||
|
log_error "Failed to add whitelist user"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3a: Original test user should be blocked (not whitelisted)
|
||||||
|
log_info "Testing that non-whitelisted user is blocked..."
|
||||||
|
local timestamp=$(date +%s)
|
||||||
|
local content="Non-whitelisted test event at timestamp $timestamp"
|
||||||
|
|
||||||
|
NON_WHITELIST_EVENT=$(nak event \
|
||||||
|
--kind 1 \
|
||||||
|
--content "$content" \
|
||||||
|
--sec "$TEST_PRIVKEY" \
|
||||||
|
--tag 't=whitelist-test')
|
||||||
|
|
||||||
|
POST_RESULT=$(echo "$NON_WHITELIST_EVENT" | nak event "$RELAY_URL" 2>&1)
|
||||||
|
|
||||||
|
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
|
||||||
|
log_success "Non-whitelisted user correctly blocked"
|
||||||
|
else
|
||||||
|
log_error "Non-whitelisted user was not blocked - whitelist may not be working"
|
||||||
|
log_error "Post result: $POST_RESULT"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3b: Whitelisted user should be allowed
|
||||||
|
log_info "Testing that whitelisted user can post..."
|
||||||
|
content="Whitelisted test event at timestamp $timestamp"
|
||||||
|
|
||||||
|
WHITELIST_EVENT=$(nak event \
|
||||||
|
--kind 1 \
|
||||||
|
--content "$content" \
|
||||||
|
--sec "$WHITELIST_PRIVKEY" \
|
||||||
|
--tag 't=whitelist-test')
|
||||||
|
|
||||||
|
POST_RESULT=$(echo "$WHITELIST_EVENT" | nak event "$RELAY_URL" 2>&1)
|
||||||
|
|
||||||
|
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
|
||||||
|
log_error "Whitelisted user was blocked - whitelist not working correctly"
|
||||||
|
log_error "Post result: $POST_RESULT"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
log_success "Whitelisted user can post successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the whitelisted event can be retrieved
|
||||||
|
WHITELIST_EVENT_ID=$(echo "$WHITELIST_EVENT" | jq -r '.id')
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
RETRIEVE_RESULT=$(nak req \
|
||||||
|
--id "$WHITELIST_EVENT_ID" \
|
||||||
|
"$RELAY_URL")
|
||||||
|
|
||||||
|
if echo "$RETRIEVE_RESULT" | grep -q "$WHITELIST_EVENT_ID"; then
|
||||||
|
log_success "Whitelisted event successfully retrieved"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Failed to retrieve whitelisted event"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main test function
|
||||||
|
main() {
|
||||||
|
log_info "Starting C-Relay Whitelist/Blacklist Test"
|
||||||
|
log_info "=========================================="
|
||||||
|
|
||||||
|
# Check prerequisites
|
||||||
|
check_nak
|
||||||
|
|
||||||
|
# Generate test keypair
|
||||||
|
generate_test_keypair
|
||||||
|
|
||||||
|
# Create test event
|
||||||
|
create_test_event
|
||||||
|
|
||||||
|
# Test 1: Post and retrieve
|
||||||
|
if test_post_and_retrieve; then
|
||||||
|
log_success "TEST 1 PASSED: Event posting and retrieval works"
|
||||||
|
else
|
||||||
|
log_error "TEST 1 FAILED: Event posting/retrieval failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add user to blacklist
|
||||||
|
if add_to_blacklist; then
|
||||||
|
log_success "Blacklist command sent successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to send blacklist command"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Try posting after blacklist
|
||||||
|
if test_blacklist_post; then
|
||||||
|
log_success "TEST 2 PASSED: Blacklist functionality works correctly"
|
||||||
|
else
|
||||||
|
log_error "TEST 2 FAILED: Blacklist functionality not working"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Test whitelist functionality
|
||||||
|
if test_whitelist_functionality; then
|
||||||
|
log_success "TEST 3 PASSED: Whitelist functionality works correctly"
|
||||||
|
else
|
||||||
|
log_error "TEST 3 FAILED: Whitelist functionality not working"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "All tests passed! Whitelist/blacklist functionality is working correctly."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user