1 Commits

15 changed files with 3585 additions and 23 deletions

View File

@@ -8,7 +8,7 @@ BUILDDIR = build
TARGET = $(BUILDDIR)/ginxsom-fcgi
# Source files
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c $(SRCDIR)/relay_client.c
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c $(SRCDIR)/relay_client.c $(SRCDIR)/admin_commands.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
# Add core_relay_pool.c from nostr_core_lib

126
README.md
View File

@@ -369,6 +369,132 @@ Error responses include specific error codes:
- `no_blob_hashes`: Missing valid SHA-256 hashes
- `unsupported_media_type`: Non-JSON Content-Type
## Administrator API
Ginxsom uses an **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key. All admin commands use **NIP-44 encrypted command arrays** for security.
### Authentication
All admin commands require signing with the admin private key configured in the server. The admin public key is stored in the database and checked against incoming Kind 23458 events.
### Event Structure
**Admin Command Event (Kind 23458):**
```json
{
"id": "event_id",
"pubkey": "admin_public_key",
"created_at": 1234587890,
"kind": 23458,
"content": "NIP44_ENCRYPTED_COMMAND_ARRAY",
"tags": [
["p", "blossom_server_pubkey"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event (Kind 23459):**
```json
{
"id": "response_event_id",
"pubkey": "blossom_server_pubkey",
"created_at": 1234587890,
"kind": 23459,
"content": "NIP44_ENCRYPTED_RESPONSE_OBJECT",
"tags": [
["p", "admin_public_key"],
["e", "request_event_id"]
],
"sig": "response_event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON response object.
### Admin Commands
All commands are sent as NIP-44 encrypted JSON arrays in the event content:
| Command Type | Command Format | Description |
|--------------|----------------|-------------|
| **Configuration Management** |
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| `config_update` | `["config_update", [{"key": "max_file_size", "value": "209715200", ...}]]` | Update configuration parameters |
| **Statistics & Monitoring** |
| `stats_query` | `["stats_query"]` | Get comprehensive database and storage statistics |
| `system_status` | `["system_command", "system_status"]` | Get system status and health metrics |
| **Blossom Operations** |
| `blob_list` | `["blob_list", "all"]` or `["blob_list", "pubkey", "abc123..."]` | List blobs with filtering |
| `storage_stats` | `["storage_stats"]` | Get detailed storage statistics |
| `mirror_status` | `["mirror_status"]` | Get status of mirroring operations |
| `report_query` | `["report_query", "all"]` | Query content reports (BUD-09) |
| **Database Queries** |
| `sql_query` | `["sql_query", "SELECT * FROM blobs LIMIT 10"]` | Execute read-only SQL query |
### Configuration Categories
**Blossom Settings:**
- `max_file_size`: Maximum upload size in bytes
- `storage_path`: Blob storage directory path
- `cdn_origin`: CDN URL for blob descriptors
- `enable_nip94`: Include NIP-94 tags in responses
**Relay Client Settings:**
- `enable_relay_connect`: Enable relay client functionality
- `kind_0_content`: Profile metadata JSON
- `kind_10002_tags`: Relay list JSON array
**Authentication Settings:**
- `auth_enabled`: Enable auth rules system
- `require_auth_upload`: Require authentication for uploads
- `require_auth_delete`: Require authentication for deletes
**Limits:**
- `max_blobs_per_user`: Per-user blob limit
- `rate_limit_uploads`: Uploads per minute
- `max_total_storage`: Total storage limit in bytes
### Response Format
All admin commands return signed EVENT responses via the relay connection. Responses use NIP-44 encrypted JSON content with structured data.
**Success Response Example:**
```json
{
"query_type": "stats_query",
"timestamp": 1234587890,
"database_size_bytes": 1048576,
"storage_size_bytes": 10737418240,
"total_blobs": 1543,
"blob_types": [
{"type": "image/jpeg", "count": 856, "size_bytes": 5368709120}
]
}
```
**Error Response Example:**
```json
{
"query_type": "config_update",
"status": "error",
"error": "invalid configuration value",
"timestamp": 1234587890
}
```
### Security Features
- **Cryptographic Authentication**: Only admin pubkey can send commands
- **NIP-44 Encryption**: All commands and responses are encrypted
- **Command Logging**: All admin actions logged to database
- **SQL Safety**: Only SELECT statements allowed with timeout and row limits
- **Rate Limiting**: Prevents admin command flooding
For detailed command specifications and examples, see [`docs/ADMIN_COMMANDS_PLAN.md`](docs/ADMIN_COMMANDS_PLAN.md).
## File Storage
### Current (Flat) Structure

BIN
build/admin_commands.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2232
debug.log

File diff suppressed because it is too large Load Diff

535
docs/ADMIN_COMMANDS_PLAN.md Normal file
View File

@@ -0,0 +1,535 @@
# Ginxsom Admin Commands Implementation Plan
## Overview
This document outlines the implementation plan for Ginxsom's admin command system, adapted from c-relay's event-based administration system. Commands are sent as NIP-44 encrypted Kind 23456 events and responses are returned as Kind 23457 events.
## Command Analysis: c-relay vs Ginxsom
### Commands to Implement (Blossom-Relevant)
| c-relay Command | Ginxsom Equivalent | Rationale |
|-----------------|-------------------|-----------|
| `config_query` | `config_query` | Query Blossom server configuration |
| `config_update` | `config_update` | Update server settings dynamically |
| `stats_query` | `stats_query` | Database statistics (blobs, storage, etc.) |
| `system_status` | `system_status` | Server health and status |
| `sql_query` | `sql_query` | Direct database queries for debugging |
| N/A | `blob_list` | List blobs by pubkey or criteria |
| N/A | `storage_stats` | Storage usage and capacity info |
| N/A | `mirror_status` | Status of mirroring operations |
| N/A | `report_query` | Query content reports (BUD-09) |
### Commands to Exclude (Not Blossom-Relevant)
| c-relay Command | Reason for Exclusion |
|-----------------|---------------------|
| `auth_add_blacklist` | Blossom uses different auth model (per-blob, not per-pubkey) |
| `auth_add_whitelist` | Same as above |
| `auth_delete_rule` | Same as above |
| `auth_query_all` | Same as above |
| `system_clear_auth` | Same as above |
**Note**: Blossom's authentication is event-based per operation (upload/delete), not relay-level whitelist/blacklist. Auth rules in Ginxsom are configured via the `auth_rules` table but managed differently than c-relay.
## Event Structure
### Admin Command Event (Kind 23456)
```json
{
"id": "event_id",
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "NIP44_ENCRYPTED_COMMAND_ARRAY",
"tags": [
["p", "blossom_server_pubkey"]
],
"sig": "event_signature"
}
```
### Admin Response Event (Kind 23457)
```json
{
"id": "response_event_id",
"pubkey": "blossom_server_pubkey",
"created_at": 1234567890,
"kind": 23457,
"content": "NIP44_ENCRYPTED_RESPONSE_OBJECT",
"tags": [
["p", "admin_public_key"],
["e", "request_event_id"]
],
"sig": "response_event_signature"
}
```
## Command Specifications
### 1. Configuration Management
#### `config_query`
Query server configuration parameters.
**Command Format:**
```json
["config_query", "all"]
["config_query", "category", "blossom"]
["config_query", "key", "max_file_size"]
```
**Response:**
```json
{
"query_type": "config_all",
"total_results": 15,
"timestamp": 1234567890,
"data": [
{
"key": "max_file_size",
"value": "104857600",
"data_type": "integer",
"category": "blossom",
"description": "Maximum file size in bytes"
},
{
"key": "enable_relay_connect",
"value": "true",
"data_type": "boolean",
"category": "relay",
"description": "Enable relay client functionality"
}
]
}
```
**Configuration Categories:**
- `blossom`: Blossom protocol settings (max_file_size, storage_path, etc.)
- `relay`: Relay client settings (enable_relay_connect, kind_0_content, etc.)
- `auth`: Authentication settings (auth_enabled, nip42_required, etc.)
- `limits`: Rate limits and quotas
- `system`: System-level settings
#### `config_update`
Update configuration parameters dynamically.
**Command Format:**
```json
["config_update", [
{
"key": "max_file_size",
"value": "209715200",
"data_type": "integer",
"category": "blossom"
},
{
"key": "enable_relay_connect",
"value": "true",
"data_type": "boolean",
"category": "relay"
}
]]
```
**Response:**
```json
{
"query_type": "config_update",
"status": "success",
"total_results": 2,
"timestamp": 1234567890,
"data": [
{
"key": "max_file_size",
"value": "209715200",
"status": "updated",
"restart_required": false
},
{
"key": "enable_relay_connect",
"value": "true",
"status": "updated",
"restart_required": true
}
]
}
```
### 2. Statistics and Monitoring
#### `stats_query`
Get comprehensive database and storage statistics.
**Command Format:**
```json
["stats_query"]
```
**Response:**
```json
{
"query_type": "stats_query",
"timestamp": 1234567890,
"database_size_bytes": 1048576,
"storage_size_bytes": 10737418240,
"total_blobs": 1543,
"unique_uploaders": 234,
"blob_types": [
{"type": "image/jpeg", "count": 856, "size_bytes": 5368709120, "percentage": 55.4},
{"type": "image/png", "count": 432, "size_bytes": 3221225472, "percentage": 28.0},
{"type": "video/mp4", "count": 123, "size_bytes": 2147483648, "percentage": 8.0}
],
"time_stats": {
"total": 1543,
"last_24h": 45,
"last_7d": 234,
"last_30d": 876
},
"top_uploaders": [
{"pubkey": "abc123...", "blob_count": 234, "total_bytes": 1073741824, "percentage": 15.2},
{"pubkey": "def456...", "blob_count": 187, "total_bytes": 858993459, "percentage": 12.1}
]
}
```
#### `system_status`
Get current system status and health metrics.
**Command Format:**
```json
["system_command", "system_status"]
```
**Response:**
```json
{
"query_type": "system_status",
"timestamp": 1234567890,
"uptime_seconds": 86400,
"version": "0.1.0",
"relay_client": {
"enabled": true,
"connected_relays": 1,
"relay_status": [
{
"url": "wss://relay.laantungir.net",
"state": "connected",
"events_received": 12,
"events_published": 3
}
]
},
"storage": {
"path": "/home/teknari/lt_gitea/ginxsom/blobs",
"total_bytes": 10737418240,
"available_bytes": 53687091200,
"usage_percentage": 16.7
},
"database": {
"path": "db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db",
"size_bytes": 1048576,
"total_blobs": 1543
}
}
```
### 3. Blossom-Specific Commands
#### `blob_list`
List blobs with filtering options.
**Command Format:**
```json
["blob_list", "all"]
["blob_list", "pubkey", "abc123..."]
["blob_list", "type", "image/jpeg"]
["blob_list", "recent", 50]
```
**Response:**
```json
{
"query_type": "blob_list",
"total_results": 50,
"timestamp": 1234567890,
"data": [
{
"sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
"size": 184292,
"type": "application/pdf",
"uploaded_at": 1725105921,
"uploader_pubkey": "abc123...",
"url": "https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"
}
]
}
```
#### `storage_stats`
Get detailed storage statistics.
**Command Format:**
```json
["storage_stats"]
```
**Response:**
```json
{
"query_type": "storage_stats",
"timestamp": 1234567890,
"storage_path": "/home/teknari/lt_gitea/ginxsom/blobs",
"total_bytes": 10737418240,
"available_bytes": 53687091200,
"used_bytes": 10737418240,
"usage_percentage": 16.7,
"blob_count": 1543,
"average_blob_size": 6958592,
"largest_blob": {
"sha256": "abc123...",
"size": 104857600,
"type": "video/mp4"
},
"by_type": [
{"type": "image/jpeg", "count": 856, "total_bytes": 5368709120},
{"type": "image/png", "count": 432, "total_bytes": 3221225472}
]
}
```
#### `mirror_status`
Get status of blob mirroring operations (BUD-04).
**Command Format:**
```json
["mirror_status"]
["mirror_status", "sha256", "abc123..."]
```
**Response:**
```json
{
"query_type": "mirror_status",
"timestamp": 1234567890,
"total_mirrors": 23,
"data": [
{
"sha256": "abc123...",
"source_url": "https://cdn.example.com/abc123.jpg",
"status": "completed",
"mirrored_at": 1725105921,
"size": 1048576
}
]
}
```
#### `report_query`
Query content reports (BUD-09).
**Command Format:**
```json
["report_query", "all"]
["report_query", "blob", "abc123..."]
["report_query", "type", "nudity"]
```
**Response:**
```json
{
"query_type": "report_query",
"total_results": 12,
"timestamp": 1234567890,
"data": [
{
"report_id": 1,
"blob_sha256": "abc123...",
"report_type": "nudity",
"reporter_pubkey": "def456...",
"content": "Inappropriate content",
"reported_at": 1725105921
}
]
}
```
### 4. Database Queries
#### `sql_query`
Execute read-only SQL queries for debugging.
**Command Format:**
```json
["sql_query", "SELECT * FROM blobs LIMIT 10"]
```
**Response:**
```json
{
"query_type": "sql_query",
"request_id": "request_event_id",
"timestamp": 1234567890,
"query": "SELECT * FROM blobs LIMIT 10",
"execution_time_ms": 12,
"row_count": 10,
"columns": ["sha256", "size", "type", "uploaded_at", "uploader_pubkey"],
"rows": [
["b1674191...", 184292, "application/pdf", 1725105921, "abc123..."]
]
}
```
**Security:**
- Only SELECT statements allowed
- Query timeout: 5 seconds
- Result row limit: 1000 rows
- All queries logged
## Implementation Architecture
### 1. Command Processing Flow
```
1. Relay client receives Kind 23456 event
2. Verify sender is admin_pubkey
3. Decrypt content using NIP-44
4. Parse command array
5. Validate command structure
6. Execute command handler
7. Generate response object
8. Encrypt response using NIP-44
9. Create Kind 23457 event
10. Publish to relays
```
### 2. Code Structure
**New Files:**
- `src/admin_commands.c` - Command handlers
- `src/admin_commands.h` - Command interface
- `src/nip44.c` - NIP-44 encryption wrapper (uses nostr_core_lib)
- `src/nip44.h` - NIP-44 interface
**Modified Files:**
- `src/relay_client.c` - Add command processing to `on_admin_command_event()`
- `src/main.c` - Initialize admin command system
### 3. Database Schema Additions
```sql
-- Admin command log
CREATE TABLE IF NOT EXISTS admin_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL,
command_type TEXT NOT NULL,
admin_pubkey TEXT NOT NULL,
executed_at INTEGER NOT NULL,
execution_time_ms INTEGER,
status TEXT NOT NULL,
error TEXT
);
-- Create index for command history queries
CREATE INDEX IF NOT EXISTS idx_admin_commands_executed
ON admin_commands(executed_at DESC);
```
### 4. Configuration Keys
**Blossom Category:**
- `max_file_size` - Maximum upload size in bytes
- `storage_path` - Blob storage directory
- `cdn_origin` - CDN URL for blob descriptors
- `enable_nip94` - Include NIP-94 tags in responses
**Relay Category:**
- `enable_relay_connect` - Enable relay client
- `kind_0_content` - Profile metadata JSON
- `kind_10002_tags` - Relay list JSON array
**Auth Category:**
- `auth_enabled` - Enable auth rules system
- `require_auth_upload` - Require auth for uploads
- `require_auth_delete` - Require auth for deletes
**Limits Category:**
- `max_blobs_per_user` - Per-user blob limit
- `rate_limit_uploads` - Uploads per minute
- `max_total_storage` - Total storage limit in bytes
## Implementation Phases
### Phase 1: NIP-44 Encryption Support
- Integrate nostr_core_lib NIP-44 functions
- Create encryption/decryption wrappers
- Test with sample data
### Phase 2: Command Infrastructure
- Create admin_commands.c/h
- Implement command parser
- Add command logging to database
- Implement response builder
### Phase 3: Core Commands
- Implement `config_query`
- Implement `config_update`
- Implement `stats_query`
- Implement `system_status`
### Phase 4: Blossom Commands
- Implement `blob_list`
- Implement `storage_stats`
- Implement `mirror_status`
- Implement `report_query`
### Phase 5: Advanced Features
- Implement `sql_query` with security
- Add command history tracking
- Implement rate limiting for admin commands
### Phase 6: Testing & Documentation
- Create test suite for each command
- Update README.md with admin API section
- Create example scripts using nak tool
## Security Considerations
1. **Authentication**: Only admin_pubkey can send commands
2. **Encryption**: All commands/responses use NIP-44
3. **Logging**: All admin actions logged to database
4. **Rate Limiting**: Prevent admin command flooding
5. **SQL Safety**: Only SELECT allowed, with timeout and row limits
6. **Input Validation**: Strict validation of all command parameters
## Testing Strategy
1. **Unit Tests**: Test each command handler independently
2. **Integration Tests**: Test full command flow with encryption
3. **Security Tests**: Verify auth checks and SQL injection prevention
4. **Performance Tests**: Ensure commands don't block relay operations
5. **Manual Tests**: Use nak tool to send real encrypted commands
## Documentation Updates
Add new section to README.md after "Content Reporting (BUD-09)":
```markdown
## Administrator API
Ginxsom uses an event-based administration system where commands are sent as
NIP-44 encrypted Kind 23456 events and responses are returned as Kind 23457
events. This provides secure, cryptographically authenticated remote management.
[Full admin API documentation here]

316
src/admin_commands.c Normal file
View File

@@ -0,0 +1,316 @@
/*
* Ginxsom Admin Commands Implementation
*/
#include "admin_commands.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declare app_log
typedef enum {
LOG_DEBUG = 0,
LOG_INFO = 1,
LOG_WARN = 2,
LOG_ERROR = 3
} log_level_t;
void app_log(log_level_t level, const char* format, ...);
// Global state
static struct {
int initialized;
char db_path[512];
} g_admin_state = {0};
// Initialize admin command system
int admin_commands_init(const char *db_path) {
if (g_admin_state.initialized) {
return 0;
}
strncpy(g_admin_state.db_path, db_path, sizeof(g_admin_state.db_path) - 1);
g_admin_state.initialized = 1;
app_log(LOG_INFO, "Admin command system initialized");
return 0;
}
// NIP-44 encryption helper
int admin_encrypt_response(
const unsigned char* server_privkey,
const unsigned char* admin_pubkey,
const char* plaintext_json,
char* output,
size_t output_size
) {
int result = nostr_nip44_encrypt(
server_privkey,
admin_pubkey,
plaintext_json,
output,
output_size
);
if (result != 0) {
app_log(LOG_ERROR, "Failed to encrypt admin response: %d", result);
return -1;
}
return 0;
}
// NIP-44 decryption helper
int admin_decrypt_command(
const unsigned char* server_privkey,
const unsigned char* admin_pubkey,
const char* encrypted_data,
char* output,
size_t output_size
) {
int result = nostr_nip44_decrypt(
server_privkey,
admin_pubkey,
encrypted_data,
output,
output_size
);
if (result != 0) {
app_log(LOG_ERROR, "Failed to decrypt admin command: %d", result);
return -1;
}
return 0;
}
// Create error response
static cJSON* create_error_response(const char* query_type, const char* error_msg) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", query_type);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
// Process admin command array and generate response
cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id) {
(void)request_event_id; // Reserved for future use (e.g., logging, tracking)
if (!cJSON_IsArray(command_array) || cJSON_GetArraySize(command_array) < 1) {
return create_error_response("unknown", "Invalid command format");
}
cJSON* cmd_type = cJSON_GetArrayItem(command_array, 0);
if (!cJSON_IsString(cmd_type)) {
return create_error_response("unknown", "Command type must be string");
}
const char* command = cmd_type->valuestring;
app_log(LOG_INFO, "Processing admin command: %s", command);
// Route to appropriate handler
if (strcmp(command, "config_query") == 0) {
return admin_cmd_config_query(command_array);
}
else if (strcmp(command, "config_update") == 0) {
return admin_cmd_config_update(command_array);
}
else if (strcmp(command, "stats_query") == 0) {
return admin_cmd_stats_query(command_array);
}
else if (strcmp(command, "system_command") == 0) {
// Check second parameter for system_status
if (cJSON_GetArraySize(command_array) >= 2) {
cJSON* subcmd = cJSON_GetArrayItem(command_array, 1);
if (cJSON_IsString(subcmd) && strcmp(subcmd->valuestring, "system_status") == 0) {
return admin_cmd_system_status(command_array);
}
}
return create_error_response("system_command", "Unknown system command");
}
else if (strcmp(command, "blob_list") == 0) {
return admin_cmd_blob_list(command_array);
}
else if (strcmp(command, "storage_stats") == 0) {
return admin_cmd_storage_stats(command_array);
}
else if (strcmp(command, "sql_query") == 0) {
return admin_cmd_sql_query(command_array);
}
else {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Unknown command: %s", command);
return create_error_response("unknown", error_msg);
}
}
// ============================================================================
// COMMAND HANDLERS (Stub implementations - to be completed)
// ============================================================================
cJSON* admin_cmd_config_query(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "config_query");
// Open database
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to open database");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
// Check if specific keys were requested (args[1] should be array of keys or null for all)
cJSON* keys_array = NULL;
if (cJSON_GetArraySize(args) >= 2) {
keys_array = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsArray(keys_array) && !cJSON_IsNull(keys_array)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Keys parameter must be array or null");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
}
sqlite3_stmt* stmt;
const char* sql;
if (keys_array && cJSON_IsArray(keys_array) && cJSON_GetArraySize(keys_array) > 0) {
// Query specific keys
int key_count = cJSON_GetArraySize(keys_array);
// Build SQL with placeholders
char sql_buffer[1024] = "SELECT key, value, description FROM config WHERE key IN (?";
for (int i = 1; i < key_count && i < 50; i++) { // Limit to 50 keys
strncat(sql_buffer, ",?", sizeof(sql_buffer) - strlen(sql_buffer) - 1);
}
strncat(sql_buffer, ")", sizeof(sql_buffer) - strlen(sql_buffer) - 1);
rc = sqlite3_prepare_v2(db, sql_buffer, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
// Bind keys
for (int i = 0; i < key_count && i < 50; i++) {
cJSON* key_item = cJSON_GetArrayItem(keys_array, i);
if (cJSON_IsString(key_item)) {
sqlite3_bind_text(stmt, i + 1, key_item->valuestring, -1, SQLITE_STATIC);
}
}
} else {
// Query all config values
sql = "SELECT key, value, description FROM config ORDER BY key";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
}
// Execute query and build result
cJSON* config_obj = cJSON_CreateObject();
int count = 0;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* value = (const char*)sqlite3_column_text(stmt, 1);
const char* description = (const char*)sqlite3_column_text(stmt, 2);
cJSON* entry = cJSON_CreateObject();
cJSON_AddStringToObject(entry, "value", value ? value : "");
if (description && strlen(description) > 0) {
cJSON_AddStringToObject(entry, "description", description);
}
cJSON_AddItemToObject(config_obj, key, entry);
count++;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "count", count);
cJSON_AddItemToObject(response, "config", config_obj);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "Config query returned %d entries", count);
return response;
}
cJSON* admin_cmd_config_update(cJSON* args) {
(void)args; // TODO: Parse args for config updates
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "config_update");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_stats_query(cJSON* args) {
(void)args; // TODO: Parse args for stats filtering
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "stats_query");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_system_status(cJSON* args) {
(void)args; // TODO: Parse args for status filtering
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "system_status");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_blob_list(cJSON* args) {
(void)args; // TODO: Parse args for blob filtering
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "blob_list");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_storage_stats(cJSON* args) {
(void)args; // TODO: Parse args for storage filtering
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "storage_stats");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_sql_query(cJSON* args) {
(void)args; // TODO: Parse and validate SQL query
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "sql_query");
cJSON_AddStringToObject(response, "status", "not_implemented");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}

56
src/admin_commands.h Normal file
View File

@@ -0,0 +1,56 @@
/*
* Ginxsom Admin Commands Interface
*
* Handles encrypted admin commands sent via Kind 23456 events
* and generates encrypted responses as Kind 23457 events.
*/
#ifndef ADMIN_COMMANDS_H
#define ADMIN_COMMANDS_H
#include <cjson/cJSON.h>
// Command handler result codes
typedef enum {
ADMIN_CMD_SUCCESS = 0,
ADMIN_CMD_ERROR_PARSE = -1,
ADMIN_CMD_ERROR_UNKNOWN = -2,
ADMIN_CMD_ERROR_INVALID = -3,
ADMIN_CMD_ERROR_DATABASE = -4,
ADMIN_CMD_ERROR_PERMISSION = -5
} admin_cmd_result_t;
// Initialize admin command system
int admin_commands_init(const char *db_path);
// Process an admin command and generate response
// Returns cJSON response object (caller must free with cJSON_Delete)
cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id);
// Individual command handlers
cJSON* admin_cmd_config_query(cJSON* args);
cJSON* admin_cmd_config_update(cJSON* args);
cJSON* admin_cmd_stats_query(cJSON* args);
cJSON* admin_cmd_system_status(cJSON* args);
cJSON* admin_cmd_blob_list(cJSON* args);
cJSON* admin_cmd_storage_stats(cJSON* args);
cJSON* admin_cmd_sql_query(cJSON* args);
// NIP-44 encryption/decryption helpers
int admin_encrypt_response(
const unsigned char* server_privkey,
const unsigned char* admin_pubkey,
const char* plaintext_json,
char* output,
size_t output_size
);
int admin_decrypt_command(
const unsigned char* server_privkey,
const unsigned char* admin_pubkey,
const char* encrypted_data,
char* output,
size_t output_size
);
#endif /* ADMIN_COMMANDS_H */

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 12
#define VERSION "v0.1.12"
#define VERSION_PATCH 13
#define VERSION "v0.1.13"
#include <stddef.h>
#include <stdint.h>

View File

@@ -6,6 +6,7 @@
#define _GNU_SOURCE
#include "ginxsom.h"
#include "relay_client.h"
#include "admin_commands.h"
#include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/utils.h"
#include <getopt.h>
@@ -2263,6 +2264,16 @@ if (!config_loaded /* && !initialize_server_config() */) {
}
}
// Initialize admin commands system
app_log(LOG_INFO, "Initializing admin commands system...");
int admin_cmd_result = admin_commands_init(g_db_path);
if (admin_cmd_result != 0) {
app_log(LOG_WARN, "Failed to initialize admin commands system (result: %d)", admin_cmd_result);
app_log(LOG_WARN, "Continuing without admin commands functionality");
} else {
app_log(LOG_INFO, "Admin commands system initialized successfully");
}
/////////////////////////////////////////////////////////////////////
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI
/////////////////////////////////////////////////////////////////////

View File

@@ -5,6 +5,7 @@
*/
#include "relay_client.h"
#include "admin_commands.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include <sqlite3.h>
#include <stdio.h>
@@ -529,7 +530,7 @@ int relay_client_publish_kind10002(void) {
}
}
// Send Kind 23457 admin response event
// Send Kind 23459 admin response event
int relay_client_send_admin_response(const char *recipient_pubkey, const char *response_content) {
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
return -1;
@@ -539,7 +540,7 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
return -1;
}
app_log(LOG_INFO, "Sending Kind 23457 admin response to %s", recipient_pubkey);
app_log(LOG_INFO, "Sending Kind 23459 admin response to %s", recipient_pubkey);
// TODO: Encrypt response_content using NIP-44
// For now, use plaintext (stub implementation)
@@ -560,9 +561,9 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
return -1;
}
// Create and sign Kind 23457 event
// Create and sign Kind 23459 event
cJSON* event = nostr_create_and_sign_event(
23457, // kind
23459, // kind
encrypted_content, // content
tags, // tags
privkey_bytes, // private key
@@ -572,7 +573,7 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
cJSON_Delete(tags);
if (!event) {
app_log(LOG_ERROR, "Failed to create Kind 23457 event");
app_log(LOG_ERROR, "Failed to create Kind 23459 event");
return -1;
}
@@ -583,16 +584,16 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
g_relay_state.relay_count,
event,
on_publish_response,
(void*)"Kind 23457" // user_data to identify event type
(void*)"Kind 23459" // user_data to identify event type
);
cJSON_Delete(event);
if (result == 0) {
app_log(LOG_INFO, "Kind 23457 admin response publish initiated");
app_log(LOG_INFO, "Kind 23459 admin response publish initiated");
return 0;
} else {
app_log(LOG_ERROR, "Failed to initiate Kind 23457 admin response publish");
app_log(LOG_ERROR, "Failed to initiate Kind 23459 admin response publish");
return -1;
}
}
@@ -610,11 +611,11 @@ static void on_publish_response(const char* relay_url, const char* event_id, int
}
}
// Callback for received Kind 23456 admin command events
// Callback for received Kind 23458 admin command events
static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
app_log(LOG_INFO, "Received Kind 23456 admin command from relay: %s", relay_url);
app_log(LOG_INFO, "Received Kind 23458 admin command from relay: %s", relay_url);
// Extract event fields
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
@@ -632,7 +633,7 @@ static void on_admin_command_event(cJSON* event, const char* relay_url, void* us
const char* encrypted_content = cJSON_GetStringValue(content_json);
const char* event_id = cJSON_GetStringValue(id_json);
if (kind != 23456) {
if (kind != 23458) {
app_log(LOG_WARN, "Unexpected event kind: %d", kind);
return;
}
@@ -645,12 +646,98 @@ static void on_admin_command_event(cJSON* event, const char* relay_url, void* us
app_log(LOG_INFO, "Processing admin command (event ID: %s)", event_id);
// TODO: Decrypt content using NIP-44
// For now, log the encrypted content
app_log(LOG_DEBUG, "Encrypted command content: %s", encrypted_content);
// Convert keys from hex to bytes
unsigned char server_privkey[32];
unsigned char admin_pubkey_bytes[32];
// TODO: Parse and execute command
// TODO: Send response using relay_client_send_admin_response()
if (nostr_hex_to_bytes(g_blossom_seckey, server_privkey, 32) != 0) {
app_log(LOG_ERROR, "Failed to convert server private key from hex");
return;
}
if (nostr_hex_to_bytes(sender_pubkey, admin_pubkey_bytes, 32) != 0) {
app_log(LOG_ERROR, "Failed to convert admin public key from hex");
return;
}
// Decrypt command content using NIP-44
char decrypted_command[4096];
if (admin_decrypt_command(server_privkey, admin_pubkey_bytes, encrypted_content,
decrypted_command, sizeof(decrypted_command)) != 0) {
app_log(LOG_ERROR, "Failed to decrypt admin command");
// Send error response
cJSON* error_response = cJSON_CreateObject();
cJSON_AddStringToObject(error_response, "status", "error");
cJSON_AddStringToObject(error_response, "message", "Failed to decrypt command");
char* error_json = cJSON_PrintUnformatted(error_response);
cJSON_Delete(error_response);
char encrypted_response[4096];
if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, error_json,
encrypted_response, sizeof(encrypted_response)) == 0) {
relay_client_send_admin_response(sender_pubkey, encrypted_response);
}
free(error_json);
return;
}
app_log(LOG_DEBUG, "Decrypted command: %s", decrypted_command);
// Parse command JSON
cJSON* command_json = cJSON_Parse(decrypted_command);
if (!command_json) {
app_log(LOG_ERROR, "Failed to parse command JSON");
cJSON* error_response = cJSON_CreateObject();
cJSON_AddStringToObject(error_response, "status", "error");
cJSON_AddStringToObject(error_response, "message", "Invalid JSON format");
char* error_json = cJSON_PrintUnformatted(error_response);
cJSON_Delete(error_response);
char encrypted_response[4096];
if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, error_json,
encrypted_response, sizeof(encrypted_response)) == 0) {
relay_client_send_admin_response(sender_pubkey, encrypted_response);
}
free(error_json);
return;
}
// Process command and get response
cJSON* response_json = admin_commands_process(command_json, event_id);
cJSON_Delete(command_json);
if (!response_json) {
app_log(LOG_ERROR, "Failed to process admin command");
response_json = cJSON_CreateObject();
cJSON_AddStringToObject(response_json, "status", "error");
cJSON_AddStringToObject(response_json, "message", "Failed to process command");
}
// Convert response to JSON string
char* response_str = cJSON_PrintUnformatted(response_json);
cJSON_Delete(response_json);
if (!response_str) {
app_log(LOG_ERROR, "Failed to serialize response JSON");
return;
}
// Encrypt and send response
char encrypted_response[4096];
if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, response_str,
encrypted_response, sizeof(encrypted_response)) != 0) {
app_log(LOG_ERROR, "Failed to encrypt admin response");
free(response_str);
return;
}
free(response_str);
if (relay_client_send_admin_response(sender_pubkey, encrypted_response) != 0) {
app_log(LOG_ERROR, "Failed to send admin response");
}
}
// Callback for EOSE (End Of Stored Events) - new signature
@@ -661,18 +748,18 @@ static void on_admin_subscription_eose(cJSON** events, int event_count, void* us
app_log(LOG_INFO, "Received EOSE for admin command subscription");
}
// Subscribe to admin commands (Kind 23456)
// Subscribe to admin commands (Kind 23458)
static int subscribe_to_admin_commands(void) {
if (!g_relay_state.pool) {
return -1;
}
app_log(LOG_INFO, "Subscribing to Kind 23456 admin commands...");
app_log(LOG_INFO, "Subscribing to Kind 23458 admin commands...");
// Create subscription filter for Kind 23456 events addressed to us
// Create subscription filter for Kind 23458 events addressed to us
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23456));
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23458));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* p_tags = cJSON_CreateArray();

199
tests/23458_test.sh Executable file
View File

@@ -0,0 +1,199 @@
#!/bin/bash
# Simple test for Kind 23458 relay-based admin commands
# Tests config_query command via Nostr relay subscription
set -e
# Configuration
TEST_KEYS_FILE=".test_keys"
RELAY_URL="wss://relay.laantungir.net"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Load test keys
if [[ ! -f "$TEST_KEYS_FILE" ]]; then
log_error "$TEST_KEYS_FILE not found"
exit 1
fi
source "$TEST_KEYS_FILE"
# Check dependencies
for cmd in nak jq websocat; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd is not installed"
exit 1
fi
done
echo "=== Kind 23458 Admin Command Test ==="
echo ""
log_info "Configuration:"
log_info " Admin Privkey: ${ADMIN_PRIVKEY:0:16}..."
log_info " Server Pubkey: $SERVER_PUBKEY"
log_info " Relay URL: $RELAY_URL"
echo ""
# Test 1: Send config_query command
log_info "Test: Sending config_query command"
echo ""
# Encrypt command with NIP-44
# Command format: ["config_query"]
PLAINTEXT_COMMAND='["config_query"]'
log_info "Encrypting command with NIP-44..."
ENCRYPTED_COMMAND=$(nak encrypt --sec "$ADMIN_PRIVKEY" -p "$SERVER_PUBKEY" "$PLAINTEXT_COMMAND")
if [[ -z "$ENCRYPTED_COMMAND" ]]; then
log_error "Failed to encrypt command"
exit 1
fi
log_success "Command encrypted"
log_info "Encrypted content: ${ENCRYPTED_COMMAND:0:50}..."
echo ""
log_info "Creating Kind 23458 event..."
EVENT=$(nak event -k 23458 \
-c "$ENCRYPTED_COMMAND" \
--tag p="$SERVER_PUBKEY" \
--sec "$ADMIN_PRIVKEY")
if [[ -z "$EVENT" ]]; then
log_error "Failed to create event"
exit 1
fi
log_success "Event created"
echo "$EVENT" | jq .
echo ""
# Step 1: Create pipes for bidirectional communication
log_info "Step 1: Setting up websocat connection..."
SINCE=$(date +%s)
# Create named pipes for input and output
INPUT_PIPE=$(mktemp -u)
OUTPUT_PIPE=$(mktemp -u)
mkfifo "$INPUT_PIPE"
mkfifo "$OUTPUT_PIPE"
# Start websocat in background with bidirectional communication
(websocat "$RELAY_URL" < "$INPUT_PIPE" > "$OUTPUT_PIPE" 2>/dev/null) &
WEBSOCAT_PID=$!
# Open pipes for writing and reading
exec 3>"$INPUT_PIPE" # File descriptor 3 for writing
exec 4<"$OUTPUT_PIPE" # File descriptor 4 for reading
# Give connection time to establish
sleep 1
log_success "WebSocket connection established"
echo ""
# Step 2: Subscribe to Kind 23459 responses
log_info "Step 2: Subscribing to Kind 23459 responses..."
# Create subscription filter
SUBSCRIPTION_FILTER='["REQ","admin-response",{"kinds":[23459],"authors":["'$SERVER_PUBKEY'"],"#p":["'$ADMIN_PUBKEY'"],"since":'$SINCE'}]'
# Send subscription
echo "$SUBSCRIPTION_FILTER" >&3
sleep 1
log_success "Subscription sent"
echo ""
# Step 3: Publish the command event
log_info "Step 3: Publishing Kind 23458 command event..."
# Create EVENT message
EVENT_MSG='["EVENT",'$EVENT']'
# Send event
echo "$EVENT_MSG" >&3
sleep 1
log_success "Event published"
echo ""
# Step 4: Wait for response
log_info "Step 4: Waiting for Kind 23459 response (timeout: 15s)..."
RESPONSE_RECEIVED=0
TIMEOUT=15
START_TIME=$(date +%s)
while [[ $(($(date +%s) - START_TIME)) -lt $TIMEOUT ]]; do
if read -t 1 -r line <&4; then
if [[ -n "$line" ]]; then
# Parse the relay message
MSG_TYPE=$(echo "$line" | jq -r '.[0] // empty' 2>/dev/null)
if [[ "$MSG_TYPE" == "EVENT" ]]; then
# Extract the event (third element in array)
EVENT_DATA=$(echo "$line" | jq '.[2]' 2>/dev/null)
if [[ -n "$EVENT_DATA" ]]; then
log_success "Received Kind 23459 response!"
echo "$EVENT_DATA" | jq .
echo ""
# Extract and decrypt content
ENCRYPTED_CONTENT=$(echo "$EVENT_DATA" | jq -r '.content // empty')
SENDER_PUBKEY=$(echo "$EVENT_DATA" | jq -r '.pubkey // empty')
if [[ -n "$ENCRYPTED_CONTENT" ]] && [[ -n "$SENDER_PUBKEY" ]]; then
log_info "Encrypted response: ${ENCRYPTED_CONTENT:0:50}..."
log_info "Sender pubkey: $SENDER_PUBKEY"
log_info "Decrypting response..."
# Try decryption with error output and timeout
DECRYPT_OUTPUT=$(timeout 5s nak decrypt --sec "$ADMIN_PRIVKEY" -p "$SENDER_PUBKEY" "$ENCRYPTED_CONTENT" 2>&1)
DECRYPT_EXIT=$?
if [[ $DECRYPT_EXIT -eq 0 ]] && [[ -n "$DECRYPT_OUTPUT" ]]; then
log_success "Response decrypted successfully:"
echo "$DECRYPT_OUTPUT" | jq . 2>/dev/null || echo "$DECRYPT_OUTPUT"
RESPONSE_RECEIVED=1
else
log_error "Failed to decrypt response (exit code: $DECRYPT_EXIT)"
if [[ -n "$DECRYPT_OUTPUT" ]]; then
log_error "Decryption error: $DECRYPT_OUTPUT"
fi
fi
fi
break
fi
fi
fi
fi
done
# Cleanup
exec 3>&- # Close write pipe
exec 4<&- # Close read pipe
kill $WEBSOCAT_PID 2>/dev/null
rm -f "$INPUT_PIPE" "$OUTPUT_PIPE"
if [[ $RESPONSE_RECEIVED -eq 0 ]]; then
log_error "No response received within timeout period"
log_info "This could mean:"
log_info " 1. The server didn't receive the command"
log_info " 2. The server received but didn't process the command"
log_info " 3. The response was sent but not received by subscription"
exit 1
fi
echo ""
log_success "Test complete!"
echo ""
log_info "This test uses full NIP-44 encryption for both commands and responses."