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

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ c-relay/
text_graph/
.test_keys
ginxsom-local.service
*.tar.gz

View File

@@ -59,12 +59,8 @@ RUN cd /tmp && \
make install && \
rm -rf /tmp/secp256k1
# Fetch nostr_core_lib source directly over HTTPS.
# This avoids relying on local git metadata/submodule state in the Docker build context.
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"
# 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

BIN
build/ginxsom-fcgi_static_arm64 Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -325,15 +325,20 @@ create_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='*.db' \
--exclude='*.db-*' \
--exclude='*.log' \
--exclude='*.tar.gz' \
--exclude='Trash/*' \
. > /dev/null 2>&1; then
--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

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 2
#define VERSION_PATCH 2
#define VERSION "v0.2.2"
#define VERSION_PATCH 3
#define VERSION "v0.2.3"
#include <stddef.h>
#include <stdint.h>

View File

@@ -314,7 +314,7 @@ int initialize_database(const char *db_path) {
const char *insert_config =
"INSERT OR IGNORE INTO config (key, value, description) VALUES"
" ('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'),"
" ('admin_pubkey', '', 'Admin public key for API access'),"
" ('admin_enabled', 'true', 'Whether admin API is enabled'),"
@@ -471,6 +471,7 @@ void handle_auth_challenge_request(void);
// Handler function declarations with validation support
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
@@ -1051,7 +1052,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
sqlite3_stmt *stmt;
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) {
app_log(LOG_ERROR, "Can't open database: %s\n", sqlite3_errmsg(db));
return 0;
@@ -1170,6 +1171,100 @@ void handle_head_request(const char *sha256) {
// 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)
const char *extract_sha256_from_uri(const char *uri) {
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;
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;
} 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
@@ -1519,15 +1610,16 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
sqlite3_finalize(stmt);
// Check ownership - only the uploader can delete
if (!uploader_pubkey_copy[0] ||
strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
// Enforce ownership only when an authenticated pubkey is available.
// Anonymous delete is allowed when authentication is bypassed.
if (auth_pubkey && auth_pubkey[0]) {
if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
sqlite3_close(db);
send_error_response(403, "access_denied", "Access denied",
"You can only delete blobs that you uploaded");
log_request("DELETE", "/delete", "ownership_denied", 403);
return;
} else {
}
}
// Delete from database first
@@ -2837,8 +2929,13 @@ if (!config_loaded /* && !initialize_server_config() */) {
"Pubkey must be 64 hex characters");
log_request("GET", request_uri, "none", 400);
}
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/") == 0) {
} else if (strcmp(request_method, "GET") == 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)
printf("Status: 200 OK\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(" \"authentication\": {\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(" \"nip42_enabled\": true\n");
printf(" \"nip42_enabled\": false\n");
printf(" }\n");
printf("}\n");
log_request("GET", "/", "server_info", 200);
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/auth") == 0) {
} else if (strcmp(request_uri, "/auth") == 0) {
// Handle GET /auth requests using the existing handler
handle_auth_challenge_request();
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/health") == 0) {
} else if (strcmp(request_uri, "/health") == 0) {
// Handle GET /health requests - simple public health response
time_t now = time(NULL);
long uptime_seconds = 0;
@@ -2907,6 +3002,12 @@ if (!config_loaded /* && !initialize_server_config() */) {
printf(" \"uptime\": %ld\n", uptime_seconds);
printf("}\n");
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) {
// Handle DELETE /<sha256> requests with pre-validated auth
const char *sha256 = extract_sha256_from_uri(request_uri);

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)
/////////////////////////////////////////////////////////////////////
// Check if authentication is disabled first (regardless of header presence)
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");
// 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 disabled");
strcpy(result->reason, "Authentication bypassed for non-admin operation");
// Even when auth is disabled, try to extract uploader pubkey from a provided
// auth header so uploads can still be attributed in blob metadata.
// 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(
@@ -321,7 +328,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
strncpy(result->pubkey, optional_pubkey, 64);
result->pubkey[64] = '\0';
strcpy(result->reason,
"Authentication disabled (pubkey extracted from auth header)");
"Authentication bypassed (pubkey extracted from optional auth header)");
}
}
cJSON_Delete(optional_event);
@@ -332,6 +339,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
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
if (request->operation && strcmp(request->operation, "report") == 0) {
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation