Compare commits

...

25 Commits

Author SHA1 Message Date
Laan Tungir
b1ea256076 v0.2.3 - Fix HEAD blob request 404: deployed binary had invalid sqlite3_open_v2 flags (READONLY|CREATE=5) causing SQLITE_MISUSE; rebuilt from current source (v0.2.2) and deployed. Also fixed Dockerfile to use local nostr_core_lib submodule instead of unavailable remote commit. 2026-07-15 20:06:52 -04:00
Laan Tungir
09654fa557 v0.2.2 - Fix ARM64 cross-compilation in Dockerfile 2026-04-23 07:25:06 -04:00
Laan Tungir
469d2180c1 v0.2.1 - Add arm64 cross-compilation and release support 2026-04-23 07:16:17 -04:00
Laan Tungir
fa5f5e60cb v0.2.0 - Cut v0.2.0 release 2026-04-22 16:22:05 -04:00
Laan Tungir
015094949e v0.1.38 - Update main.c and request_validator.c with latest changes 2026-04-22 16:15:20 -04:00
Laan Tungir
9491f5c373 v0.1.37 - Use static-only build flow in increment_and_push and include blob browser sorting/delete deployment fix 2026-04-01 20:29:28 -04:00
Your Name
485e351998 v0.1.33 - just cleaning up 2026-03-23 09:02:54 -04:00
Your Name
66ac92b4a0 v0.1.32 - Migrate to nostr_core_lib v0.4.13 and unify app/nostr logging via callback API 2026-03-15 08:37:03 -04:00
Your Name
ba8b51d1c6 v0.1.31 - Add MIME/extension + nginx fallback support for html/js/mjs/css blob uploads and retrieval 2026-03-11 18:21:53 -04:00
Your Name
b9e9a08e9e v0.1.30 - add public GET /health JSON endpoint with status version and uptime 2026-03-09 17:34:23 -04:00
Your Name
d400744f68 v0.1.29 - fix auth routing: allow public GET /health operation bypass 2026-03-09 17:29:44 -04:00
Your Name
4b12cb19dc v0.1.28 - Latest changes 2026-02-20 15:32:33 -04:00
Your Name
e5b1556a80 v0.1.27 - Fix nginx blob serving: correct try_files pattern and set proper directory permissions for www-data access 2026-02-17 08:26:42 -04:00
Your Name
1905e1586d v0.1.26 - Add local deployment system with deploy_local.sh, ginxsom-local.service, nginx SSL config, and fix GET blob retrieval 2026-02-16 09:23:23 -04:00
Your Name
640386e2c1 v0.1.25 - Fix 100% CPU bug in relay client - SSL_pending busy-wait 2026-02-03 14:18:25 -04:00
Your Name
840a5bbf5f v0.1.24 - . 2025-12-21 09:17:43 -04:00
Your Name
0f420fc6d0 v0.1.23 - Fixed auth rules on web page 2025-12-16 09:18:37 -04:00
Your Name
29e2421771 v0.1.22 - Cleaned out legacy websocket code in index.js 2025-12-16 07:50:16 -04:00
Your Name
cce1f2f0fd v0.1.21 - Fixed some web page errors. About to clean out websocket functions in index.js. Last push before we do this. 2025-12-16 07:17:19 -04:00
Your Name
281c686fde v0.1.20 - Fixed auth white and black lists 2025-12-16 06:54:26 -04:00
Your Name
a5880ebdf6 v0.1.19 - Lots of remote hosting fixes 2025-12-13 14:53:25 -04:00
Your Name
a5f92e4da3 v0.1.18 - Add automatic cleanup of dynamic build artifacts after static builds 2025-12-13 07:54:20 -04:00
Your Name
64b9f28444 v0.1.17 - Fixed more on stats page 2025-12-12 17:06:43 -04:00
Your Name
fe27b5e41a v0.1.16 - Got the admin page kinda working 2025-12-12 16:41:38 -04:00
Your Name
d0bf851e86 v0.1.15 - Fix config page display and rebrand admin UI from relay to Blossom terminology 2025-12-12 13:12:46 -04:00
78 changed files with 764181 additions and 7421 deletions

5
.gitignore vendored
View File

@@ -3,4 +3,7 @@ logs/
nostr_core_lib/
blobs/
c-relay/
text_graph/
.test_keys
ginxsom-local.service
*.tar.gz

5
.roo/commands/push.md Normal file
View File

@@ -0,0 +1,5 @@
---
description: "increment and push"
---
Run increment_and_push.sh adding a good comment on the command line following the command. Don't run any other git commands.

137
Dockerfile.alpine-musl Normal file
View File

@@ -0,0 +1,137 @@
# Alpine-based MUSL static binary builder for Ginxsom
# Produces truly portable binaries with zero runtime dependencies
ARG DEBUG_BUILD=false
FROM alpine:3.19 AS builder
# Re-declare build argument in this stage
ARG DEBUG_BUILD=false
# Install build dependencies
RUN apk add --no-cache \
build-base \
musl-dev \
git \
cmake \
pkgconfig \
autoconf \
automake \
libtool \
openssl-dev \
openssl-libs-static \
zlib-dev \
zlib-static \
curl-dev \
curl-static \
sqlite-dev \
sqlite-static \
fcgi-dev \
fcgi \
linux-headers \
wget \
bash \
openssh-client \
nghttp2-dev \
nghttp2-static \
c-ares-dev \
c-ares-static \
libidn2-dev \
libidn2-static \
libunistring-dev \
libunistring-static \
libpsl-dev \
libpsl-static \
brotli-dev \
brotli-static
# Set working directory
WORKDIR /build
# Build libsecp256k1 static (cached layer - only rebuilds if Alpine version changes)
RUN cd /tmp && \
git clone https://github.com/bitcoin-core/secp256k1.git && \
cd secp256k1 && \
./autogen.sh && \
./configure --enable-static --disable-shared --prefix=/usr \
CFLAGS="-fPIC" && \
make -j$(nproc) && \
make install && \
rm -rf /tmp/secp256k1
# Copy nostr_core_lib from local submodule (avoids remote git dependency)
COPY nostr_core_lib /build/nostr_core_lib
# Build nostr_core_lib with required NIPs (cached unless submodule changes)
# Disable fortification in build.sh to prevent __*_chk symbol issues
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 042(Auth), 044(Encryption), 059(Gift Wrap)
RUN cd nostr_core_lib && \
chmod +x build.sh && \
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
ln -sf /usr/bin/gcc /usr/local/bin/aarch64-linux-gnu-gcc; \
fi && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=1,6,13,17,19,42,44,59 && \
if [ -f libnostr_core_arm64.a ]; then \
cp libnostr_core_arm64.a libnostr_core.a; \
elif [ -f libnostr_core_x64.a ]; then \
cp libnostr_core_x64.a libnostr_core.a; \
else \
echo "ERROR: No supported nostr_core static library produced"; \
ls -la *.a 2>/dev/null || true; \
exit 1; \
fi
# Copy web interface files for embedding
# Note: Changes to api/ files will trigger rebuild from this point
COPY api/ /build/api/
COPY scripts/embed_web_files.sh /build/scripts/
# Copy Ginxsom source files
COPY src/ /build/src/
COPY include/ /build/include/
# Embed web files AFTER source copy so src/admin_interface_embedded.h is always fresh
RUN chmod +x scripts/embed_web_files.sh && \
./scripts/embed_web_files.sh
# Build Ginxsom with full static linking (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
# Use conditional compilation flags based on DEBUG_BUILD argument
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
CFLAGS="-g -O0 -DDEBUG"; \
STRIP_CMD=""; \
echo "Building with DEBUG symbols enabled"; \
else \
CFLAGS="-O2"; \
STRIP_CMD="strip /build/ginxsom-fcgi_static"; \
echo "Building optimized production binary"; \
fi && \
gcc -static $CFLAGS -Wall -Wextra -std=gnu99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Iinclude -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
src/main.c src/admin_api.c src/admin_auth.c src/admin_event.c \
src/admin_handlers.c src/admin_interface.c src/admin_commands.c \
src/bud04.c src/bud06.c src/bud08.c src/bud09.c \
src/request_validator.c src/relay_client.c \
nostr_core_lib/nostr_core/core_relay_pool.c \
-o /build/ginxsom-fcgi_static \
nostr_core_lib/libnostr_core.a \
-lfcgi -lsqlite3 -lsecp256k1 -lssl -lcrypto -lcurl \
-lnghttp2 -lcares -lidn2 -lunistring -lpsl -lbrotlidec -lbrotlicommon \
-lz -lpthread -lm -ldl && \
eval "$STRIP_CMD"
# Verify it's truly static
RUN echo "=== Binary Information ===" && \
file /build/ginxsom-fcgi_static && \
ls -lh /build/ginxsom-fcgi_static && \
echo "=== Checking for dynamic dependencies ===" && \
(ldd /build/ginxsom-fcgi_static 2>&1 || echo "Binary is static") && \
echo "=== Build complete ==="
# Output stage - just the binary
FROM scratch AS output
COPY --from=builder /build/ginxsom-fcgi_static /ginxsom-fcgi_static

View File

@@ -8,15 +8,24 @@ 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 $(SRCDIR)/admin_commands.c
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/admin_interface.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)
# Embedded web interface files
EMBEDDED_HEADER = $(SRCDIR)/admin_interface_embedded.h
EMBED_SCRIPT = scripts/embed_web_files.sh
# Add core_relay_pool.c from nostr_core_lib
POOL_SRC = nostr_core_lib/nostr_core/core_relay_pool.c
POOL_OBJ = $(BUILDDIR)/core_relay_pool.o
# Default target
all: $(TARGET)
all: $(EMBEDDED_HEADER) $(TARGET)
# Generate embedded web interface files
$(EMBEDDED_HEADER): $(EMBED_SCRIPT) api/*.html api/*.css api/*.js
@echo "Embedding web interface files..."
@$(EMBED_SCRIPT)
# Create build directory
$(BUILDDIR):
@@ -34,9 +43,18 @@ $(POOL_OBJ): $(POOL_SRC) | $(BUILDDIR)
$(TARGET): $(OBJECTS) $(POOL_OBJ)
$(CC) $(OBJECTS) $(POOL_OBJ) $(LIBS) -o $@
# Clean build files
# Clean build files (preserves static binaries)
clean:
rm -rf $(BUILDDIR)
rm -f $(EMBEDDED_HEADER)
@echo "Note: Static binaries (ginxsom-fcgi_static_*) are preserved."
@echo "To remove everything: make clean-all"
# Clean everything including static binaries
clean-all:
rm -rf $(BUILDDIR)
rm -f $(EMBEDDED_HEADER)
@echo "✓ All build artifacts removed"
# Install (copy to system location)
install: $(TARGET)
@@ -55,4 +73,16 @@ run: $(TARGET)
debug: CFLAGS += -g -DDEBUG
debug: $(TARGET)
.PHONY: all clean install uninstall run debug
# Rebuild embedded files
embed:
@$(EMBED_SCRIPT)
# Static MUSL build via Docker
static:
./build_static.sh
# Static MUSL build with debug symbols
static-debug:
./build_static.sh --debug
.PHONY: all clean clean-all install uninstall run debug embed static static-debug

View File

@@ -431,6 +431,13 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content:
| `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) |
| **Authorization Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
| **Database Queries** |
| `sql_query` | `["sql_query", "SELECT * FROM blobs LIMIT 10"]` | Execute read-only SQL query |
@@ -448,10 +455,16 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content:
- `kind_10002_tags`: Relay list JSON array
**Authentication Settings:**
- `auth_enabled`: Enable auth rules system
- `auth_rules_enabled`: Enable auth rules system
- `require_auth_upload`: Require authentication for uploads
- `require_auth_delete`: Require authentication for deletes
**Authorization Rules:**
- `rule_type`: Type of rule (`pubkey_blacklist`, `pubkey_whitelist`, `hash_blacklist`, `mime_blacklist`, `mime_whitelist`)
- `pattern_type`: Pattern matching type (`pubkey`, `hash`, `mime`)
- `pattern_value`: The actual value to match (64-char hex for pubkey/hash, MIME type string for mime)
- `active`: Whether rule is active (1) or disabled (0)
**Limits:**
- `max_blobs_per_user`: Per-user blob limit
- `rate_limit_uploads`: Uploads per minute

0
Trash/[builder Normal file
View File

View File

@@ -49,7 +49,22 @@ if [[ $FOLLOW_LOGS -eq 1 ]]; then
wait
exit 0
fi
FCGI_BINARY="./build/ginxsom-fcgi"
# Detect architecture for static binary name
ARCH=$(uname -m)
case "$ARCH" in
x86_64) STATIC_BINARY="./build/ginxsom-fcgi_static_x86_64" ;;
aarch64|arm64) STATIC_BINARY="./build/ginxsom-fcgi_static_arm64" ;;
*) STATIC_BINARY="./build/ginxsom-fcgi_static_${ARCH}" ;;
esac
# Use static binary if available, fallback to dynamic
if [ -f "$STATIC_BINARY" ]; then
FCGI_BINARY="$STATIC_BINARY"
echo "Using static binary: $FCGI_BINARY"
else
FCGI_BINARY="./build/ginxsom-fcgi"
echo "Static binary not found, using dynamic binary: $FCGI_BINARY"
fi
SOCKET_PATH="/tmp/ginxsom-fcgi.sock"
PID_FILE="/tmp/ginxsom-fcgi.pid"
NGINX_CONFIG="config/local-nginx.conf"
@@ -173,15 +188,28 @@ fi
echo -e "${GREEN}FastCGI cleanup complete${NC}"
# Step 3: Always rebuild FastCGI binary with clean build
echo -e "\n${YELLOW}3. Rebuilding FastCGI binary (clean build)...${NC}"
echo "Performing clean rebuild to ensure all changes are compiled..."
make clean && make
# Step 3: Always rebuild FastCGI binary with static build
echo -e "\n${YELLOW}3. Rebuilding FastCGI binary (static build)...${NC}"
echo "Cleaning old build artifacts to ensure fresh embedding..."
make clean
echo "Removing local embedded header to prevent Docker cache issues..."
rm -f src/admin_interface_embedded.h
echo "Building static binary with Docker..."
make static
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed! Cannot continue.${NC}"
echo -e "${RED}Static build failed! Cannot continue.${NC}"
echo -e "${RED}Docker must be available and running for static builds.${NC}"
exit 1
fi
echo -e "${GREEN}Clean rebuild complete${NC}"
# Update FCGI_BINARY to use the newly built static binary
ARCH=$(uname -m)
case "$ARCH" in
x86_64) FCGI_BINARY="./build/ginxsom-fcgi_static_x86_64" ;;
aarch64|arm64) FCGI_BINARY="./build/ginxsom-fcgi_static_arm64" ;;
*) FCGI_BINARY="./build/ginxsom-fcgi_static_${ARCH}" ;;
esac
echo -e "${GREEN}Static build complete: $FCGI_BINARY${NC}"
# Step 3.5: Clean database directory for fresh testing
echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}"

58
api/embedded.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-container {
/* No styling - let embedded modal blend seamlessly */
}
</style>
</head>
<body>
<div class="container">
<div id="login-container"></div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme:'default',
methods: {
extension: true,
local: true,
seedphrase: true,
readonly: true,
connect: true,
remote: true,
otp: true
}
});
window.NOSTR_LOGIN_LITE.embed('#login-container', {
seamless: true
});
});
</script>
</body>
</html>

1465
api/index.css Normal file

File diff suppressed because it is too large Load Diff

492
api/index.html Normal file
View File

@@ -0,0 +1,492 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blossom Admin</title>
<link rel="stylesheet" href="/api/index.css">
</head>
<body>
<!-- Side Navigation Menu -->
<nav class="side-nav" id="side-nav">
<ul class="nav-menu">
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
<li><button class="nav-item" data-page="blob-browser">Blob Browser</button></li>
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
<li><button class="nav-item" data-page="relay-events">Blossom Events</button></li>
<li><button class="nav-item" data-page="database">Database Query</button></li>
</ul>
<div class="nav-footer">
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
<button class="nav-footer-btn" id="nav-logout-btn">LOGOUT</button>
</div>
</nav>
<!-- Side Navigation Overlay -->
<div class="side-nav-overlay" id="side-nav-overlay"></div>
<!-- Header with title and profile display -->
<div class="section">
<div class="header-content">
<div class="header-title clickable" id="header-title">
<span class="relay-letter" data-letter="B">B</span>
<span class="relay-letter" data-letter="L">L</span>
<span class="relay-letter" data-letter="O">O</span>
<span class="relay-letter" data-letter="S">S</span>
<span class="relay-letter" data-letter="S">S</span>
<span class="relay-letter" data-letter="O">O</span>
<span class="relay-letter" data-letter="M">M</span>
</div>
<div class="relay-info">
<div id="relay-name" class="relay-name">Blossom</div>
<div id="relay-description" class="relay-description">Loading...</div>
<div id="relay-pubkey-container" class="relay-pubkey-container">
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
</div>
</div>
<div class="profile-area" id="profile-area" style="display: none;">
<div class="admin-label">admin</div>
<div class="profile-container">
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
<span id="header-user-name" class="header-user-name">Loading...</span>
</div>
<!-- Logout dropdown -->
<!-- Dropdown menu removed - buttons moved to sidebar -->
</div>
</div>
</div>
<!-- Login Modal Overlay -->
<div id="login-modal" class="login-modal-overlay" style="display: none;">
<div class="login-modal-content">
<div id="login-modal-container"></div>
</div>
</div>
<!-- DATABASE STATISTICS Section -->
<!-- Subscribe to kind 24567 events to receive real-time monitoring data -->
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
<div class="section-header">
DATABASE STATISTICS
</div>
<!-- Blob Rate Graph Container -->
<div id="event-rate-chart"></div>
<!-- Database Overview Table -->
<div class="input-group">
<div class="config-table-container">
<table class="config-table" id="stats-overview-table">
<thead>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
</thead>
<tbody id="stats-overview-table-body">
<tr>
<td>Database Size</td>
<td id="db-size">-</td>
</tr>
<tr>
<td>Total Blobs</td>
<td id="total-events">-</td>
</tr>
<tr>
<td>Total Size</td>
<td id="total-size">-</td>
</tr>
<tr>
<td>Version</td>
<td id="version">-</td>
</tr>
<tr>
<td>Process ID</td>
<td id="process-id">-</td>
</tr>
<tr>
<td>Memory Usage</td>
<td id="memory-usage">-</td>
</tr>
<tr>
<td>CPU Core</td>
<td id="cpu-core">-</td>
</tr>
<tr>
<td>CPU Usage</td>
<td id="cpu-usage">-</td>
</tr>
<tr>
<td>Filesystem Blob Count</td>
<td id="fs-blob-count">-</td>
</tr>
<tr>
<td>Filesystem Blob Size</td>
<td id="fs-blob-size">-</td>
</tr>
<tr>
<td>Oldest Blob</td>
<td id="oldest-event">-</td>
</tr>
<tr>
<td>Newest Blob</td>
<td id="newest-event">-</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Blob Type Distribution Table -->
<div class="input-group">
<label>Blob Type Distribution:</label>
<div class="config-table-container">
<table class="config-table" id="stats-kinds-table">
<thead>
<tr>
<th>Blob Type</th>
<th>Count</th>
<th>Percentage</th>
</tr>
</thead>
<tbody id="stats-kinds-table-body">
<tr>
<td colspan="3" style="text-align: center; font-style: italic;">No data loaded</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Time-based Statistics Table -->
<div class="input-group">
<label>Time-based Statistics:</label>
<div class="config-table-container">
<table class="config-table" id="stats-time-table">
<thead>
<tr>
<th>Period</th>
<th>Blobs</th>
</tr>
</thead>
<tbody id="stats-time-table-body">
<tr>
<td>Last 24 Hours</td>
<td id="events-24h">-</td>
</tr>
<tr>
<td>Last 7 Days</td>
<td id="events-7d">-</td>
</tr>
<tr>
<td>Last 30 Days</td>
<td id="events-30d">-</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Top Pubkeys Table -->
<div class="input-group">
<label>Top Pubkeys by Event Count:</label>
<div class="config-table-container">
<table class="config-table" id="stats-pubkeys-table">
<thead>
<tr>
<th>Rank</th>
<th>Pubkey</th>
<th>Blob Count</th>
<th>Total Size</th>
<th>Percentage</th>
</tr>
</thead>
<tbody id="stats-pubkeys-table-body">
<tr>
<td colspan="4" style="text-align: center; font-style: italic;">No data loaded</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<div class="section-header">
BLOSSOM CONFIGURATION
</div>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">REFRESH</button>
</div>
</div>
</div>
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
AUTH RULES MANAGEMENT
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" class="config-table-container">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
<tr>
<td colspan="5" style="text-align: center; font-style: italic;">Loading auth rules...</td>
</tr>
</tbody>
</table>
</div>
<!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;">
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (npub or hex):</label>
<input type="text" id="authRulePubkey" placeholder="npub1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
</div>
<!-- BLOSSOM EVENTS Section -->
<div class="section" id="relayEventsSection" style="display: none;">
<div class="section-header">
BLOSSOM EVENTS MANAGEMENT
</div>
<!-- Kind 0: User Metadata -->
<div class="input-group">
<h3>Kind 0: User Metadata</h3>
<div class="form-group">
<label for="kind0-name">Name:</label>
<input type="text" id="kind0-name" placeholder="Blossom Server Name">
</div>
<div class="form-group">
<label for="kind0-about">About:</label>
<textarea id="kind0-about" rows="3" placeholder="Blossom Server Description"></textarea>
</div>
<div class="form-group">
<label for="kind0-picture">Picture URL:</label>
<input type="url" id="kind0-picture" placeholder="https://example.com/logo.png">
</div>
<div class="form-group">
<label for="kind0-banner">Banner URL:</label>
<input type="url" id="kind0-banner" placeholder="https://example.com/banner.png">
</div>
<div class="form-group">
<label for="kind0-nip05">NIP-05:</label>
<input type="text" id="kind0-nip05" placeholder="blossom@example.com">
</div>
<div class="form-group">
<label for="kind0-website">Website:</label>
<input type="url" id="kind0-website" placeholder="https://example.com">
</div>
<div class="inline-buttons">
<button type="button" id="submit-kind0-btn">UPDATE METADATA</button>
</div>
<div id="kind0-status" class="status-message"></div>
</div>
<!-- Kind 10050: DM Blossom List -->
<div class="input-group">
<h3>Kind 10050: DM Blossom List</h3>
<div class="form-group">
<label for="kind10050-relays">Blossom URLs (one per line):</label>
<textarea id="kind10050-relays" rows="4" placeholder="https://blossom1.com&#10;https://blossom2.com"></textarea>
</div>
<div class="inline-buttons">
<button type="button" id="submit-kind10050-btn">UPDATE DM BLOSSOM SERVERS</button>
</div>
<div id="kind10050-status" class="status-message"></div>
</div>
<!-- Kind 10002: Blossom List -->
<div class="input-group">
<h3>Kind 10002: Blossom Server List</h3>
<div id="kind10002-relay-entries">
<!-- Dynamic blossom server entries will be added here -->
</div>
<div class="inline-buttons">
<button type="button" id="add-relay-entry-btn">ADD SERVER</button>
<button type="button" id="submit-kind10002-btn">UPDATE SERVERS</button>
</div>
<div id="kind10002-status" class="status-message"></div>
</div>
</div>
<!-- BLOB BROWSER Section -->
<div class="section" id="blobBrowserSection" style="display: none;">
<div class="section-header">
<h2>BLOB BROWSER</h2>
</div>
<div class="input-group">
<label for="blob-filter-pubkey">Uploader Pubkey (hex or npub):</label>
<input type="text" id="blob-filter-pubkey" placeholder="Optional: npub1... or 64-char hex pubkey">
</div>
<div class="inline-buttons blob-browser-controls">
<button type="button" id="blob-filter-apply-btn">APPLY FILTER</button>
<button type="button" id="blob-filter-clear-btn">CLEAR FILTER</button>
<button type="button" id="blob-view-toggle-btn">SWITCH TO LIST VIEW</button>
<button type="button" id="blob-refresh-btn">REFRESH</button>
</div>
<div id="blob-browser-summary" class="info-box">No data loaded.</div>
<div id="blob-grid-view" class="blob-grid-view"></div>
<div id="blob-list-view" class="blob-list-view" style="display: none;">
<div class="config-table-container">
<table class="config-table" id="blob-list-table">
<thead>
<tr>
<th>Delete</th>
<th>Preview</th>
<th class="blob-sortable-th" data-sort-key="hash">Hash <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploader">Uploader <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="type">Type <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="size">Size <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploaded">Uploaded <span class="blob-sort-indicator"></span></th>
</tr>
</thead>
<tbody id="blob-list-table-body">
<tr>
<td colspan="7" style="text-align: center; font-style: italic;">No blobs loaded</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="inline-buttons blob-pagination-controls">
<button type="button" id="blob-prev-page-btn">PREV</button>
<button type="button" id="blob-next-page-btn">NEXT</button>
</div>
</div>
<!-- SQL QUERY Section -->
<div class="section" id="sqlQuerySection" style="display: none;">
<div class="section-header">
<h2>SQL QUERY CONSOLE</h2>
</div>
<!-- Query Selector -->
<div class="input-group">
<label for="query-dropdown">Quick Queries & History:</label>
<select id="query-dropdown" onchange="loadSelectedQuery()">
<option value="">-- Select a query --</option>
<optgroup label="Common Queries">
<option value="recent_events">Recent Events</option>
<option value="event_stats">Event Statistics</option>
<option value="subscriptions">Active Subscriptions</option>
<option value="top_pubkeys">Top Pubkeys</option>
<option value="event_kinds">Event Kinds Distribution</option>
<option value="time_stats">Time-based Statistics</option>
</optgroup>
<optgroup label="Query History" id="history-group">
<!-- Dynamically populated from localStorage -->
</optgroup>
</select>
</div>
<!-- Query Editor -->
<div class="input-group">
<label for="sql-input">SQL Query:</label>
<textarea id="sql-input" rows="5" placeholder="SELECT * FROM events LIMIT 10"></textarea>
</div>
<!-- Query Actions -->
<div class="input-group">
<div class="inline-buttons">
<button type="button" id="execute-sql-btn">EXECUTE QUERY</button>
<button type="button" id="clear-sql-btn">CLEAR</button>
<button type="button" id="clear-history-btn">CLEAR HISTORY</button>
</div>
</div>
<!-- Query Results -->
<div class="input-group">
<label>Query Results:</label>
<div id="query-info" class="info-box"></div>
<div id="query-table" class="config-table-container"></div>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> -->
<script src="/api/nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library -->
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script> -->
<script src="/api/nostr-lite.js"></script>
<!-- Load text_graph library -->
<script src="/api/text_graph.js"></script>
<script src="/api/index.js"></script>
</body>
</html>

5080
api/index.js Normal file

File diff suppressed because it is too large Load Diff

4282
api/nostr-lite.js Normal file

File diff suppressed because it is too large Load Diff

11534
api/nostr.bundle.js Normal file

File diff suppressed because it is too large Load Diff

463
api/text_graph.js Normal file
View File

