diff --git a/.gitignore b/.gitignore index ad3c91c..33d2063 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ blobs/ c-relay/ text_graph/ .test_keys -ginxsom-local.service \ No newline at end of file +ginxsom-local.service +*.tar.gz diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl index 0718714..d64c303 100644 --- a/Dockerfile.alpine-musl +++ b/Dockerfile.alpine-musl @@ -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 diff --git a/build/ginxsom-fcgi_static_arm64 b/build/ginxsom-fcgi_static_arm64 new file mode 100755 index 0000000..a6a8d95 Binary files /dev/null and b/build/ginxsom-fcgi_static_arm64 differ diff --git a/build/ginxsom-fcgi_static_x86_64 b/build/ginxsom-fcgi_static_x86_64 index b1b46cd..cca8bd8 100755 Binary files a/build/ginxsom-fcgi_static_x86_64 and b/build/ginxsom-fcgi_static_x86_64 differ diff --git a/increment_and_push.sh b/increment_and_push.sh index a9dda8c..bb99e01 100755 --- a/increment_and_push.sh +++ b/increment_and_push.sh @@ -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 diff --git a/src/ginxsom.h b/src/ginxsom.h index 88a4ee1..a97324d 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -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 #include diff --git a/src/main.c b/src/main.c index 069d40f..7cd26c1 100644 --- a/src/main.c +++ b/src/main.c @@ -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) { - 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 { + // 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; + } } // 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 / 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) { - // Handle GET /auth requests using the existing handler - handle_auth_challenge_request(); - } else if (strcmp(request_method, "GET") == 0 && - strcmp(request_uri, "/health") == 0) { + log_request("GET", "/", "server_info", 200); + } else if (strcmp(request_uri, "/auth") == 0) { + // Handle GET /auth requests using the existing handler + handle_auth_challenge_request(); + } else if (strcmp(request_uri, "/health") == 0) { // Handle GET /health requests - simple public health response time_t now = time(NULL); long uptime_seconds = 0; @@ -2906,7 +3001,13 @@ if (!config_loaded /* && !initialize_server_config() */) { printf(" \"version\": \"%s\",\n", VERSION); printf(" \"uptime\": %ld\n", uptime_seconds); 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) { // Handle DELETE / requests with pre-validated auth const char *sha256 = extract_sha256_from_uri(request_uri); diff --git a/src/request_validator.c b/src/request_validator.c index 50e97f4..f81925d 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -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