Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ea256076 | ||
|
|
09654fa557 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@ blobs/
|
||||
c-relay/
|
||||
text_graph/
|
||||
.test_keys
|
||||
ginxsom-local.service
|
||||
ginxsom-local.service
|
||||
*.tar.gz
|
||||
|
||||
@@ -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
|
||||
@@ -72,8 +68,20 @@ RUN git clone https://git.laantungir.net/laantungir/nostr_core_lib.git /build/no
|
||||
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
|
||||
./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
|
||||
@@ -110,7 +118,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
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_x64.a \
|
||||
nostr_core_lib/libnostr_core.a \
|
||||
-lfcgi -lsqlite3 -lsecp256k1 -lssl -lcrypto -lcurl \
|
||||
-lnghttp2 -lcares -lidn2 -lunistring -lpsl -lbrotlidec -lbrotlicommon \
|
||||
-lz -lpthread -lm -ldl && \
|
||||
|
||||
BIN
build/ginxsom-fcgi_static_arm64
Executable file
BIN
build/ginxsom-fcgi_static_arm64
Executable file
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 2
|
||||
#define VERSION_PATCH 1
|
||||
#define VERSION "v0.2.1"
|
||||
#define VERSION_PATCH 3
|
||||
#define VERSION "v0.2.3"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
163
src/main.c
163
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 /<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) {
|
||||
// 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 /<sha256> requests with pre-validated auth
|
||||
const char *sha256 = extract_sha256_from_uri(request_uri);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user