@@ -0,0 +1,463 @@
/**
* ASCIIBarChart - A dynamic ASCII-based vertical bar chart renderer
*
* Creates real-time animated bar charts using monospaced characters (X)
* with automatic scaling, labels, and responsive font sizing.
*/
class ASCIIBarChart {
/**
* Create a new ASCII bar chart
* @param {string} containerId - The ID of the HTML element to render the chart in
* @param {Object} options - Configuration options
* @param {number} [options.maxHeight=20] - Maximum height of the chart in rows
* @param {number} [options.maxDataPoints=30] - Maximum number of data columns before scrolling
* @param {string} [options.title=''] - Chart title (displayed centered at top)
* @param {string} [options.xAxisLabel=''] - X-axis label (displayed centered at bottom)
* @param {string} [options.yAxisLabel=''] - Y-axis label (displayed vertically on left)
* @param {boolean} [options.autoFitWidth=true] - Automatically adjust font size to fit container width
* @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
* @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
* @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
* @param {boolean} [options.debug=false] - Enable debug logging
*/
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.data = [];
this.maxHeight = options.maxHeight || 20;
this.maxDataPoints = options.maxDataPoints || 30;
this.totalDataPoints = 0; // Track total number of data points added
this.title = options.title || '';
this.xAxisLabel = options.xAxisLabel || '';
this.yAxisLabel = options.yAxisLabel || '';
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
this.debug = options.debug || false; // Debug logging option
// Time bin configuration
this.useBinMode = options.useBinMode !== false; // Default to true
this.binDuration = options.binDuration || 4000; // 4 seconds default
this.xAxisLabelFormat = options.xAxisLabelFormat || 'elapsed';
// Time bin data structures
this.bins = [];
this.currentBinIndex = -1;
this.binStartTime = null;
this.binCheckInterval = null;
this.chartStartTime = Date.now();
// Set up resize observer if auto-fit is enabled
if (this.autoFitWidth) {
this.resizeObserver = new ResizeObserver(() => {
this.adjustFontSize();
});
this.resizeObserver.observe(this.container);
}
// Initialize first bin if bin mode is enabled
if (this.useBinMode) {
this.initializeBins();
}
}
/**
* Add a new data point to the chart
* @param {number} value - The numeric value to add
*/
addValue(value) {
// Time bin mode: add value to current active bin count
this.checkBinRotation(); // Ensure we have an active bin
this.bins[this.currentBinIndex].count += value; // Changed from ++ to += value
this.totalDataPoints++;
this.render();
this.updateInfo();
}
/**
* Clear all data from the chart
*/
clear() {
this.data = [];
this.totalDataPoints = 0;
if (this.useBinMode) {
this.bins = [];
this.currentBinIndex = -1;
this.binStartTime = null;
this.initializeBins();
}
this.render();
this.updateInfo();
}
/**
* Calculate the width of the chart in characters
* @returns {number} The chart width in characters
* @private
*/
getChartWidth() {
let dataLength = this.maxDataPoints; // Always use maxDataPoints for consistent width
if (dataLength === 0) return 50; // Default width for empty chart
const yAxisPadding = this.yAxisLabel ? 2 : 0;
const yAxisNumbers = 3; // Width of Y-axis numbers
const separator = 1; // The '|' character
// const dataWidth = dataLength * 2; // Each column is 2 characters wide // TEMP: commented for no-space test
const dataWidth = dataLength; // Each column is 1 character wide // TEMP: adjusted for no-space columns
const padding = 1; // Extra padding
const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
// Only log when width changes
if (this.debug && this.lastChartWidth !== totalWidth) {
console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
this.lastChartWidth = totalWidth;
}
return totalWidth;
}
/**
* Adjust font size to fit container width
* @private
*/
adjustFontSize() {
if (!this.autoFitWidth) return;
const containerWidth = this.container.clientWidth;
const chartWidth = this.getChartWidth();
if (chartWidth === 0) return;
// Calculate optimal font size
// For monospace fonts, character width is approximately 0.6 * font size
// Use a slightly smaller ratio to fit more content
const charWidthRatio = 0.7;
const padding = 30; // Reduce padding to fit more content
const availableWidth = containerWidth - padding;
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
// Set reasonable bounds (min 4px, max 20px)
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
// Only log when font size changes
if (this.debug && this.lastFontSize !== fontSize) {
console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
this.lastFontSize = fontSize;
}
this.container.style.fontSize = fontSize + 'px';
this.container.style.lineHeight = '1.0';
}
/**
* Render the chart to the container
* @private
*/
render() {
let dataToRender = [];
let maxValue = 0;
let minValue = 0;
let valueRange = 0;
if (this.useBinMode) {
// Bin mode: render bin counts
if (this.bins.length === 0) {
this.container.textContent = 'No data yet. Click Start to begin.';
return;
}
// Always create a fixed-length array filled with 0s, then overlay actual bin data
dataToRender = new Array(this.maxDataPoints).fill(0);
// Overlay actual bin data (most recent bins, reversed for left-to-right display)
const startIndex = Math.max(0, this.bins.length - this.maxDataPoints);
const recentBins = this.bins.slice(startIndex);
// Reverse the bins so most recent is on the left, and overlay onto the fixed array
recentBins.reverse().forEach((bin, index) => {
if (index < this.maxDataPoints) {
dataToRender[index] = bin.count;
}
});
if (this.debug) {
console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
}
maxValue = Math.max(...dataToRender);
minValue = Math.min(...dataToRender);
valueRange = maxValue - minValue;
} else {
// Legacy mode: render individual values
if (this.data.length === 0) {
this.container.textContent = 'No data yet. Click Start to begin.';
return;
}
dataToRender = this.data;
maxValue = Math.max(...this.data);
minValue = Math.min(...this.data);
valueRange = maxValue - minValue;
}
let output = '';
const scale = this.maxHeight;
// Calculate scaling factor: each X represents at least 1 count
const maxCount = Math.max(...dataToRender);
const scaleFactor = Math.max(1, Math.ceil(maxCount / scale)); // 1 X = scaleFactor counts
const scaledMax = Math.ceil(maxCount / scaleFactor) * scaleFactor;
// Calculate Y-axis label width (for vertical text)
const yLabelWidth = this.yAxisLabel ? 2 : 0;
const yAxisPadding = this.yAxisLabel ? ' ' : '';
// Add title if provided (centered)
if (this.title) {
// const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test
const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space columns
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
}
// Draw from top to bottom
for (let row = scale; row > 0; row--) {
let line = '';
// Add vertical Y-axis label character
if (this.yAxisLabel) {
const L = this.yAxisLabel.length;
const startRow = Math.floor((scale - L) / 2) + 1;
const relativeRow = scale - row + 1; // 1 at top, scale at bottom
if (relativeRow >= startRow && relativeRow < startRow + L) {
const labelIndex = relativeRow - startRow;
line += this.yAxisLabel[labelIndex] + ' ';
} else {
line += ' ';
}
}
// Calculate the actual count value this row represents (1 at bottom, increasing upward)
const rowCount = (row - 1) * scaleFactor + 1;
// Add Y-axis label (show actual count values)
line += String(rowCount).padStart(3, ' ') + ' |';
// Draw each column
for (let i = 0; i < dataToRender.length; i++) {
const count = dataToRender[i];
const scaledHeight = Math.ceil(count / scaleFactor);
if (scaledHeight >= row) {
// line += ' X'; // TEMP: commented out space between columns
line += 'X'; // TEMP: no space between columns
} else {
// line += ' '; // TEMP: commented out space between columns
line += ' '; // TEMP: single space for empty columns
}
}
output += line + '\n';
}
// Draw X-axis
// output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length
// Draw X-axis labels based on mode and format
let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis
// Determine label interval (every 5 columns)
const labelInterval = 5;
// Generate all labels first and store in array
let labels = [];
for (let i = 0; i < this.maxDataPoints; i++) {
if (i % labelInterval === 0) {
let label = '';
if (this.useBinMode) {
// For bin mode, show labels for all possible positions
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
const elapsedSec = (i * this.binDuration) / 1000;
// Format with appropriate precision for sub-second bins
if (this.binDuration < 1000) {
// Show decimal seconds for sub-second bins
label = elapsedSec.toFixed(1) + 's';
} else {
// Show whole seconds for 1+ second bins
label = String(Math.round(elapsedSec)) + 's';
}
} else {
// For legacy mode, show data point numbers
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
label = String(startIndex + i);
}
labels.push(label);
}
}
// Build the label string with calculated spacing
for (let i = 0; i < labels.length; i++) {
const label = labels[i];
xAxisLabels += label;
// Add spacing: labelInterval - label.length (except for last label)
if (i < labels.length - 1) {
const spacing = labelInterval - label.length;
xAxisLabels += ' '.repeat(spacing);
}
}
// Ensure the label line extends to match the X-axis dash line length
// The dash line is this.maxDataPoints characters long, starting after " +"
const dashLineLength = this.maxDataPoints;
const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " "
if (xAxisLabels.length < minLabelLineLength) {
xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
}
output += xAxisLabels + '\n';
// Add X-axis label if provided
if (this.xAxisLabel) {
// const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test
const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
}
this.container.textContent = output;
// Adjust font size to fit width (only once at initialization)
if (this.autoFitWidth) {
this.adjustFontSize();
}
// Update the external info display
if (this.useBinMode) {
const binCounts = this.bins.map(bin => bin.count);
const scaleFactor = Math.max(1, Math.ceil(maxValue / scale));
document.getElementById('values').textContent = `[${dataToRender.join(', ')}]`;
document.getElementById('max-value').textContent = maxValue;
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, 1X=${scaleFactor} counts`;
} else {
document.getElementById('values').textContent = `[${this.data.join(', ')}]`;
document.getElementById('max-value').textContent = maxValue;
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
}
}
/**
* Update the info display
* @private
*/
updateInfo() {
if (this.useBinMode) {
const totalCount = this.bins.reduce((sum, bin) => sum + bin.count, 0);
document.getElementById('count').textContent = totalCount;
} else {
document.getElementById('count').textContent = this.data.length;
}
}
/**
* Initialize the bin system
* @private
*/
initializeBins() {
this.bins = [];
this.currentBinIndex = -1;
this.binStartTime = null;
this.chartStartTime = Date.now();
// Create first bin
this.rotateBin();
// Set up automatic bin rotation check
this.binCheckInterval = setInterval(() => {
this.checkBinRotation();
}, 100); // Check every 100ms for responsiveness
}
/**
* Check if current bin should rotate and create new bin if needed
* @private
*/
checkBinRotation() {
if (!this.useBinMode || !this.binStartTime) return;
const now = Date.now();
if ((now - this.binStartTime) >= this.binDuration) {
this.rotateBin();
}
}
/**
* Rotate to a new bin, finalizing the current one
*/
rotateBin() {
// Finalize current bin if it exists
if (this.currentBinIndex >= 0) {
this.bins[this.currentBinIndex].isActive = false;
}
// Create new bin
const newBin = {
startTime: Date.now(),
count: 0,
isActive: true
};
this.bins.push(newBin);
this.currentBinIndex = this.bins.length - 1;
this.binStartTime = newBin.startTime;
// Keep only the most recent bins
if (this.bins.length > this.maxDataPoints) {
this.bins.shift();
this.currentBinIndex--;
}
// Ensure currentBinIndex points to the last bin (the active one)
this.currentBinIndex = this.bins.length - 1;
// Force a render to update the display immediately
this.render();
this.updateInfo();
}
/**
* Format X-axis label for a bin based on the configured format
* @param {number} binIndex - Index of the bin
* @returns {string} Formatted label
* @private
*/
formatBinLabel(binIndex) {
const bin = this.bins[binIndex];
if (!bin) return ' ';
switch (this.xAxisLabelFormat) {
case 'bins':
return String(binIndex + 1).padStart(2, ' ');
case 'timestamps':
const time = new Date(bin.startTime);
return time.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/:/g, '');
case 'ranges':
const startSec = Math.floor((bin.startTime - this.chartStartTime) / 1000);
const endSec = startSec + Math.floor(this.binDuration / 1000);
return `${startSec}-${endSec}`;
case 'elapsed':
default:
// For elapsed time, always show time relative to the first bin (index 0)
// This keeps the leftmost label as 0s and increases to the right
const firstBinTime = this.bins[0] ? this.bins[0].startTime : this.chartStartTime;
const elapsedSec = Math.floor((bin.startTime - firstBinTime) / 1000);
return String(elapsedSec).padStart(2, ' ') + 's';
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/ginxsom-fcgi_static_arm64 Executable file

Binary file not shown.

BIN
build/ginxsom-fcgi_static_x86_64 Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

325
build_static.sh Executable file
View File

@@ -0,0 +1,325 @@
#!/bin/bash
# Build fully static MUSL binaries for Ginxsom using Alpine Docker
# Produces truly portable binaries with zero runtime dependencies
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
# Parse command line arguments
DEBUG_BUILD=false
TARGET_ARCH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_BUILD=true
shift
;;
--arch)
if [[ -z "$2" ]]; then
echo "ERROR: --arch requires a value"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
fi
case "$2" in
arm64|armv7|x86_64)
TARGET_ARCH="$2"
;;
*)
echo "ERROR: Unsupported architecture '$2'"
echo "Supported values: arm64, armv7, x86_64"
exit 1
esac
shift 2
;;
*)
echo "ERROR: Unknown argument '$1'"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
;;
esac
done
if [ "$DEBUG_BUILD" = true ]; then
echo "=========================================="
echo "Ginxsom MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
else
echo "=========================================="
echo "Ginxsom MUSL Static Binary Builder (PRODUCTION MODE)"
echo "=========================================="
fi
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
echo "Debug build: $DEBUG_BUILD"
if [[ -n "$TARGET_ARCH" ]]; then
echo "Requested target architecture: $TARGET_ARCH"
fi
echo ""
# Create build directory
mkdir -p "$BUILD_DIR"
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "ERROR: Docker is not installed or not in PATH"
echo ""
echo "Docker is required to build MUSL static binaries."
echo "Please install Docker:"
echo " - Ubuntu/Debian: sudo apt install docker.io"
echo " - Or visit: https://docs.docker.com/engine/install/"
echo ""
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
echo "ERROR: Docker daemon is not running or user not in docker group"
echo ""
echo "Please start Docker and ensure you're in the docker group:"
echo " - sudo systemctl start docker"
echo " - sudo usermod -aG docker $USER && newgrp docker"
echo " - Or start Docker Desktop"
echo ""
exit 1
fi
DOCKER_CMD="docker"
echo "✓ Docker is available and running"
echo ""
# Detect host architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
HOST_ARCH="x86_64"
;;
aarch64|arm64)
HOST_ARCH="arm64"
;;
armv7l|armv7)
HOST_ARCH="armv7"
;;
*)
HOST_ARCH="unknown"
;;
esac
# Resolve target architecture
if [[ -n "$TARGET_ARCH" ]]; then
ARCH="$TARGET_ARCH"
fi
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="ginxsom-fcgi_static_x86_64"
;;
aarch64|arm64)
ARCH="arm64"
PLATFORM="linux/arm64"
OUTPUT_NAME="ginxsom-fcgi_static_arm64"
;;
armv7l|armv7)
ARCH="armv7"
PLATFORM="linux/arm/v7"
OUTPUT_NAME="ginxsom-fcgi_static_armv7"
;;
*)
echo "WARNING: Unknown architecture: $ARCH"
echo "Defaulting to linux/amd64"
PLATFORM="linux/amd64"
OUTPUT_NAME="ginxsom-fcgi_static_${ARCH}"
;;
esac
# When cross-building, ensure buildx exists and warn about QEMU/binfmt setup
if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then
echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'."
echo " Docker QEMU/binfmt support is required for this to work."
if ! docker buildx version >/dev/null 2>&1; then
echo "ERROR: docker buildx is required for cross-architecture builds but is not available."
echo "Install/enable buildx and binfmt support, then retry."
exit 1
fi
echo "✓ docker buildx is available for cross-architecture build"
echo ""
fi
# Append _debug suffix to output name for debug builds so production binary is never overwritten
if [ "$DEBUG_BUILD" = true ]; then
OUTPUT_NAME="${OUTPUT_NAME}_debug"
fi
echo "Building for platform: $PLATFORM"
echo "Output binary: $OUTPUT_NAME"
echo ""
# Check if Alpine base image is cached
echo "Checking for cached Alpine Docker image..."
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
echo "⚠ Alpine 3.19 image not found in cache"
echo "Attempting to pull Alpine 3.19 image..."
if ! docker pull alpine:3.19; then
echo ""
echo "ERROR: Failed to pull Alpine 3.19 image"
echo "This is required for the static build."
echo ""
echo "Possible solutions:"
echo " 1. Check your internet connection"
echo " 2. Try again later (Docker Hub may be temporarily unavailable)"
echo " 3. If you have IPv6 issues, disable IPv6 for Docker"
echo ""
exit 1
fi
echo "✓ Alpine 3.19 image pulled successfully"
else
echo "✓ Alpine 3.19 image found in cache"
fi
echo ""
# Build the Docker image
echo "=========================================="
echo "Step 1: Building Alpine Docker image"
echo "=========================================="
echo "This will:"
echo " - Use Alpine Linux (native MUSL)"
echo " - Build all dependencies statically"
echo " - Compile Ginxsom with full static linking"
echo ""
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
-f "$DOCKERFILE" \
-t ginxsom-musl-builder:latest \
--progress=plain \
. || {
echo ""
echo "ERROR: Docker build failed"
echo "Check the output above for details"
exit 1
}
echo ""
echo "✓ Docker image built successfully"
echo ""
# Extract the binary from the container
echo "=========================================="
echo "Step 2: Extracting static binary"
echo "=========================================="
# Build the builder stage to extract the binary
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
--target builder \
-f "$DOCKERFILE" \
-t ginxsom-static-builder-stage:latest \
. > /dev/null 2>&1
# Create a temporary container to copy the binary
CONTAINER_ID=$($DOCKER_CMD create ginxsom-static-builder-stage:latest)
# Copy binary from container
$DOCKER_CMD cp "$CONTAINER_ID:/build/ginxsom-fcgi_static" "$BUILD_DIR/$OUTPUT_NAME" || {
echo "ERROR: Failed to extract binary from container"
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
exit 1
}
# Clean up container
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
echo ""
# Make binary executable
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
# Verify the binary
echo "=========================================="
echo "Step 3: Verifying static binary"
echo "=========================================="
echo ""
echo "Checking for dynamic dependencies:"
if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
echo "✓ Binary is fully static (no dynamic dependencies)"
TRULY_STATIC=true
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
echo "✓ Binary is statically linked"
TRULY_STATIC=true
else
echo "⚠ WARNING: Binary may have dynamic dependencies:"
echo "$LDD_OUTPUT"
TRULY_STATIC=false
fi
else
# ldd failed or timed out - check with file command instead
if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
echo "✓ Binary is statically linked (verified with file command)"
TRULY_STATIC=true
else
echo "⚠ Could not verify static linking (ldd check failed)"
TRULY_STATIC=false
fi
fi
echo ""
echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')"
echo ""
# Summary
echo "=========================================="
echo "Build Summary"
echo "=========================================="
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
echo "Platform: $PLATFORM"
if [ "$DEBUG_BUILD" = true ]; then
echo "Build Type: DEBUG (with symbols, no optimization)"
else
echo "Build Type: PRODUCTION (optimized, stripped)"
fi
if [ "$TRULY_STATIC" = true ]; then
echo "Linkage: Fully static binary (Alpine MUSL-based)"
echo "Portability: Works on ANY Linux distribution"
else
echo "Linkage: Static binary (may have minimal dependencies)"
fi
echo ""
echo "✓ Build complete!"
echo ""
# Clean up old dynamic build artifacts
echo "=========================================="
echo "Cleaning up old build artifacts"
echo "=========================================="
echo ""
if ls build/*.o 2>/dev/null | grep -q .; then
echo "Removing old .o files from dynamic builds..."
rm -f build/*.o
echo "✓ Cleanup complete"
else
echo "No .o files to clean"
fi
# Also remove old dynamic binary if it exists
if [ -f "build/ginxsom-fcgi" ]; then
echo "Removing old dynamic binary..."
rm -f build/ginxsom-fcgi
echo "✓ Old binary removed"
fi
echo ""
echo "Deployment:"
echo " scp $BUILD_DIR/$OUTPUT_NAME user@server:/path/to/ginxsom/"
echo ""

151
config/ginxsom-local.conf Normal file
View File

@@ -0,0 +1,151 @@
# Ginxsom Blossom Server nginx configuration
# Local deployment configuration
# Port: 9443 with SSL
server {
listen 9443 ssl http2;
server_name localhost;
# SSL configuration
ssl_certificate /home/teknari/.ssl_for_local_servers/cert.pem;
ssl_certificate_key /home/teknari/.ssl_for_local_servers/key.pem;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
# CORS headers for all responses (including error responses)
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, HEAD, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-SHA-256, X-Content-Type, X-Content-Length" always;
add_header Access-Control-Max-Age 86400 always;
# Root directory for blob storage
root /home/teknari/Storage/Blossom;
# Maximum upload size
client_max_body_size 100M;
# OPTIONS preflight handler
if ($request_method = OPTIONS) {
return 204;
}
# PUT /upload - File uploads
location = /upload {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
fastcgi_read_timeout 300s;
fastcgi_request_buffering off;
fastcgi_buffering off;
}
# GET /list/<pubkey> - List user blobs
location ~ "^/list/([a-f0-9]{64})$" {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# PUT /mirror - Mirror content
location = /mirror {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# PUT /report - Report content
location = /report {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# GET /auth - NIP-42 challenges
location = /auth {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# GET /health - Health check
location = /health {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# Admin API
location /api/ {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# Admin interface
location /admin {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# Blob serving - SHA256 patterns (serve directly from filesystem)
location ~ "^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$" {
# Handle DELETE via rewrite to internal location
if ($request_method = DELETE) {
rewrite ^/(.*)$ /fcgi-delete/$1 last;
}
# Handle HEAD via rewrite to internal location
if ($request_method = HEAD) {
rewrite ^/(.*)$ /fcgi-head/$1 last;
}
# GET requests - serve directly from filesystem
if ($request_method != GET) {
return 405;
}
try_files /blobs/$1.html /blobs/$1.js /blobs/$1.mjs /blobs/$1.css /blobs/$1.txt /blobs/$1.jpg /blobs/$1.jpeg /blobs/$1.png /blobs/$1.webp /blobs/$1.gif /blobs/$1.pdf /blobs/$1.mp4 /blobs/$1.mp3 /blobs/$1.md =404;
# Cache control for blobs
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Internal FastCGI handler for DELETE
location ~ "^/fcgi-delete/([a-f0-9]{64}).*$" {
internal;
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
fastcgi_param REQUEST_URI /$1;
}
# Internal FastCGI handler for HEAD
location ~ "^/fcgi-head/([a-f0-9]{64}).*$" {
internal;
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
fastcgi_param REQUEST_URI /$1;
}
# Root endpoint - server info
location = / {
include fastcgi_params;
fastcgi_pass unix:/tmp/ginxsom.sock;
fastcgi_param SCRIPT_FILENAME /home/teknari/Storage/Blossom/ginxsom;
}
# Deny access to sensitive files
location ~ /\.(ht|git|svn) {
deny all;
}
}

View File

@@ -220,6 +220,35 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# Admin web interface (/admin)
location /admin {
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT|POST)$) {
@@ -269,7 +298,7 @@ http {
}
# GET requests - serve files directly with extension fallback
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
# Cache headers for blob content
add_header Cache-Control "public, max-age=31536000, immutable";
@@ -571,6 +600,35 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# Admin web interface (/admin)
location /admin {
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT|POST)$) {
@@ -620,7 +678,7 @@ http {
}
# GET requests - serve files directly with extension fallback
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
# Cache headers for blob content
add_header Cache-Control "public, max-age=31536000, immutable";

4391
debug.log

File diff suppressed because it is too large Load Diff

203
deploy_local.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/bin/bash
# Deploy Ginxsom to local installation directory
# Builds static binary and deploys to ~/Storage/Blossom
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$HOME/Storage/Blossom"
SERVICE_NAME="ginxsom.service"
NGINX_SITE_NAME="ginxsom"
NGINX_PORT="9443"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}=== Ginxsom Local Deployment ===${NC}"
echo "Source: $SCRIPT_DIR"
echo "Target: $DEPLOY_DIR"
echo "Nginx Port: $NGINX_PORT"
echo ""
# Step 1: Build static binary
echo -e "${YELLOW}Step 1: Building static binary...${NC}"
cd "$SCRIPT_DIR"
./build_static.sh
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed! Cannot continue.${NC}"
exit 1
fi
echo -e "${GREEN}✓ Build complete${NC}"
echo ""
# Step 2: Create deployment directory
echo -e "${YELLOW}Step 2: Preparing deployment directory...${NC}"
mkdir -p "$DEPLOY_DIR"
mkdir -p "$DEPLOY_DIR/blobs"
mkdir -p "$DEPLOY_DIR/db"
mkdir -p "$DEPLOY_DIR/logs"
echo -e "${GREEN}✓ Directories created${NC}"
echo ""
# Step 3: Stop service before copying binary
echo -e "${YELLOW}Step 3: Stopping service before binary update...${NC}"
if systemctl list-unit-files | grep -q "$SERVICE_NAME"; then
sudo systemctl stop "$SERVICE_NAME" || true
sleep 1
echo -e "${GREEN}✓ Service stopped${NC}"
fi
echo ""
# Step 4: Copy binary
echo -e "${YELLOW}Step 4: Copying binary...${NC}"
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64) BINARY_NAME="ginxsom-fcgi_static_x86_64" ;;
aarch64|arm64) BINARY_NAME="ginxsom-fcgi_static_arm64" ;;
*) BINARY_NAME="ginxsom-fcgi_static_${ARCH}" ;;
esac
SOURCE_BINARY="$SCRIPT_DIR/build/$BINARY_NAME"
TARGET_BINARY="$DEPLOY_DIR/ginxsom"
if [ ! -f "$SOURCE_BINARY" ]; then
echo -e "${RED}Error: Binary not found at $SOURCE_BINARY${NC}"
exit 1
fi
cp "$SOURCE_BINARY" "$TARGET_BINARY"
chmod +x "$TARGET_BINARY"
echo -e "${GREEN}✓ Binary copied to $TARGET_BINARY${NC}"
echo ""
# Step 5: Copy configuration files if they exist
echo -e "${YELLOW}Step 5: Copying configuration files...${NC}"
if [ -f "$SCRIPT_DIR/mime.types" ]; then
cp "$SCRIPT_DIR/mime.types" "$DEPLOY_DIR/"
echo " - mime.types"
fi
if [ -f "$SCRIPT_DIR/.admin_keys" ]; then
cp "$SCRIPT_DIR/.admin_keys" "$DEPLOY_DIR/"
chmod 600 "$DEPLOY_DIR/.admin_keys"
echo " - .admin_keys"
fi
if [ -f "$SCRIPT_DIR/config/fastcgi_params" ]; then
cp "$SCRIPT_DIR/config/fastcgi_params" "$DEPLOY_DIR/"
echo " - fastcgi_params"
fi
echo -e "${GREEN}✓ Configuration files copied${NC}"
echo ""
# Step 6: Setup nginx configuration
echo -e "${YELLOW}Step 6: Setting up nginx configuration...${NC}"
# Copy nginx config to sites-available
if [ -f "$SCRIPT_DIR/config/ginxsom-local.conf" ]; then
sudo cp "$SCRIPT_DIR/config/ginxsom-local.conf" "/etc/nginx/sites-available/$NGINX_SITE_NAME"
echo " - Copied nginx config to sites-available"
# Create symlink in sites-enabled if it doesn't exist
if [ ! -L "/etc/nginx/sites-enabled/$NGINX_SITE_NAME" ]; then
sudo ln -s "/etc/nginx/sites-available/$NGINX_SITE_NAME" "/etc/nginx/sites-enabled/$NGINX_SITE_NAME"
echo " - Created symlink in sites-enabled"
else
echo " - Symlink already exists in sites-enabled"
fi
# Test nginx configuration
echo " - Testing nginx configuration..."
if sudo nginx -t; then
echo -e "${GREEN}✓ Nginx configuration valid${NC}"
else
echo -e "${RED}✗ Nginx configuration test failed${NC}"
exit 1
fi
else
echo -e "${YELLOW}Warning: nginx config not found at $SCRIPT_DIR/config/ginxsom-local.conf${NC}"
fi
echo ""
# Step 7: Restart service
echo -e "${YELLOW}Step 7: Restarting service...${NC}"
# Check if service exists
if systemctl list-unit-files | grep -q "$SERVICE_NAME"; then
echo "Stopping service..."
sudo systemctl stop "$SERVICE_NAME" || true
echo "Reloading systemd daemon..."
sudo systemctl daemon-reload
echo "Starting service..."
sudo systemctl start "$SERVICE_NAME"
# Wait a moment for service to start
sleep 2
# Check status
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo -e "${GREEN}✓ Service started successfully${NC}"
systemctl status "$SERVICE_NAME" --no-pager -l
else
echo -e "${RED}✗ Service failed to start${NC}"
echo "Check logs with: journalctl -u $SERVICE_NAME -n 50"
exit 1
fi
else
echo -e "${YELLOW}Service not found. Install with:${NC}"
echo " sudo cp ginxsom-local.service /etc/systemd/system/ginxsom.service"
echo " sudo systemctl daemon-reload"
echo " sudo systemctl enable ginxsom.service"
echo " sudo systemctl start ginxsom.service"
fi
echo ""
# Step 8: Start/restart nginx
echo -e "${YELLOW}Step 8: Starting nginx...${NC}"
if systemctl is-active --quiet nginx; then
echo "Reloading nginx..."
sudo systemctl reload nginx
else
echo "Starting nginx..."
sudo systemctl start nginx
fi
if systemctl is-active --quiet nginx; then
echo -e "${GREEN}✓ Nginx is running${NC}"
else
echo -e "${RED}✗ Nginx failed to start${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}=== Deployment complete ===${NC}"
echo ""
echo "Service Information:"
echo " FastCGI Service: $SERVICE_NAME"
echo " Nginx Port: $NGINX_PORT"
echo " Socket: /tmp/ginxsom.sock"
echo " Storage: $DEPLOY_DIR/blobs"
echo " Database: $DEPLOY_DIR/db"
echo ""
echo "Service commands:"
echo " Status: systemctl status $SERVICE_NAME"
echo " Stop: sudo systemctl stop $SERVICE_NAME"
echo " Start: sudo systemctl start $SERVICE_NAME"
echo " Restart: sudo systemctl restart $SERVICE_NAME"
echo " Logs: journalctl -u $SERVICE_NAME -f"
echo ""
echo "Nginx commands:"
echo " Status: systemctl status nginx"
echo " Reload: sudo systemctl reload nginx"
echo " Logs: sudo tail -f /var/log/nginx/error.log"
echo ""
echo "Test the server:"
echo " curl -k https://localhost:$NGINX_PORT/health"
echo " curl -k https://localhost:$NGINX_PORT/"

View File

@@ -22,285 +22,336 @@ fi
# Configuration
REMOTE_HOST="laantungir.net"
REMOTE_USER="ubuntu"
REMOTE_DIR="/home/ubuntu/ginxsom"
REMOTE_DB_PATH="/home/ubuntu/ginxsom/db/ginxsom.db"
REMOTE_NGINX_CONFIG="/etc/nginx/conf.d/default.conf"
REMOTE_BINARY_PATH="/home/ubuntu/ginxsom/ginxsom.fcgi"
# Deployment paths
REMOTE_BINARY_DIR="/usr/local/bin/ginxsom"
REMOTE_BINARY_PATH="$REMOTE_BINARY_DIR/ginxsom-fcgi"
REMOTE_DB_PATH="$REMOTE_BINARY_DIR"
REMOTE_BLOB_DIR="/var/www/blobs"
REMOTE_SOCKET="/tmp/ginxsom-fcgi.sock"
REMOTE_DATA_DIR="/var/www/html/blossom"
print_status "Starting deployment to $REMOTE_HOST..."
# Production keys
ADMIN_PUBKEY="1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139"
SERVER_PRIVKEY="90df3fe61e7d19e50f387e4c5db87eff1a7d2a1037cd55026c4b21a4fda8ecf6"
# Step 1: Build and prepare local binary
print_status "Building ginxsom binary..."
make clean && make
if [[ ! -f "build/ginxsom-fcgi" ]]; then
print_error "Build failed - binary not found"
# Local paths
LOCAL_BINARY="build/ginxsom-fcgi_static_x86_64"
print_status "=========================================="
print_status "Ginxsom Static Binary Deployment"
print_status "=========================================="
print_status "Target: $REMOTE_HOST"
print_status "Binary: $REMOTE_BINARY_PATH"
print_status "Database: $REMOTE_DB_PATH"
print_status "Blobs: $REMOTE_BLOB_DIR"
print_status "Fresh install: $FRESH_INSTALL"
print_status "=========================================="
echo ""
# Step 1: Verify local binary exists
print_status "Step 1: Verifying local static binary..."
if [[ ! -f "$LOCAL_BINARY" ]]; then
print_error "Static binary not found: $LOCAL_BINARY"
print_status "Please run: ./build_static.sh"
exit 1
fi
print_success "Binary built successfully"
# Step 2: Setup remote environment first (before copying files)
print_status "Setting up remote environment..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
# Verify it's actually static
if ldd "$LOCAL_BINARY" 2>&1 | grep -q "not a dynamic executable\|statically linked"; then
print_success "Binary is static"
else
print_warning "Binary may not be fully static - proceeding anyway"
fi
# Create data directory if it doesn't exist (using existing /var/www/html/blossom)
sudo mkdir -p /var/www/html/blossom
sudo chown www-data:www-data /var/www/html/blossom
sudo chmod 755 /var/www/html/blossom
BINARY_SIZE=$(du -h "$LOCAL_BINARY" | cut -f1)
print_success "Found static binary ($BINARY_SIZE)"
echo ""
# Ensure socket directory exists
sudo mkdir -p /tmp
sudo chmod 755 /tmp
# Install required dependencies
echo "Installing required dependencies..."
sudo apt-get update
sudo apt-get install -y spawn-fcgi libfcgi-dev
# Stop any existing ginxsom processes
echo "Stopping existing ginxsom processes..."
sudo pkill -f ginxsom-fcgi || true
sudo rm -f /tmp/ginxsom-fcgi.sock || true
echo "Remote environment setup complete"
EOF
print_success "Remote environment configured"
# Step 3: Copy files to remote server
print_status "Copying files to remote server..."
# Copy entire project directory (excluding unnecessary files)
print_status "Copying entire ginxsom project..."
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
# Build on remote server to ensure compatibility
print_status "Building ginxsom on remote server..."
ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && make clean && make" || {
print_error "Build failed on remote server"
print_status "Checking what packages are actually installed..."
ssh $REMOTE_USER@$REMOTE_HOST "dpkg -l | grep -E '(sqlite|fcgi)'"
# Step 2: Upload binary to server
print_status "Step 2: Uploading binary to server..."
scp "$LOCAL_BINARY" $REMOTE_USER@$REMOTE_HOST:~/ginxsom-fcgi_new || {
print_error "Failed to upload binary"
exit 1
}
print_success "Binary uploaded to ~/ginxsom-fcgi_new"
echo ""
# Copy binary to application directory
print_status "Copying ginxsom binary to application directory..."
# Step 3: Setup directories
print_status "Step 3: Setting up directories..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Stop any running process first
sudo pkill -f ginxsom-fcgi || true
sleep 1
# Remove old binary if it exists
rm -f $REMOTE_BINARY_PATH
# Copy new binary
cp $REMOTE_DIR/build/ginxsom-fcgi $REMOTE_BINARY_PATH
chmod +x $REMOTE_BINARY_PATH
chown ubuntu:ubuntu $REMOTE_BINARY_PATH
echo "Binary copied successfully"
set -e
# Create binary/database directory
echo "Creating application directory..."
sudo mkdir -p $REMOTE_BINARY_DIR
sudo chown www-data:www-data $REMOTE_BINARY_DIR
sudo chmod 755 $REMOTE_BINARY_DIR
# Create blob storage directory
echo "Creating blob storage directory..."
sudo mkdir -p $REMOTE_BLOB_DIR
sudo chown www-data:www-data $REMOTE_BLOB_DIR
sudo chmod 755 $REMOTE_BLOB_DIR
# Create logs directory
echo "Creating logs directory..."
sudo mkdir -p $REMOTE_BINARY_DIR/logs/app
sudo chown -R www-data:www-data $REMOTE_BINARY_DIR/logs
sudo chmod -R 755 $REMOTE_BINARY_DIR/logs
echo "Directories created successfully"
EOF
# NOTE: Do NOT update nginx configuration automatically
# The deployment script should only update ginxsom binaries and do nothing else with the system
# Nginx configuration should be managed manually by the system administrator
print_status "Skipping nginx configuration update (manual control required)"
if [ $? -ne 0 ]; then
print_error "Failed to create directories"
exit 1
fi
print_success "Directories created"
echo ""
print_success "Files copied to remote server"
# Step 3: Setup remote environment
print_status "Setting up remote environment..."
# Step 4: Handle fresh install if requested
if [ "$FRESH_INSTALL" = true ]; then
print_status "Step 4: Fresh install - removing existing data..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
sudo rm -f $REMOTE_DB_PATH/*.db
sudo rm -rf $REMOTE_BLOB_DIR/*
echo "Existing data removed"
EOF
print_success "Fresh install prepared"
echo ""
else
print_status "Step 4: Preserving existing data"
echo ""
fi
# Step 5: Install minimal dependencies
print_status "Step 5: Installing minimal dependencies..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
# Create data directory if it doesn't exist (using existing /var/www/html/blossom)
sudo mkdir -p /var/www/html/blossom
sudo chown www-data:www-data /var/www/html/blossom
sudo chmod 755 /var/www/html/blossom
# Ensure socket directory exists
sudo mkdir -p /tmp
sudo chmod 755 /tmp
# Install required dependencies
echo "Installing required dependencies..."
sudo apt-get update 2>/dev/null || true # Continue even if apt update has issues
sudo apt-get install -y spawn-fcgi libfcgi-dev libsqlite3-dev sqlite3 libcurl4-openssl-dev
# Verify installations
echo "Verifying installations..."
if ! dpkg -l libsqlite3-dev >/dev/null 2>&1; then
echo "libsqlite3-dev not found, trying alternative..."
sudo apt-get install -y libsqlite3-dev || {
echo "Failed to install libsqlite3-dev"
exit 1
}
fi
if ! dpkg -l libfcgi-dev >/dev/null 2>&1; then
echo "libfcgi-dev not found"
exit 1
fi
# Check if sqlite3.h exists
if [ ! -f /usr/include/sqlite3.h ]; then
echo "sqlite3.h not found in /usr/include/"
find /usr -name "sqlite3.h" 2>/dev/null || echo "sqlite3.h not found anywhere"
exit 1
fi
# Stop any existing ginxsom processes
echo "Stopping existing ginxsom processes..."
sudo pkill -f ginxsom-fcgi || true
sudo rm -f /tmp/ginxsom-fcgi.sock || true
echo "Remote environment setup complete"
EOF
print_success "Remote environment configured"
# Step 4: Setup database directory and migrate database
print_status "Setting up database directory..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Create db directory if it doesn't exist
mkdir -p $REMOTE_DIR/db
if [ "$FRESH_INSTALL" = "true" ]; then
echo "Fresh install: removing existing database and blobs..."
# Remove existing database
sudo rm -f $REMOTE_DB_PATH
sudo rm -f /var/www/html/blossom/ginxsom.db
# Remove existing blobs
sudo rm -rf $REMOTE_DATA_DIR/*
echo "Existing data removed"
else
# Backup current database if it exists in old location
if [ -f /var/www/html/blossom/ginxsom.db ]; then
echo "Backing up existing database..."
cp /var/www/html/blossom/ginxsom.db /var/www/html/blossom/ginxsom.db.backup.\$(date +%Y%m%d_%H%M%S)
# Migrate database to new location if not already there
if [ ! -f $REMOTE_DB_PATH ]; then
echo "Migrating database to new location..."
cp /var/www/html/blossom/ginxsom.db $REMOTE_DB_PATH
else
echo "Database already exists at new location"
fi
elif [ ! -f $REMOTE_DB_PATH ]; then
echo "No existing database found - will be created on first run"
else
echo "Database already exists at $REMOTE_DB_PATH"
fi
fi
# Set proper permissions - www-data needs write access to db directory for SQLite journal files
sudo chown -R www-data:www-data $REMOTE_DIR/db
sudo chmod 755 $REMOTE_DIR/db
sudo chmod 644 $REMOTE_DB_PATH 2>/dev/null || true
# Allow www-data to access the application directory for spawn-fcgi chdir
chmod 755 $REMOTE_DIR
echo "Database directory setup complete"
EOF
print_success "Database directory configured"
# Step 5: Start ginxsom FastCGI process
print_status "Starting ginxsom FastCGI process..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Clean up any existing socket
sudo rm -f $REMOTE_SOCKET
# Start FastCGI process with explicit paths
echo "Starting ginxsom FastCGI with configuration:"
echo " Working directory: $REMOTE_DIR"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Storage: $REMOTE_DATA_DIR"
sudo spawn-fcgi -M 666 -u www-data -g www-data -s $REMOTE_SOCKET -U www-data -G www-data -d $REMOTE_DIR -- $REMOTE_BINARY_PATH --db-path "$REMOTE_DB_PATH" --storage-dir "$REMOTE_DATA_DIR"
# Give it a moment to start
sleep 2
# Verify process is running
if pgrep -f "ginxsom-fcgi" > /dev/null; then
echo "FastCGI process started successfully"
echo "PID: \$(pgrep -f ginxsom-fcgi)"
# Check if spawn-fcgi is installed
if ! command -v spawn-fcgi &> /dev/null; then
echo "Installing spawn-fcgi..."
sudo apt-get update -qq
sudo apt-get install -y spawn-fcgi
echo "spawn-fcgi installed"
else
echo "Process not found by pgrep, but socket exists - this may be normal for FastCGI"
echo "Checking socket..."
ls -la $REMOTE_SOCKET
echo "Checking if binary exists and is executable..."
ls -la $REMOTE_BINARY_PATH
echo "Testing if we can connect to the socket..."
# Try to test the FastCGI connection
if command -v cgi-fcgi >/dev/null 2>&1; then
echo "Testing FastCGI connection..."
SCRIPT_NAME=/health SCRIPT_FILENAME=$REMOTE_BINARY_PATH REQUEST_METHOD=GET cgi-fcgi -bind -connect $REMOTE_SOCKET 2>/dev/null | head -5 || echo "Connection test failed"
else
echo "cgi-fcgi not available for testing"
fi
# Don't exit - the socket existing means spawn-fcgi worked
echo "spawn-fcgi already installed"
fi
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI process started"
print_success "Dependencies verified"
else
print_error "Failed to start FastCGI process"
print_error "Failed to install dependencies"
exit 1
fi
echo ""
# Step 6: Test nginx configuration and reload
print_status "Testing and reloading nginx..."
# Step 6: Upload and install systemd service file
print_status "Step 6: Installing systemd service file..."
scp ginxsom.service $REMOTE_USER@$REMOTE_HOST:~/ginxsom.service || {
print_error "Failed to upload service file"
exit 1
}
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
sudo cp ~/ginxsom.service /etc/systemd/system/
sudo systemctl daemon-reload
echo "Service file installed"
EOF
if [ $? -eq 0 ]; then
print_success "Service file installed"
else
print_error "Failed to install service file"
exit 1
fi
echo ""
# Step 7: Stop existing service and install new binary
print_status "Step 7: Stopping existing service and installing new binary..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
set -e
# Stop any existing ginxsom processes
echo "Stopping existing ginxsom processes..."
sudo pkill -f ginxsom-fcgi || true
sleep 2
# Remove old socket
sudo rm -f $REMOTE_SOCKET
# Install new binary
echo "Installing new binary..."
sudo mv ~/ginxsom-fcgi_new $REMOTE_BINARY_PATH
sudo chmod +x $REMOTE_BINARY_PATH
sudo chown www-data:www-data $REMOTE_BINARY_PATH
echo "Binary installed successfully"
EOF
if [ $? -eq 0 ]; then
print_success "Binary installed"
else
print_error "Failed to install binary"
exit 1
fi
echo ""
# Step 8: Start ginxsom through systemd (keeps process supervised and auto-restarted)
print_status "Step 8: Starting ginxsom service..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
echo "Restarting systemd service..."
sudo systemctl restart ginxsom.service
sleep 2
if sudo systemctl is-active --quiet ginxsom.service; then
echo "Service is active"
else
echo "ERROR: ginxsom.service is not active"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Verify socket is listening
if [ -S /tmp/ginxsom-fcgi.sock ]; then
echo "FastCGI socket created successfully"
ls -la /tmp/ginxsom-fcgi.sock
else
echo "ERROR: Socket not created"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Show process list for diagnostics
pgrep -af ginxsom-fcgi || true
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI service started"
else
print_error "Failed to start FastCGI service"
exit 1
fi
echo ""
# Step 8: Test nginx configuration and reload
print_status "Step 8: Testing and reloading nginx..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
# Test nginx configuration
if sudo nginx -t; then
if sudo nginx -t 2>&1; then
echo "Nginx configuration test passed"
sudo nginx -s reload
echo "Nginx reloaded successfully"
else
echo "Nginx configuration test failed"
exit 1
echo "WARNING: Nginx configuration test failed"
echo "You may need to update nginx configuration manually"
echo "See docs/STATIC_DEPLOYMENT_PLAN.md for details"
fi
EOF
print_success "Nginx reloaded"
if [ $? -eq 0 ]; then
print_success "Nginx reloaded"
else
print_warning "Nginx reload had issues - check configuration"
fi
echo ""
# Step 7: Test deployment
print_status "Testing deployment..."
# Step 9: Test deployment
print_status "Step 9: Testing deployment..."
echo ""
# Wait a moment for service to fully start
sleep 2
VERIFY_FAILED=false
# Test health endpoint
echo "Testing health endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
print_success "Health check passed"
print_success "Health check passed"
else
print_warning "Health check failed - checking response..."
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
print_error "Health check failed"
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10 || true
VERIFY_FAILED=true
fi
# Test basic endpoints
# Test API health endpoint
echo ""
echo "Testing API health endpoint..."
API_HEALTH_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/health" || echo "000")
if [[ "$API_HEALTH_STATUS" == "200" ]]; then
print_success "✓ API health endpoint responding (200)"
else
print_error "✗ API health endpoint failed (HTTP $API_HEALTH_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/health" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test files API endpoint
echo ""
echo "Testing files API endpoint..."
API_FILES_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" || echo "000")
if [[ "$API_FILES_STATUS" == "200" ]]; then
print_success "✓ Files API endpoint responding (200)"
elif [[ "$API_FILES_STATUS" == "401" || "$API_FILES_STATUS" == "403" ]]; then
print_success "✓ Files API endpoint reachable (HTTP $API_FILES_STATUS indicates auth is enforced)"
else
print_error "✗ Files API endpoint failed (HTTP $API_FILES_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test root endpoint
echo ""
echo "Testing root endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/" | grep -q "Ginxsom"; then
print_success "Root endpoint responding"
print_success "Root endpoint responding"
else
print_warning "Root endpoint not responding as expected - checking response..."
curl -k -v --max-time 10 "https://blossom.laantungir.net/" 2>&1 | head -10
print_warning "Root endpoint not responding as expected"
fi
print_success "Deployment to $REMOTE_HOST completed!"
print_status "Ginxsom should now be available at: https://blossom.laantungir.net"
print_status "Test endpoints:"
if [ "$VERIFY_FAILED" = true ]; then
print_error "Deployment verification failed - one or more critical endpoints are unhealthy"
exit 1
fi
echo ""
print_status "=========================================="
print_success "Deployment completed!"
print_status "=========================================="
echo ""
print_status "Service Information:"
echo " URL: https://blossom.laantungir.net"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Blobs: $REMOTE_BLOB_DIR"
echo " Socket: $REMOTE_SOCKET"
echo ""
print_status "Test Commands:"
echo " Health: curl -k https://blossom.laantungir.net/health"
echo " Root: curl -k https://blossom.laantungir.net/"
echo " List: curl -k https://blossom.laantungir.net/list"
if [ "$FRESH_INSTALL" = "true" ]; then
echo " API Health: curl -k https://blossom.laantungir.net/api/health"
echo " Files API: curl -k 'https://blossom.laantungir.net/api/files?limit=5'"
echo " Info: curl -k https://blossom.laantungir.net/"
echo " Upload: ./tests/file_put_bud02.sh"
echo ""
print_status "Server Commands:"
echo " Check status: ssh $REMOTE_USER@$REMOTE_HOST 'ps aux | grep ginxsom-fcgi'"
echo " View logs: ssh $REMOTE_USER@$REMOTE_HOST 'sudo journalctl -f | grep ginxsom'"
echo " Restart: ssh $REMOTE_USER@$REMOTE_HOST 'sudo pkill ginxsom-fcgi && sudo spawn-fcgi ...'"
echo ""
if [ "$FRESH_INSTALL" = true ]; then
print_warning "Fresh install completed - database and blobs have been reset"
fi
else
print_status "Existing data preserved - verify database and blobs"
echo " Check blobs: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_BLOB_DIR | wc -l'"
echo " Check DB: ssh $REMOTE_USER@$REMOTE_HOST 'sudo -u www-data sqlite3 $REMOTE_DB_PATH \"SELECT COUNT(*) FROM blobs;\"'"
fi
echo ""

162
deploy_static.sh Executable file
View File

@@ -0,0 +1,162 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Configuration
REMOTE_HOST="laantungir.net"
REMOTE_USER="ubuntu"
REMOTE_DIR="/home/ubuntu/ginxsom"
REMOTE_BINARY_PATH="/home/ubuntu/ginxsom/ginxsom-fcgi_static"
REMOTE_SOCKET="/tmp/ginxsom-fcgi.sock"
REMOTE_DATA_DIR="/var/www/html/blossom"
REMOTE_DB_PATH="/home/ubuntu/ginxsom/db/ginxsom.db"
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
BINARY_NAME="ginxsom-fcgi_static_x86_64"
;;
aarch64|arm64)
BINARY_NAME="ginxsom-fcgi_static_arm64"
;;
*)
print_error "Unsupported architecture: $ARCH"
exit 1
;;
esac
LOCAL_BINARY="./build/$BINARY_NAME"
print_status "Starting static binary deployment to $REMOTE_HOST..."
# Check if static binary exists
if [ ! -f "$LOCAL_BINARY" ]; then
print_error "Static binary not found: $LOCAL_BINARY"
print_status "Building static binary..."
./build_static.sh
if [ ! -f "$LOCAL_BINARY" ]; then
print_error "Build failed - binary still not found"
exit 1
fi
fi
print_success "Static binary found: $LOCAL_BINARY"
print_status "Binary size: $(du -h "$LOCAL_BINARY" | cut -f1)"
# Verify binary is static
if ldd "$LOCAL_BINARY" 2>&1 | grep -q "not a dynamic executable"; then
print_success "Binary is fully static"
elif ldd "$LOCAL_BINARY" 2>&1 | grep -q "statically linked"; then
print_success "Binary is statically linked"
else
print_warning "Binary may have dynamic dependencies"
ldd "$LOCAL_BINARY" 2>&1 || true
fi
# Setup remote environment
print_status "Setting up remote environment..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
# Create directories
mkdir -p /home/ubuntu/ginxsom/db
sudo mkdir -p /var/www/html/blossom
sudo chown www-data:www-data /var/www/html/blossom
sudo chmod 755 /var/www/html/blossom
# Stop existing processes
echo "Stopping existing ginxsom processes..."
sudo pkill -f ginxsom-fcgi || true
sudo rm -f /tmp/ginxsom-fcgi.sock || true
echo "Remote environment ready"
EOF
print_success "Remote environment configured"
# Copy static binary
print_status "Copying static binary to remote server..."
scp "$LOCAL_BINARY" $REMOTE_USER@$REMOTE_HOST:$REMOTE_BINARY_PATH
print_success "Binary copied successfully"
# Set permissions and start service
print_status "Starting ginxsom FastCGI process..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Make binary executable
chmod +x $REMOTE_BINARY_PATH
# Clean up any existing socket
sudo rm -f $REMOTE_SOCKET
# Start FastCGI process
echo "Starting ginxsom FastCGI..."
sudo spawn-fcgi -M 666 -u www-data -g www-data -s $REMOTE_SOCKET -U www-data -G www-data -d $REMOTE_DIR -- $REMOTE_BINARY_PATH --db-path "$REMOTE_DB_PATH" --storage-dir "$REMOTE_DATA_DIR"
# Give it a moment to start
sleep 2
# Verify process is running
if pgrep -f "ginxsom-fcgi" > /dev/null; then
echo "FastCGI process started successfully"
echo "PID: \$(pgrep -f ginxsom-fcgi)"
else
echo "Process verification: socket exists"
ls -la $REMOTE_SOCKET
fi
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI process started"
else
print_error "Failed to start FastCGI process"
exit 1
fi
# Reload nginx
print_status "Reloading nginx..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
if sudo nginx -t; then
sudo nginx -s reload
echo "Nginx reloaded successfully"
else
echo "Nginx configuration test failed"
exit 1
fi
EOF
print_success "Nginx reloaded"
# Test deployment
print_status "Testing deployment..."
echo "Testing health endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
print_success "Health check passed"
else
print_warning "Health check failed - checking response..."
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
fi
print_success "Deployment to $REMOTE_HOST completed!"
print_status "Ginxsom should now be available at: https://blossom.laantungir.net"
print_status ""
print_status "Deployment Summary:"
echo " Binary: $BINARY_NAME"
echo " Size: $(du -h "$LOCAL_BINARY" | cut -f1)"
echo " Type: Fully static MUSL binary"
echo " Portability: Works on any Linux distribution"
echo " Deployment time: ~10 seconds (vs ~5 minutes for dynamic build)"

302
docs/AUTH_RULES_STATUS.md Normal file
View File

@@ -0,0 +1,302 @@
# Auth Rules Management System - Current Status
## Executive Summary
The auth rules management system is **fully implemented** with a database schema that differs from c-relay. This document outlines the current state and proposes alignment with c-relay's schema.
## Current Database Schema
### Ginxsom Schema (Current)
```sql
CREATE TABLE auth_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL, -- 'pubkey_blacklist', 'pubkey_whitelist', etc.
rule_target TEXT NOT NULL, -- The pubkey, hash, or MIME type to match
operation TEXT NOT NULL DEFAULT '*', -- 'upload', 'delete', 'list', or '*'
enabled INTEGER NOT NULL DEFAULT 1, -- 1 = enabled, 0 = disabled
priority INTEGER NOT NULL DEFAULT 100,-- Lower number = higher priority
description TEXT, -- Human-readable description
created_by TEXT, -- Admin pubkey who created the rule
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist',
'hash_blacklist', 'mime_blacklist', 'mime_whitelist')),
CHECK (operation IN ('upload', 'delete', 'list', '*')),
CHECK (enabled IN (0, 1)),
CHECK (priority >= 0),
UNIQUE(rule_type, rule_target, operation)
);
```
### C-Relay Schema (Target)
```sql
CREATE TABLE auth_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL,
pattern_type TEXT NOT NULL,
pattern_value TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
```
## Schema Differences
| Field | Ginxsom | C-Relay | Notes |
|-------|---------|---------|-------|
| `id` | ✅ | ✅ | Same |
| `rule_type` | ✅ | ✅ | Same |
| `rule_target` | ✅ | ❌ | Ginxsom-specific |
| `pattern_type` | ❌ | ✅ | C-relay-specific |
| `pattern_value` | ❌ | ✅ | C-relay-specific |
| `operation` | ✅ | ❌ | Ginxsom-specific |
| `enabled` | ✅ (1/0) | ❌ | Ginxsom uses `enabled` |
| `active` | ❌ | ✅ (1/0) | C-relay uses `active` |
| `priority` | ✅ | ❌ | Ginxsom-specific |
| `description` | ✅ | ❌ | Ginxsom-specific |
| `created_by` | ✅ | ❌ | Ginxsom-specific |
| `created_at` | ✅ | ✅ | Same |
| `updated_at` | ✅ | ✅ | Same |
## What Has Been Implemented
### ✅ Database Layer
- **Schema Created**: [`auth_rules`](../db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db) table exists with full schema
- **Indexes**: 5 indexes for performance optimization
- **Constraints**: CHECK constraints for data validation
- **Unique Constraint**: Prevents duplicate rules
### ✅ Rule Evaluation Engine
Location: [`src/request_validator.c:1318-1592`](../src/request_validator.c#L1318-L1592)
**Implemented Features:**
1. **Pubkey Blacklist** (Priority 1) - Lines 1346-1377
2. **Hash Blacklist** (Priority 2) - Lines 1382-1420
3. **MIME Blacklist** (Priority 3) - Lines 1423-1462
4. **Pubkey Whitelist** (Priority 4) - Lines 1464-1491
5. **MIME Whitelist** (Priority 5) - Lines 1493-1526
6. **Whitelist Default Denial** (Priority 6) - Lines 1528-1591
**Features:**
- ✅ Priority-based rule evaluation
- ✅ Wildcard operation matching (`*`)
- ✅ MIME type pattern matching (`image/*`)
- ✅ Whitelist default-deny behavior
- ✅ Detailed violation tracking
- ✅ Performance-optimized queries
### ✅ Admin API Commands
Location: [`src/admin_commands.c`](../src/admin_commands.c)
**Implemented Commands:**
-`config_query` - Query configuration values
-`config_update` - Update configuration
-`stats_query` - Get system statistics (includes auth_rules count)
-`system_status` - System health check
-`blob_list` - List stored blobs
-`storage_stats` - Storage statistics
-`sql_query` - Direct SQL queries (read-only)
**Note:** The stats_query command already queries auth_rules:
```c
// Line 390-395
sql = "SELECT COUNT(*) FROM auth_rules WHERE enabled = 1";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) {
cJSON_AddNumberToObject(stats, "active_auth_rules", sqlite3_column_int(stmt, 0));
}
```
### ❌ Missing Admin API Endpoints
The following endpoints from [`docs/AUTH_RULES_IMPLEMENTATION_PLAN.md`](../docs/AUTH_RULES_IMPLEMENTATION_PLAN.md) are **NOT implemented**:
1. **GET /api/rules** - List authentication rules
2. **POST /api/rules** - Create new rule
3. **PUT /api/rules/:id** - Update existing rule
4. **DELETE /api/rules/:id** - Delete rule
5. **POST /api/rules/clear-cache** - Clear auth cache
6. **GET /api/rules/test** - Test rule evaluation
### ✅ Configuration System
-`auth_rules_enabled` config flag (checked in [`reload_auth_config()`](../src/request_validator.c#L1049-L1145))
- ✅ Cache system with 5-minute TTL
- ✅ Environment variable support (`GINX_NO_CACHE`, `GINX_CACHE_TIMEOUT`)
### ✅ Documentation
- ✅ [`docs/AUTH_API.md`](../docs/AUTH_API.md) - Complete authentication flow
- ✅ [`docs/AUTH_RULES_IMPLEMENTATION_PLAN.md`](../docs/AUTH_RULES_IMPLEMENTATION_PLAN.md) - Implementation plan
- ✅ Flow diagrams and performance metrics
## Proposed Schema Migration to C-Relay Format
### Option 1: Minimal Changes (Recommended)
Keep Ginxsom's richer schema but rename fields for compatibility:
```sql
ALTER TABLE auth_rules RENAME COLUMN enabled TO active;
ALTER TABLE auth_rules ADD COLUMN pattern_type TEXT;
ALTER TABLE auth_rules ADD COLUMN pattern_value TEXT;
-- Populate new fields from existing data
UPDATE auth_rules SET
pattern_type = CASE
WHEN rule_type LIKE '%pubkey%' THEN 'pubkey'
WHEN rule_type LIKE '%hash%' THEN 'hash'
WHEN rule_type LIKE '%mime%' THEN 'mime'
END,
pattern_value = rule_target;
```
**Pros:**
- Maintains all Ginxsom features (operation, priority, description)
- Adds c-relay compatibility fields
- No data loss
- Backward compatible
**Cons:**
- Redundant fields (`rule_target` + `pattern_value`)
- Larger schema
### Option 2: Full Migration to C-Relay Schema
Drop Ginxsom-specific fields and adopt c-relay schema:
```sql
-- Create new table with c-relay schema
CREATE TABLE auth_rules_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL,
pattern_type TEXT NOT NULL,
pattern_value TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- Migrate data
INSERT INTO auth_rules_new (id, rule_type, pattern_type, pattern_value, active, created_at, updated_at)
SELECT
id,
rule_type,
CASE
WHEN rule_type LIKE '%pubkey%' THEN 'pubkey'
WHEN rule_type LIKE '%hash%' THEN 'hash'
WHEN rule_type LIKE '%mime%' THEN 'mime'
END as pattern_type,
rule_target as pattern_value,
enabled as active,
created_at,
updated_at
FROM auth_rules;
-- Replace old table
DROP TABLE auth_rules;
ALTER TABLE auth_rules_new RENAME TO auth_rules;
```
**Pros:**
- Full c-relay compatibility
- Simpler schema
- Smaller database
**Cons:**
- **Loss of operation-specific rules** (upload/delete/list)
- **Loss of priority system**
- **Loss of description and created_by tracking**
- **Breaking change** - requires code updates in [`request_validator.c`](../src/request_validator.c)
## Code Impact Analysis
### Files Requiring Updates for C-Relay Schema
1. **[`src/request_validator.c`](../src/request_validator.c)**
- Lines 1346-1591: Rule evaluation queries need field name changes
- Change `enabled``active`
- Change `rule_target``pattern_value`
- Add `pattern_type` to queries if using Option 1
2. **[`src/admin_commands.c`](../src/admin_commands.c)**
- Line 390: Stats query uses `enabled` field
- Any future rule management endpoints
3. **[`docs/AUTH_RULES_IMPLEMENTATION_PLAN.md`](../docs/AUTH_RULES_IMPLEMENTATION_PLAN.md)**
- Update schema documentation
- Update API endpoint specifications
## Recommendations
### For C-Relay Alignment
**Use Option 1 (Minimal Changes)** because:
1. Preserves Ginxsom's advanced features (operation-specific rules, priority)
2. Adds c-relay compatibility without breaking existing functionality
3. Minimal code changes required
4. No data loss
### For Admin API Completion
Implement the missing endpoints in priority order:
1. **POST /api/rules** - Create rules (highest priority)
2. **GET /api/rules** - List rules
3. **DELETE /api/rules/:id** - Delete rules
4. **PUT /api/rules/:id** - Update rules
5. **GET /api/rules/test** - Test rules
6. **POST /api/rules/clear-cache** - Clear cache
### Migration Script
```bash
#!/bin/bash
# migrate_auth_rules_to_crelay.sh
DB_PATH="db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db"
sqlite3 "$DB_PATH" <<EOF
-- Backup current table
CREATE TABLE auth_rules_backup AS SELECT * FROM auth_rules;
-- Add c-relay compatibility fields
ALTER TABLE auth_rules ADD COLUMN pattern_type TEXT;
ALTER TABLE auth_rules ADD COLUMN pattern_value TEXT;
-- Populate new fields
UPDATE auth_rules SET
pattern_type = CASE
WHEN rule_type LIKE '%pubkey%' THEN 'pubkey'
WHEN rule_type LIKE '%hash%' THEN 'hash'
WHEN rule_type LIKE '%mime%' THEN 'mime'
END,
pattern_value = rule_target;
-- Rename enabled to active for c-relay compatibility
-- Note: SQLite doesn't support RENAME COLUMN directly in older versions
-- So we'll keep both fields for now
ALTER TABLE auth_rules ADD COLUMN active INTEGER NOT NULL DEFAULT 1;
UPDATE auth_rules SET active = enabled;
-- Verify migration
SELECT COUNT(*) as total_rules FROM auth_rules;
SELECT COUNT(*) as rules_with_pattern FROM auth_rules WHERE pattern_type IS NOT NULL;
EOF
```
## Summary
**Current State:**
- ✅ Database schema exists and is functional
- ✅ Rule evaluation engine fully implemented
- ✅ Configuration system working
- ✅ Documentation complete
- ❌ Admin API endpoints for rule management missing
**To Align with C-Relay:**
- Add `pattern_type` and `pattern_value` fields
- Optionally rename `enabled` to `active`
- Keep Ginxsom's advanced features (operation, priority, description)
- Update queries to use new field names
**Next Steps:**
1. Decide on migration strategy (Option 1 recommended)
2. Run migration script
3. Update code to use new field names
4. Implement missing Admin API endpoints
5. Test rule evaluation with new schema

388
docs/NEW_DEPLOY_SCRIPT.md Normal file
View File

@@ -0,0 +1,388 @@
# New deploy_lt.sh Script
This is the complete new deployment script for static binary deployment. Save this as `deploy_lt.sh` in the project root.
```bash
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Parse command line arguments
FRESH_INSTALL=false
MIGRATE_DATA=true
if [[ "$1" == "--fresh" ]]; then
FRESH_INSTALL=true
MIGRATE_DATA=false
elif [[ "$1" == "--no-migrate" ]]; then
MIGRATE_DATA=false
fi
# Configuration
REMOTE_HOST="laantungir.net"
REMOTE_USER="ubuntu"
# New paths (static binary deployment)
REMOTE_BINARY_DIR="/usr/local/bin/ginxsom"
REMOTE_BINARY_PATH="$REMOTE_BINARY_DIR/ginxsom-fcgi"
REMOTE_DB_DIR="/var/lib/ginxsom"
REMOTE_DB_PATH="$REMOTE_DB_DIR/ginxsom.db"
REMOTE_BLOB_DIR="/var/www/blobs"
REMOTE_SOCKET="/tmp/ginxsom-fcgi.sock"
# Old paths (for migration)
OLD_BINARY_PATH="/home/ubuntu/ginxsom/ginxsom.fcgi"
OLD_DB_PATH="/home/ubuntu/ginxsom/db/ginxsom.db"
OLD_BLOB_DIR="/var/www/html/blossom"
# Local paths
LOCAL_BINARY="build/ginxsom-fcgi_static_x86_64"
print_status "=========================================="
print_status "Ginxsom Static Binary Deployment"
print_status "=========================================="
print_status "Target: $REMOTE_HOST"
print_status "Binary: $REMOTE_BINARY_PATH"
print_status "Database: $REMOTE_DB_PATH"
print_status "Blobs: $REMOTE_BLOB_DIR"
print_status "Fresh install: $FRESH_INSTALL"
print_status "Migrate data: $MIGRATE_DATA"
print_status "=========================================="
echo ""
# Step 1: Verify local binary exists
print_status "Step 1: Verifying local static binary..."
if [[ ! -f "$LOCAL_BINARY" ]]; then
print_error "Static binary not found: $LOCAL_BINARY"
print_status "Please run: ./build_static.sh"
exit 1
fi
# Verify it's actually static
if ldd "$LOCAL_BINARY" 2>&1 | grep -q "not a dynamic executable\|statically linked"; then
print_success "Binary is static"
else
print_warning "Binary may not be fully static - proceeding anyway"
fi
BINARY_SIZE=$(du -h "$LOCAL_BINARY" | cut -f1)
print_success "Found static binary ($BINARY_SIZE)"
echo ""
# Step 2: Upload binary to server
print_status "Step 2: Uploading binary to server..."
scp "$LOCAL_BINARY" $REMOTE_USER@$REMOTE_HOST:/tmp/ginxsom-fcgi_new || {
print_error "Failed to upload binary"
exit 1
}
print_success "Binary uploaded to /tmp/ginxsom-fcgi_new"
echo ""
# Step 3: Setup directories and install binary
print_status "Step 3: Setting up directories and installing binary..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
set -e
# Create binary directory
echo "Creating binary directory..."
sudo mkdir -p $REMOTE_BINARY_DIR
# Create database directory
echo "Creating database directory..."
sudo mkdir -p $REMOTE_DB_DIR/backups
sudo chown www-data:www-data $REMOTE_DB_DIR
sudo chmod 755 $REMOTE_DB_DIR
# Create blob storage directory
echo "Creating blob storage directory..."
sudo mkdir -p $REMOTE_BLOB_DIR
sudo chown www-data:www-data $REMOTE_BLOB_DIR
sudo chmod 755 $REMOTE_BLOB_DIR
echo "Directories created successfully"
EOF
if [ $? -ne 0 ]; then
print_error "Failed to create directories"
exit 1
fi
print_success "Directories created"
echo ""
# Step 4: Migrate data if requested
if [ "$MIGRATE_DATA" = true ] && [ "$FRESH_INSTALL" = false ]; then
print_status "Step 4: Migrating existing data..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
set -e
# Migrate database
if [ -f $OLD_DB_PATH ]; then
echo "Migrating database from $OLD_DB_PATH..."
sudo cp $OLD_DB_PATH $REMOTE_DB_PATH
sudo chown www-data:www-data $REMOTE_DB_PATH
sudo chmod 644 $REMOTE_DB_PATH
echo "Database migrated"
elif [ -f $OLD_BLOB_DIR/ginxsom.db ]; then
echo "Migrating database from $OLD_BLOB_DIR/ginxsom.db..."
sudo cp $OLD_BLOB_DIR/ginxsom.db $REMOTE_DB_PATH
sudo chown www-data:www-data $REMOTE_DB_PATH
sudo chmod 644 $REMOTE_DB_PATH
echo "Database migrated"
else
echo "No existing database found - will be created on first run"
fi
# Migrate blobs
if [ -d $OLD_BLOB_DIR ] && [ "\$(ls -A $OLD_BLOB_DIR 2>/dev/null)" ]; then
echo "Migrating blobs from $OLD_BLOB_DIR..."
# Copy only blob files (SHA256 hashes with extensions)
sudo find $OLD_BLOB_DIR -type f -regextype posix-extended -regex '.*/[a-f0-9]{64}\.[a-z0-9]+' -exec cp {} $REMOTE_BLOB_DIR/ \; 2>/dev/null || true
sudo chown -R www-data:www-data $REMOTE_BLOB_DIR
BLOB_COUNT=\$(ls -1 $REMOTE_BLOB_DIR | wc -l)
echo "Migrated \$BLOB_COUNT blob files"
else
echo "No existing blobs found"
fi
EOF
if [ $? -eq 0 ]; then
print_success "Data migration completed"
else
print_warning "Data migration had issues - check manually"
fi
echo ""
elif [ "$FRESH_INSTALL" = true ]; then
print_status "Step 4: Fresh install - removing existing data..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
sudo rm -f $REMOTE_DB_PATH
sudo rm -rf $REMOTE_BLOB_DIR/*
echo "Existing data removed"
EOF
print_success "Fresh install prepared"
echo ""
else
print_status "Step 4: Skipping data migration (--no-migrate)"
echo ""
fi
# Step 5: Install minimal dependencies
print_status "Step 5: Installing minimal dependencies..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
# Check if spawn-fcgi is installed
if ! command -v spawn-fcgi &> /dev/null; then
echo "Installing spawn-fcgi..."
sudo apt-get update -qq
sudo apt-get install -y spawn-fcgi
echo "spawn-fcgi installed"
else
echo "spawn-fcgi already installed"
fi
EOF
if [ $? -eq 0 ]; then
print_success "Dependencies verified"
else
print_error "Failed to install dependencies"
exit 1
fi
echo ""
# Step 6: Stop existing service and install new binary
print_status "Step 6: Stopping existing service and installing new binary..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
set -e
# Stop any existing ginxsom processes
echo "Stopping existing ginxsom processes..."
sudo pkill -f ginxsom-fcgi || true
sleep 2
# Remove old socket
sudo rm -f $REMOTE_SOCKET
# Install new binary
echo "Installing new binary..."
sudo mv /tmp/ginxsom-fcgi_new $REMOTE_BINARY_PATH
sudo chmod +x $REMOTE_BINARY_PATH
sudo chown root:root $REMOTE_BINARY_PATH
echo "Binary installed successfully"
EOF
if [ $? -eq 0 ]; then
print_success "Binary installed"
else
print_error "Failed to install binary"
exit 1
fi
echo ""
# Step 7: Start ginxsom FastCGI process
print_status "Step 7: Starting ginxsom FastCGI process..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
set -e
echo "Starting ginxsom FastCGI with configuration:"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Storage: $REMOTE_BLOB_DIR"
echo " Socket: $REMOTE_SOCKET"
echo ""
sudo spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s $REMOTE_SOCKET \
-U www-data \
-G www-data \
-d $REMOTE_DB_DIR \
-- $REMOTE_BINARY_PATH \
--db-path $REMOTE_DB_PATH \
--storage-dir $REMOTE_BLOB_DIR
# Give it a moment to start
sleep 2
# Verify process is running
if [ -S $REMOTE_SOCKET ]; then
echo "FastCGI socket created successfully"
ls -la $REMOTE_SOCKET
else
echo "ERROR: Socket not created"
exit 1
fi
# Check if process is running
if pgrep -f ginxsom-fcgi > /dev/null; then
echo "Process is running (PID: \$(pgrep -f ginxsom-fcgi))"
else
echo "WARNING: Process not found by pgrep (may be normal for FastCGI)"
fi
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI process started"
else
print_error "Failed to start FastCGI process"
exit 1
fi
echo ""
# Step 8: Test nginx configuration and reload
print_status "Step 8: Testing and reloading nginx..."
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
# Test nginx configuration
if sudo nginx -t 2>&1; then
echo "Nginx configuration test passed"
sudo nginx -s reload
echo "Nginx reloaded successfully"
else
echo "WARNING: Nginx configuration test failed"
echo "You may need to update nginx configuration manually"
echo "See docs/STATIC_DEPLOYMENT_PLAN.md for details"
fi
EOF
if [ $? -eq 0 ]; then
print_success "Nginx reloaded"
else
print_warning "Nginx reload had issues - check configuration"
fi
echo ""
# Step 9: Test deployment
print_status "Step 9: Testing deployment..."
echo ""
# Wait a moment for service to fully start
sleep 2
# Test health endpoint
echo "Testing health endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
print_success "✓ Health check passed"
else
print_warning "✗ Health check failed - checking response..."
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
fi
# Test root endpoint
echo ""
echo "Testing root endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/" | grep -q "Ginxsom"; then
print_success "✓ Root endpoint responding"
else
print_warning "✗ Root endpoint not responding as expected"
fi
echo ""
print_status "=========================================="
print_success "Deployment completed!"
print_status "=========================================="
echo ""
print_status "Service Information:"
echo " URL: https://blossom.laantungir.net"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Blobs: $REMOTE_BLOB_DIR"
echo " Socket: $REMOTE_SOCKET"
echo ""
print_status "Test Commands:"
echo " Health: curl -k https://blossom.laantungir.net/health"
echo " Info: curl -k https://blossom.laantungir.net/"
echo " Upload: ./tests/file_put_bud02.sh"
echo ""
print_status "Server Commands:"
echo " Check status: ssh $REMOTE_USER@$REMOTE_HOST 'ps aux | grep ginxsom-fcgi'"
echo " View logs: ssh $REMOTE_USER@$REMOTE_HOST 'sudo journalctl -f | grep ginxsom'"
echo " Restart: ssh $REMOTE_USER@$REMOTE_HOST 'sudo pkill ginxsom-fcgi && sudo spawn-fcgi ...'"
echo ""
if [ "$FRESH_INSTALL" = true ]; then
print_warning "Fresh install completed - database and blobs have been reset"
fi
if [ "$MIGRATE_DATA" = true ] && [ "$FRESH_INSTALL" = false ]; then
print_status "Data migration completed - verify blob count and database"
echo " Check blobs: ssh $REMOTE_USER@$REMOTE_HOST 'ls -la $REMOTE_BLOB_DIR | wc -l'"
echo " Check DB: ssh $REMOTE_USER@$REMOTE_HOST 'sudo -u www-data sqlite3 $REMOTE_DB_PATH \"SELECT COUNT(*) FROM blobs;\"'"
fi
echo ""
print_status "For nginx configuration updates, see: docs/STATIC_DEPLOYMENT_PLAN.md"
print_status "=========================================="
```
## Usage
```bash
# Normal deployment with data migration
./deploy_lt.sh
# Fresh install (removes all data)
./deploy_lt.sh --fresh
# Deploy without migrating data
./deploy_lt.sh --no-migrate
```
## Key Changes from Old Script
1. **No remote compilation** - uploads pre-built static binary
2. **New directory structure** - follows FHS standards
3. **Minimal dependencies** - only spawn-fcgi needed
4. **Data migration** - automatically migrates from old locations
5. **Simplified process** - ~30 seconds vs ~5-10 minutes

View File

@@ -0,0 +1,478 @@
# Nginx Configuration Updates for Static Binary Deployment
## Overview
This document describes the required nginx configuration changes to support the new static binary deployment with updated directory paths.
## Changes Required
### 1. Blob Storage Root Directory
**Change from:**
```nginx
root /var/www/html/blossom;
```
**Change to:**
```nginx
root /var/www/blobs;
```
### 2. FastCGI Script Filename
**Change from:**
```nginx
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
```
**Change to:**
```nginx
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
```
## Complete Updated Configuration
Save this as `/etc/nginx/conf.d/default.conf` on the server (or update the existing file):
```nginx
# FastCGI upstream configuration
upstream ginxsom_backend {
server unix:/tmp/ginxsom-fcgi.sock;
}
# Main domains
server {
if ($host = laantungir.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name laantungir.com www.laantungir.com laantungir.net www.laantungir.net laantungir.org www.laantungir.org;
root /var/www/html;
index index.html index.htm;
# CORS for Nostr NIP-05 verification
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
location / {
try_files $uri $uri/ =404;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/html;
}
}
# Main domains HTTPS - using the main certificate
server {
listen 443 ssl;
server_name laantungir.com www.laantungir.com laantungir.net www.laantungir.net laantungir.org www.laantungir.org;
ssl_certificate /etc/letsencrypt/live/laantungir.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/laantungir.net/privkey.pem; # managed by Certbot
root /var/www/html;
index index.html index.htm;
# CORS for Nostr NIP-05 verification
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
location / {
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/html;
}
}
# Blossom subdomains HTTP - redirect to HTTPS (keep for ACME)
server {
listen 80;
server_name blossom.laantungir.net;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# Blossom subdomains HTTPS - ginxsom FastCGI
server {
listen 443 ssl;
server_name blossom.laantungir.net;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
# CORS for Blossom protocol
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
# UPDATED: Root directory for blob storage
root /var/www/blobs;
# Maximum upload size
client_max_body_size 100M;
# OPTIONS preflight handler
if ($request_method = OPTIONS) {
return 204;
}
# PUT /upload - File uploads
location = /upload {
if ($request_method !~ ^(PUT|HEAD)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# GET /list/<pubkey> - List user blobs
location ~ "^/list/([a-f0-9]{64})$" {
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# PUT /mirror - Mirror content
location = /mirror {
if ($request_method !~ ^(PUT)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# PUT /report - Report content
location = /report {
if ($request_method !~ ^(PUT)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# GET /auth - NIP-42 challenges
location = /auth {
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# Admin API
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# Blob serving - SHA256 patterns
location ~ "^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$" {
# Handle DELETE via rewrite
if ($request_method = DELETE) {
rewrite ^/(.*)$ /fcgi-delete/$1 last;
}
# Route HEAD to FastCGI
if ($request_method = HEAD) {
rewrite ^/(.*)$ /fcgi-head/$1 last;
}
# GET requests - serve files directly
if ($request_method != GET) {
return 405;
}
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
# Cache headers
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Internal FastCGI handlers
location ~ "^/fcgi-delete/([a-f0-9]{64}).*$" {
internal;
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
fastcgi_param REQUEST_URI /$1;
}
location ~ "^/fcgi-head/([a-f0-9]{64}).*$" {
internal;
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
fastcgi_param REQUEST_URI /$1;
}
# Health check
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
}
# Default location - Server info from FastCGI
location / {
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
# UPDATED: Direct path to binary
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
}
server {
listen 80;
server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 60s;
proxy_buffering off;
proxy_request_buffering off;
gzip off;
}
}
# Relay HTTPS - proxy to c-relay
server {
listen 443 ssl;
server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 60s;
proxy_buffering off;
proxy_request_buffering off;
gzip off;
}
}
# Git subdomains HTTP - redirect to HTTPS
server {
listen 80;
server_name git.laantungir.com git.laantungir.net git.laantungir.org;
# Allow larger file uploads for Git releases
client_max_body_size 50M;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# Auth subdomains HTTP - redirect to HTTPS
server {
listen 80;
server_name auth.laantungir.com auth.laantungir.net auth.laantungir.org;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
}
}
# Git subdomains HTTPS - proxy to gitea
server {
listen 443 ssl;
server_name git.laantungir.com git.laantungir.net git.laantungir.org;
# Allow larger file uploads for Git releases
client_max_body_size 50M;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 60s;
gzip off;
proxy_set_header Host $host;
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# Auth subdomains HTTPS - proxy to nostr-auth
server {
listen 443 ssl;
server_name auth.laantungir.com auth.laantungir.net auth.laantungir.org;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 60s;
gzip off;
proxy_set_header Host $host;
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
## Manual Update Steps
If you prefer to update the existing configuration manually:
```bash
# 1. Backup current configuration
ssh ubuntu@laantungir.net
sudo cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.backup
# 2. Edit the configuration
sudo nano /etc/nginx/conf.d/default.conf
# 3. Find and replace (in the blossom server block):
# - Change: root /var/www/html/blossom;
# - To: root /var/www/blobs;
# 4. Find and replace (all FastCGI locations):
# - Change: fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
# - To: fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
# 5. Test configuration
sudo nginx -t
# 6. If test passes, reload nginx
sudo nginx -s reload
# 7. If test fails, restore backup
sudo cp /etc/nginx/conf.d/default.conf.backup /etc/nginx/conf.d/default.conf
sudo nginx -s reload
```
## Verification
After updating the configuration:
```bash
# Check nginx syntax
sudo nginx -t
# Check if ginxsom is responding
curl -k https://blossom.laantungir.net/health
# Check blob serving (if you have existing blobs)
curl -k https://blossom.laantungir.net/<some-sha256-hash>.jpg
```
## Summary of Changes
| Item | Old Value | New Value |
|------|-----------|-----------|
| Blob root | `/var/www/html/blossom` | `/var/www/blobs` |
| Binary path | `$document_root/ginxsom.fcgi` | `/usr/local/bin/ginxsom/ginxsom-fcgi` |
| Binary location | `/home/ubuntu/ginxsom/ginxsom.fcgi` | `/usr/local/bin/ginxsom/ginxsom-fcgi` |
These changes align with the new static binary deployment architecture and Linux FHS standards.

296
docs/STATIC_BUILD.md Normal file
View File

@@ -0,0 +1,296 @@
# Ginxsom Static MUSL Build Guide
This guide explains how to build and deploy Ginxsom as a fully static MUSL binary with zero runtime dependencies.
## Overview
Ginxsom now supports building as a static MUSL binary using Alpine Linux and Docker. This produces a truly portable binary that works on **any Linux distribution** without requiring any system libraries.
## Benefits
| Feature | Static MUSL | Dynamic glibc |
|---------|-------------|---------------|
| **Portability** | ✓ Any Linux | ✗ Requires matching libs |
| **Dependencies** | None | libfcgi, libsqlite3, etc. |
| **Deployment** | Copy one file | Build on target |
| **Binary Size** | ~7-10 MB | ~2-3 MB + libraries |
| **Deployment Time** | ~10 seconds | ~5-10 minutes |
## Prerequisites
- Docker installed and running
- Internet connection (for first build only)
- ~2GB disk space for Docker images
## Quick Start
### 1. Build Static Binary
```bash
# Build production binary (optimized, stripped)
make static
# Or build debug binary (with symbols)
make static-debug
# Or use the script directly
./build_static.sh
./build_static.sh --debug
```
The binary will be created in `build/ginxsom-fcgi_static_x86_64` (or `_arm64` for ARM systems).
### 2. Verify Binary
```bash
# Check if truly static
ldd build/ginxsom-fcgi_static_x86_64
# Should output: "not a dynamic executable"
# Check file info
file build/ginxsom-fcgi_static_x86_64
# Should show: "statically linked"
# Check size
ls -lh build/ginxsom-fcgi_static_x86_64
```
### 3. Deploy to Server
```bash
# Use the simplified deployment script
./deploy_static.sh
# Or manually copy and start
scp build/ginxsom-fcgi_static_x86_64 user@server:/path/to/ginxsom/
ssh user@server
chmod +x /path/to/ginxsom/ginxsom-fcgi_static_x86_64
sudo spawn-fcgi -M 666 -u www-data -g www-data \
-s /tmp/ginxsom-fcgi.sock \
-- /path/to/ginxsom/ginxsom-fcgi_static_x86_64 \
--db-path /path/to/db/ginxsom.db \
--storage-dir /var/www/html/blossom
```
## Build Process Details
### What Happens During Build
1. **Docker Image Creation** (5-10 minutes first time, cached after):
- Uses Alpine Linux 3.19 (native MUSL)
- Builds secp256k1 statically
- Builds nostr_core_lib with required NIPs
- Embeds web interface files
- Compiles Ginxsom with full static linking
2. **Binary Extraction**:
- Extracts binary from Docker container
- Verifies static linking
- Makes executable
3. **Verification**:
- Checks for dynamic dependencies
- Reports file size
- Tests execution
### Docker Layers (Cached)
The Dockerfile uses multi-stage builds with caching:
```
Layer 1: Alpine base + dependencies (cached)
Layer 2: Build secp256k1 (cached)
Layer 3: Initialize git submodules (cached unless .gitmodules changes)
Layer 4: Build nostr_core_lib (cached unless nostr_core_lib changes)
Layer 5: Embed web files (cached unless api/ changes)
Layer 6: Build Ginxsom (rebuilds when src/ changes)
```
This means subsequent builds are **much faster** (~1-2 minutes) since only changed layers rebuild.
## Deployment Comparison
### Old Dynamic Build Deployment
```bash
# 1. Sync entire project (30 seconds)
rsync -avz . user@server:/path/
# 2. Build on remote server (5-10 minutes)
ssh user@server "cd /path && make clean && make"
# 3. Restart service (10 seconds)
ssh user@server "sudo systemctl restart ginxsom"
# Total: ~6-11 minutes
```
### New Static Build Deployment
```bash
# 1. Build locally once (5-10 minutes first time, cached after)
make static
# 2. Copy binary (10 seconds)
scp build/ginxsom-fcgi_static_x86_64 user@server:/path/
# 3. Restart service (10 seconds)
ssh user@server "sudo systemctl restart ginxsom"
# Total: ~20 seconds (after first build)
```
## Cleanup
### Automatic Cleanup
The static build script automatically cleans up old dynamic build artifacts (`.o` files and `ginxsom-fcgi` binary) after successfully building the static binary. This keeps your `build/` directory clean.
### Manual Cleanup
```bash
# Clean dynamic build artifacts (preserves static binaries)
make clean
# Clean everything including static binaries
make clean-all
# Or manually remove specific files
rm -f build/*.o
rm -f build/ginxsom-fcgi
rm -f build/ginxsom-fcgi_static_*
```
## Troubleshooting
### Docker Not Found
```bash
# Install Docker
sudo apt install docker.io
# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker
```
### Build Fails
```bash
# Clean Docker cache and rebuild
docker system prune -a
make static
```
### Binary Won't Run on Target
```bash
# Verify it's static
ldd build/ginxsom-fcgi_static_x86_64
# Check architecture matches
file build/ginxsom-fcgi_static_x86_64
uname -m # On target system
```
### Alpine Package Not Found
If you get errors about missing Alpine packages, the package name may have changed. Check Alpine's package database:
- https://pkgs.alpinelinux.org/packages
## Advanced Usage
### Cross-Compilation
Build for different architectures:
```bash
# Build for ARM64 on x86_64 machine
docker build --platform linux/arm64 -f Dockerfile.alpine-musl -t ginxsom-arm64 .
```
### Custom NIPs
Edit `Dockerfile.alpine-musl` line 66 to change which NIPs are included:
```dockerfile
./build.sh --nips=1,6,19 # Minimal
./build.sh --nips=1,6,13,17,19,44,59 # Full (default)
```
### Debug Build
```bash
# Build with debug symbols (no optimization)
make static-debug
# Binary will be larger but include debugging info
gdb build/ginxsom-fcgi_static_x86_64
```
## File Structure
```
ginxsom/
├── Dockerfile.alpine-musl # Alpine Docker build definition
├── build_static.sh # Build script wrapper
├── deploy_static.sh # Simplified deployment script
├── Makefile # Updated with 'static' target
└── build/
└── ginxsom-fcgi_static_x86_64 # Output binary
```
## CI/CD Integration
### GitHub Actions Example
```yaml
name: Build Static Binary
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Build static binary
run: make static
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: ginxsom-static
path: build/ginxsom-fcgi_static_x86_64
```
## Performance
Static MUSL binaries have minimal performance impact:
| Metric | Static MUSL | Dynamic glibc |
|--------|-------------|---------------|
| Startup Time | ~50ms | ~40ms |
| Memory Usage | Similar | Similar |
| Request Latency | Identical | Identical |
| Binary Size | 7-10 MB | 2-3 MB + libs |
The slight startup delay is negligible for a long-running FastCGI process.
## References
- [MUSL libc](https://musl.libc.org/)
- [Alpine Linux](https://alpinelinux.org/)
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
- [c-relay Static Build](../c-relay/STATIC_BUILD.md)
## Support
For issues with static builds:
1. Check Docker is running: `docker info`
2. Verify submodules: `git submodule status`
3. Clean and rebuild: `docker system prune -a && make static`
4. Check logs in Docker build output

View File

@@ -0,0 +1,383 @@
# Static MUSL Binary Deployment Plan
## Overview
This document outlines the deployment architecture for ginxsom using static MUSL binaries. The new approach eliminates remote compilation and simplifies deployment to a single binary upload.
## Architecture Changes
### Current Deployment (Old)
```
Local Machine:
- Build dynamic binary with make
- Upload entire project via rsync
- Remote server compiles from source
- Install dependencies (libsqlite3-dev, libfcgi-dev, etc.)
- Build nostr_core_lib submodules remotely
- Binary location: /home/ubuntu/ginxsom/ginxsom.fcgi
- Database: /home/ubuntu/ginxsom/db/ginxsom.db
- Blobs: /var/www/html/blossom/
```
### New Deployment (Static MUSL)
```
Local Machine:
- Build static MUSL binary with Docker (build_static.sh)
- Upload only the binary (no source code needed)
- No remote compilation required
- Minimal dependencies (only spawn-fcgi)
- Binary location: /usr/local/bin/ginxsom/ginxsom-fcgi
- Database: /var/lib/ginxsom/ginxsom.db
- Blobs: /var/www/blobs/
```
## Directory Structure
### Production Server Layout
```
/usr/local/bin/ginxsom/
├── ginxsom-fcgi # Static binary (executable)
└── README.md # Version info and deployment notes
/var/lib/ginxsom/
├── ginxsom.db # SQLite database
└── backups/ # Database backups
/var/www/blobs/
├── <sha256>.jpg # Blob files
├── <sha256>.png
└── ...
/tmp/
└── ginxsom-fcgi.sock # FastCGI socket
```
## Deployment Process
### Phase 1: Build Static Binary (Local)
```bash
# Build the static binary
./build_static.sh
# Output: build/ginxsom-fcgi_static_x86_64
# Size: ~7-10 MB
# Dependencies: NONE (fully static)
```
### Phase 2: Upload Binary
```bash
# Upload to server
scp build/ginxsom-fcgi_static_x86_64 ubuntu@laantungir.net:/tmp/
# Install to /usr/local/bin/ginxsom/
ssh ubuntu@laantungir.net << 'EOF'
sudo mkdir -p /usr/local/bin/ginxsom
sudo mv /tmp/ginxsom-fcgi_static_x86_64 /usr/local/bin/ginxsom/ginxsom-fcgi
sudo chmod +x /usr/local/bin/ginxsom/ginxsom-fcgi
sudo chown root:root /usr/local/bin/ginxsom/ginxsom-fcgi
EOF
```
### Phase 3: Setup Data Directories
```bash
ssh ubuntu@laantungir.net << 'EOF'
# Create database directory
sudo mkdir -p /var/lib/ginxsom/backups
sudo chown www-data:www-data /var/lib/ginxsom
sudo chmod 755 /var/lib/ginxsom
# Create blob storage directory
sudo mkdir -p /var/www/blobs
sudo chown www-data:www-data /var/www/blobs
sudo chmod 755 /var/www/blobs
# Migrate existing data if needed
if [ -f /var/www/html/blossom/ginxsom.db ]; then
sudo cp /var/www/html/blossom/ginxsom.db /var/lib/ginxsom/
sudo chown www-data:www-data /var/lib/ginxsom/ginxsom.db
fi
if [ -d /var/www/html/blossom ]; then
sudo cp -r /var/www/html/blossom/* /var/www/blobs/ 2>/dev/null || true
sudo chown -R www-data:www-data /var/www/blobs
fi
EOF
```
### Phase 4: Install Minimal Dependencies
```bash
ssh ubuntu@laantungir.net << 'EOF'
# Only spawn-fcgi is needed (no build tools!)
sudo apt-get update
sudo apt-get install -y spawn-fcgi
EOF
```
### Phase 5: Start Service
```bash
ssh ubuntu@laantungir.net << 'EOF'
# Stop existing process
sudo pkill -f ginxsom-fcgi || true
sudo rm -f /tmp/ginxsom-fcgi.sock
# Start with spawn-fcgi
sudo spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s /tmp/ginxsom-fcgi.sock \
-U www-data \
-G www-data \
-d /var/lib/ginxsom \
-- /usr/local/bin/ginxsom/ginxsom-fcgi \
--db-path /var/lib/ginxsom/ginxsom.db \
--storage-dir /var/www/blobs
EOF
```
## Nginx Configuration Updates
### Required Changes to `/etc/nginx/conf.d/default.conf`
```nginx
# Blossom subdomains HTTPS - ginxsom FastCGI
server {
listen 443 ssl;
server_name blossom.laantungir.net;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
# CORS for Blossom protocol
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
# CHANGED: Root directory for blob storage
root /var/www/blobs; # Was: /var/www/html/blossom
# Maximum upload size
client_max_body_size 100M;
# ... rest of configuration remains the same ...
# CHANGED: Update SCRIPT_FILENAME references
location = /upload {
if ($request_method !~ ^(PUT|HEAD)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi; # Was: $document_root/ginxsom.fcgi
}
# Apply same change to all other FastCGI locations...
}
```
## Benefits of New Architecture
### 1. Simplified Deployment
- **Before**: Upload source → Install deps → Build submodules → Compile → Deploy
- **After**: Upload binary → Start service
### 2. Reduced Dependencies
- **Before**: gcc, make, git, libsqlite3-dev, libfcgi-dev, libcurl4-openssl-dev, etc.
- **After**: spawn-fcgi only
### 3. Better Security
- No build tools on production server
- No source code on production server
- Smaller attack surface
### 4. Faster Deployments
- **Before**: ~5-10 minutes (build time)
- **After**: ~30 seconds (upload + restart)
### 5. Consistent Binaries
- Same binary works on any Linux distribution
- No "works on my machine" issues
- Reproducible builds via Docker
### 6. Cleaner Organization
- Binary in standard location (`/usr/local/bin/`)
- Data in standard location (`/var/lib/`)
- Blobs separate from web root (`/var/www/blobs/`)
## Migration Strategy
### Option 1: In-Place Migration (Recommended)
1. Build static binary locally
2. Upload to `/tmp/`
3. Stop current service
4. Create new directories
5. Migrate data
6. Update nginx config
7. Start new service
8. Verify functionality
9. Clean up old files
### Option 2: Blue-Green Deployment
1. Setup new directories alongside old
2. Deploy static binary
3. Test on different port
4. Switch nginx config
5. Remove old deployment
### Option 3: Fresh Install
1. Backup database and blobs
2. Remove old installation
3. Deploy static binary
4. Restore data
5. Configure nginx
6. Start service
## Rollback Plan
If issues occur, rollback is simple:
```bash
# Stop new service
sudo pkill -f ginxsom-fcgi
# Restore old binary location
sudo spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s /tmp/ginxsom-fcgi.sock \
-U www-data \
-G www-data \
-d /home/ubuntu/ginxsom \
-- /home/ubuntu/ginxsom/ginxsom.fcgi \
--db-path /home/ubuntu/ginxsom/db/ginxsom.db \
--storage-dir /var/www/html/blossom
# Revert nginx config
sudo cp /etc/nginx/conf.d/default.conf.backup /etc/nginx/conf.d/default.conf
sudo nginx -s reload
```
## SystemD Service (Future Enhancement)
Create `/etc/systemd/system/ginxsom.service`:
```ini
[Unit]
Description=Ginxsom Blossom Server
After=network.target
[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/lib/ginxsom
ExecStart=/usr/bin/spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s /tmp/ginxsom-fcgi.sock \
-U www-data \
-G www-data \
-d /var/lib/ginxsom \
-- /usr/local/bin/ginxsom/ginxsom-fcgi \
--db-path /var/lib/ginxsom/ginxsom.db \
--storage-dir /var/www/blobs
ExecStop=/usr/bin/pkill -f ginxsom-fcgi
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ginxsom
sudo systemctl start ginxsom
sudo systemctl status ginxsom
```
## Verification Steps
After deployment, verify:
1. **Binary is static**:
```bash
ldd /usr/local/bin/ginxsom/ginxsom-fcgi
# Should show: "not a dynamic executable"
```
2. **Service is running**:
```bash
ps aux | grep ginxsom-fcgi
ls -la /tmp/ginxsom-fcgi.sock
```
3. **Health endpoint**:
```bash
curl -k https://blossom.laantungir.net/health
# Should return: OK
```
4. **Upload test**:
```bash
# Use existing test scripts
./tests/file_put_bud02.sh
```
5. **Database access**:
```bash
sudo -u www-data sqlite3 /var/lib/ginxsom/ginxsom.db "SELECT COUNT(*) FROM blobs;"
```
6. **Blob storage**:
```bash
ls -la /var/www/blobs/ | head
```
## Monitoring
Key metrics to monitor:
- Binary size: `du -h /usr/local/bin/ginxsom/ginxsom-fcgi`
- Database size: `du -h /var/lib/ginxsom/ginxsom.db`
- Blob storage: `du -sh /var/www/blobs/`
- Process status: `systemctl status ginxsom` (if using systemd)
- Socket status: `ls -la /tmp/ginxsom-fcgi.sock`
## Backup Strategy
### Database Backups
```bash
# Daily backup
sudo -u www-data sqlite3 /var/lib/ginxsom/ginxsom.db ".backup /var/lib/ginxsom/backups/ginxsom-$(date +%Y%m%d).db"
# Keep last 7 days
find /var/lib/ginxsom/backups/ -name "ginxsom-*.db" -mtime +7 -delete
```
### Blob Backups
```bash
# Sync to backup location
rsync -av /var/www/blobs/ /backup/ginxsom-blobs/
```
## Conclusion
The static MUSL binary deployment provides:
- ✅ Simpler deployment process
- ✅ Fewer dependencies
- ✅ Better security
- ✅ Faster updates
- ✅ Universal compatibility
- ✅ Cleaner organization
This architecture follows Linux FHS (Filesystem Hierarchy Standard) best practices and provides a solid foundation for production deployment.

File diff suppressed because it is too large Load Diff

25
ginxsom.service Normal file
View File

@@ -0,0 +1,25 @@
[Unit]
Description=Ginxsom Blossom Server
After=network.target
[Service]
Type=forking
ExecStartPre=/bin/rm -f /tmp/ginxsom-fcgi.sock
ExecStart=/usr/bin/spawn-fcgi \
-s /tmp/ginxsom-fcgi.sock \
-M 666 \
-u www-data \
-g www-data \
-d /usr/local/bin/ginxsom \
-- /usr/local/bin/ginxsom/ginxsom-fcgi \
--admin-pubkey 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139 \
--server-privkey 90df3fe61e7d19e50f387e4c5db87eff1a7d2a1037cd55026c4b21a4fda8ecf6 \
--db-path /usr/local/bin/ginxsom \
--storage-dir /var/www/blobs
ExecStop=/usr/bin/pkill -f ginxsom-fcgi
ExecStopPost=/bin/rm -f /tmp/ginxsom-fcgi.sock
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -8,17 +8,65 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
# Global variables
COMMIT_MESSAGE=""
RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
VERSION_INCREMENT_EXPLICIT=false
# TODO: Update this URL to match your actual Gitea repository
GITEA_REPO_URL="https://git.example.com/api/v1/repos/username/ginxsom"
GITEA_REPO_URL="https://git.laantungir.net/api/v1/repos/laantungir/ginxsom"
show_usage() {
echo "Ginxsom Increment and Push Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
echo ""
echo "COMMANDS:"
echo " $0 \"commit message\" Default: increment patch, commit & push"
echo " $0 -p \"commit message\" Increment patch version"
echo " $0 -m \"commit message\" Increment minor version"
echo " $0 -M \"commit message\" Increment major version"
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
echo " $0 -r -p \"commit message\" Create release with patch version increment"
echo " $0 -r -m \"commit message\" Create release with minor version increment"
echo " $0 -h Show this help message"
echo ""
echo "OPTIONS:"
echo " -p, --patch Increment patch version (default)"
echo " -m, --minor Increment minor version"
echo " -M, --major Increment major version"
echo " -r, --release Create release with assets"
echo " -h, --help Show this help message"
echo ""
echo "EXAMPLES:"
echo " $0 \"Fixed authentication bug\""
echo " $0 -m \"Added new features\""
echo " $0 -M \"Breaking API changes\""
echo " $0 -r \"Release current version\""
echo " $0 -r -p \"Release with patch increment\""
echo " $0 -r -m \"Release with minor increment\""
echo ""
echo "VERSION INCREMENT MODES:"
echo " -p, --patch (default): Increment patch version (v1.2.3 → v1.2.4)"
echo " -m, --minor: Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " -M, --major: Increment major version, zero minor+patch (v1.2.3 → v2.0.0)"
echo ""
echo "RELEASE MODE (-r flag):"
echo " - Build static binaries for x86_64 and ARM64 using build_static.sh"
echo " - Create source tarball"
echo " - Git add, commit, push, and create Gitea release with assets"
echo " - Can be combined with version increment flags"
echo ""
echo "REQUIREMENTS FOR RELEASE MODE:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Docker installed for static binary builds"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
@@ -27,6 +75,21 @@ while [[ $# -gt 0 ]]; do
RELEASE_MODE=true
shift
;;
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-h|--help)
show_usage
exit 0
@@ -41,32 +104,6 @@ while [[ $# -gt 0 ]]; do
esac
done
show_usage() {
echo "Ginxsom Build and Push Script"
echo ""
echo "Usage:"
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: compile, increment minor, create release"
echo ""
echo "Examples:"
echo " $0 \"Fixed authentication bug\""
echo " $0 --release \"Major release with admin API\""
echo ""
echo "Default Mode (patch increment):"
echo " - Compile Ginxsom FastCGI server"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "Release Mode (-r flag):"
echo " - Compile Ginxsom FastCGI server"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Update GITEA_REPO_URL in script for your repository"
}
# Validate inputs
if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required"
@@ -85,20 +122,20 @@ check_git_repo() {
# Function to get current version and increment appropriately
increment_version() {
local increment_type="$1" # "patch" or "minor"
local increment_type="$1" # "patch", "minor", or "major"
print_status "Getting current version..."
# Get the highest version tag (not chronologically latest)
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
# Extract version components (remove 'v' prefix)
VERSION=${LATEST_TAG#v}
# Parse major.minor.patch using regex
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
@@ -109,121 +146,74 @@ increment_version() {
print_error "Expected format: v0.1.0"
exit 1
fi
# Increment version based on type
if [[ "$increment_type" == "minor" ]]; then
# Minor release: increment minor, zero patch
if [[ "$increment_type" == "major" ]]; then
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Major version increment: incrementing major version"
elif [[ "$increment_type" == "minor" ]]; then
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Release mode: incrementing minor version"
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Minor version increment: incrementing minor version"
else
# Default: increment patch
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
print_status "Default mode: incrementing patch version"
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Patch version increment: incrementing patch version"
fi
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Update version in src/ginxsom.h
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
# Export for use in other functions
export NEW_VERSION
}
# Function to update version in header file
# Function to update version macros in src/ginxsom.h
update_version_in_header() {
local version="$1"
print_status "Updating version in src/ginxsom.h to $version..."
local new_version="$1"
local major="$2"
local minor="$3"
local patch="$4"
# Extract version components (remove 'v' prefix)
local version_no_v=${version#v}
print_status "Updating version in src/ginxsom.h..."
# Parse major.minor.patch using regex
if [[ $version_no_v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
local major=${BASH_REMATCH[1]}
local minor=${BASH_REMATCH[2]}
local patch=${BASH_REMATCH[3]}
# Update the header file
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$version\"/" src/ginxsom.h
print_success "Updated version in header file"
else
print_error "Invalid version format: $version"
if [[ ! -f "src/ginxsom.h" ]]; then
print_error "src/ginxsom.h not found"
exit 1
fi
}
# Function to compile the Ginxsom project
compile_project() {
print_status "Compiling Ginxsom FastCGI server..."
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$new_version\"/" src/ginxsom.h
# Clean previous build
if make clean > /dev/null 2>&1; then
print_success "Cleaned previous build"
else
print_warning "Clean failed or no Makefile found"
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "Ginxsom compiled successfully"
# Verify the binary was created
if [[ -f "build/ginxsom-fcgi" ]]; then
print_success "Binary created: build/ginxsom-fcgi"
else
print_error "Binary not found after compilation"
exit 1
fi
else
print_error "Compilation failed"
exit 1
fi
}
# Function to build release binary
build_release_binary() {
print_status "Building release binary..."
# Build the FastCGI server
print_status "Building Ginxsom FastCGI server..."
make clean > /dev/null 2>&1
if make > /dev/null 2>&1; then
if [[ -f "build/ginxsom-fcgi" ]]; then
cp build/ginxsom-fcgi ginxsom-fcgi-linux-x86_64
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
else
print_error "Binary not found after compilation"
exit 1
fi
else
print_error "Build failed"
exit 1
fi
print_success "Updated version in src/ginxsom.h to $new_version"
}
# Function to commit and push changes
git_commit_and_push() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
@@ -231,15 +221,13 @@ git_commit_and_push() {
exit 1
fi
fi
# Create new git tag
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists"
fi
# Push changes and tags
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
@@ -247,8 +235,7 @@ git_commit_and_push() {
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
@@ -265,20 +252,17 @@ git_commit_and_push() {
# Function to commit and push changes without creating a tag (tag already created)
git_commit_and_push_no_tag() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
@@ -286,8 +270,7 @@ git_commit_and_push_no_tag() {
exit 1
fi
fi
# Push changes and tags
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
@@ -295,8 +278,7 @@ git_commit_and_push_no_tag() {
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
@@ -310,42 +292,198 @@ git_commit_and_push_no_tag() {
fi
}
# Function to build release binaries
build_release_binary() {
print_status "Building release binaries..."
if [[ ! -f "build_static.sh" ]]; then
print_error "build_static.sh not found"
return 1
fi
# Build x86_64 first (required for success)
if ./build_static.sh > /dev/null 2>&1; then
print_success "Built x86_64 static binary successfully"
else
print_error "Failed to build x86_64 static binary"
return 1
fi
# Build arm64 second (warning-only on failure)
if ./build_static.sh --arch arm64 > /dev/null 2>&1; then
print_success "Built ARM64 static binary successfully"
else
print_warning "Failed to build ARM64 static binary (continuing with release)"
fi
return 0
}
# Function to create source tarball
create_source_tarball() {
print_status "Creating source tarball..."
local tarball_name="ginxsom-${NEW_VERSION#v}.tar.gz"
# Remove any existing tarball first to avoid self-inclusion
rm -f "$tarball_name"
if tar -czf "$tarball_name" \
--exclude='./build' \
--exclude='./.git' \
--exclude='./.gitmodules' \
--exclude='./db/*.db' \
--exclude='./db/*.db-*' \
--exclude='./*.log' \
--exclude="./$tarball_name" \
--exclude='./Trash' \
--exclude='./*.tar.gz' \
. 2>/dev/null; then
print_success "Created source tarball: $tarball_name"
echo "$tarball_name"
return 0
else
print_error "Failed to create source tarball"
return 1
fi
}
# Function to upload release assets to Gitea
upload_release_assets() {
local release_id="$1"
local binary_path_x86="$2"
local tarball_path="$3"
local binary_path_arm64="$4"
print_status "Uploading release assets..."
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping asset uploads."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local assets_url="$GITEA_REPO_URL/releases/$release_id/assets"
print_status "Assets URL: $assets_url"
# Upload x86_64 binary
if [[ -f "$binary_path_x86" ]]; then
print_status "Uploading binary: $(basename "$binary_path_x86")"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path_x86;filename=$(basename "$binary_path_x86")" \
-F "name=$(basename "$binary_path_x86")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded x86_64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
done
fi
# Upload ARM64 binary
if [[ -f "$binary_path_arm64" ]]; then
print_status "Uploading binary: $(basename "$binary_path_arm64")"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path_arm64;filename=$(basename "$binary_path_arm64")" \
-F "name=$(basename "$binary_path_arm64")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded ARM64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload ARM64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
done
fi
# Upload source tarball
if [[ -f "$tarball_path" ]]; then
print_status "Uploading source tarball: $(basename "$tarball_path")"
local tarball_response=$(curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
if echo "$tarball_response" | grep -q '"id"'; then
print_success "Uploaded source tarball successfully"
else
print_warning "Failed to upload source tarball: $tarball_response"
fi
fi
}
# Function to create Gitea release
create_gitea_release() {
print_status "Creating Gitea release..."
# Check for Gitea token
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping release creation."
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
# Create release
print_status "Creating release $NEW_VERSION..."
local response=$(curl -s -X POST "$GITEA_REPO_URL/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
upload_release_binary "$token"
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists"
upload_release_binary "$token"
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
print_status "Using existing release ID: $release_id"
echo $release_id
else
print_error "Could not find existing release ID"
return 1
fi
else
print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response"
# Try to check if the release exists anyway
print_status "Checking if release exists..."
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected"
upload_release_binary "$token"
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
else
print_error "Release does not exist and creation failed"
return 1
@@ -353,62 +491,26 @@ create_gitea_release() {
fi
}
# Function to upload release binary
upload_release_binary() {
local token="$1"
# Get release ID with more robust parsing
print_status "Getting release ID for $NEW_VERSION..."
local response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
if [[ -z "$release_id" ]]; then
print_error "Could not get release ID for $NEW_VERSION"
print_error "API Response: $response"
# Try to list all releases to debug
print_status "Available releases:"
curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases" | grep -o '"tag_name":"[^"]*"' | head -5
return 1
fi
print_success "Found release ID: $release_id"
# Upload FastCGI binary
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
print_status "Uploading Ginxsom FastCGI binary..."
if curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@ginxsom-fcgi-linux-x86_64;filename=ginxsom-fcgi-${NEW_VERSION}-linux-x86_64" > /dev/null; then
print_success "Uploaded FastCGI binary"
else
print_warning "Failed to upload FastCGI binary"
fi
fi
}
# Function to clean up release binary
cleanup_release_binary() {
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
rm -f ginxsom-fcgi-linux-x86_64
print_status "Cleaned up release binary"
fi
}
# Main execution
main() {
print_status "Ginxsom Build and Push Script"
print_status "Ginxsom Increment and Push Script"
# Check prerequisites
check_git_repo
if [[ "$RELEASE_MODE" == true ]]; then
print_status "=== RELEASE MODE ==="
# Increment minor version for releases
increment_version "minor"
# Create new git tag BEFORE compilation
# Only increment version if explicitly requested
if [[ "$VERSION_INCREMENT_EXPLICIT" == true ]]; then
increment_version "$VERSION_INCREMENT_TYPE"
else
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0")
NEW_VERSION="$LATEST_TAG"
export NEW_VERSION
fi
# Create new git tag BEFORE compilation so version is picked up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
@@ -416,35 +518,63 @@ main() {
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Build release binary
build_release_binary
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
# Create Gitea release with binary
create_gitea_release
# Cleanup
cleanup_release_binary
print_success "Release $NEW_VERSION completed successfully!"
print_status "Binary uploaded to Gitea release"
# Build release binaries
local binary_path_x86=""
local binary_path_arm64=""
if build_release_binary; then
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
fi
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
fi
else
print_warning "x86_64 binary build failed, continuing with release creation"
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
print_status "Using existing x86_64 binary from previous build"
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
fi
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
print_status "Using existing ARM64 binary from previous build"
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
fi
fi
# Create source tarball
local tarball_path=""
if tarball_path=$(create_source_tarball); then
: # tarball_path is set by the function
else
print_warning "Source tarball creation failed, continuing with release creation"
fi
# Create Gitea release
local release_id=""
if release_id=$(create_gitea_release); then
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
if [[ -n "$release_id" && (-n "$binary_path_x86" || -n "$binary_path_arm64" || -n "$tarball_path") ]]; then
upload_release_assets "$release_id" "$binary_path_x86" "$tarball_path" "$binary_path_arm64"
fi
print_success "Release $NEW_VERSION completed successfully!"
else
print_error "Invalid release_id: $release_id"
exit 1
fi
else
print_error "Release creation failed"
fi
else
print_status "=== DEFAULT MODE ==="
# Increment patch version for regular commits
increment_version "patch"
# Create new git tag BEFORE compilation
# Increment version based on type (default to patch)
increment_version "$VERSION_INCREMENT_TYPE"
# Create new git tag BEFORE compilation so version is picked up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
@@ -452,17 +582,11 @@ main() {
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
print_success "Build and push completed successfully!"
print_success "Increment and push completed successfully!"
print_status "Version $NEW_VERSION pushed to repository"
fi
}

View File

@@ -0,0 +1,149 @@
# Fix: Ginxsom 100% CPU Busy-Wait Bug
## Root Cause Analysis
The ginxsom-fcgi process consumes 100% CPU due to a **busy-wait spin loop** in the relay management thread.
### The Bug Chain
1. **`relay_management_thread()`** in `src/relay_client.c:328` runs a `while` loop calling `nostr_relay_pool_poll()` with a 1000ms timeout
2. **`nostr_relay_pool_poll()`** in `nostr_core_lib/nostr_core/core_relay_pool.c:1699` iterates over all relays, but **skips disconnected relays** at line 1725-1728 — it only calls `nostr_ws_receive()` (which actually blocks) for connected relays
3. **When no relays are connected** (or all are in error/disconnected state), the entire poll function returns 0 instantly with zero blocking
4. **The caller has no sleep** for the `events_processed == 0` case — it immediately loops back and calls poll again
5. This creates a **tight spin loop** consuming 100% of a CPU core
### Additional Issues Found
- **No signal handling**: `main.c` has zero `signal()`, `SIGTERM`, or `SIGINT` handling. When systemd sends SIGTERM to stop the service, the process ignores it and must be SIGKILL'd after timeout (confirmed in journal: `State 'stop-sigterm' timed out. Killing.`)
- **`relay_client_stop()` never called**: The cleanup function exists but is never invoked — there's no shutdown path
- **`g_relay_state.running` not volatile**: The `running` flag is read by the management thread but set by the main thread (or signal handler). Without `volatile`, the compiler may optimize away the check
```mermaid
graph TD
A["relay_management_thread()"] -->|"while running"| B["nostr_relay_pool_poll(pool, 1000)"]
B --> C{"Any relays connected?"}
C -->|"Yes"| D["nostr_ws_receive() blocks up to timeout_ms"]
C -->|"No - all disconnected"| E["Returns 0 immediately"]
D --> F{"events_processed > 0?"}
E --> F
F -->|"> 0"| A
F -->|"== 0"| G["No sleep! Loops immediately"]
F -->|"< 0"| H["sleep(1) then loop"]
G -->|"BUSY WAIT"| A
```
## Fixes Required
### Fix 1: Add idle sleep to poll loop (`src/relay_client.c`)
**File:** `src/relay_client.c`, lines 327-337
**Before:**
```c
while (g_relay_state.running) {
int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000);
if (events_processed < 0) {
app_log(LOG_ERROR, "Error polling relay pool");
sleep(1);
}
}
```
**After:**
```c
while (g_relay_state.running) {
int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000);
if (events_processed < 0) {
app_log(LOG_ERROR, "Error polling relay pool");
sleep(1);
} else if (events_processed == 0) {
// Prevent busy-wait when no relays are connected or no events pending.
// nostr_relay_pool_poll() returns immediately if all relays are disconnected
// because it skips the blocking nostr_ws_receive() call for non-connected relays.
usleep(100000); // 100ms idle sleep
}
}
```
### Fix 2: Make `running` flag volatile (`src/relay_client.c`)
**File:** `src/relay_client.c`, line 39
**Before:**
```c
static struct {
int enabled;
int initialized;
int running;
...
```
**After:**
```c
static struct {
int enabled;
int initialized;
volatile int running;
...
```
### Fix 3: Add signal handler for graceful shutdown (`src/main.c`)
**File:** `src/main.c`
Add near the top (after includes):
```c
#include <signal.h>
static volatile sig_atomic_t g_shutdown_requested = 0;
static void shutdown_handler(int signum) {
(void)signum;
g_shutdown_requested = 1;
}
```
Register the handler before the FCGI loop (before line 2664):
```c
// Register signal handlers for graceful shutdown
signal(SIGTERM, shutdown_handler);
signal(SIGINT, shutdown_handler);
```
### Fix 4: Call `relay_client_stop()` on shutdown (`src/main.c`)
**File:** `src/main.c`
Modify the FCGI loop condition (line 2667) to also check the shutdown flag:
```c
while (FCGI_Accept() >= 0 && !g_shutdown_requested) {
```
Add cleanup after the FCGI loop exits (before `return 0` at line 3068):
```c
// Graceful shutdown
app_log(LOG_INFO, "Shutting down ginxsom...");
relay_client_stop();
app_log(LOG_INFO, "Ginxsom shutdown complete");
```
Also add the `relay_client.h` include at the top of `main.c`.
## Summary of Changes
| File | Change | Purpose |
|------|--------|---------|
| `src/relay_client.c:328-336` | Add `usleep(100000)` when poll returns 0 | Fix 100% CPU busy-wait |
| `src/relay_client.c:39` | Make `running` field `volatile` | Thread-safe flag checking |
| `src/main.c` (top) | Add signal handler function | Respond to SIGTERM/SIGINT |
| `src/main.c` (before FCGI loop) | Register signal handlers | Enable graceful shutdown |
| `src/main.c:2667` | Add `&& !g_shutdown_requested` to loop | Exit FCGI loop on signal |
| `src/main.c` (after FCGI loop) | Call `relay_client_stop()` | Clean up relay thread |
## Testing
1. Build locally: `make clean && make`
2. Run locally and verify CPU usage stays near 0% when idle
3. Send SIGTERM and verify clean shutdown (no SIGKILL needed)
4. Deploy to remote server via `deploy_lt.sh`
5. Monitor with `top` to confirm CPU usage is normal

View File

@@ -0,0 +1,142 @@
# Unified Logging Migration Plan
## Overview
Migrate ginxsom to use nostr_core_lib v0.4.13's callback-based logging API and unify all application logging into a single sink (`logs/app/app.log`).
## Architecture Context
```mermaid
graph TD
subgraph Current State - v0.4.8
A[ginxsom app] -->|fprintf stderr ~122 calls| B[nginx error_log]
A -->|app_log ~80+ calls| C[logs/app/app.log]
A -->|validator_debug_log ~10 calls| D[logs/app/debug.log]
E[nostr_core_lib] -->|direct file write| F[nostr_core_lib/debug.log]
G[systemd journald] -->|spawn-fcgi parent only| H[journal]
end
```
```mermaid
graph TD
subgraph Target State - v0.4.13
A2[ginxsom app] -->|stderr: pre-init fatal only ~10 calls| B2[nginx error_log]
A2 -->|app_log: all operational logs| C2[logs/app/app.log]
E2[nostr_core_lib] -->|callback| A2
G2[systemd journald] -->|spawn-fcgi parent only| H2[journal]
end
```
## Logging Policy
| Sink | Purpose | When Used |
|------|---------|-----------|
| `stderr` -> nginx error_log | Pre-init fatal errors only | Before `app_log` is available, or truly unrecoverable errors during early `main` |
| `logs/app/app.log` via `app_log` | All application operational logging | Everything after logger init: app events, nostr_core_lib callback, validator, relay, admin |
| `logs/app/debug.log` | **ELIMINATED** | Replaced by `app_log(LOG_DEBUG, ...)` |
| `nostr_core_lib/debug.log` | **ELIMINATED** | v0.4.13 removes direct file logging; callback replaces it |
## Log Levels
```c
typedef enum {
LOG_TRACE = 0, // NEW: maps to NOSTR_LOG_LEVEL_TRACE
LOG_DEBUG = 1, // maps to NOSTR_LOG_LEVEL_DEBUG
LOG_INFO = 2, // maps to NOSTR_LOG_LEVEL_INFO
LOG_WARN = 3, // maps to NOSTR_LOG_LEVEL_WARN
LOG_ERROR = 4 // maps to NOSTR_LOG_LEVEL_ERROR
} log_level_t;
```
**Note:** Existing code uses `LOG_DEBUG=0, LOG_INFO=1, LOG_WARN=2, LOG_ERROR=3`. Adding `LOG_TRACE` at 0 shifts all values up by 1. All existing call sites use the enum names (not raw integers), so this is safe.
## Implementation Steps
### Phase 1: Update nostr_core_lib submodule
1. Update the `nostr_core_lib` git submodule to v0.4.13
2. Rebuild `libnostr_core_x64.a` from the updated source
3. Verify the new headers exist: `nostr_core/nostr_log.h` (or equivalent in `nostr_core.h`)
4. Verify build still compiles with `make clean && make`
### Phase 2: Enhance app_log in ginxsom.h and main.c
5. Add `LOG_TRACE` level to `log_level_t` enum in `ginxsom.h`
6. Add a global `g_log_level` variable to control minimum log level
7. Update `app_log()` in `main.c` to:
- Check message level against `g_log_level` before writing
- Handle the new `LOG_TRACE` level string
- Keep the existing file-based sink (`logs/app/app.log`)
### Phase 3: Register nostr_core_lib log callback
8. Add `#include "nostr_core/nostr_core.h"` (or `nostr_log.h`) to `main.c` if not already present
9. Create `nostr_log_cb()` callback function that maps nostr log levels to `app_log()` levels and prefixes messages with `[nostr:component]`
10. In `main()`, after `nostr_init()` / `nostr_crypto_init()`, call:
- `nostr_set_log_callback(nostr_log_cb, NULL)`
- `nostr_set_log_level(NOSTR_LOG_LEVEL_INFO)` (or TRACE for debug builds)
11. In shutdown path, call `nostr_set_log_callback(NULL, NULL)` before `nostr_cleanup()`
### Phase 4: Eliminate debug.log
12. Remove `validator_debug_log()` function from `request_validator.c`
13. Replace all `validator_debug_log()` calls with `app_log(LOG_DEBUG, "VALIDATOR: %s", ...)` calls
14. Delete stale `logs/app/debug.log` and `nostr_core_lib/debug.log` references from `.gitignore` or docs if present
15. Remove `debug.log` from project root if it was generated by nostr_core_lib
### Phase 5: Migrate fprintf(stderr) calls to app_log
Migrate post-startup `fprintf(stderr, ...)` calls file-by-file. Keep only pre-init and truly fatal stderr calls in early `main()`.
**Files to migrate (by priority):**
16. `src/admin_auth.c` (~28 fprintf calls) - Replace with `app_log(LOG_ERROR, "AUTH: ...")` etc.
17. `src/request_validator.c` (~2 fprintf calls) - Replace with `app_log(LOG_DEBUG, "VALIDATOR: ...")`
18. `src/main.c` - Categorize ~90 fprintf calls:
- **Keep as stderr** (~10): Pre-logger-init errors in early `main()`, help text output, keypair display banner
- **Migrate to app_log** (~80): All post-init startup messages, database operations, request logging, upload handling, shutdown messages
### Phase 6: Remove duplicate log_level_t declarations
19. Remove the duplicate `typedef enum { LOG_DEBUG... } log_level_t` and `void app_log(...)` forward declarations from `src/relay_client.c` and `src/admin_commands.c` — these files should just `#include "ginxsom.h"` instead
### Phase 7: Update nostr_crypto_init to nostr_init
20. Replace `nostr_crypto_init()` calls with `nostr_init()` in `main.c` and `request_validator.c` if v0.4.13 consolidates initialization under `nostr_init()`
21. Add `nostr_cleanup()` call in shutdown path if not already present
### Phase 8: Update Makefile and build
22. Verify `Makefile` CFLAGS and LIBS are compatible with v0.4.13 (check if new link flags needed)
23. Remove any `LOGGING_FLAGS` build-time defines that are no longer needed
24. Run `make clean && make` and verify clean build
### Phase 9: Verify and test
25. Run the application and verify:
- `logs/app/app.log` contains both app logs and `[nostr:websocket]` / `[nostr:nip013]` tagged lines
- `logs/app/debug.log` is no longer created
- `nostr_core_lib/debug.log` is no longer created
- `logs/nginx/error.log` only contains pre-init errors and nginx's own messages
26. Run existing test suite to verify no regressions
## Files Modified
| File | Changes |
|------|---------|
| `nostr_core_lib/` | Submodule updated to v0.4.13 |
| `src/ginxsom.h` | Add LOG_TRACE, add g_log_level extern |
| `src/main.c` | Enhanced app_log, nostr_log_cb, callback registration, fprintf migration |
| `src/request_validator.c` | Remove validator_debug_log, replace with app_log |
| `src/admin_auth.c` | Replace fprintf(stderr) with app_log |
| `src/relay_client.c` | Remove duplicate log_level_t typedef |
| `src/admin_commands.c` | Remove duplicate log_level_t typedef |
| `Makefile` | Verify/update build flags |
| `.gitignore` | Add/update debug.log exclusions |
## Risk Mitigation
- **Backward compatibility**: Enum names are used everywhere, not raw integers, so shifting values is safe
- **Build breakage**: Phase 1 verifies build before any code changes
- **Runtime breakage**: Callback registration is additive — if it fails, app still works, just without nostr_core_lib log routing
- **stderr removal safety**: Only post-init calls are migrated; pre-init fatal errors stay on stderr for nginx capture

View File

@@ -97,7 +97,7 @@ server {
add_header Access-Control-Max-Age 86400 always;
# Root directory for blob storage
root /var/www/html/blossom;
root /var/www/blobs;
# Maximum upload size
client_max_body_size 100M;
@@ -114,7 +114,7 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# GET /list/<pubkey> - List user blobs
@@ -124,7 +124,7 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# PUT /mirror - Mirror content
@@ -134,7 +134,7 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# PUT /report - Report content
@@ -144,7 +144,7 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# GET /auth - NIP-42 challenges
@@ -154,17 +154,17 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# Admin API
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
if ($request_method !~ ^(GET|POST|PUT)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
# Blob serving - SHA256 patterns
@@ -184,7 +184,7 @@ server {
return 405;
}
try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
try_files /$1.html /$1.js /$1.mjs /$1.css /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404;
# Cache headers
add_header Cache-Control "public, max-age=31536000, immutable";
@@ -195,7 +195,7 @@ server {
internal;
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
fastcgi_param REQUEST_URI /$1;
}
@@ -203,7 +203,7 @@ server {
internal;
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
fastcgi_param REQUEST_URI /$1;
}
@@ -225,7 +225,7 @@ server {
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param SCRIPT_FILENAME /usr/local/bin/ginxsom/ginxsom-fcgi;
}
}

88
scripts/embed_web_files.sh Executable file
View File

@@ -0,0 +1,88 @@
#!/bin/bash
# Embed web interface files into C source code
# This script converts HTML, CSS, and JS files into C byte arrays
set -e
# Configuration
API_DIR="api"
OUTPUT_DIR="src"
OUTPUT_FILE="${OUTPUT_DIR}/admin_interface_embedded.h"
# Files to embed
FILES=(
"index.html"
"index.css"
"index.js"
"nostr-lite.js"
"nostr.bundle.js"
"text_graph.js"
)
echo "=== Embedding Web Interface Files ==="
echo "Source directory: ${API_DIR}"
echo "Output file: ${OUTPUT_FILE}"
echo ""
# Start output file
cat > "${OUTPUT_FILE}" << 'EOF'
/*
* Embedded Web Interface Files
* Auto-generated by scripts/embed_web_files.sh
* DO NOT EDIT MANUALLY
*/
#ifndef ADMIN_INTERFACE_EMBEDDED_H
#define ADMIN_INTERFACE_EMBEDDED_H
#include <stddef.h>
EOF
# Process each file
for file in "${FILES[@]}"; do
filepath="${API_DIR}/${file}"
if [[ ! -f "${filepath}" ]]; then
echo "WARNING: File not found: ${filepath}"
continue
fi
# Create variable name from filename (replace . and - with _)
varname=$(echo "${file}" | tr '.-' '__')
echo "Embedding: ${file} -> embedded_${varname}"
# Get file size
filesize=$(stat -f%z "${filepath}" 2>/dev/null || stat -c%s "${filepath}" 2>/dev/null)
# Add comment
echo "" >> "${OUTPUT_FILE}"
echo "// Embedded file: ${file} (${filesize} bytes)" >> "${OUTPUT_FILE}"
# Convert file to C byte array
echo "static const unsigned char embedded_${varname}[] = {" >> "${OUTPUT_FILE}"
# Use xxd when available, otherwise fall back to od
if command -v xxd >/dev/null 2>&1; then
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
else
od -An -tx1 -v "${filepath}" | tr -s ' ' '\n' | sed '/^$/d' | while read -r byte; do
printf "0x%s,\n" "${byte}" >> "${OUTPUT_FILE}"
done
fi
echo "};" >> "${OUTPUT_FILE}"
echo "static const size_t embedded_${varname}_size = sizeof(embedded_${varname});" >> "${OUTPUT_FILE}"
done
# Close header guard
cat >> "${OUTPUT_FILE}" << 'EOF'
#endif /* ADMIN_INTERFACE_EMBEDDED_H */
EOF
echo ""
echo "=== Embedding Complete ==="
echo "Generated: ${OUTPUT_FILE}"
echo "Total files embedded: ${#FILES[@]}"

View File

@@ -33,6 +33,7 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
static int admin_nip94_get_origin(char* out, size_t out_size);
static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
static const char* admin_mime_to_extension(const char* mime_type);
static int validate_hex64_format(const char* hash);
// Local utility functions (from main.c but implemented here for admin API)
static int admin_nip94_get_origin(char* out, size_t out_size) {
@@ -45,34 +46,45 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
int rc;
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
// Default on DB error
strncpy(out, "http://localhost:9001", out_size - 1);
out[out_size - 1] = '\0';
if (rc == SQLITE_OK) {
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value && value[0] != '\0' && strcmp(value, "http://localhost:9001") != 0) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
}
// Dynamic fallback from request headers (preferred over localhost)
const char* host = getenv("HTTP_HOST");
const char* forwarded_proto = getenv("HTTP_X_FORWARDED_PROTO");
const char* https = getenv("HTTPS");
if (host && host[0] != '\0') {
const char* proto = "http";
if (forwarded_proto && forwarded_proto[0] != '\0') {
proto = forwarded_proto;
} else if (https && (strcmp(https, "on") == 0 || strcmp(https, "1") == 0)) {
proto = "https";
}
snprintf(out, out_size, "%s://%s", proto, host);
return 1;
}
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Default fallback
strncpy(out, "http://localhost:9001", out_size - 1);
// Final fallback
strncpy(out, "https://blossom.laantungir.net", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
@@ -88,35 +100,25 @@ static void admin_nip94_build_blob_url(const char* origin, const char* sha256,
snprintf(out, out_size, "%s/%s%s", origin, sha256, extension);
}
// Centralized MIME type to file extension mapping (from main.c)
// Centralized MIME type to file extension mapping (delegates to shared BUD-08 mapping)
static const char* admin_mime_to_extension(const char* mime_type) {
if (!mime_type) {
return ".bin";
return mime_to_extension(mime_type);
}
static int validate_hex64_format(const char* hash) {
if (!hash || strlen(hash) != 64) {
return 0;
}
if (strstr(mime_type, "image/jpeg")) {
return ".jpg";
} else if (strstr(mime_type, "image/webp")) {
return ".webp";
} else if (strstr(mime_type, "image/png")) {
return ".png";
} else if (strstr(mime_type, "image/gif")) {
return ".gif";
} else if (strstr(mime_type, "video/mp4")) {
return ".mp4";
} else if (strstr(mime_type, "video/webm")) {
return ".webm";
} else if (strstr(mime_type, "audio/mpeg")) {
return ".mp3";
} else if (strstr(mime_type, "audio/ogg")) {
return ".ogg";
} else if (strstr(mime_type, "text/plain")) {
return ".txt";
} else if (strstr(mime_type, "application/pdf")) {
return ".pdf";
} else {
return ".bin";
for (size_t i = 0; i < 64; i++) {
if (!((hash[i] >= '0' && hash[i] <= '9') ||
(hash[i] >= 'a' && hash[i] <= 'f') ||
(hash[i] >= 'A' && hash[i] <= 'F'))) {
return 0;
}
}
return 1;
}
// Main API request handler
@@ -133,7 +135,8 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
// Kind 23456 events authenticate themselves via signed event validation
int skip_auth = (strcmp(path, "/health") == 0) ||
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0);
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0) ||
(strcmp(method, "GET") == 0 && strncmp(path, "/files", 6) == 0);
if (!skip_auth) {
if (!is_authenticated || !validated_pubkey) {
@@ -603,14 +606,15 @@ void handle_config_key_put_api(const char* key) {
}
void handle_files_api(void) {
// Parse query parameters for pagination
// Parse query parameters for pagination and optional pubkey filtering
const char* query_string = getenv("QUERY_STRING");
int limit = 50;
int offset = 0;
char pubkey_filter[65] = {0};
if (query_string) {
char params[10][256];
int param_count = parse_query_params(query_string, params, 10);
char params[12][256];
int param_count = parse_query_params(query_string, params, 12);
for (int i = 0; i < param_count; i++) {
char* key = params[i];
@@ -623,6 +627,12 @@ void handle_files_api(void) {
} else if (strcmp(key, "offset") == 0) {
offset = atoi(value);
if (offset < 0) offset = 0;
} else if (strcmp(key, "pubkey") == 0) {
strncpy(pubkey_filter, value, sizeof(pubkey_filter) - 1);
pubkey_filter[sizeof(pubkey_filter) - 1] = '\0';
if (!validate_hex64_format(pubkey_filter)) {
pubkey_filter[0] = '\0';
}
}
}
}
@@ -638,9 +648,12 @@ void handle_files_api(void) {
return;
}
// Query recent files with pagination
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
// Query recent files with pagination and optional uploader filter
const char* sql = (pubkey_filter[0] != '\0')
? "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs WHERE uploader_pubkey = ? ORDER BY uploaded_at DESC LIMIT ? OFFSET ?"
: "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
@@ -649,23 +662,38 @@ void handle_files_api(void) {
return;
}
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
int bind_index = 1;
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(stmt, bind_index++, pubkey_filter, -1, SQLITE_STATIC);
}
sqlite3_bind_int(stmt, bind_index++, limit);
sqlite3_bind_int(stmt, bind_index++, offset);
cJSON* response = cJSON_CreateObject();
cJSON* data = cJSON_CreateObject();
cJSON* files_array = cJSON_CreateArray();
if (!response || !data || !files_array) {
if (response) cJSON_Delete(response);
if (data) cJSON_Delete(data);
if (files_array) cJSON_Delete(files_array);
sqlite3_finalize(stmt);
sqlite3_close(db);
send_json_error(500, "memory_error", "Failed to allocate response JSON");
return;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddItemToObject(response, "data", data);
cJSON_AddItemToObject(data, "files", files_array);
cJSON_AddNumberToObject(data, "limit", limit);
cJSON_AddNumberToObject(data, "offset", offset);
int total_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
total_count++;
cJSON* file_obj = cJSON_CreateObject();
if (!file_obj) {
continue;
}
cJSON_AddItemToArray(files_array, file_obj);
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
@@ -695,12 +723,18 @@ void handle_files_api(void) {
}
}
// Get total count for pagination info
const char* count_sql = "SELECT COUNT(*) FROM blobs";
// Get total count for pagination info (respecting optional pubkey filter)
const char* count_sql = (pubkey_filter[0] != '\0')
? "SELECT COUNT(*) FROM blobs WHERE uploader_pubkey = ?"
: "SELECT COUNT(*) FROM blobs";
sqlite3_stmt* count_stmt;
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
if (rc == SQLITE_OK) {
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(count_stmt, 1, pubkey_filter, -1, SQLITE_STATIC);
}
rc = sqlite3_step(count_stmt);
if (rc == SQLITE_ROW) {
int total = sqlite3_column_int(count_stmt, 0);
@@ -709,6 +743,10 @@ void handle_files_api(void) {
sqlite3_finalize(count_stmt);
}
if (pubkey_filter[0] != '\0') {
cJSON_AddStringToObject(data, "pubkey_filter", pubkey_filter);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -811,15 +849,14 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
memcpy(query_copy, query_string, query_len + 1);
int count = 0;
char* token = strtok(query_copy, "&");
char* saveptr = NULL;
char* token = strtok_r(query_copy, "&", &saveptr);
while (token && count < max_params) {
if (strlen(token) >= sizeof(params[0])) {
token[sizeof(params[0]) - 1] = '\0';
}
strcpy(params[count], token);
strncpy(params[count], token, sizeof(params[0]) - 1);
params[count][sizeof(params[0]) - 1] = '\0';
count++;
token = strtok(NULL, "&");
token = strtok_r(NULL, "&", &saveptr);
}
free(query_copy);

View File

@@ -28,7 +28,7 @@ static int g_keys_loaded = 0; // Whether keys have been loaded
static int ensure_keys_loaded(void) {
if (!g_keys_loaded) {
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
fprintf(stderr, "ERROR: Cannot load blossom private key for admin auth\n");
app_log(LOG_ERROR, "ERROR: Cannot load blossom private key for admin auth\n");
return -1;
}
g_keys_loaded = 1;
@@ -83,13 +83,13 @@ int validate_admin_event(cJSON *event) {
cJSON *sig = cJSON_GetObjectItem(event, "sig");
if (!cJSON_IsString(pubkey) || !cJSON_IsString(sig)) {
fprintf(stderr, "AUTH: Invalid event format - missing pubkey or sig\n");
app_log(LOG_ERROR, "AUTH: Invalid event format - missing pubkey or sig\n");
return 0;
}
// Check if pubkey matches configured admin pubkey
if (!validate_admin_pubkey(pubkey->valuestring)) {
fprintf(stderr, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring);
app_log(LOG_ERROR, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring);
return 0;
}
@@ -114,14 +114,14 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
// Get admin pubkey from event
cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (!cJSON_IsString(admin_pubkey_json)) {
fprintf(stderr, "AUTH: Missing or invalid pubkey in event\n");
app_log(LOG_ERROR, "AUTH: Missing or invalid pubkey in event\n");
return -1;
}
// Get encrypted content
cJSON *content = cJSON_GetObjectItem(event, "content");
if (!cJSON_IsString(content)) {
fprintf(stderr, "AUTH: Missing or invalid content in event\n");
app_log(LOG_ERROR, "AUTH: Missing or invalid content in event\n");
return -1;
}
@@ -130,12 +130,12 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
unsigned char admin_public_key[32];
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key\n");
return -1;
}
if (nostr_hex_to_bytes(admin_pubkey_json->valuestring, admin_public_key, 32) != 0) {
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
app_log(LOG_ERROR, "AUTH: Failed to parse admin public key\n");
return -1;
}
@@ -152,14 +152,14 @@ int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
);
if (result != NOSTR_SUCCESS) {
fprintf(stderr, "AUTH: NIP-44 decryption failed with error code %d\n", result);
app_log(LOG_ERROR, "AUTH: NIP-44 decryption failed with error code %d\n", result);
return -1;
}
// Allocate and copy decrypted content
*decrypted_command_out = malloc(strlen(decrypted_buffer) + 1);
if (!*decrypted_command_out) {
fprintf(stderr, "AUTH: Failed to allocate memory for decrypted content\n");
app_log(LOG_ERROR, "AUTH: Failed to allocate memory for decrypted content\n");
return -1;
}
strcpy(*decrypted_command_out, decrypted_buffer);
@@ -176,19 +176,19 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
// Parse the decrypted content as JSON array
cJSON *content_json = cJSON_Parse(decrypted_content);
if (!content_json) {
fprintf(stderr, "AUTH: Failed to parse decrypted content as JSON\n");
app_log(LOG_ERROR, "AUTH: Failed to parse decrypted content as JSON\n");
return -1;
}
if (!cJSON_IsArray(content_json)) {
fprintf(stderr, "AUTH: Decrypted content is not a JSON array\n");
app_log(LOG_ERROR, "AUTH: Decrypted content is not a JSON array\n");
cJSON_Delete(content_json);
return -1;
}
int array_size = cJSON_GetArraySize(content_json);
if (array_size < 1) {
fprintf(stderr, "AUTH: Command array is empty\n");
app_log(LOG_ERROR, "AUTH: Command array is empty\n");
cJSON_Delete(content_json);
return -1;
}
@@ -196,7 +196,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
// Allocate command array
char **command_array = malloc(array_size * sizeof(char *));
if (!command_array) {
fprintf(stderr, "AUTH: Failed to allocate command array\n");
app_log(LOG_ERROR, "AUTH: Failed to allocate command array\n");
cJSON_Delete(content_json);
return -1;
}
@@ -205,7 +205,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
for (int i = 0; i < array_size; i++) {
cJSON *item = cJSON_GetArrayItem(content_json, i);
if (!cJSON_IsString(item)) {
fprintf(stderr, "AUTH: Command array element %d is not a string\n", i);
app_log(LOG_ERROR, "AUTH: Command array element %d is not a string\n", i);
// Clean up allocated strings
for (int j = 0; j < i; j++) {
free(command_array[j]);
@@ -217,7 +217,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
command_array[i] = malloc(strlen(item->valuestring) + 1);
if (!command_array[i]) {
fprintf(stderr, "AUTH: Failed to allocate command string\n");
app_log(LOG_ERROR, "AUTH: Failed to allocate command string\n");
// Clean up allocated strings
for (int j = 0; j < i; j++) {
free(command_array[j]);
@@ -228,7 +228,7 @@ int parse_admin_command(const char *decrypted_content, char ***command_array_out
}
strcpy(command_array[i], item->valuestring);
if (!command_array[i]) {
fprintf(stderr, "AUTH: Failed to duplicate command string\n");
app_log(LOG_ERROR, "AUTH: Failed to duplicate command string\n");
// Clean up allocated strings
for (int j = 0; j < i; j++) {
free(command_array[j]);
@@ -274,7 +274,7 @@ int process_admin_command(cJSON *event, char ***command_array_out, int *command_
sqlite3_close(db);
if (strlen(blossom_pubkey) != 64) {
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for admin auth\n");
app_log(LOG_ERROR, "ERROR: Cannot determine blossom pubkey for admin auth\n");
return -1;
}
@@ -296,7 +296,7 @@ int process_admin_command(cJSON *event, char ***command_array_out, int *command_
*admin_pubkey_out = malloc(strlen(admin_pubkey_json->valuestring) + 1);
if (!*admin_pubkey_out) {
fprintf(stderr, "AUTH: Failed to allocate admin pubkey string\n");
app_log(LOG_ERROR, "AUTH: Failed to allocate admin pubkey string\n");
return -1;
}
strcpy(*admin_pubkey_out, admin_pubkey_json->valuestring);
@@ -386,7 +386,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
sqlite3_close(db);
if (strlen(blossom_pubkey) != 64) {
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for response\n");
app_log(LOG_ERROR, "ERROR: Cannot determine blossom pubkey for response\n");
return -1;
}
@@ -395,12 +395,12 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
unsigned char admin_public_key[32];
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key\n");
return -1;
}
if (nostr_hex_to_bytes(admin_pubkey, admin_public_key, 32) != 0) {
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
app_log(LOG_ERROR, "AUTH: Failed to parse admin public key\n");
return -1;
}
@@ -415,14 +415,14 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
);
if (result != NOSTR_SUCCESS) {
fprintf(stderr, "AUTH: NIP-44 encryption failed with error code %d\n", result);
app_log(LOG_ERROR, "AUTH: NIP-44 encryption failed with error code %d\n", result);
return -1;
}
// Create Kind 23457 response event
cJSON *response_event = cJSON_CreateObject();
if (!response_event) {
fprintf(stderr, "AUTH: Failed to create response event JSON\n");
app_log(LOG_ERROR, "AUTH: Failed to create response event JSON\n");
return -1;
}
@@ -444,7 +444,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
// Convert private key hex to bytes
unsigned char blossom_private_key_bytes[32];
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key_bytes, 32) != 0) {
fprintf(stderr, "AUTH: Failed to parse blossom private key for signing\n");
app_log(LOG_ERROR, "AUTH: Failed to parse blossom private key for signing\n");
cJSON_Delete(response_event);
return -1;
}
@@ -452,7 +452,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
// Create a temporary event structure for signing
cJSON* temp_event = cJSON_Duplicate(response_event, 1);
if (!temp_event) {
fprintf(stderr, "AUTH: Failed to create temp event for signing\n");
app_log(LOG_ERROR, "AUTH: Failed to create temp event for signing\n");
cJSON_Delete(response_event);
return -1;
}
@@ -467,7 +467,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
);
if (!signed_event) {
fprintf(stderr, "AUTH: Failed to sign admin response event\n");
app_log(LOG_ERROR, "AUTH: Failed to sign admin response event\n");
cJSON_Delete(response_event);
cJSON_Delete(temp_event);
return -1;
@@ -481,7 +481,7 @@ int create_admin_response(const char *response_json, const char *admin_pubkey, c
cJSON_AddStringToObject(response_event, "id", cJSON_GetStringValue(signed_id));
cJSON_AddStringToObject(response_event, "sig", cJSON_GetStringValue(signed_sig));
} else {
fprintf(stderr, "AUTH: Signed event missing id or sig\n");
app_log(LOG_ERROR, "AUTH: Signed event missing id or sig\n");
cJSON_Delete(response_event);
cJSON_Delete(signed_event);
cJSON_Delete(temp_event);

View File

@@ -3,22 +3,17 @@
*/
#include "admin_commands.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "app_log.h"
#include "../nostr_core_lib/nostr_core/nip044.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.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, ...);
#include <unistd.h>
#include <errno.h>
#include "ginxsom.h"
// Global state
static struct {
@@ -136,12 +131,28 @@ cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id
else if (strcmp(command, "blob_list") == 0) {
return admin_cmd_blob_list(command_array);
}
else if (strcmp(command, "blob_delete") == 0) {
return admin_cmd_blob_delete(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 if (strcmp(command, "query_view") == 0) {
return admin_cmd_query_view(command_array);
}
// Auth rules management commands (c-relay compatible)
else if (strcmp(command, "blacklist") == 0 || strcmp(command, "whitelist") == 0) {
return admin_cmd_auth_add_rule(command_array);
}
else if (strcmp(command, "delete_auth_rule") == 0) {
return admin_cmd_auth_delete_rule(command_array);
}
else if (strcmp(command, "auth_query") == 0) {
return admin_cmd_auth_query(command_array);
}
else {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Unknown command: %s", command);
@@ -167,16 +178,23 @@ cJSON* admin_cmd_config_query(cJSON* args) {
return response;
}
// Check if specific keys were requested (args[1] should be array of keys or null for all)
// Check if specific keys were requested (args[1] should be array of keys, null, or "all" for all)
cJSON* keys_array = NULL;
if (cJSON_GetArraySize(args) >= 2) {
keys_array = cJSON_GetArrayItem(args, 1);
// Accept array, null, or string "all" for querying all configs
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;
// Check if it's the string "all"
if (cJSON_IsString(keys_array) && strcmp(keys_array->valuestring, "all") == 0) {
// Treat "all" as null (query all configs)
keys_array = NULL;
} else {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Keys parameter must be array, null, or \"all\"");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
}
}
@@ -259,18 +277,18 @@ cJSON* admin_cmd_config_update(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "config_update");
// Expected format: ["config_update", {"key1": "value1", "key2": "value2"}]
// Expected format: ["config_update", [{key: "x", value: "y", data_type: "z", category: "w"}]]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing config updates object");
cJSON_AddStringToObject(response, "error", "Missing config updates array");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* updates = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsObject(updates)) {
if (!cJSON_IsArray(updates)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Updates must be an object");
cJSON_AddStringToObject(response, "error", "Updates must be an array of config objects");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
@@ -297,50 +315,66 @@ cJSON* admin_cmd_config_update(cJSON* args) {
return response;
}
// Process each update
cJSON* updated_keys = cJSON_CreateArray();
cJSON* failed_keys = cJSON_CreateArray();
// Process each update - expecting array of config objects
cJSON* data_array = cJSON_CreateArray();
int success_count = 0;
int fail_count = 0;
cJSON* item = NULL;
cJSON_ArrayForEach(item, updates) {
const char* key = item->string;
const char* value = cJSON_GetStringValue(item);
if (!value) {
cJSON_AddItemToArray(failed_keys, cJSON_CreateString(key));
cJSON* config_obj = NULL;
cJSON_ArrayForEach(config_obj, updates) {
if (!cJSON_IsObject(config_obj)) {
fail_count++;
continue;
}
cJSON* key_item = cJSON_GetObjectItem(config_obj, "key");
cJSON* value_item = cJSON_GetObjectItem(config_obj, "value");
if (!cJSON_IsString(key_item) || !cJSON_IsString(value_item)) {
fail_count++;
continue;
}
const char* key = key_item->valuestring;
const char* value = value_item->valuestring;
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
// Create result object for this config update
cJSON* result_obj = cJSON_CreateObject();
cJSON_AddStringToObject(result_obj, "key", key);
if (rc == SQLITE_DONE && sqlite3_changes(db) > 0) {
cJSON_AddItemToArray(updated_keys, cJSON_CreateString(key));
cJSON_AddStringToObject(result_obj, "status", "success");
cJSON_AddStringToObject(result_obj, "value", value);
// Add optional fields if present
cJSON* data_type_item = cJSON_GetObjectItem(config_obj, "data_type");
if (cJSON_IsString(data_type_item)) {
cJSON_AddStringToObject(result_obj, "data_type", data_type_item->valuestring);
}
success_count++;
app_log(LOG_INFO, "Updated config key: %s", key);
app_log(LOG_INFO, "Updated config key: %s = %s", key, value);
} else {
cJSON_AddItemToArray(failed_keys, cJSON_CreateString(key));
cJSON_AddStringToObject(result_obj, "status", "error");
cJSON_AddStringToObject(result_obj, "error", "Failed to update");
fail_count++;
}
cJSON_AddItemToArray(data_array, result_obj);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "updated_count", success_count);
cJSON_AddNumberToObject(response, "failed_count", fail_count);
cJSON_AddItemToObject(response, "updated_keys", updated_keys);
if (fail_count > 0) {
cJSON_AddItemToObject(response, "failed_keys", failed_keys);
} else {
cJSON_Delete(failed_keys);
}
cJSON_AddStringToObject(response, "status", success_count > 0 ? "success" : "error");
cJSON_AddNumberToObject(response, "updates_applied", success_count);
cJSON_AddItemToObject(response, "data", data_array);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
@@ -387,7 +421,7 @@ cJSON* admin_cmd_stats_query(cJSON* args) {
sqlite3_finalize(stmt);
// Get auth rules count
sql = "SELECT COUNT(*) FROM auth_rules WHERE enabled = 1";
sql = "SELECT COUNT(*) FROM auth_rules WHERE active = 1";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) {
cJSON_AddNumberToObject(stats, "active_auth_rules", sqlite3_column_int(stmt, 0));
@@ -557,6 +591,142 @@ cJSON* admin_cmd_blob_list(cJSON* args) {
return response;
}
cJSON* admin_cmd_blob_delete(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "blob_delete");
// Expected format: ["blob_delete", "<sha256>"]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing blob hash");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* hash_item = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsString(hash_item) || !validate_sha256_format(hash_item->valuestring)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid SHA-256 hash format");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* sha256 = hash_item->valuestring;
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READWRITE, 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;
}
rc = sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to begin transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_stmt* stmt = NULL;
const char* select_sql = "SELECT type FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare metadata query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
char blob_type[128] = {0};
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* type = (const char*)sqlite3_column_text(stmt, 0);
if (type) {
strncpy(blob_type, type, sizeof(blob_type) - 1);
blob_type[sizeof(blob_type) - 1] = '\0';
}
} else {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Blob not found");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_finalize(stmt);
const char* extension = mime_to_extension(blob_type);
char filepath[4096];
snprintf(filepath, sizeof(filepath), "%s/%s%s", g_storage_dir, sha256, extension ? extension : "");
int file_deleted = 0;
if (unlink(filepath) == 0) {
file_deleted = 1;
} else if (errno == ENOENT) {
// File already absent; still remove DB reference
file_deleted = 0;
} else {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
char errbuf[256];
snprintf(errbuf, sizeof(errbuf), "Failed to delete file: %s", strerror(errno));
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", errbuf);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
const char* delete_sql = "DELETE FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, delete_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare delete query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to delete blob metadata");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
sqlite3_close(db);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to commit blob delete transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddStringToObject(response, "sha256", sha256);
cJSON_AddBoolToObject(response, "file_deleted", file_deleted ? 1 : 0);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_storage_stats(cJSON* args) {
(void)args;
@@ -637,7 +807,7 @@ cJSON* admin_cmd_sql_query(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "sql_query");
// Expected format: ["sql_query", "SELECT ..."]
// Expected format: ["sql_query", "SQL STATEMENT"]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing SQL query");
@@ -654,20 +824,26 @@ cJSON* admin_cmd_sql_query(cJSON* args) {
}
const char* sql = query_item->valuestring;
const char* trimmed_sql = sql;
while (*trimmed_sql && isspace((unsigned char)*trimmed_sql)) {
trimmed_sql++;
}
// Security: Only allow SELECT queries
const char* sql_upper = sql;
while (*sql_upper == ' ' || *sql_upper == '\t' || *sql_upper == '\n') sql_upper++;
if (strncasecmp(sql_upper, "SELECT", 6) != 0) {
int is_select = strncasecmp(trimmed_sql, "SELECT", 6) == 0;
int is_delete = strncasecmp(trimmed_sql, "DELETE", 6) == 0;
int is_update = strncasecmp(trimmed_sql, "UPDATE", 6) == 0;
int is_insert = strncasecmp(trimmed_sql, "INSERT", 6) == 0;
if (!is_select && !is_delete && !is_update && !is_insert) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Only SELECT queries are allowed");
cJSON_AddStringToObject(response, "error", "Only SELECT, INSERT, UPDATE, or DELETE queries are allowed");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
// Open database (read-only for safety)
int open_flags = is_select ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READONLY, NULL);
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, open_flags, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to open database");
@@ -675,7 +851,70 @@ cJSON* admin_cmd_sql_query(cJSON* args) {
return response;
}
// Prepare and execute query
if (is_select) {
sqlite3_stmt* stmt;
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "SQL error: %s", sqlite3_errmsg(db));
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
int col_count = sqlite3_column_count(stmt);
cJSON* columns = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
cJSON_AddItemToArray(columns, cJSON_CreateString(sqlite3_column_name(stmt, i)));
}
cJSON* rows = cJSON_CreateArray();
int row_count = 0;
const int MAX_ROWS = 1000;
while (row_count < MAX_ROWS && (rc = sqlite3_step(stmt)) == SQLITE_ROW) {
cJSON* row = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
int col_type = sqlite3_column_type(stmt, i);
switch (col_type) {
case SQLITE_INTEGER:
cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_int64(stmt, i)));
break;
case SQLITE_FLOAT:
cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_double(stmt, i)));
break;
case SQLITE_TEXT:
cJSON_AddItemToArray(row, cJSON_CreateString((const char*)sqlite3_column_text(stmt, i)));
break;
case SQLITE_NULL:
cJSON_AddItemToArray(row, cJSON_CreateNull());
break;
default:
cJSON_AddItemToArray(row, cJSON_CreateString(""));
}
}
cJSON_AddItemToArray(rows, row);
row_count++;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddItemToObject(response, "columns", columns);
cJSON_AddItemToObject(response, "rows", rows);
cJSON_AddNumberToObject(response, "row_count", row_count);
if (row_count >= MAX_ROWS) {
cJSON_AddBoolToObject(response, "truncated", 1);
}
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "SQL query executed: %d rows returned", row_count);
return response;
}
// Handle DELETE/UPDATE/INSERT
sqlite3_stmt* stmt;
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
@@ -688,19 +927,113 @@ cJSON* admin_cmd_sql_query(cJSON* args) {
return response;
}
// Get column names
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
cJSON_AddStringToObject(response, "status", "error");
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "SQL execution error: %s", sqlite3_errmsg(db));
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_finalize(stmt);
sqlite3_close(db);
return response;
}
int affected_rows = sqlite3_changes(db);
sqlite3_finalize(stmt);
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "affected_rows", affected_rows);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "SQL modification executed: %d rows affected", affected_rows);
return response;
}
cJSON* admin_cmd_query_view(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "query_view");
// Expected format: ["query_view", "view_name"]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing view name");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* view_name_item = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsString(view_name_item)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "View name must be a string");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* view_name = view_name_item->valuestring;
// 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;
}
// Build SQL query based on view name
char sql[512];
if (strcmp(view_name, "blob_overview") == 0) {
// Query blob_overview view
snprintf(sql, sizeof(sql), "SELECT * FROM blob_overview");
} else if (strcmp(view_name, "storage_stats") == 0) {
// Query storage_stats view
snprintf(sql, sizeof(sql), "SELECT * FROM storage_stats");
} else if (strcmp(view_name, "blob_type_distribution") == 0) {
// Query blob_type_distribution view
snprintf(sql, sizeof(sql), "SELECT * FROM blob_type_distribution");
} else if (strcmp(view_name, "blob_time_stats") == 0) {
// Query blob_time_stats view
snprintf(sql, sizeof(sql), "SELECT * FROM blob_time_stats");
} else if (strcmp(view_name, "top_uploaders") == 0) {
// Query top_uploaders view
snprintf(sql, sizeof(sql), "SELECT * FROM top_uploaders");
} else {
cJSON_AddStringToObject(response, "status", "error");
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Unknown view: %s", view_name);
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_stmt* stmt;
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to prepare query: %s", sqlite3_errmsg(db));
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
// Execute query and build results
int col_count = sqlite3_column_count(stmt);
cJSON* columns = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
cJSON_AddItemToArray(columns, cJSON_CreateString(sqlite3_column_name(stmt, i)));
}
// Execute and collect rows (limit to 1000 rows for safety)
cJSON* rows = cJSON_CreateArray();
int row_count = 0;
const int MAX_ROWS = 1000;
while (row_count < MAX_ROWS && (rc = sqlite3_step(stmt)) == SQLITE_ROW) {
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
cJSON* row = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
int col_type = sqlite3_column_type(stmt, i);
@@ -729,15 +1062,313 @@ cJSON* admin_cmd_sql_query(cJSON* args) {
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddStringToObject(response, "view_name", view_name);
cJSON_AddItemToObject(response, "columns", columns);
cJSON_AddItemToObject(response, "rows", rows);
cJSON_AddNumberToObject(response, "row_count", row_count);
if (row_count >= MAX_ROWS) {
cJSON_AddBoolToObject(response, "truncated", 1);
}
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "SQL query executed: %d rows returned", row_count);
app_log(LOG_INFO, "View query executed: %s (%d rows)", view_name, row_count);
return response;
}
// ============================================================================
// AUTH RULES MANAGEMENT COMMANDS (c-relay compatible)
// ============================================================================
// Add blacklist or whitelist rule
// Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
cJSON* admin_cmd_auth_add_rule(cJSON* args) {
cJSON* response = cJSON_CreateObject();
// Get command type (blacklist or whitelist)
cJSON* cmd_type = cJSON_GetArrayItem(args, 0);
if (!cJSON_IsString(cmd_type)) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid command type");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* command = cmd_type->valuestring;
const char* rule_type_prefix = command; // "blacklist" or "whitelist"
// Expected format: ["blacklist/whitelist", "pattern_type", "pattern_value"]
if (cJSON_GetArraySize(args) < 3) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing parameters. Format: [\"blacklist/whitelist\", \"pattern_type\", \"pattern_value\"]");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* pattern_type_item = cJSON_GetArrayItem(args, 1);
cJSON* pattern_value_item = cJSON_GetArrayItem(args, 2);
if (!cJSON_IsString(pattern_type_item) || !cJSON_IsString(pattern_value_item)) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Pattern type and value must be strings");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* pattern_type = pattern_type_item->valuestring;
const char* pattern_value = pattern_value_item->valuestring;
char rule_type[64];
snprintf(rule_type, sizeof(rule_type), "%s_%s", rule_type_prefix, pattern_type);
// Validate pattern_type
if (strcmp(pattern_type, "pubkey") != 0 && strcmp(pattern_type, "hash") != 0 && strcmp(pattern_type, "mime") != 0) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid pattern_type. Must be 'pubkey', 'hash', or 'mime'");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
// Open database
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to open database");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
// Insert rule
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)";
sqlite3_stmt* stmt;
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare insert statement");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
int rule_id = 0;
if (rc == SQLITE_DONE) {
rule_id = sqlite3_last_insert_rowid(db);
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "rule_id", rule_id);
cJSON_AddStringToObject(response, "rule_type", rule_type);
cJSON_AddStringToObject(response, "pattern_type", pattern_type);
cJSON_AddStringToObject(response, "pattern_value", pattern_value);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "Added %s rule: %s=%s (ID: %d)", rule_type, pattern_type, pattern_value, rule_id);
} else {
cJSON_AddStringToObject(response, "query_type", "auth_add_rule");
cJSON_AddStringToObject(response, "status", "error");
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to insert rule: %s", sqlite3_errmsg(db));
cJSON_AddStringToObject(response, "error", error_msg);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return response;
}
// Delete auth rule
// Format: ["delete_auth_rule", "blacklist", "pubkey", "abc123..."]
cJSON* admin_cmd_auth_delete_rule(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "delete_auth_rule");
// Expected format: ["delete_auth_rule", "rule_type", "pattern_type", "pattern_value"]
if (cJSON_GetArraySize(args) < 4) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing parameters. Format: [\"delete_auth_rule\", \"blacklist/whitelist\", \"pattern_type\", \"pattern_value\"]");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* rule_type_item = cJSON_GetArrayItem(args, 1);
cJSON* pattern_type_item = cJSON_GetArrayItem(args, 2);
cJSON* pattern_value_item = cJSON_GetArrayItem(args, 3);
if (!cJSON_IsString(rule_type_item) || !cJSON_IsString(pattern_type_item) || !cJSON_IsString(pattern_value_item)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "All parameters must be strings");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* rule_type_str = cJSON_GetStringValue(rule_type_item);
const char* pattern_type = cJSON_GetStringValue(pattern_type_item);
const char* pattern_value = cJSON_GetStringValue(pattern_value_item);
char full_rule_type[64];
snprintf(full_rule_type, sizeof(full_rule_type), "%s_%s", rule_type_str, pattern_type);
// Open database
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READWRITE, 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;
}
// Delete rule
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
sqlite3_stmt* stmt;
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 delete statement");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, full_rule_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
int changes = sqlite3_changes(db);
if (rc == SQLITE_DONE) {
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "deleted_count", changes);
cJSON_AddStringToObject(response, "rule_type", full_rule_type);
cJSON_AddStringToObject(response, "pattern_type", pattern_type);
cJSON_AddStringToObject(response, "pattern_value", pattern_value);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "Deleted %d %s rule(s): %s=%s", changes, full_rule_type, pattern_type, pattern_value);
} else {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to delete rule");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return response;
}
// Query auth rules
// Format: ["auth_query", "all"] or ["auth_query", "whitelist"] or ["auth_query", "pattern", "abc123..."]
cJSON* admin_cmd_auth_query(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "auth_query");
// Get query type
const char* query_type = "all";
const char* filter_value = NULL;
if (cJSON_GetArraySize(args) >= 2) {
cJSON* query_type_item = cJSON_GetArrayItem(args, 1);
if (cJSON_IsString(query_type_item)) {
query_type = query_type_item->valuestring;
}
}
if (cJSON_GetArraySize(args) >= 3) {
cJSON* filter_value_item = cJSON_GetArrayItem(args, 2);
if (cJSON_IsString(filter_value_item)) {
filter_value = filter_value_item->valuestring;
}
}
// 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;
}
// Build SQL query based on query type
char sql[512];
sqlite3_stmt* stmt;
if (strcmp(query_type, "all") == 0) {
snprintf(sql, sizeof(sql), "SELECT id, rule_type, pattern_type, pattern_value, active, created_at, updated_at FROM auth_rules ORDER BY id");
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
}
else if (strcmp(query_type, "blacklist") == 0 || strcmp(query_type, "whitelist") == 0) {
snprintf(sql, sizeof(sql), "SELECT id, rule_type, pattern_type, pattern_value, active, created_at, updated_at FROM auth_rules WHERE rule_type LIKE ? || '_%%' ORDER BY id");
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, query_type, -1, SQLITE_STATIC);
}
}
else if (strcmp(query_type, "pattern") == 0 && filter_value) {
snprintf(sql, sizeof(sql), "SELECT id, rule_type, pattern_type, pattern_value, active, created_at, updated_at FROM auth_rules WHERE pattern_value = ? ORDER BY id");
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, filter_value, -1, SQLITE_STATIC);
}
}
else {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid query type. Use 'all', 'blacklist', 'whitelist', or 'pattern'");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
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 results
cJSON* rules = cJSON_CreateArray();
int count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* rule = cJSON_CreateObject();
cJSON_AddNumberToObject(rule, "id", sqlite3_column_int(stmt, 0));
cJSON_AddStringToObject(rule, "rule_type", (const char*)sqlite3_column_text(stmt, 1));
cJSON_AddStringToObject(rule, "pattern_type", (const char*)sqlite3_column_text(stmt, 2));
cJSON_AddStringToObject(rule, "pattern_value", (const char*)sqlite3_column_text(stmt, 3));
cJSON_AddNumberToObject(rule, "active", sqlite3_column_int(stmt, 4));
cJSON_AddNumberToObject(rule, "created_at", sqlite3_column_int64(stmt, 5));
cJSON_AddNumberToObject(rule, "updated_at", sqlite3_column_int64(stmt, 6));
cJSON_AddItemToArray(rules, rule);
count++;
}
sqlite3_finalize(stmt);
sqlite3_close(db);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "count", count);
cJSON_AddStringToObject(response, "filter", query_type);
cJSON_AddItemToObject(response, "rules", rules);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
app_log(LOG_INFO, "Auth query executed: %d rules returned (filter: %s)", count, query_type);
return response;
}

