Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f1fbee52c |
2
Makefile
2
Makefile
@@ -8,7 +8,7 @@ BUILDDIR = build
|
|||||||
TARGET = $(BUILDDIR)/ginxsom-fcgi
|
TARGET = $(BUILDDIR)/ginxsom-fcgi
|
||||||
|
|
||||||
# Source files
|
# 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)
|
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
|
||||||
|
|
||||||
# Add core_relay_pool.c from nostr_core_lib
|
# Add core_relay_pool.c from nostr_core_lib
|
||||||
|
|||||||
126
README.md
126
README.md
@@ -369,6 +369,132 @@ Error responses include specific error codes:
|
|||||||
- `no_blob_hashes`: Missing valid SHA-256 hashes
|
- `no_blob_hashes`: Missing valid SHA-256 hashes
|
||||||
- `unsupported_media_type`: Non-JSON Content-Type
|
- `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
|
## File Storage
|
||||||
|
|
||||||
### Current (Flat) Structure
|
### Current (Flat) Structure
|
||||||
|
|||||||
BIN
build/admin_commands.o
Normal file
BIN
build/admin_commands.o
Normal file
Binary file not shown.
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
Binary file not shown.
Binary file not shown.
535
docs/ADMIN_COMMANDS_PLAN.md
Normal file
535
docs/ADMIN_COMMANDS_PLAN.md
Normal 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
316
src/admin_commands.c
Normal 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
56
src/admin_commands.h
Normal 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 */
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 1
|
#define VERSION_MINOR 1
|
||||||
#define VERSION_PATCH 12
|
#define VERSION_PATCH 13
|
||||||
#define VERSION "v0.1.12"
|
#define VERSION "v0.1.13"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|||||||
11
src/main.c
11
src/main.c
@@ -6,6 +6,7 @@
|
|||||||
#define _GNU_SOURCE
|
#define _GNU_SOURCE
|
||||||
#include "ginxsom.h"
|
#include "ginxsom.h"
|
||||||
#include "relay_client.h"
|
#include "relay_client.h"
|
||||||
|
#include "admin_commands.h"
|
||||||
#include "../nostr_core_lib/nostr_core/nostr_common.h"
|
#include "../nostr_core_lib/nostr_core/nostr_common.h"
|
||||||
#include "../nostr_core_lib/nostr_core/utils.h"
|
#include "../nostr_core_lib/nostr_core/utils.h"
|
||||||
#include <getopt.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
|
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "relay_client.h"
|
#include "relay_client.h"
|
||||||
|
#include "admin_commands.h"
|
||||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
#include <stdio.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) {
|
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) {
|
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -539,7 +540,7 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
|
|||||||
return -1;
|
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
|
// TODO: Encrypt response_content using NIP-44
|
||||||
// For now, use plaintext (stub implementation)
|
// 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and sign Kind 23457 event
|
// Create and sign Kind 23459 event
|
||||||
cJSON* event = nostr_create_and_sign_event(
|
cJSON* event = nostr_create_and_sign_event(
|
||||||
23457, // kind
|
23459, // kind
|
||||||
encrypted_content, // content
|
encrypted_content, // content
|
||||||
tags, // tags
|
tags, // tags
|
||||||
privkey_bytes, // private key
|
privkey_bytes, // private key
|
||||||
@@ -572,7 +573,7 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
|
|||||||
cJSON_Delete(tags);
|
cJSON_Delete(tags);
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
app_log(LOG_ERROR, "Failed to create Kind 23457 event");
|
app_log(LOG_ERROR, "Failed to create Kind 23459 event");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,16 +584,16 @@ int relay_client_send_admin_response(const char *recipient_pubkey, const char *r
|
|||||||
g_relay_state.relay_count,
|
g_relay_state.relay_count,
|
||||||
event,
|
event,
|
||||||
on_publish_response,
|
on_publish_response,
|
||||||
(void*)"Kind 23457" // user_data to identify event type
|
(void*)"Kind 23459" // user_data to identify event type
|
||||||
);
|
);
|
||||||
|
|
||||||
cJSON_Delete(event);
|
cJSON_Delete(event);
|
||||||
|
|
||||||
if (result == 0) {
|
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;
|
return 0;
|
||||||
} else {
|
} 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;
|
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) {
|
static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||||
(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
|
// Extract event fields
|
||||||
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
|
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* encrypted_content = cJSON_GetStringValue(content_json);
|
||||||
const char* event_id = cJSON_GetStringValue(id_json);
|
const char* event_id = cJSON_GetStringValue(id_json);
|
||||||
|
|
||||||
if (kind != 23456) {
|
if (kind != 23458) {
|
||||||
app_log(LOG_WARN, "Unexpected event kind: %d", kind);
|
app_log(LOG_WARN, "Unexpected event kind: %d", kind);
|
||||||
return;
|
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);
|
app_log(LOG_INFO, "Processing admin command (event ID: %s)", event_id);
|
||||||
|
|
||||||
// TODO: Decrypt content using NIP-44
|
// Convert keys from hex to bytes
|
||||||
// For now, log the encrypted content
|
unsigned char server_privkey[32];
|
||||||
app_log(LOG_DEBUG, "Encrypted command content: %s", encrypted_content);
|
unsigned char admin_pubkey_bytes[32];
|
||||||
|
|
||||||
// TODO: Parse and execute command
|
if (nostr_hex_to_bytes(g_blossom_seckey, server_privkey, 32) != 0) {
|
||||||
// TODO: Send response using relay_client_send_admin_response()
|
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
|
// 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");
|
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) {
|
static int subscribe_to_admin_commands(void) {
|
||||||
if (!g_relay_state.pool) {
|
if (!g_relay_state.pool) {
|
||||||
return -1;
|
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* filter = cJSON_CreateObject();
|
||||||
cJSON* kinds = cJSON_CreateArray();
|
cJSON* kinds = cJSON_CreateArray();
|
||||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23456));
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23458));
|
||||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||||
|
|
||||||
cJSON* p_tags = cJSON_CreateArray();
|
cJSON* p_tags = cJSON_CreateArray();
|
||||||
|
|||||||
199
tests/23458_test.sh
Executable file
199
tests/23458_test.sh
Executable 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."
|
||||||
Reference in New Issue
Block a user