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.

This commit is contained in:
Laan Tungir
2026-07-15 20:06:52 -04:00
parent 09654fa557
commit b1ea256076
8 changed files with 176 additions and 56 deletions
+2 -1
View File
@@ -5,4 +5,5 @@ blobs/
c-relay/ c-relay/
text_graph/ text_graph/
.test_keys .test_keys
ginxsom-local.service ginxsom-local.service
*.tar.gz
+2 -6
View File
@@ -59,12 +59,8 @@ RUN cd /tmp && \
make install && \ make install && \
rm -rf /tmp/secp256k1 rm -rf /tmp/secp256k1
# Fetch nostr_core_lib source directly over HTTPS. # Copy nostr_core_lib from local submodule (avoids remote git dependency)
# This avoids relying on local git metadata/submodule state in the Docker build context. COPY nostr_core_lib /build/nostr_core_lib
ARG NOSTR_CORE_LIB_REF=2e7eacc02eef0ccf261f655330dc6a43d6cebdfb
RUN git clone https://git.laantungir.net/laantungir/nostr_core_lib.git /build/nostr_core_lib && \
cd /build/nostr_core_lib && \
git checkout "$NOSTR_CORE_LIB_REF"
# Build nostr_core_lib with required NIPs (cached unless submodule changes) # Build nostr_core_lib with required NIPs (cached unless submodule changes)
# Disable fortification in build.sh to prevent __*_chk symbol issues # Disable fortification in build.sh to prevent __*_chk symbol issues
BIN
View File
Binary file not shown.
Binary file not shown.
+13 -8
View File
@@ -325,15 +325,20 @@ create_source_tarball() {
local tarball_name="ginxsom-${NEW_VERSION#v}.tar.gz" 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" \ if tar -czf "$tarball_name" \
--exclude='build/*' \ --exclude='./build' \
--exclude='.git*' \ --exclude='./.git' \
--exclude='*.db' \ --exclude='./.gitmodules' \
--exclude='*.db-*' \ --exclude='./db/*.db' \
--exclude='*.log' \ --exclude='./db/*.db-*' \
--exclude='*.tar.gz' \ --exclude='./*.log' \
--exclude='Trash/*' \ --exclude="./$tarball_name" \
. > /dev/null 2>&1; then --exclude='./Trash' \
--exclude='./*.tar.gz' \
. 2>/dev/null; then
print_success "Created source tarball: $tarball_name" print_success "Created source tarball: $tarball_name"
echo "$tarball_name" echo "$tarball_name"
return 0 return 0
+2 -2
View File
@@ -10,8 +10,8 @@
// Version information (auto-updated by build system) // Version information (auto-updated by build system)
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 2 #define VERSION_MINOR 2
#define VERSION_PATCH 2 #define VERSION_PATCH 3
#define VERSION "v0.2.2" #define VERSION "v0.2.3"
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
+132 -31
View File
@@ -314,7 +314,7 @@ int initialize_database(const char *db_path) {
const char *insert_config = const char *insert_config =
"INSERT OR IGNORE INTO config (key, value, description) VALUES" "INSERT OR IGNORE INTO config (key, value, description) VALUES"
" ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)')," " ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)'),"
" ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads')," " ('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),"
" ('server_name', 'ginxsom', 'Server name for responses')," " ('server_name', 'ginxsom', 'Server name for responses'),"
" ('admin_pubkey', '', 'Admin public key for API access')," " ('admin_pubkey', '', 'Admin public key for API access'),"
" ('admin_enabled', 'true', 'Whether admin API is enabled')," " ('admin_enabled', 'true', 'Whether admin API is enabled'),"
@@ -471,6 +471,7 @@ void handle_auth_challenge_request(void);
// Handler function declarations with validation support // Handler function declarations with validation support
void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result); void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result);
void handle_get_request(const char *sha256);
// Key management function implementations // Key management function implementations
@@ -1051,7 +1052,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
sqlite3_stmt *stmt; sqlite3_stmt *stmt;
int rc; int rc;
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL); rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) { if (rc) {
app_log(LOG_ERROR, "Can't open database: %s\n", sqlite3_errmsg(db)); app_log(LOG_ERROR, "Can't open database: %s\n", sqlite3_errmsg(db));
return 0; return 0;
@@ -1170,6 +1171,100 @@ void handle_head_request(const char *sha256) {
// HEAD request - no body content // HEAD request - no body content
} }
// Handle GET request for blob content
void handle_get_request(const char *sha256) {
blob_metadata_t metadata = {0};
// Validate SHA-256 format (64 hex characters)
if (!sha256 || strlen(sha256) != 64) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Invalid SHA-256 hash format\n");
return;
}
// Check if blob exists in database. If metadata is missing, fall back to
// filesystem lookup for anonymous/basic deployments where DB continuity may
// be unavailable.
int has_metadata = get_blob_metadata(sha256, &metadata);
const char *extension = NULL;
if (has_metadata) {
extension = mime_to_extension(metadata.type);
} else {
// Fallback to default .bin blob path
strncpy(metadata.sha256, sha256, sizeof(metadata.sha256) - 1);
metadata.sha256[sizeof(metadata.sha256) - 1] = '\0';
strncpy(metadata.type, "application/octet-stream", sizeof(metadata.type) - 1);
metadata.type[sizeof(metadata.type) - 1] = '\0';
metadata.filename[0] = '\0';
extension = ".bin";
}
// Resolve file path from extension
char filepath[MAX_PATH_LEN];
size_t dir_len = strlen(g_storage_dir);
size_t sha_len = strlen(sha256);
size_t ext_len = strlen(extension);
size_t total_len = dir_len + 1 + sha_len + ext_len + 1;
if (total_len > sizeof(filepath)) {
send_error_response(500, "path_too_long", "Blob path too long",
"Internal path construction overflow prevention triggered");
return;
}
memcpy(filepath, g_storage_dir, dir_len);
filepath[dir_len] = '/';
memcpy(filepath + dir_len + 1, sha256, sha_len);
memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len);
filepath[total_len - 1] = '\0';
FILE *infile = fopen(filepath, "rb");
if (!infile) {
if (!has_metadata) {
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Blob not found\n");
return;
}
send_error_response(404, "blob_not_found", "Blob file not found",
"Metadata exists but blob file is missing on disk");
return;
}
if (!has_metadata) {
struct stat st;
if (stat(filepath, &st) == 0) {
metadata.size = st.st_size;
}
}
// Return response headers
printf("Status: 200 OK\r\n");
printf("Content-Type: %s\r\n", metadata.type);
printf("Content-Length: %ld\r\n", metadata.size);
printf("Cache-Control: public, max-age=31536000, immutable\r\n");
printf("ETag: \"%s\"\r\n", metadata.sha256);
if (strlen(metadata.filename) > 0) {
printf("X-Original-Filename: %s\r\n", metadata.filename);
}
printf("\r\n");
// Stream file body
unsigned char buffer[8192];
size_t nread;
while ((nread = fread(buffer, 1, sizeof(buffer), infile)) > 0) {
if (fwrite(buffer, 1, nread, stdout) != nread) {
break;
}
}
fclose(infile);
}
// Extract SHA-256 from request URI (Blossom compliant - ignores any extension) // Extract SHA-256 from request URI (Blossom compliant - ignores any extension)
const char *extract_sha256_from_uri(const char *uri) { const char *extract_sha256_from_uri(const char *uri) {
static char sha256_buffer[MAX_SHA256_LEN]; static char sha256_buffer[MAX_SHA256_LEN];
@@ -1451,16 +1546,12 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
} }
} }
// Get authenticated pubkey from centralized validation result // Authentication is optional for delete when auth bypass is enabled.
// If a valid pubkey is present, we preserve ownership enforcement.
const char *auth_pubkey = NULL; const char *auth_pubkey = NULL;
if (validation_result && validation_result->valid && strlen(validation_result->pubkey) == 64) { if (validation_result && validation_result->valid &&
strlen(validation_result->pubkey) == 64) {
auth_pubkey = validation_result->pubkey; auth_pubkey = validation_result->pubkey;
} else {
// No valid authentication - this should have been caught by centralized validation
send_error_response(401, "unauthorized", "Authentication required for delete operations",
"Valid authentication is required to delete blobs");
log_request("DELETE", "/delete", "unauthenticated", 401);
return;
} }
// Check if blob exists in database // Check if blob exists in database
@@ -1519,15 +1610,16 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
// Check ownership - only the uploader can delete // Enforce ownership only when an authenticated pubkey is available.
if (!uploader_pubkey_copy[0] || // Anonymous delete is allowed when authentication is bypassed.
strcmp(uploader_pubkey_copy, auth_pubkey) != 0) { if (auth_pubkey && auth_pubkey[0]) {
sqlite3_close(db); if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
send_error_response(403, "access_denied", "Access denied", sqlite3_close(db);
"You can only delete blobs that you uploaded"); send_error_response(403, "access_denied", "Access denied",
log_request("DELETE", "/delete", "ownership_denied", 403); "You can only delete blobs that you uploaded");
return; log_request("DELETE", "/delete", "ownership_denied", 403);
} else { return;
}
} }
// Delete from database first // Delete from database first
@@ -2837,8 +2929,13 @@ if (!config_loaded /* && !initialize_server_config() */) {
"Pubkey must be 64 hex characters"); "Pubkey must be 64 hex characters");
log_request("GET", request_uri, "none", 400); log_request("GET", request_uri, "none", 400);
} }
} else if (strcmp(request_method, "GET") == 0 && } else if (strcmp(request_method, "GET") == 0) {
strcmp(request_uri, "/") == 0) { // Handle GET /<sha256> blob retrieval
const char *sha256 = extract_sha256_from_uri(request_uri);
if (sha256) {
handle_get_request(sha256);
log_request("GET", request_uri, "public", 200);
} else if (strcmp(request_uri, "/") == 0) {
// Handle GET / requests - Server info endpoint (NIP-11) // Handle GET / requests - Server info endpoint (NIP-11)
printf("Status: 200 OK\r\n"); printf("Status: 200 OK\r\n");
printf("Content-Type: application/nostr+json\r\n\r\n"); printf("Content-Type: application/nostr+json\r\n\r\n");
@@ -2880,18 +2977,16 @@ if (!config_loaded /* && !initialize_server_config() */) {
printf(" },\n"); printf(" },\n");
printf(" \"authentication\": {\n"); printf(" \"authentication\": {\n");
printf(" \"required_for_upload\": false,\n"); printf(" \"required_for_upload\": false,\n");
printf(" \"required_for_delete\": true,\n"); printf(" \"required_for_delete\": false,\n");
printf(" \"required_for_list\": false,\n"); printf(" \"required_for_list\": false,\n");
printf(" \"nip42_enabled\": true\n"); printf(" \"nip42_enabled\": false\n");
printf(" }\n"); printf(" }\n");
printf("}\n"); printf("}\n");
log_request("GET", "/", "server_info", 200); log_request("GET", "/", "server_info", 200);
} else if (strcmp(request_method, "GET") == 0 && } else if (strcmp(request_uri, "/auth") == 0) {
strcmp(request_uri, "/auth") == 0) { // Handle GET /auth requests using the existing handler
// Handle GET /auth requests using the existing handler handle_auth_challenge_request();
handle_auth_challenge_request(); } else if (strcmp(request_uri, "/health") == 0) {
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/health") == 0) {
// Handle GET /health requests - simple public health response // Handle GET /health requests - simple public health response
time_t now = time(NULL); time_t now = time(NULL);
long uptime_seconds = 0; long uptime_seconds = 0;
@@ -2906,7 +3001,13 @@ if (!config_loaded /* && !initialize_server_config() */) {
printf(" \"version\": \"%s\",\n", VERSION); printf(" \"version\": \"%s\",\n", VERSION);
printf(" \"uptime\": %ld\n", uptime_seconds); printf(" \"uptime\": %ld\n", uptime_seconds);
printf("}\n"); printf("}\n");
log_request("GET", "/health", "public", 200); log_request("GET", "/health", "public", 200);
} else {
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Not Found\n");
log_request("GET", request_uri, "none", 404);
}
} else if (strcmp(request_method, "DELETE") == 0) { } else if (strcmp(request_method, "DELETE") == 0) {
// Handle DELETE /<sha256> requests with pre-validated auth // Handle DELETE /<sha256> requests with pre-validated auth
const char *sha256 = extract_sha256_from_uri(request_uri); const char *sha256 = extract_sha256_from_uri(request_uri);
+25 -8
View File
@@ -296,16 +296,23 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms) // 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.
if (!g_auth_cache.auth_required) { // This makes blob upload/download/list/delete anonymous by default while
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication " // keeping admin endpoints gated by the normal validation flow below.
"disabled, allowing request\n"); 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->valid = 1;
result->error_code = NOSTR_SUCCESS; result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication disabled"); strcpy(result->reason, "Authentication bypassed for non-admin operation");
// Even when auth is disabled, try to extract uploader pubkey from a provided // Preserve uploader attribution when a valid auth header is optionally
// auth header so uploads can still be attributed in blob metadata. // provided by extracting the pubkey without requiring it.
if (request->auth_header) { if (request->auth_header) {
char optional_event_json[4096]; char optional_event_json[4096];
int optional_parse = parse_authorization_header( int optional_parse = parse_authorization_header(
@@ -321,7 +328,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
strncpy(result->pubkey, optional_pubkey, 64); strncpy(result->pubkey, optional_pubkey, 64);
result->pubkey[64] = '\0'; result->pubkey[64] = '\0';
strcpy(result->reason, strcpy(result->reason,
"Authentication disabled (pubkey extracted from auth header)"); "Authentication bypassed (pubkey extracted from optional auth header)");
} }
} }
cJSON_Delete(optional_event); cJSON_Delete(optional_event);
@@ -332,6 +339,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
return NOSTR_SUCCESS; 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");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication disabled");
return NOSTR_SUCCESS;
}
// Check if this is a BUD-09 report request - allow anonymous reporting // Check if this is a BUD-09 report request - allow anonymous reporting
if (request->operation && strcmp(request->operation, "report") == 0) { if (request->operation && strcmp(request->operation, "report") == 0) {
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation // BUD-09 allows anonymous reporting - pass through to bud09.c for validation