View File

@@ -1,8 +1,8 @@
/*
* Ginxsom Admin Commands Interface
*
* Handles encrypted admin commands sent via Kind 23456 events
* and generates encrypted responses as Kind 23457 events.
* Handles encrypted admin commands sent via Kind 23458 events
* and generates encrypted responses as Kind 23459 events.
*/
#ifndef ADMIN_COMMANDS_H
@@ -33,8 +33,15 @@ 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_blob_delete(cJSON* args);
cJSON* admin_cmd_storage_stats(cJSON* args);
cJSON* admin_cmd_sql_query(cJSON* args);
cJSON* admin_cmd_query_view(cJSON* args);
// Auth rules management handlers (c-relay compatible)
cJSON* admin_cmd_auth_add_rule(cJSON* args);
cJSON* admin_cmd_auth_delete_rule(cJSON* args);
cJSON* admin_cmd_auth_query(cJSON* args);
// NIP-44 encryption/decryption helpers
int admin_encrypt_response(

View File

@@ -1,9 +1,12 @@
// Admin event handler for Kind 23456/23457 admin commands
// Admin event handler for Kind 23458/23459 admin commands
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include "ginxsom.h"
#include "admin_commands.h"
// Forward declarations for nostr_core_lib functions
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len);
@@ -26,91 +29,161 @@ extern char g_db_path[];
// Forward declarations
static int get_server_privkey(unsigned char* privkey_bytes);
static int get_server_pubkey(char* pubkey_hex, size_t size);
static int handle_config_query_command(cJSON* response_data);
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data);
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data);
static cJSON* parse_authorization_header(void);
static int process_admin_event(cJSON* event);
/**
* Handle Kind 23456 admin command event
* Expects POST to /api/admin with JSON body containing the event
* Handle Kind 23458 admin command event
* Supports two delivery methods:
* 1. POST body with JSON event
* 2. Authorization header with Nostr event
*/
void handle_admin_event_request(void) {
// Read request body
const char* content_length_str = getenv("CONTENT_LENGTH");
if (!content_length_str) {
printf("Status: 411 Length Required\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Content-Length header required\"}\n");
return;
}
cJSON* event = NULL;
int should_free_event = 1;
long content_length = atol(content_length_str);
if (content_length <= 0 || content_length > 65536) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid content length\"}\n");
return;
}
char* json_body = malloc(content_length + 1);
if (!json_body) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Memory allocation failed\"}\n");
return;
}
size_t bytes_read = fread(json_body, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(json_body);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to read complete request body\"}\n");
return;
}
json_body[content_length] = '\0';
// Parse event JSON
cJSON* event = cJSON_Parse(json_body);
free(json_body);
// First, try to get event from Authorization header
event = parse_authorization_header();
// If not in header, try POST body
if (!event) {
const char* content_length_str = getenv("CONTENT_LENGTH");
if (!content_length_str) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event required in POST body or Authorization header\"}\n");
return;
}
long content_length = atol(content_length_str);
if (content_length <= 0 || content_length > 65536) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid content length\"}\n");
return;
}
char* json_body = malloc(content_length + 1);
if (!json_body) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Memory allocation failed\"}\n");
return;
}
size_t bytes_read = fread(json_body, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(json_body);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to read complete request body\"}\n");
return;
}
json_body[content_length] = '\0';
// Parse event JSON
event = cJSON_Parse(json_body);
// Debug: Log the received JSON
app_log(LOG_DEBUG, "ADMIN_EVENT: Received POST body: %s", json_body);
free(json_body);
if (!event) {
app_log(LOG_ERROR, "ADMIN_EVENT: Failed to parse JSON");
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid JSON\"}\n");
return;
}
// Debug: Log parsed event
char* event_str = cJSON_Print(event);
if (event_str) {
app_log(LOG_DEBUG, "ADMIN_EVENT: Parsed event: %s", event_str);
free(event_str);
}
}
// Process the event (handles validation, decryption, command execution, response)
int result = process_admin_event(event);
// Clean up
if (should_free_event && event) {
cJSON_Delete(event);
}
(void)result; // Result already handled by process_admin_event
}
/**
* Parse Kind 23458 event from Authorization header
* Format: Authorization: Nostr <base64-encoded-event-json>
* Returns: cJSON event object or NULL if not present/invalid
*/
static cJSON* parse_authorization_header(void) {
const char* auth_header = getenv("HTTP_AUTHORIZATION");
if (!auth_header) {
return NULL;
}
// Check for "Nostr " prefix (case-insensitive)
if (strncasecmp(auth_header, "Nostr ", 6) != 0) {
return NULL;
}
// Skip "Nostr " prefix
const char* base64_event = auth_header + 6;
// Decode base64 (simple implementation - in production use proper base64 decoder)
// For now, assume the event is JSON directly (not base64 encoded)
// This matches the pattern from c-relay's admin interface
cJSON* event = cJSON_Parse(base64_event);
return event;
}
/**
* Process a Kind 23458 admin event (from POST body or Authorization header)
* Returns: 0 on success, -1 on error (error response already sent)
*/
static int process_admin_event(cJSON* event) {
if (!event) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid JSON\"}\n");
return;
printf("{\"error\":\"Invalid event\"}\n");
return -1;
}
// Verify it's Kind 23456
// Verify it's Kind 23458
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
cJSON_Delete(event);
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23458) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event must be Kind 23456\"}\n");
return;
printf("{\"error\":\"Event must be Kind 23458\"}\n");
return -1;
}
// Get event ID for response correlation
cJSON* id_obj = cJSON_GetObjectItem(event, "id");
if (!id_obj || !cJSON_IsString(id_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing id\"}\n");
return;
return -1;
}
const char* request_id = cJSON_GetStringValue(id_obj);
// Get admin pubkey from event
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing pubkey\"}\n");
return;
return -1;
}
const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj);
@@ -118,11 +191,10 @@ void handle_admin_event_request(void) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_Delete(event);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Database error\"}\n");
return;
return -1;
}
sqlite3_stmt* stmt;
@@ -141,42 +213,38 @@ void handle_admin_event_request(void) {
sqlite3_close(db);
if (!is_admin) {
cJSON_Delete(event);
printf("Status: 403 Forbidden\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Not authorized as admin\"}\n");
return;
return -1;
}
// Get encrypted content
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing content\"}\n");
return;
return -1;
}
const char* encrypted_content = cJSON_GetStringValue(content_obj);
// Get server private key for decryption
unsigned char server_privkey[32];
if (get_server_privkey(server_privkey) != 0) {
cJSON_Delete(event);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to get server private key\"}\n");
return;
return -1;
}
// Convert admin pubkey to bytes
unsigned char admin_pubkey_bytes[32];
if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid admin pubkey format\"}\n");
return;
return -1;
}
// Decrypt content using NIP-44 (or use plaintext for testing)
@@ -195,34 +263,37 @@ void handle_admin_event_request(void) {
);
if (decrypt_result != 0) {
cJSON_Delete(event);
app_log(LOG_ERROR, "ADMIN_EVENT: Decryption failed with result: %d", decrypt_result);
app_log(LOG_ERROR, "ADMIN_EVENT: Encrypted content: %s", encrypted_content);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to decrypt content\"}\n");
return;
return -1;
}
content_to_parse = decrypted_content;
app_log(LOG_DEBUG, "ADMIN_EVENT: Decrypted content: %s", decrypted_content);
} else {
app_log(LOG_DEBUG, "ADMIN_EVENT: Using plaintext content (starts with '['): %s", encrypted_content);
}
// Parse command array (either decrypted or plaintext)
app_log(LOG_DEBUG, "ADMIN_EVENT: Parsing command array from: %s", content_to_parse);
cJSON* command_array = cJSON_Parse(content_to_parse);
if (!command_array || !cJSON_IsArray(command_array)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Decrypted content is not a valid command array\"}\n");
return;
return -1;
}
// Get command type
cJSON* command_type = cJSON_GetArrayItem(command_array, 0);
if (!command_type || !cJSON_IsString(command_type)) {
cJSON_Delete(command_array);
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid command format\"}\n");
return;
return -1;
}
const char* cmd = cJSON_GetStringValue(command_type);
@@ -232,26 +303,53 @@ void handle_admin_event_request(void) {
cJSON_AddStringToObject(response_data, "query_type", cmd);
cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL));
// Handle command
// Handle command - use admin_commands system for processing
cJSON* command_response = admin_commands_process(command_array, request_id);
int result = -1;
if (strcmp(cmd, "config_query") == 0) {
result = handle_config_query_command(response_data);
if (command_response) {
// Check if command was successful
cJSON* status = cJSON_GetObjectItem(command_response, "status");
if (status && cJSON_IsString(status)) {
const char* status_str = cJSON_GetStringValue(status);
if (strcmp(status_str, "success") == 0) {
result = 0;
}
}
// Copy response data from command_response to response_data
cJSON* item = NULL;
cJSON_ArrayForEach(item, command_response) {
if (item->string) {
cJSON* copy = cJSON_Duplicate(item, 1);
cJSON_AddItemToObject(response_data, item->string, copy);
}
}
cJSON_Delete(command_response);
app_log(LOG_DEBUG, "ADMIN_EVENT: Command processed with result: %d", result);
} else {
app_log(LOG_ERROR, "ADMIN_EVENT: Command processing returned NULL");
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Unknown command");
cJSON_AddStringToObject(response_data, "error", "Command processing failed");
result = -1;
}
cJSON_Delete(command_array);
cJSON_Delete(event);
if (result == 0) {
// Send Kind 23457 response
send_admin_response_event(admin_pubkey, request_id, response_data);
app_log(LOG_DEBUG, "ADMIN_EVENT: Sending Kind 23459 response");
// Send Kind 23459 response
int send_result = send_admin_response_event(admin_pubkey, request_id, response_data);
app_log(LOG_DEBUG, "ADMIN_EVENT: Response sent with result: %d", send_result);
return send_result;
} else {
app_log(LOG_ERROR, "ADMIN_EVENT: Command processing failed");
cJSON_Delete(response_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Command processing failed\"}\n");
return -1;
}
}
@@ -313,44 +411,9 @@ static int get_server_pubkey(char* pubkey_hex, size_t size) {
return result;
}
/**
* Handle config_query command - returns all config values
*/
static int handle_config_query_command(cJSON* response_data) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Database error");
return -1;
}
cJSON_AddStringToObject(response_data, "status", "success");
cJSON* data = cJSON_CreateObject();
// Query all config settings
sqlite3_stmt* stmt;
const char* sql = "SELECT key, value FROM config ORDER BY key";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (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);
if (key && value) {
cJSON_AddStringToObject(data, key, value);
}
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response_data, "data", data);
sqlite3_close(db);
return 0;
}
/**
* Send Kind 23457 admin response event
* Send Kind 23459 admin response event
*/
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data) {
@@ -407,11 +470,11 @@ static int send_admin_response_event(const char* admin_pubkey, const char* reque
return -1;
}
// Create Kind 23457 response event
// Create Kind 23459 response event
cJSON* response_event = cJSON_CreateObject();
cJSON_AddStringToObject(response_event, "pubkey", server_pubkey);
cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL));
cJSON_AddNumberToObject(response_event, "kind", 23457);
cJSON_AddNumberToObject(response_event, "kind", 23459);
cJSON_AddStringToObject(response_event, "content", encrypted_response);
// Add tags
@@ -433,7 +496,7 @@ static int send_admin_response_event(const char* admin_pubkey, const char* reque
// Sign the event
cJSON* signed_event = nostr_create_and_sign_event(
23457,
23459,
encrypted_response,
tags,
server_privkey,

62
src/admin_interface.c Normal file
View File

@@ -0,0 +1,62 @@
// Admin interface handler - serves embedded web UI files
#include <stdio.h>
#include <string.h>
#include "ginxsom.h"
#include "admin_interface_embedded.h"
/**
* Serve embedded file with appropriate content type
*/
static void serve_embedded_file(const unsigned char* data, size_t size, const char* content_type) {
printf("Status: 200 OK\r\n");
printf("Content-Type: %s\r\n", content_type);
printf("Content-Length: %lu\r\n", (unsigned long)size);
printf("Cache-Control: public, max-age=3600\r\n");
printf("\r\n");
fwrite((void*)data, 1, size, stdout);
fflush(stdout);
}
/**
* Handle admin interface requests
* Serves embedded web UI files from /api path (consistent with c-relay)
*/
void handle_admin_interface_request(const char* path) {
// Normalize path - remove trailing slash
char normalized_path[256];
strncpy(normalized_path, path, sizeof(normalized_path) - 1);
normalized_path[sizeof(normalized_path) - 1] = '\0';
size_t len = strlen(normalized_path);
if (len > 1 && normalized_path[len - 1] == '/') {
normalized_path[len - 1] = '\0';
}
// Route to appropriate embedded file
// All paths use /api/ prefix for consistency with c-relay
if (strcmp(normalized_path, "/api") == 0 || strcmp(normalized_path, "/api/index.html") == 0) {
serve_embedded_file(embedded_index_html, embedded_index_html_size, "text/html; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/index.css") == 0) {
serve_embedded_file(embedded_index_css, embedded_index_css_size, "text/css; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/index.js") == 0) {
serve_embedded_file(embedded_index_js, embedded_index_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/nostr-lite.js") == 0) {
serve_embedded_file(embedded_nostr_lite_js, embedded_nostr_lite_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/nostr.bundle.js") == 0) {
serve_embedded_file(embedded_nostr_bundle_js, embedded_nostr_bundle_js_size, "application/javascript; charset=utf-8");
}
else if (strcmp(normalized_path, "/api/text_graph.js") == 0) {
serve_embedded_file(embedded_text_graph_js, embedded_text_graph_js_size, "application/javascript; charset=utf-8");
}
else {
// 404 Not Found
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/html; charset=utf-8\r\n");
printf("\r\n");
printf("<html><body><h1>404 Not Found</h1><p>File not found: %s</p></body></html>\n", normalized_path);
}
}

733137
src/admin_interface_embedded.h Normal file

File diff suppressed because it is too large Load Diff

24
src/app_log.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef APP_LOG_H
#define APP_LOG_H
#ifdef __cplusplus
extern "C" {
#endif
// Centralized application logging (writes to logs/app/app.log)
typedef enum {
LOG_TRACE = 0,
LOG_DEBUG = 1,
LOG_INFO = 2,
LOG_WARN = 3,
LOG_ERROR = 4
} log_level_t;
extern log_level_t g_log_level;
void app_log(log_level_t level, const char* format, ...);
#ifdef __cplusplus
}
#endif
#endif // APP_LOG_H

View File

@@ -95,35 +95,60 @@ int nip94_get_origin(char* out, size_t out_size) {
return 1;
}
#define GINXSOM_MIME_EXTENSION_ENTRIES(X) \
X("image/jpeg", ".jpg") \
X("image/webp", ".webp") \
X("image/png", ".png") \
X("image/gif", ".gif") \
X("video/mp4", ".mp4") \
X("video/webm", ".webm") \
X("audio/mpeg", ".mp3") \
X("audio/ogg", ".ogg") \
X("text/plain", ".txt") \
X("text/html", ".html") \
X("text/css", ".css") \
X("application/javascript", ".js") \
X("text/javascript", ".mjs") \
X("application/pdf", ".pdf")
typedef struct {
const char* mime;
const char* extension;
} mime_extension_entry_t;
static const mime_extension_entry_t g_mime_extension_map[] = {
#define MAKE_MIME_ENTRY(mime, ext) {mime, ext},
GINXSOM_MIME_EXTENSION_ENTRIES(MAKE_MIME_ENTRY)
#undef MAKE_MIME_ENTRY
};
static const char* g_supported_mime_types[] = {
#define MAKE_MIME_ONLY(mime, ext) mime,
GINXSOM_MIME_EXTENSION_ENTRIES(MAKE_MIME_ONLY)
#undef MAKE_MIME_ONLY
};
const char* const* ginxsom_supported_mime_types(size_t* count) {
if (count) {
*count = sizeof(g_supported_mime_types) / sizeof(g_supported_mime_types[0]);
}
return g_supported_mime_types;
}
// Centralized MIME type to file extension mapping
const char* mime_to_extension(const char* mime_type) {
if (!mime_type) {
return ".bin";
}
if (strstr(mime_type, "image/jpeg")) {
return ".jpg";
} else if (strstr(mime_type, "image/webp")) {
return ".webp";
} else if (strstr(mime_type, "image/png")) {
return ".png";
} else if (strstr(mime_type, "image/gif")) {
return ".gif";
} else if (strstr(mime_type, "video/mp4")) {
return ".mp4";
} else if (strstr(mime_type, "video/webm")) {
return ".webm";
} else if (strstr(mime_type, "audio/mpeg")) {
return ".mp3";
} else if (strstr(mime_type, "audio/ogg")) {
return ".ogg";
} else if (strstr(mime_type, "text/plain")) {
return ".txt";
} else if (strstr(mime_type, "application/pdf")) {
return ".pdf";
} else {
return ".bin";
size_t count = sizeof(g_mime_extension_map) / sizeof(g_mime_extension_map[0]);
for (size_t i = 0; i < count; i++) {
if (strstr(mime_type, g_mime_extension_map[i].mime)) {
return g_mime_extension_map[i].extension;
}
}
return ".bin";
}
// Build canonical blob URL from origin + sha256 + extension

View File

@@ -9,9 +9,9 @@
#define GINXSOM_H
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 14
#define VERSION "v0.1.14"
#define VERSION_MINOR 2
#define VERSION_PATCH 3
#define VERSION "v0.2.3"
#include <stddef.h>
#include <stdint.h>
@@ -20,6 +20,8 @@
#include <sqlite3.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "app_log.h"
#ifdef __cplusplus
extern "C" {
#endif
@@ -226,6 +228,7 @@ int nip94_get_origin(char* out, size_t out_size);
// MIME type and file extension handling
const char* mime_to_extension(const char* mime_type);
const char* const* ginxsom_supported_mime_types(size_t* count);
void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
// Image dimension parsing
@@ -250,15 +253,6 @@ void send_json_response(int status_code, const char* json_content);
// Logging utilities
void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
// Centralized application logging (writes to logs/app/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, ...);
// SHA-256 validation helper (used by multiple BUDs)
int validate_sha256_format(const char* sha256);
@@ -272,9 +266,12 @@ int validate_sha256_format(const char* sha256);
// Admin API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
// Admin event handler (Kind 23456/23457)
// Admin event handler (Kind 23458/23459)
void handle_admin_event_request(void);
// Admin interface handler (serves embedded web UI)
void handle_admin_interface_request(const char* path);
// Individual endpoint handlers
void handle_stats_api(void);
void handle_config_get_api(void);

1410
src/main.c

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
#include "relay_client.h"
#include "admin_commands.h"
#include "app_log.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include <sqlite3.h>
#include <stdio.h>
@@ -15,16 +16,6 @@
#include <unistd.h>
#include <time.h>
// Forward declare app_log to avoid including ginxsom.h (which has typedef conflicts)
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, ...);
// Maximum number of relays to connect to
#define MAX_RELAYS 10
@@ -36,7 +27,7 @@ void app_log(log_level_t level, const char* format, ...);
static struct {
int enabled;
int initialized;
int running;
volatile int running;
char db_path[512];
nostr_relay_pool_t* pool;
char** relay_urls;
@@ -272,14 +263,30 @@ int relay_client_start(void) {
}
app_log(LOG_INFO, "Starting relay client...");
// Use a larger stack for websocket/TLS internals to avoid stack overflow in static builds
pthread_attr_t thread_attr;
size_t stack_size = 8 * 1024 * 1024; // 8MB
if (pthread_attr_init(&thread_attr) != 0) {
app_log(LOG_ERROR, "Failed to initialize thread attributes");
return -1;
}
if (pthread_attr_setstacksize(&thread_attr, stack_size) != 0) {
app_log(LOG_WARN, "Failed to set relay management thread stack size, using default");
}
// Start management thread
g_relay_state.running = 1;
if (pthread_create(&g_relay_state.management_thread, NULL, relay_management_thread, NULL) != 0) {
if (pthread_create(&g_relay_state.management_thread, &thread_attr, relay_management_thread, NULL) != 0) {
app_log(LOG_ERROR, "Failed to create relay management thread");
g_relay_state.running = 0;
pthread_attr_destroy(&thread_attr);
return -1;
}
pthread_attr_destroy(&thread_attr);
app_log(LOG_INFO, "Relay client started successfully");
return 0;
@@ -332,6 +339,11 @@ static void *relay_management_thread(void *arg) {
if (events_processed < 0) {
app_log(LOG_ERROR, "Error polling relay pool");
sleep(1);
} else if (events_processed == 0) {
// Prevent busy-wait: nostr_relay_pool_poll() returns immediately when all
// relays are disconnected because it skips the blocking nostr_ws_receive()
// call for non-connected relays. Without this sleep, the loop spins at 100% CPU.
usleep(100000); // 100ms idle sleep
}
// Pool handles all connection management, reconnection, and message processing
}

View File

@@ -23,6 +23,8 @@
#include <strings.h>
#include <time.h>
#define MAX_MIME_TYPE_LEN 128 // Define here for direct use
// Additional error codes for ginxsom-specific functionality
#define NOSTR_ERROR_CRYPTO_INIT -100
#define NOSTR_ERROR_AUTH_REQUIRED -101
@@ -90,14 +92,25 @@ struct {
} g_last_rule_violation = {0};
/**
* Helper function for consistent debug logging to our debug.log file
* Helper function for consistent validator debug logging via app logger
*/
static void validator_debug_log(const char *message) {
FILE *debug_log = fopen("logs/app/debug.log", "a");
if (debug_log) {
fprintf(debug_log, "%ld %s", (long)time(NULL), message);
fclose(debug_log);
const char *msg = message ? message : "";
size_t msg_len = strlen(msg);
if (msg_len > 0 && msg[msg_len - 1] == '\n') {
char trimmed[1024];
size_t copy_len = msg_len - 1;
if (copy_len >= sizeof(trimmed)) {
copy_len = sizeof(trimmed) - 1;
}
memcpy(trimmed, msg, copy_len);
trimmed[copy_len] = '\0';
app_log(LOG_DEBUG, "%s", trimmed);
return;
}
app_log(LOG_DEBUG, "%s", msg);
}
//=============================================================================
@@ -141,9 +154,9 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
}
// Initialize nostr_core_lib if not already done
if (nostr_crypto_init() != NOSTR_SUCCESS) {
if (nostr_init() != NOSTR_SUCCESS) {
validator_debug_log(
"VALIDATOR: Failed to initialize nostr crypto system\n");
"VALIDATOR: Failed to initialize nostr core system\n");
return NOSTR_ERROR_CRYPTO_INIT;
}
@@ -283,7 +296,50 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
/////////////////////////////////////////////////////////////////////
// Check if authentication is disabled first (regardless of header presence)
// Global authentication bypass for all non-admin operations.
// This makes blob upload/download/list/delete anonymous by default while
// keeping admin endpoints gated by the normal validation flow below.
int is_admin_operation =
(request->operation &&
(strcmp(request->operation, "admin") == 0 ||
strcmp(request->operation, "admin_event") == 0));
if (!is_admin_operation) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 4 PASSED - Authentication bypassed for non-admin operation\n");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication bypassed for non-admin operation");
// Preserve uploader attribution when a valid auth header is optionally
// provided by extracting the pubkey without requiring it.
if (request->auth_header) {
char optional_event_json[4096];
int optional_parse = parse_authorization_header(
request->auth_header, optional_event_json, sizeof(optional_event_json));
if (optional_parse == NOSTR_SUCCESS) {
cJSON *optional_event = cJSON_Parse(optional_event_json);
if (optional_event) {
if (nostr_validate_event(optional_event) == NOSTR_SUCCESS) {
char optional_pubkey[65] = {0};
int optional_extract = extract_pubkey_from_event(
optional_event, optional_pubkey, sizeof(optional_pubkey));
if (optional_extract == NOSTR_SUCCESS && strlen(optional_pubkey) == 64) {
strncpy(result->pubkey, optional_pubkey, 64);
result->pubkey[64] = '\0';
strcpy(result->reason,
"Authentication bypassed (pubkey extracted from optional auth header)");
}
}
cJSON_Delete(optional_event);
}
}
}
return NOSTR_SUCCESS;
}
// For admin operations, keep config-driven authentication behavior.
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");
@@ -671,8 +727,8 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
"VALIDATOR_DEBUG: STEP 10 PASSED - Blossom authentication succeeded\n");
strcpy(result->reason, "Blossom authentication passed");
} else if (event_kind == 33335) {
// 10. Admin/Configuration Event Validation (Kind 33335)
} else if (event_kind == 33335 || event_kind == 23459 || event_kind == 23458) {
// 10. Admin/Configuration Event Validation (Kind 33335, 23459, 23458)
// Verify admin authorization, check required tags, validate expiration
validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Admin/Configuration "
"authentication (kind 33335)\n");
@@ -775,6 +831,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
cJSON_Delete(event);
// Skip rule evaluation for admin events
if (event_kind == 33335 || event_kind == 23459 || event_kind == 23458) {
char admin_skip_msg[256];
snprintf(admin_skip_msg, sizeof(admin_skip_msg),
"VALIDATOR_DEBUG: Admin event (kind %d) - skipping rule evaluation\n", event_kind);
validator_debug_log(admin_skip_msg);
strcpy(result->reason, "Admin event validated - rules bypassed");
return NOSTR_SUCCESS;
}
// STEP 12 PASSED: Protocol validation complete - continue to database rule
// evaluation
validator_debug_log("VALIDATOR_DEBUG: STEP 12 PASSED - Protocol validation "
@@ -1130,15 +1196,15 @@ static int reload_auth_config(void) {
}
// Debug logging
fprintf(stderr,
app_log(LOG_DEBUG,
"VALIDATOR: Configuration loaded from unified config table - "
"auth_required: %d, max_file_size: %ld, nip42_mode: %d, "
"cache_timeout: %d\n",
"cache_timeout: %d",
g_auth_cache.auth_required, g_auth_cache.max_file_size,
g_auth_cache.nip42_mode, cache_timeout);
fprintf(stderr,
app_log(LOG_DEBUG,
"VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, "
"1=optional/enabled, 2=required)\n",
"1=optional/enabled, 2=required)",
g_auth_cache.nip42_mode);
return NOSTR_SUCCESS;
@@ -1321,6 +1387,13 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
int pubkey_whitelisted = 0;
int pubkey_whitelist_exists = 0;
int mime_whitelisted = 0;
int mime_whitelist_exists = 0;
int mime_whitelist_count = 0;
int pubkey_whitelist_count = 0;
char rules_msg[256];
if (!pubkey) {
validator_debug_log(
@@ -1328,7 +1401,12 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
return NOSTR_ERROR_INVALID_INPUT;
}
char rules_msg[256];
if (operation && (strcmp(operation, "admin_event") == 0 ||
strcmp(operation, "admin") == 0)) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - Admin management request, skipping auth rules\n");
return NOSTR_SUCCESS;
}
sprintf(rules_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., "
"operation=%s, mime_type=%s\n",
@@ -1344,18 +1422,14 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
}
// Step 1: Check pubkey blacklist (highest priority)
// Match both exact operation and wildcard '*'
const char *blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type LIKE 'blacklist_pubkey' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *description = "Pubkey blacklisted";
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - "
"Pubkey blacklisted\n");
char blacklist_msg[256];
@@ -1380,18 +1454,14 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 2: Check hash blacklist
if (resource_hash) {
// Match both exact operation and wildcard '*'
const char *hash_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'hash_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type LIKE 'blacklist_hash' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *description = "Hash blacklisted";
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - "
"Hash blacklisted\n");
char hash_blacklist_msg[256];
@@ -1423,17 +1493,14 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
const char *mime_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_blacklist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type LIKE 'blacklist_mime' AND pattern_type = 'mime' AND (pattern_value = ? OR pattern_value LIKE '%/*' AND ? LIKE REPLACE(pattern_value, '*', '%')) AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *description = "MIME type blacklisted";
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - "
"MIME type blacklisted\n");
char mime_blacklist_msg[256];
@@ -1462,133 +1529,151 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
}
// Step 4: Check pubkey whitelist
// Match both exact operation and wildcard '*'
const char *whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_whitelist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type LIKE 'whitelist_pubkey' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - "
const char *description = "Pubkey whitelisted";
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 PASSED - "
"Pubkey whitelisted\n");
char whitelist_msg[256];
sprintf(whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n",
description ? description : "Unknown");
snprintf(whitelist_msg,
sizeof(whitelist_msg),
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted pubkey
pubkey_whitelisted = 1;
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 - Pubkey not whitelisted\n");
}
sqlite3_finalize(stmt);
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 FAILED - Pubkey whitelist query failed\n");
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey "
"not whitelisted\n");
// Step 5: Check MIME type whitelist (only if not already denied)
// Step 5: Check MIME type whitelist
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
char mime_pattern_wildcard[MAX_MIME_TYPE_LEN + 2];
const char *mime_whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_whitelist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type LIKE 'whitelist_mime' AND pattern_type = 'mime' AND (pattern_value = ? OR pattern_value LIKE ? ) AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
const char *slash_pos = strchr(mime_type, '/');
if (slash_pos != NULL) {
size_t prefix_len = slash_pos - mime_type;
if (prefix_len < MAX_MIME_TYPE_LEN) {
snprintf(mime_pattern_wildcard, sizeof(mime_pattern_wildcard), "%.*s/%%", (int)prefix_len, mime_type);
} else {
snprintf(mime_pattern_wildcard, sizeof(mime_pattern_wildcard), "%%/%%");
}
} else {
snprintf(mime_pattern_wildcard, sizeof(mime_pattern_wildcard), "%s/%%", mime_type);
}
sqlite3_bind_text(stmt, 2, mime_pattern_wildcard, -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - "
"MIME type whitelisted\n");
const char *description = "MIME type whitelisted";
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - MIME type whitelisted\n");
char mime_whitelist_msg[256];
sprintf(mime_whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s\n",
description ? description : "Unknown");
snprintf(mime_whitelist_msg,
sizeof(mime_whitelist_msg),
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s (pattern=%s)\n",
description ? description : "Unknown",
mime_pattern_wildcard);
validator_debug_log(mime_whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted MIME type
mime_whitelisted = 1;
} else {
char mime_not_msg[256];
snprintf(mime_not_msg,
sizeof(mime_not_msg),
"VALIDATOR_DEBUG: RULES ENGINE - STEP 5 - MIME type not whitelisted (pattern=%s)\n",
mime_pattern_wildcard);
validator_debug_log(mime_not_msg);
}
sqlite3_finalize(stmt);
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - Failed to prepare MIME whitelist query\n");
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - MIME "
"type not whitelisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No "
"MIME type provided\n");
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No MIME type provided\n");
}
// Step 6: Check if any MIME whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
// Step 6: Count MIME whitelist rules
const char *mime_whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'mime_whitelist' "
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
"SELECT COUNT(*) FROM auth_rules WHERE rule_type LIKE 'whitelist_mime' "
"AND pattern_type = 'mime' AND active = 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int mime_whitelist_count = sqlite3_column_int(stmt, 0);
if (mime_whitelist_count > 0) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - "
"MIME whitelist exists but type not in it\n");
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation");
strcpy(g_last_rule_violation.reason,
"MIME type not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
mime_whitelist_count = sqlite3_column_int(stmt, 0);
char mime_cnt_msg[256];
snprintf(mime_cnt_msg, sizeof(mime_cnt_msg),
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist count: %d\n",
mime_whitelist_count);
validator_debug_log(mime_cnt_msg);
}
sqlite3_finalize(stmt);
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - Failed to prepare MIME whitelist count query\n");
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 PASSED - No "
"MIME whitelist restrictions apply\n");
// Step 7: Check if any whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
if (mime_whitelist_count > 0 && !mime_whitelisted) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist exists but MIME type not allowed\n");
strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation");
strcpy(g_last_rule_violation.reason, "MIME type not whitelisted for this operation");
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
// Step 7: Count pubkey whitelist rules
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
"SELECT COUNT(*) FROM auth_rules WHERE (rule_type LIKE 'whitelist_pubkey' OR rule_type LIKE 'pubkey_whitelist') "
"AND pattern_type = 'pubkey' AND active = 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int whitelist_count = sqlite3_column_int(stmt, 0);
if (whitelist_count > 0) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 FAILED - "
"Whitelist exists but pubkey not in it\n");
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "whitelist_violation");
strcpy(g_last_rule_violation.reason,
"Public key not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
pubkey_whitelist_count = sqlite3_column_int(stmt, 0);
char pubkey_cnt_msg[256];
snprintf(pubkey_cnt_msg, sizeof(pubkey_cnt_msg),
"VALIDATOR_DEBUG: RULES ENGINE - Pubkey whitelist count: %d\n",
pubkey_whitelist_count);
validator_debug_log(pubkey_cnt_msg);
}
sqlite3_finalize(stmt);
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 7 FAILED - Failed to prepare pubkey whitelist count query\n");
}
if (pubkey_whitelist_count > 0) {
char pubkey_whitelist_msg[256];
snprintf(pubkey_whitelist_msg, sizeof(pubkey_whitelist_msg),
"VALIDATOR_DEBUG: RULES ENGINE - Pubkey whitelist exists (%d entries)\n",
pubkey_whitelist_count);
validator_debug_log(pubkey_whitelist_msg);
}
if (pubkey_whitelist_count > 0 && !pubkey_whitelisted) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - Pubkey whitelist exists but pubkey not allowed\n");
strcpy(g_last_rule_violation.violation_type, "whitelist_violation");
strcpy(g_last_rule_violation.reason, "Public key not whitelisted for this operation");
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
if ((mime_whitelist_count > 0 && !mime_whitelisted) ||
(pubkey_whitelist_count > 0 && !pubkey_whitelisted)) {
// Already handled above but include fallback
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 4 PASSED - No "
"whitelist restrictions apply\n");
sqlite3_close(db);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 7 PASSED - All "
"rule checks completed, default ALLOW\n");
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - Completed whitelist checks\n");
return NOSTR_SUCCESS;
}
/**

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Ginxsom Admin Event Test Script
# Tests Kind 23456/23457 admin command system with NIP-44 encryption
# Tests Kind 23458/23459 admin command system with NIP-44 encryption
#
# Prerequisites:
# - nak: https://github.com/fiatjaf/nak
@@ -72,12 +72,12 @@ check_dependencies() {
log_success "All dependencies found"
}
# Create NIP-44 encrypted admin command event (Kind 23456)
# Create NIP-44 encrypted admin command event (Kind 23458)
create_admin_command_event() {
local command="$1"
local expiration=$(($(date +%s) + 3600)) # 1 hour from now
log_info "Creating Kind 23456 admin command event..."
log_info "Creating Kind 23458 admin command event..."
log_info "Command: $command"
# For now, we'll create the event structure manually since nak may not support NIP-44 encryption yet
@@ -87,9 +87,9 @@ create_admin_command_event() {
local content="[\"$command\"]"
# Create event with nak
# Kind 23456 = admin command
# Kind 23458 = admin command
# Tags: p = server pubkey, expiration
local event=$(nak event -k 23456 \
local event=$(nak event -k 23458 \
-c "$content" \
--tag p="$SERVER_PUBKEY" \
--tag expiration="$expiration" \
@@ -104,7 +104,7 @@ send_admin_command() {
log_info "=== Testing Admin Command: $command ==="
# Create Kind 23456 event
# Create Kind 23458 event
local event=$(create_admin_command_event "$command")
if [[ -z "$event" ]]; then
@@ -132,10 +132,10 @@ send_admin_command() {
log_success "HTTP $http_code - Response received"
echo "$body" | jq . 2>/dev/null || echo "$body"
# Try to parse as Kind 23457 event
# Try to parse as Kind 23459 event
local kind=$(echo "$body" | jq -r '.kind // empty' 2>/dev/null)
if [[ "$kind" == "23457" ]]; then
log_success "Received Kind 23457 response event"
if [[ "$kind" == "23459" ]]; then
log_success "Received Kind 23459 response event"
local response_content=$(echo "$body" | jq -r '.content // empty' 2>/dev/null)
log_info "Response content (encrypted): $response_content"
# TODO: Decrypt NIP-44 content to see actual response
@@ -174,7 +174,7 @@ test_server_health() {
main() {
echo "=== Ginxsom Admin Event Test Suite ==="
echo "Testing Kind 23456/23457 admin command system"
echo "Testing Kind 23458/23459 admin command system"
echo ""
log_info "Test Configuration:"

View File

@@ -11,8 +11,13 @@ SERVER_URL="https://localhost:9443"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
TEST_FILE="test_blob_$(date +%s).txt"
CLEANUP_FILES=()
NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
NOSTR_PRIVKEY="39079f9fbdead31b5ec1724479e62c892a6866699c7873613c19832caff447bd"
NOSTR_PUBKEY="2a38db7fc1ffdabb43c79b5ad525f7d97102d4d235efc257dfd1514571f8159f"
# NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
# NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
# Colors for output
RED='\033[0;31m'
@@ -241,7 +246,7 @@ main() {
generate_nostr_event
create_auth_header
perform_upload
# test_retrieval
test_retrieval
echo
log_info "Test completed!"

View File

@@ -0,0 +1,5 @@
<!doctype html>
<meta charset="utf-8">
<title>test_html</title>
/* payload type: text/html */
console.log("test_html");

View File

@@ -0,0 +1,266 @@
#!/bin/bash
# file_put_production.sh - Test script for production Ginxsom Blossom server
# Tests upload functionality on blossom.laantungir.net
set -e # Exit on any error
# Configuration
SERVER_URL="https://blossom.laantungir.net"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
TEST_FILE="test_blob_$(date +%s).html"
CLEANUP_FILES=()
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Cleanup function
cleanup() {
echo -e "${YELLOW}Cleaning up temporary files...${NC}"
for file in "${CLEANUP_FILES[@]}"; do
if [[ -f "$file" ]]; then
rm -f "$file"
echo "Removed: $file"
fi
done
}
# Set up cleanup on exit
trap cleanup EXIT
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
# Check if nak is installed
if ! command -v nak &> /dev/null; then
log_error "nak command not found. Please install nak first."
log_info "Install with: go install github.com/fiatjaf/nak@latest"
exit 1
fi
log_success "nak is installed"
# Check if curl is available
if ! command -v curl &> /dev/null; then
log_error "curl command not found. Please install curl."
exit 1
fi
log_success "curl is available"
# Check if sha256sum is available
if ! command -v sha256sum &> /dev/null; then
log_error "sha256sum command not found."
exit 1
fi
log_success "sha256sum is available"
# Check if base64 is available
if ! command -v base64 &> /dev/null; then
log_error "base64 command not found."
exit 1
fi
log_success "base64 is available"
}
# Check if server is running
check_server() {
log_info "Checking if server is running..."
if curl -s -f "${SERVER_URL}/health" > /dev/null 2>&1; then
log_success "Server is running at ${SERVER_URL}"
else
log_error "Server is not responding at ${SERVER_URL}"
exit 1
fi
}
# Create test file
create_test_file() {
log_info "Creating test file: ${TEST_FILE}"
# Create test content with timestamp and random data
cat > "${TEST_FILE}" << EOF
Test blob content for Ginxsom Blossom server (PRODUCTION)
Timestamp: $(date -Iseconds)
Random data: $(openssl rand -hex 32)
Test message: Hello from production test!
This file is used to test the upload functionality
of the Ginxsom Blossom server on blossom.laantungir.net
EOF
CLEANUP_FILES+=("${TEST_FILE}")
log_success "Created test file with $(wc -c < "${TEST_FILE}") bytes"
}
# Calculate file hash
calculate_hash() {
log_info "Calculating SHA-256 hash..."
HASH=$(sha256sum "${TEST_FILE}" | cut -d' ' -f1)
log_success "Data to hash: ${TEST_FILE}"
log_success "File hash: ${HASH}"
}
# Generate nostr event
generate_nostr_event() {
log_info "Generating kind 24242 nostr event with nak using WSB's private key..."
# Calculate expiration time (1 hour from now)
EXPIRATION=$(date -d '+1 hour' +%s)
# Generate the event using nak with WSB's private key
EVENT_JSON=$(nak event -k 24242 -c "" \
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
-t "t=upload" \
-t "x=${HASH}" \
-t "expiration=${EXPIRATION}")
if [[ -z "$EVENT_JSON" ]]; then
log_error "Failed to generate nostr event"
exit 1
fi
log_success "Generated nostr event"
echo "Event JSON: $EVENT_JSON"
}
# Create authorization header
create_auth_header() {
log_info "Creating authorization header..."
# Base64 encode the event (without newlines)
AUTH_B64=$(echo -n "$EVENT_JSON" | base64 -w 0)
AUTH_HEADER="Nostr ${AUTH_B64}"
log_success "Created authorization header"
echo "Auth header length: ${#AUTH_HEADER} characters"
}
# Perform upload
perform_upload() {
log_info "Performing upload to ${UPLOAD_ENDPOINT}..."
# Create temporary file for response
RESPONSE_FILE=$(mktemp)
CLEANUP_FILES+=("${RESPONSE_FILE}")
# Perform the upload with verbose output
HTTP_STATUS=$(curl -s -w "%{http_code}" \
-X PUT \
-H "Authorization: ${AUTH_HEADER}" \
-H "Content-Type: text/plain" \
-H "Content-Disposition: attachment; filename=\"${TEST_FILE}\"" \
--data-binary "@${TEST_FILE}" \
"${UPLOAD_ENDPOINT}" \
-o "${RESPONSE_FILE}")
echo "HTTP Status: ${HTTP_STATUS}"
echo "Response body:"
cat "${RESPONSE_FILE}"
echo
# Check response
case "${HTTP_STATUS}" in
200)
log_success "Upload successful!"
;;
201)
log_success "Upload successful (created)!"
;;
400)
log_error "Bad request - check the event format"
;;
401)
log_error "Unauthorized - authentication failed"
;;
405)
log_error "Method not allowed - check nginx configuration"
;;
413)
log_error "Payload too large"
;;
501)
log_warning "Upload endpoint not yet implemented (expected for now)"
;;
*)
log_error "Upload failed with HTTP status: ${HTTP_STATUS}"
;;
esac
}
# Test file retrieval
test_retrieval() {
log_info "Testing file retrieval..."
RETRIEVAL_URL="${SERVER_URL}/${HASH}"
if curl -s -f "${RETRIEVAL_URL}" > /dev/null 2>&1; then
log_success "File can be retrieved at: ${RETRIEVAL_URL}"
# Download and verify
DOWNLOADED_FILE=$(mktemp)
CLEANUP_FILES+=("${DOWNLOADED_FILE}")
curl -s "${RETRIEVAL_URL}" -o "${DOWNLOADED_FILE}"
DOWNLOADED_HASH=$(sha256sum "${DOWNLOADED_FILE}" | cut -d' ' -f1)
if [[ "${DOWNLOADED_HASH}" == "${HASH}" ]]; then
log_success "Downloaded file hash matches! Verification successful."
else
log_error "Hash mismatch! Expected: ${HASH}, Got: ${DOWNLOADED_HASH}"
fi
else
log_warning "File not yet available for retrieval"
fi
}
# Main execution
main() {
echo "=== Ginxsom Blossom Production Upload Test ==="
echo "Server: ${SERVER_URL}"
echo "Timestamp: $(date -Iseconds)"
echo
check_prerequisites
check_server
create_test_file
calculate_hash
generate_nostr_event
create_auth_header
perform_upload
test_retrieval
echo
log_info "Test completed!"
echo "Summary:"
echo " Test file: ${TEST_FILE}"
echo " File hash: ${HASH}"
echo " Server: ${SERVER_URL}"
echo " Upload endpoint: ${UPLOAD_ENDPOINT}"
echo " Retrieval URL: ${SERVER_URL}/${HASH}"
}
# Run main function
main "$@"

11
tests/test_upload.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Ginxsom Test</title>
</head>
<body>
<h1>Ginxsom HTML Upload Test</h1>
<p>This is a small test file to verify HTML uploads on blossom.laantungir.net.</p>
<p>Timestamp: 2026-03-11T21:55:00Z</p>
</body>
</html>

View File

@@ -0,0 +1,57 @@
#!/bin/bash
set -eu
BASE_URL="https://blossom.laantungir.net"
PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
FILE="/tmp/ginxsom_browser_sample.html"
cat > "$FILE" <<'EOF'
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Ginxsom Browser Sample</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; background: #0b1020; color: #e6e9f2; }
.card { max-width: 720px; padding: 1.25rem 1.5rem; border: 1px solid #2a3350; border-radius: 12px; background: #121a33; }
code { background: #1b2545; padding: 0.1rem 0.35rem; border-radius: 6px; }
.ok { color: #69f0ae; }
</style>
</head>
<body>
<div class="card">
<h1>Ginxsom HTML test</h1>
<p class="ok">If you can see this page, remote HTML upload and retrieval are working.</p>
<p>Timestamp: <code id="ts"></code></p>
</div>
<script>
document.getElementById("ts").textContent = new Date().toISOString();
</script>
</body>
</html>
EOF
SHA=$(sha256sum "$FILE" | cut -d' ' -f1)
EXP=$(date -d '+1 hour' +%s)
EVENT=$(nak event -k 24242 -c "" --sec "$PRIVKEY" -t "t=upload" -t "x=$SHA" -t "expiration=$EXP")
AUTH="Nostr $(echo -n "$EVENT" | base64 -w 0)"
RESP=$(mktemp)
STATUS=$(curl -s -w "%{http_code}" -o "$RESP" \
-X PUT \
-H "Authorization: $AUTH" \
-H "Content-Type: text/html" \
--data-binary "@$FILE" \
"$BASE_URL/upload")
URL=$(jq -r '.url // empty' "$RESP")
GET_CODE=0
if [ -n "$URL" ]; then
GET_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
fi
echo "UPLOAD_STATUS=$STATUS"
echo "SHA256=$SHA"
echo "URL=$URL"
echo "GET_CODE=$GET_CODE"

128
tests/upload_html_test.sh Executable file
View File

@@ -0,0 +1,128 @@
#!/bin/bash
# upload_html_test.sh - Test script for uploading HTML to production Ginxsom Blossom server
# Tests upload functionality on blossom.laantungir.net
set -e # Exit on any error
# Configuration
SERVER_URL="https://blossom.laantungir.net"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
TEST_FILE="tests/test_upload.html"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Calculate file hash
calculate_hash() {
log_info "Calculating SHA-256 hash..."
HASH=$(sha256sum "${TEST_FILE}" | cut -d' ' -f1)
log_success "File hash: ${HASH}"
}
# Generate nostr event
generate_nostr_event() {
log_info "Generating kind 24242 nostr event..."
EXPIRATION=$(date -d '+1 hour' +%s)
EVENT_JSON=$(nak event -k 24242 -c "" \
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
-t "t=upload" \
-t "x=${HASH}" \
-t "expiration=${EXPIRATION}")
if [[ -z "$EVENT_JSON" ]]; then
log_error "Failed to generate nostr event"
exit 1
fi
log_success "Generated nostr event"
}
# Create authorization header
create_auth_header() {
log_info "Creating authorization header..."
AUTH_B64=$(echo -n "$EVENT_JSON" | base64 -w 0)
AUTH_HEADER="Nostr ${AUTH_B64}"
log_success "Created authorization header"
}
# Perform upload
perform_upload() {
log_info "Performing upload to ${UPLOAD_ENDPOINT}..."
RESPONSE_FILE=$(mktemp)
HTTP_STATUS=$(curl -s -w "%{http_code}" \
-X PUT \
-H "Authorization: ${AUTH_HEADER}" \
-H "Content-Type: text/html" \
--data-binary "@${TEST_FILE}" \
"${UPLOAD_ENDPOINT}" \
-o "${RESPONSE_FILE}")
echo "HTTP Status: ${HTTP_STATUS}"
echo "Response body:"
cat "${RESPONSE_FILE}"
echo
rm -f "${RESPONSE_FILE}"
if [[ "${HTTP_STATUS}" == "200" || "${HTTP_STATUS}" == "201" ]]; then
log_success "Upload successful!"
else
log_error "Upload failed with HTTP status: ${HTTP_STATUS}"
exit 1
fi
}
# Test file retrieval
test_retrieval() {
log_info "Testing file retrieval..."
# Try with .html extension
RETRIEVAL_URL_EXT="${SERVER_URL}/${HASH}.html"
log_info "Trying retrieval with extension: ${RETRIEVAL_URL_EXT}"
if curl -s -f -I "${RETRIEVAL_URL_EXT}" > /dev/null 2>&1; then
log_success "File found with .html extension!"
else
log_warning "File NOT found with .html extension"
fi
# Try without extension
RETRIEVAL_URL="${SERVER_URL}/${HASH}"
log_info "Trying retrieval without extension: ${RETRIEVAL_URL}"
if curl -s -f -I "${RETRIEVAL_URL}" > /dev/null 2>&1; then
log_success "File found without extension!"
else
log_warning "File NOT found without extension"
fi
}
# Main execution
main() {
calculate_hash
generate_nostr_event
create_auth_header
perform_upload
test_retrieval
}
main "$@"

View File

@@ -1,19 +1,28 @@
#!/bin/bash
# white_black_list_test.sh - Whitelist/Blacklist Rules Test Suite
# Tests the auth_rules table functionality for pubkey and MIME type filtering
# Tests the auth_rules table functionality using Kind 23458 admin commands
# Configuration
SERVER_URL="http://localhost:9001"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
DB_PATH="db/ginxsom.db"
ADMIN_API_ENDPOINT="${SERVER_URL}/api/admin"
DB_PATH="db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db"
TEST_DIR="tests/auth_test_tmp"
TEST_KEYS_FILE=".test_keys"
# Test results tracking
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Load admin keys from .test_keys
if [[ ! -f "$TEST_KEYS_FILE" ]]; then
echo "$TEST_KEYS_FILE not found"
exit 1
fi
source "$TEST_KEYS_FILE"
# Test keys for different scenarios - Using WSB's keys for TEST_USER1
# Generated using: nak key public <privkey>
TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
@@ -42,6 +51,37 @@ record_test_result() {
fi
}
# Helper function to send admin command via Kind 23458
send_admin_command() {
local command_json="$1"
# Encrypt command with NIP-44
local encrypted_command=$(nak encrypt --sec "$ADMIN_PRIVKEY" -p "$SERVER_PUBKEY" "$command_json")
if [[ -z "$encrypted_command" ]]; then
echo "❌ Failed to encrypt command"
return 1
fi
# Create Kind 23458 event
local event=$(nak event -k 23458 \
-c "$encrypted_command" \
--tag p="$SERVER_PUBKEY" \
--sec "$ADMIN_PRIVKEY")
if [[ -z "$event" ]]; then
echo "❌ Failed to create admin event"
return 1
fi
# Send to admin API endpoint
local response=$(curl -s -X POST "$ADMIN_API_ENDPOINT" \
-H "Content-Type: application/json" \
-d "$event")
echo "$response"
}
# Check prerequisites
for cmd in nak curl jq sqlite3; do
if ! command -v $cmd &> /dev/null; then
@@ -130,20 +170,24 @@ test_upload() {
}
# Clean up any existing rules from previous tests
echo "Cleaning up existing auth rules..."
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;" 2>/dev/null
echo "Cleaning up existing auth rules via admin command..."
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Enable authentication rules
echo "Enabling authentication rules..."
sqlite3 "$DB_PATH" "UPDATE config SET value = 'true' WHERE key = 'auth_rules_enabled';"
ENABLE_CMD='["config_update", {"auth_rules_enabled": "true"}]'
send_admin_command "$ENABLE_CMD" > /dev/null 2>&1
echo
echo "=== SECTION 1: PUBKEY BLACKLIST TESTS ==="
echo
# Test 1: Add pubkey blacklist rule
echo "Adding blacklist rule for TEST_USER3..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER3_PUBKEY', 'upload', 10, 'Test blacklist');"
# Test 1: Add pubkey blacklist rule via admin command
echo "Adding blacklist rule for TEST_USER3 via admin API..."
BLACKLIST_CMD='["blacklist", "pubkey", "'$TEST_USER3_PUBKEY'"]'
BLACKLIST_RESPONSE=$(send_admin_command "$BLACKLIST_CMD")
echo "Response: $BLACKLIST_RESPONSE" | jq -c '.' 2>/dev/null || echo "$BLACKLIST_RESPONSE"
# Test 1a: Blacklisted user should be denied
test_file1=$(create_test_file "blacklist_test1.txt" "Content from blacklisted user")
@@ -157,13 +201,16 @@ echo
echo "=== SECTION 2: PUBKEY WHITELIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
echo "Cleaning rules via admin API..."
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 2: Add pubkey whitelist rule
echo "Adding whitelist rule for TEST_USER1..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_whitelist', '$TEST_USER1_PUBKEY', 'upload', 300, 'Test whitelist');"
# Test 2: Add pubkey whitelist rule via admin command
echo "Adding whitelist rule for TEST_USER1 via admin API..."
WHITELIST_CMD='["whitelist", "pubkey", "'$TEST_USER1_PUBKEY'"]'
WHITELIST_RESPONSE=$(send_admin_command "$WHITELIST_CMD")
echo "Response: $WHITELIST_RESPONSE" | jq -c '.' 2>/dev/null || echo "$WHITELIST_RESPONSE"
# Test 2a: Whitelisted user should succeed
test_file3=$(create_test_file "whitelist_test1.txt" "Content from whitelisted user")
@@ -177,15 +224,17 @@ echo
echo "=== SECTION 3: HASH BLACKLIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 3: Create a file and blacklist its hash
# Test 3: Create a file and blacklist its hash via admin command
test_file5=$(create_test_file "hash_blacklist_test.txt" "This specific file is blacklisted")
BLACKLISTED_HASH=$(sha256sum "$test_file5" | cut -d' ' -f1)
echo "Adding hash blacklist rule for $BLACKLISTED_HASH..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('hash_blacklist', '$BLACKLISTED_HASH', 'upload', 100, 'Test hash blacklist');"
echo "Adding hash blacklist rule for $BLACKLISTED_HASH via admin API..."
HASH_BLACKLIST_CMD='["blacklist", "hash", "'$BLACKLISTED_HASH'"]'
send_admin_command "$HASH_BLACKLIST_CMD" > /dev/null 2>&1
# Test 3a: Blacklisted hash should be denied
test_upload "Test 3a: Blacklisted Hash Upload" "$TEST_USER1_PRIVKEY" "$test_file5" "403"
@@ -198,13 +247,14 @@ echo
echo "=== SECTION 4: MIME TYPE BLACKLIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 4: Blacklist executable MIME types
echo "Adding MIME type blacklist rules..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_blacklist', 'application/x-executable', 'upload', 200, 'Block executables');"
# Test 4: Blacklist executable MIME types via admin command
echo "Adding MIME type blacklist rules via admin API..."
MIME_BLACKLIST_CMD='["blacklist", "mime", "application/x-executable"]'
send_admin_command "$MIME_BLACKLIST_CMD" > /dev/null 2>&1
# Note: This test would require the server to detect MIME types from file content
# For now, we'll test with text/plain which should be allowed
@@ -215,14 +265,16 @@ echo
echo "=== SECTION 5: MIME TYPE WHITELIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 5: Whitelist only image MIME types
echo "Adding MIME type whitelist rules..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_whitelist', 'image/jpeg', 'upload', 400, 'Allow JPEG');"
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_whitelist', 'image/png', 'upload', 400, 'Allow PNG');"
# Test 5: Whitelist only image MIME types via admin command
echo "Adding MIME type whitelist rules via admin API..."
MIME_WL1_CMD='["whitelist", "mime", "image/jpeg"]'
MIME_WL2_CMD='["whitelist", "mime", "image/png"]'
send_admin_command "$MIME_WL1_CMD" > /dev/null 2>&1
send_admin_command "$MIME_WL2_CMD" > /dev/null 2>&1
# Note: MIME type detection would need to be implemented in the server
# For now, text/plain should be denied if whitelist exists
@@ -233,14 +285,16 @@ echo
echo "=== SECTION 6: PRIORITY ORDERING TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 6: Blacklist should override whitelist (priority ordering)
echo "Adding both blacklist (priority 10) and whitelist (priority 300) for same pubkey..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER1_PUBKEY', 'upload', 10, 'Blacklist priority test');"
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_whitelist', '$TEST_USER1_PUBKEY', 'upload', 300, 'Whitelist priority test');"
echo "Adding both blacklist and whitelist for same pubkey via admin API..."
BL_CMD='["blacklist", "pubkey", "'$TEST_USER1_PUBKEY'"]'
WL_CMD='["whitelist", "pubkey", "'$TEST_USER1_PUBKEY'"]'
send_admin_command "$BL_CMD" > /dev/null 2>&1
send_admin_command "$WL_CMD" > /dev/null 2>&1
# Test 6a: Blacklist should win (lower priority number = higher priority)
test_file9=$(create_test_file "priority_test.txt" "Testing priority ordering")
@@ -250,13 +304,14 @@ echo
echo "=== SECTION 7: OPERATION-SPECIFIC RULES ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 7: Blacklist only for upload operation
echo "Adding blacklist rule for upload operation only..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER2_PUBKEY', 'upload', 10, 'Upload-only blacklist');"
# Test 7: Blacklist for user via admin command
echo "Adding blacklist rule for TEST_USER2 via admin API..."
BL_USER2_CMD='["blacklist", "pubkey", "'$TEST_USER2_PUBKEY'"]'
send_admin_command "$BL_USER2_CMD" > /dev/null 2>&1
# Test 7a: Upload should be denied
test_file10=$(create_test_file "operation_test.txt" "Testing operation-specific rules")
@@ -266,13 +321,14 @@ echo
echo "=== SECTION 8: WILDCARD OPERATION TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 8: Blacklist for all operations using wildcard
echo "Adding blacklist rule for all operations (*)..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER3_PUBKEY', '*', 10, 'All operations blacklist');"
# Test 8: Blacklist for user via admin command
echo "Adding blacklist rule for TEST_USER3 via admin API..."
BL_USER3_CMD='["blacklist", "pubkey", "'$TEST_USER3_PUBKEY'"]'
send_admin_command "$BL_USER3_CMD" > /dev/null 2>&1
# Test 8a: Upload should be denied
test_file11=$(create_test_file "wildcard_test.txt" "Testing wildcard operation")
@@ -282,13 +338,13 @@ echo
echo "=== SECTION 9: ENABLED/DISABLED RULES ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Clean rules via admin command
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Test 9: Disabled rule should not be enforced
echo "Adding disabled blacklist rule..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, enabled, description) VALUES ('pubkey_blacklist', '$TEST_USER1_PUBKEY', 'upload', 10, 0, 'Disabled blacklist');"
echo "Adding disabled blacklist rule via SQL (admin API doesn't support active=0 on create)..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, active) VALUES ('blacklist_pubkey', 'pubkey', '$TEST_USER1_PUBKEY', 0);"
# Test 9a: Upload should succeed (rule is disabled)
test_file12=$(create_test_file "disabled_rule_test.txt" "Testing disabled rule")
@@ -296,7 +352,7 @@ test_upload "Test 9a: Disabled Rule Not Enforced" "$TEST_USER1_PRIVKEY" "$test_f
# Test 9b: Enable the rule
echo "Enabling the blacklist rule..."
sqlite3 "$DB_PATH" "UPDATE auth_rules SET enabled = 1 WHERE rule_target = '$TEST_USER1_PUBKEY';"
sqlite3 "$DB_PATH" "UPDATE auth_rules SET active = 1 WHERE pattern_value = '$TEST_USER1_PUBKEY';"
# Test 9c: Upload should now be denied
test_file13=$(create_test_file "enabled_rule_test.txt" "Testing enabled rule")
@@ -307,9 +363,10 @@ echo
echo "=== SECTION 11: CLEANUP AND RESET ==="
echo
# Clean up all test rules
echo "Cleaning up test rules..."
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
# Clean up all test rules via admin command
echo "Cleaning up test rules via admin API..."
CLEANUP_CMD='["sql_query", "DELETE FROM auth_rules"]'
send_admin_command "$CLEANUP_CMD" > /dev/null 2>&1
# Verify cleanup
RULE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules;" 2>/dev/null)