List Blob Endpoint

This commit is contained in:
Your Name
2025-08-19 11:04:50 -04:00
parent ec976ab090
commit b2b1240136
17 changed files with 5337 additions and 46 deletions

View File

@@ -37,3 +37,6 @@
- If nginx fails to start, check for port conflicts or kill existing processes
- If FastCGI fails, check socket permissions and that binary exists
- Always use local paths, never assume system installations
use ./restart-nginx.sh to restart nginx
use ./start-fcgi.sh to start fcgi

View File

@@ -92,16 +92,43 @@ This document outlines the implementation plan for ginxsom, a FastCGI-based Blos
- [x] Add optional server-specific fields (uploader_pubkey, filename)
### 2.4 Error Handling
- [ ] Implement proper HTTP status codes
- [ ] 400 Bad Request for invalid data
- [ ] 401 Unauthorized for auth failures
- [ ] 409 Conflict for hash mismatches
- [ ] 413 Payload Too Large for size limits
- [ ] 500 Internal Server Error for system issues
- [ ] Add detailed error messages
- [ ] Implement request logging
- [x] Implement proper HTTP status codes
- [x] 400 Bad Request for invalid data
- [x] 401 Unauthorized for auth failures
- [x] 409 Conflict for hash mismatches
- [x] 413 Payload Too Large for size limits
- [x] 500 Internal Server Error for system issues
- [x] Add detailed error messages
- [x] Implement request logging
### 2.5 Testing & Validation
### 2.5 List Blobs Endpoint
- [ ] Implement `GET /list/<pubkey>` endpoint
- [ ] Extract pubkey from URL path
- [ ] Query database for blobs uploaded by specified pubkey
- [ ] Support `since` and `until` query parameters for date filtering
- [ ] Return JSON array of blob descriptors
- [ ] Handle empty results gracefully
- [ ] Implement optional authorization with kind 24242 event validation
- [ ] Validate `t` tag is set to "list"
- [ ] Check authorization expiration
- [ ] Verify event signature and structure
### 2.6 Delete Blob Endpoint
- [ ] Implement `DELETE /<sha256>` endpoint
- [ ] Extract SHA-256 hash from URL path
- [ ] Require authorization with kind 24242 event validation
- [ ] Validate `t` tag is set to "delete"
- [ ] Verify at least one `x` tag matches the requested hash
- [ ] Check authorization expiration
- [ ] Verify event signature and structure
- [ ] Check blob exists in database
- [ ] Verify uploader_pubkey matches authorized pubkey (ownership check)
- [ ] Remove blob file from filesystem
- [ ] Remove blob metadata from database
- [ ] Handle file deletion errors gracefully
- [ ] Return appropriate success/error responses
### 2.7 Testing & Validation
- [x] Test uploads without authentication
- [x] Test uploads with valid nostr auth
- [x] Test uploads with invalid auth
@@ -195,7 +222,7 @@ This document outlines the implementation plan for ginxsom, a FastCGI-based Blos
- [x] HEAD requests return metadata from database
- [x] Database stores blob information with proper schema
### Milestone 2: Full Upload Support (Phase 2 Complete)
### Milestone 2: Full Upload Support (Phase 2 Pending)
- [x] Basic upload functionality working (PUT requests accepted)
- [x] SHA-256 hash calculation during upload
- [x] File storage to blobs/ directory
@@ -203,6 +230,8 @@ This document outlines the implementation plan for ginxsom, a FastCGI-based Blos
- [x] Authenticated uploads working (Nostr kind 24242 event validation)
- [x] Proper error handling for upload scenarios
- [x] Database metadata storage during upload (with uploader_pubkey and filename)
- [ ] List blobs endpoint implemented (GET /list/<pubkey>)
- [ ] Delete blob endpoint implemented (DELETE /<sha256>)
### Milestone 3: Policy Compliance (Phase 3 Pending)
- [ ] Upload requirements implemented

View File

@@ -0,0 +1,7 @@
Test blob content for Ginxsom Blossom server
Timestamp: 2025-08-19T10:34:57-04:00
Random data: 52bea9fe68b05eeeee258ae97cdcf8d9e7abbc13e67d61f681927250daef6e56
Test message: Hello from put_test.sh!
This file is used to test the upload functionality
of the Ginxsom Blossom server implementation.

View File

@@ -0,0 +1,7 @@
Test blob content for Ginxsom Blossom server
Timestamp: 2025-08-19T10:34:52-04:00
Random data: 340fc3549683d7c208ffa373d5932551f9b2e53cc1a5713b55c934403d9640a2
Test message: Hello from put_test.sh!
This file is used to test the upload functionality
of the Ginxsom Blossom server implementation.

View File

@@ -0,0 +1,7 @@
Test blob content for Ginxsom Blossom server
Timestamp: 2025-08-19T10:34:56-04:00
Random data: 52e5fee5b2f73c73ed388c9df9f74c58117b5f93982c5e6202951fd9fd90b626
Test message: Hello from put_test.sh!
This file is used to test the upload functionality
of the Ginxsom Blossom server implementation.

View File

@@ -0,0 +1,7 @@
Test blob content for Ginxsom Blossom server
Timestamp: 2025-08-19T10:44:56-04:00
Random data: 162b8c0930df1f600ddd936e99ecce0866ab82a436f545d2187ba56726f0df4f
Test message: Hello from put_test.sh!
This file is used to test the upload functionality
of the Ginxsom Blossom server implementation.

Binary file not shown.

Binary file not shown.

View File

@@ -102,6 +102,19 @@ http {
}
}
# List blobs endpoint - GET /list/<pubkey>
location ~ "^/list/([a-f0-9]{64}).*$" {
# Pass to FastCGI application for processing
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_pass fastcgi_backend;
# Only allow GET method for list requests
if ($request_method !~ ^(GET)$ ) {
return 405;
}
}
# Health check endpoint
location /health {
access_log off;

Binary file not shown.

119
delete_test.sh Executable file
View File

@@ -0,0 +1,119 @@
#!/bin/bash
# delete_test.sh - Test script for DELETE /<sha256> endpoint
# This script tests the blob deletion functionality
BASE_URL="http://localhost:9001"
NOSTR_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001"
NOSTR_PUBKEY="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=== Ginxsom Delete Blob Tests ==="
echo
# Function to generate a Nostr event for delete authorization
generate_delete_auth() {
local sha256="$1"
local content="$2"
local created_at=$(date +%s)
local expiration=$((created_at + 3600)) # 1 hour from now
# Note: This is a placeholder - in real implementation, you'd use nostr tools
# to generate properly signed events. For now, we'll create the structure.
cat << EOF
{
"id": "placeholder_id",
"pubkey": "$NOSTR_PUBKEY",
"kind": 24242,
"content": "$content",
"created_at": $created_at,
"tags": [
["t", "delete"],
["x", "$sha256"],
["expiration", "$expiration"]
],
"sig": "placeholder_signature"
}
EOF
}
# Test 1: Delete without authorization (should fail)
echo -e "${YELLOW}Test 1: DELETE without authorization${NC}"
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X DELETE "$BASE_URL/708d0e8226ec17b0585417c0ec9352ce5f52c3820c904b7066fe20b00f2d9cfe")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
if [ "$HTTP_STATUS" = "401" ]; then
echo -e "${GREEN}✓ Correctly rejected unauthorized delete (401)${NC}"
else
echo -e "${RED}✗ Expected 401, got $HTTP_STATUS${NC}"
fi
echo "Response: $BODY"
echo
# Test 2: Delete with invalid authorization
echo -e "${YELLOW}Test 2: DELETE with invalid authorization${NC}"
INVALID_AUTH=$(echo '{"invalid": "event"}' | base64 -w 0)
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X DELETE \
-H "Authorization: Nostr $INVALID_AUTH" \
"$BASE_URL/708d0e8226ec17b0585417c0ec9352ce5f52c3820c904b7066fe20b00f2d9cfe")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
if [ "$HTTP_STATUS" = "401" ]; then
echo -e "${GREEN}✓ Correctly rejected invalid authorization (401)${NC}"
else
echo -e "${RED}✗ Expected 401, got $HTTP_STATUS${NC}"
fi
echo "Response: $BODY"
echo
# Test 3: Delete non-existent blob
echo -e "${YELLOW}Test 3: DELETE non-existent blob${NC}"
NONEXISTENT_HASH="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
DELETE_AUTH=$(generate_delete_auth "$NONEXISTENT_HASH" "Delete non-existent")
AUTH_B64=$(echo "$DELETE_AUTH" | base64 -w 0)
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X DELETE \
-H "Authorization: Nostr $AUTH_B64" \
"$BASE_URL/$NONEXISTENT_HASH")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
if [ "$HTTP_STATUS" = "404" ]; then
echo -e "${GREEN}✓ Correctly returned 404 for non-existent blob${NC}"
else
echo -e "${RED}✗ Expected 404, got $HTTP_STATUS${NC}"
fi
echo "Response: $BODY"
echo
# Test 4: Delete with wrong pubkey (ownership check)
echo -e "${YELLOW}Test 4: DELETE with wrong pubkey (ownership test)${NC}"
TEST_HASH="708d0e8226ec17b0585417c0ec9352ce5f52c3820c904b7066fe20b00f2d9cfe"
DELETE_AUTH=$(generate_delete_auth "$TEST_HASH" "Delete with wrong pubkey")
AUTH_B64=$(echo "$DELETE_AUTH" | base64 -w 0)
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X DELETE \
-H "Authorization: Nostr $AUTH_B64" \
"$BASE_URL/$TEST_HASH")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# Test 5: Valid delete (if implemented and authorized)
echo -e "${YELLOW}Test 5: Valid DELETE request${NC}"
echo "Note: This test requires a blob uploaded by the test pubkey"
echo "and proper Nostr event signing (not implemented in this test script)"
echo
echo "=== Delete Tests Complete ==="
echo
echo "Note: These tests use placeholder Nostr events."
echo "For real testing, use proper Nostr signing tools to generate valid events."

157
list_test.sh Executable file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# list_test.sh - Test script for GET /list/<pubkey> endpoint
# This script tests the blob listing functionality
BASE_URL="http://localhost:9001"
NOSTR_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001"
NOSTR_PUBKEY="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=== Ginxsom List Blobs Tests ==="
echo
# Function to generate a Nostr event for list authorization
generate_list_auth() {
local content="$1"
local created_at=$(date +%s)
local expiration=$((created_at + 3600)) # 1 hour from now
# Note: This is a placeholder - in real implementation, you'd use nostr tools
# to generate properly signed events. For now, we'll create the structure.
cat << EOF
{
"id": "placeholder_id",
"pubkey": "$NOSTR_PUBKEY",
"kind": 24242,
"content": "$content",
"created_at": $created_at,
"tags": [
["t", "list"],
["expiration", "$expiration"]
],
"sig": "placeholder_signature"
}
EOF
}
# Test 1: List blobs without authorization (should work if optional auth)
echo -e "${YELLOW}Test 1: GET /list/<pubkey> without authorization${NC}"
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$NOSTR_PUBKEY")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# # Test 2: List blobs with authorization
# echo -e "${YELLOW}Test 2: GET /list/<pubkey> with authorization${NC}"
# LIST_AUTH=$(generate_list_auth "List Blobs")
# AUTH_B64=$(echo "$LIST_AUTH" | base64 -w 0)
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# -H "Authorization: Nostr $AUTH_B64" \
# "$BASE_URL/list/$NOSTR_PUBKEY")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
# # Test 3: List blobs with since parameter
# echo -e "${YELLOW}Test 3: GET /list/<pubkey> with since parameter${NC}"
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
# # Test 4: List blobs with until parameter
# echo -e "${YELLOW}Test 4: GET /list/<pubkey> with until parameter${NC}"
# UNTIL_TIMESTAMP=$(date +%s) # now
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
# # Test 5: List blobs with both since and until parameters
# echo -e "${YELLOW}Test 5: GET /list/<pubkey> with since and until parameters${NC}"
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
# UNTIL_TIMESTAMP=$(date +%s) # now
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
# # Test 6: List blobs for non-existent pubkey
# echo -e "${YELLOW}Test 6: GET /list/<nonexistent_pubkey>${NC}"
# FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# if [ "$HTTP_STATUS" = "200" ]; then
# echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}"
# else
# echo "HTTP Status: $HTTP_STATUS"
# fi
# echo "Response: $BODY"
# echo
# # Test 7: List blobs with invalid pubkey format
# echo -e "${YELLOW}Test 7: GET /list/<invalid_pubkey_format>${NC}"
# INVALID_PUBKEY="invalid_pubkey"
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# if [ "$HTTP_STATUS" = "400" ]; then
# echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}"
# else
# echo "HTTP Status: $HTTP_STATUS"
# fi
# echo "Response: $BODY"
# echo
# # Test 8: List blobs with invalid since/until parameters
# echo -e "${YELLOW}Test 8: GET /list/<pubkey> with invalid timestamp parameters${NC}"
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?since=invalid&until=invalid")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
# echo "=== List Tests Complete ==="
# echo
# echo "Expected blob descriptor format:"
# echo '{'
# echo ' "url": "https://server.com/<sha256>.<ext>",'
# echo ' "sha256": "<sha256>",'
# echo ' "size": <bytes>,'
# echo ' "type": "<mime_type>",'
# echo ' "uploaded": <unix_timestamp>'
# echo '}'
# echo
# echo "Note: These tests use placeholder Nostr events."
# echo "For real testing, use proper Nostr signing tools to generate valid events."

View File

@@ -129,3 +129,23 @@
127.0.0.1 - - [19/Aug/2025:10:10:05 -0400] "GET /71300009a2840a82a5f596e833b6d0b69361ac63bed5956652e39dad53400ac5 HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:11:23 -0400] "PUT /upload HTTP/1.1" 200 323 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:11:23 -0400] "GET /a5946f8210fb87f9772263864234944d5fea43a8b3dc8eaa08abe4859eb68325 HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:53 -0400] "PUT /upload HTTP/1.1" 200 323 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:53 -0400] "GET /545a2277dd4b7a66e320e12cdd92bf6fbbe13869b5bb5d665a03c83d453ba2de HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:56 -0400] "PUT /upload HTTP/1.1" 200 323 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:56 -0400] "GET /98681900bd97aabc4a7d2341bc52cc8d687e7c7b4dbd0893f6470242614d1100 HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:57 -0400] "PUT /upload HTTP/1.1" 200 323 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:34:57 -0400] "GET /22917078337a9df119979b8df2bbb59aafcc42161c50bd7881e68e27369f343c HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:44:56 -0400] "PUT /upload HTTP/1.1" 200 323 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:44:56 -0400] "GET /b3bac1e07fa61f4668c0920b3493a571642e10c14e1325958eaac6d7e85e1fb1 HTTP/1.1" 200 296 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798?since=1755528942 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798?until=1755615342 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798?since=1755528942&until=1755615342 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/invalid_pubkey HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:55:42 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798?since=invalid&until=invalid HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:10:56:40 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 404 162 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:11:00:46 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 501 38 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:11:01:47 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 200 1984 "-" "curl/8.15.0"
127.0.0.1 - - [19/Aug/2025:11:02:33 -0400] "GET /list/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 HTTP/1.1" 200 1984 "-" "curl/8.15.0"

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
349770
390406

View File

@@ -71,11 +71,11 @@ void handle_upload_requirements_request(void);
/////////////////////////////////////////////////////////////////////////////////////////
// HTTP response helpers
void send_error_response(int status_code, const char* message);
void send_error_response(int status_code, const char* error_type, const char* message, const char* details);
void send_json_response(int status_code, const char* json_content);
// Logging utilities
void log_request(const char* method, const char* uri, int status_code);
void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
#ifdef __cplusplus
}

View File

@@ -23,6 +23,10 @@
// Database path
#define DB_PATH "db/ginxsom.db"
// Function declarations
void send_error_response(int status_code, const char* error_type, const char* message, const char* details);
void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
// Blob metadata structure
typedef struct {
char sha256[MAX_SHA256_LEN];
@@ -511,10 +515,229 @@ int authenticate_request(const char* auth_header, const char* method, const char
return NOSTR_SUCCESS;
}
// Enhanced error response helper functions
void send_error_response(int status_code, const char* error_type, const char* message, const char* details) {
const char* status_text;
switch (status_code) {
case 400: status_text = "Bad Request"; break;
case 401: status_text = "Unauthorized"; break;
case 409: status_text = "Conflict"; break;
case 413: status_text = "Payload Too Large"; break;
case 500: status_text = "Internal Server Error"; break;
default: status_text = "Error"; break;
}
printf("Status: %d %s\r\n", status_code, status_text);
printf("Content-Type: application/json\r\n\r\n");
printf("{\n");
printf(" \"error\": \"%s\",\n", error_type);
printf(" \"message\": \"%s\"", message);
if (details) {
printf(",\n \"details\": \"%s\"", details);
}
printf("\n}\n");
}
void log_request(const char* method, const char* uri, const char* auth_status, int status_code) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
// For now, log to stdout - later can be configured to log files
printf("LOG: [%s] %s %s - Auth: %s - Status: %d\r\n",
timestamp, method ? method : "NULL", uri ? uri : "NULL",
auth_status ? auth_status : "none", status_code);
}
// Handle GET /list/<pubkey> requests
void handle_list_request(const char* pubkey) {
printf("DEBUG: handle_list_request called with pubkey=%s\r\n", pubkey ? pubkey : "NULL");
// Log the incoming request
log_request("GET", "/list", "pending", 0);
// Validate pubkey format (64 hex characters)
if (!pubkey || strlen(pubkey) != 64) {
send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must be 64 hex characters");
log_request("GET", "/list", "none", 400);
return;
}
// Validate hex characters
for (int i = 0; i < 64; i++) {
char c = pubkey[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must contain only hex characters");
log_request("GET", "/list", "none", 400);
return;
}
}
// Get query parameters for since/until filtering
const char* query_string = getenv("QUERY_STRING");
long since_timestamp = 0;
long until_timestamp = 0;
if (query_string) {
printf("DEBUG: Query string: %s\r\n", query_string);
// Parse since parameter
const char* since_param = strstr(query_string, "since=");
if (since_param) {
since_timestamp = atol(since_param + 6);
printf("DEBUG: Since timestamp: %ld\r\n", since_timestamp);
}
// Parse until parameter
const char* until_param = strstr(query_string, "until=");
if (until_param) {
until_timestamp = atol(until_param + 6);
printf("DEBUG: Until timestamp: %ld\r\n", until_timestamp);
}
}
// Check for optional authorization
const char* auth_header = getenv("HTTP_AUTHORIZATION");
const char* auth_status = "none";
if (auth_header) {
printf("DEBUG: Authorization header provided for list request\r\n");
int auth_result = authenticate_request(auth_header, "list", NULL);
if (auth_result != NOSTR_SUCCESS) {
send_error_response(401, "authentication_failed", "Invalid or expired authentication",
"The provided Nostr event is invalid, expired, or does not authorize this operation");
log_request("GET", "/list", "failed", 401);
return;
}
auth_status = "authenticated";
}
// Query database for blobs uploaded by this pubkey
sqlite3* db;
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
printf("DEBUG: Database open failed: %s\r\n", sqlite3_errmsg(db));
send_error_response(500, "database_error", "Failed to access database", "Internal server error");
log_request("GET", "/list", auth_status, 500);
return;
}
// Build SQL query with optional timestamp filtering
char sql[1024];
if (since_timestamp > 0 && until_timestamp > 0) {
snprintf(sql, sizeof(sql),
"SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at >= ? AND uploaded_at <= ? ORDER BY uploaded_at DESC");
} else if (since_timestamp > 0) {
snprintf(sql, sizeof(sql),
"SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at >= ? ORDER BY uploaded_at DESC");
} else if (until_timestamp > 0) {
snprintf(sql, sizeof(sql),
"SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at <= ? ORDER BY uploaded_at DESC");
} else {
snprintf(sql, sizeof(sql),
"SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? ORDER BY uploaded_at DESC");
}
printf("DEBUG: SQL query: %s\r\n", sql);
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
printf("DEBUG: SQL prepare failed: %s\r\n", sqlite3_errmsg(db));
sqlite3_close(db);
send_error_response(500, "database_error", "Failed to prepare query", "Internal server error");
log_request("GET", "/list", auth_status, 500);
return;
}
// Bind parameters
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
int param_index = 2;
if (since_timestamp > 0) {
sqlite3_bind_int64(stmt, param_index++, since_timestamp);
}
if (until_timestamp > 0) {
sqlite3_bind_int64(stmt, param_index++, until_timestamp);
}
// Start JSON response
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("[\n");
int first_item = 1;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (!first_item) {
printf(",\n");
}
first_item = 0;
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
long size = sqlite3_column_int64(stmt, 1);
const char* type = (const char*)sqlite3_column_text(stmt, 2);
long uploaded_at = sqlite3_column_int64(stmt, 3);
const char* filename = (const char*)sqlite3_column_text(stmt, 4);
// Determine file extension from MIME type
const char* extension = "";
if (strstr(type, "image/jpeg")) {
extension = ".jpg";
} else if (strstr(type, "image/webp")) {
extension = ".webp";
} else if (strstr(type, "image/png")) {
extension = ".png";
} else if (strstr(type, "image/gif")) {
extension = ".gif";
} else if (strstr(type, "video/mp4")) {
extension = ".mp4";
} else if (strstr(type, "video/webm")) {
extension = ".webm";
} else if (strstr(type, "audio/mpeg")) {
extension = ".mp3";
} else if (strstr(type, "audio/ogg")) {
extension = ".ogg";
} else if (strstr(type, "text/plain")) {
extension = ".txt";
} else {
extension = ".bin";
}
// Output blob descriptor JSON
printf(" {\n");
printf(" \"url\": \"http://localhost:9001/%s%s\",\n", sha256, extension);
printf(" \"sha256\": \"%s\",\n", sha256);
printf(" \"size\": %ld,\n", size);
printf(" \"type\": \"%s\",\n", type);
printf(" \"uploaded\": %ld", uploaded_at);
// Add optional filename if available
if (filename && strlen(filename) > 0) {
printf(",\n \"filename\": \"%s\"", filename);
}
printf("\n }");
}
printf("\n]\n");
sqlite3_finalize(stmt);
sqlite3_close(db);
printf("DEBUG: List request completed successfully\r\n");
log_request("GET", "/list", auth_status, 200);
}
// Handle PUT /upload requests
void handle_upload_request(void) {
printf("DEBUG: handle_upload_request called\r\n");
// Log the incoming request
log_request("PUT", "/upload", "pending", 0);
// Get HTTP headers
const char* content_type = getenv("CONTENT_TYPE");
const char* content_length_str = getenv("CONTENT_LENGTH");
@@ -524,24 +747,21 @@ void handle_upload_request(void) {
// Validate required headers
if (!content_type) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Content-Type header required\n");
send_error_response(400, "missing_header", "Content-Type header required", "The Content-Type header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
return;
}
if (!content_length_str) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Content-Length header required\n");
send_error_response(400, "missing_header", "Content-Length header required", "The Content-Length header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
return;
}
long content_length = atol(content_length_str);
if (content_length <= 0 || content_length > 100 * 1024 * 1024) { // 100MB limit
printf("Status: 413 Payload Too Large\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("File size must be between 1 byte and 100MB\n");
send_error_response(413, "payload_too_large", "File size must be between 1 byte and 100MB", "Maximum allowed file size is 100MB");
log_request("PUT", "/upload", "none", 413);
return;
}
@@ -549,54 +769,43 @@ void handle_upload_request(void) {
const char* auth_header = getenv("HTTP_AUTHORIZATION");
printf("DEBUG: Raw Authorization header: %s\r\n", auth_header ? auth_header : "NULL");
// Parse the authorization header to extract the Nostr event
// For authenticated uploads, validate the authorization
const char* uploader_pubkey = NULL;
if (auth_header) {
printf("DEBUG: Authorization header present, length=%zu\r\n", strlen(auth_header));
// Authenticate the request first (before processing file)
int auth_result = authenticate_request(auth_header, "PUT", NULL);
if (auth_result != NOSTR_SUCCESS) {
send_error_response(401, "authentication_failed", "Invalid or expired authentication",
"The provided Nostr event is invalid, expired, or does not authorize this operation");
log_request("PUT", "/upload", "failed", 401);
return;
}
// Parse authorization header to get JSON
// Extract pubkey for metadata storage
char event_json[4096];
int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json));
printf("DEBUG: parse_authorization_header returned: %d\r\n", parse_result);
if (parse_result == NOSTR_SUCCESS) {
printf("DEBUG: Successfully parsed authorization header\r\n");
printf("DEBUG: Event JSON: %.200s...\r\n", event_json);
// Parse the JSON event to extract pubkey
cJSON* event = cJSON_Parse(event_json);
if (event) {
printf("DEBUG: Successfully parsed JSON event\r\n");
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (pubkey_json && cJSON_IsString(pubkey_json)) {
const char* temp_pubkey = cJSON_GetStringValue(pubkey_json);
printf("DEBUG: Found pubkey in JSON: %.16s...\r\n", temp_pubkey ? temp_pubkey : "NULL");
// Copy the pubkey to a static buffer to preserve it after cJSON_Delete
static char pubkey_buffer[256];
if (temp_pubkey) {
strncpy(pubkey_buffer, temp_pubkey, sizeof(pubkey_buffer)-1);
pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0';
uploader_pubkey = pubkey_buffer;
printf("DEBUG: Copied pubkey to static buffer: %.16s...\r\n", uploader_pubkey);
}
} else {
printf("DEBUG: No valid 'pubkey' field found in JSON event\r\n");
}
cJSON_Delete(event);
} else {
printf("DEBUG: Failed to parse JSON event\r\n");
}
} else {
printf("DEBUG: Failed to parse authorization header, error: %d\r\n", parse_result);
}
log_request("PUT", "/upload", "authenticated", 0);
} else {
printf("DEBUG: No authorization header provided\r\n");
// No authentication provided - allow anonymous uploads
log_request("PUT", "/upload", "anonymous", 0);
}
printf("DEBUG: Final uploader_pubkey after auth parsing: %s\r\n", uploader_pubkey ? uploader_pubkey : "NULL");
// Read file data from stdin
unsigned char* file_data = malloc(content_length);
if (!file_data) {
@@ -633,6 +842,18 @@ void handle_upload_request(void) {
nostr_bytes_to_hex(hash, 32, sha256_hex);
printf("DEBUG: Calculated SHA-256: %s\r\n", sha256_hex);
// For authenticated uploads, verify hash matches the one in the authorization event
if (auth_header) {
int auth_result = authenticate_request(auth_header, "PUT", sha256_hex);
if (auth_result != NOSTR_SUCCESS) {
free(file_data);
send_error_response(409, "hash_mismatch", "File hash does not match authorization",
"The calculated SHA-256 hash of the uploaded file does not match the hash specified in the authorization event");
log_request("PUT", "/upload", "hash_conflict", 409);
return;
}
}
// Determine file extension from Content-Type
const char* extension = "";
if (strstr(content_type, "image/jpeg")) {
@@ -806,19 +1027,45 @@ int main(void) {
printf("DEBUG: Extracted SHA256=%s\r\n", sha256 ? sha256 : "NULL");
if (sha256) {
handle_head_request(sha256);
log_request("HEAD", request_uri, "none", 200); // Assuming success - could be enhanced to track actual status
} else {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Invalid SHA-256 hash in URI\n");
log_request("HEAD", request_uri, "none", 400);
}
} else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/upload") == 0) {
// Handle PUT /upload requests with authentication
handle_upload_request();
} else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
// Handle GET /list/<pubkey> requests
const char* pubkey = request_uri + 6; // Skip "/list/"
// Extract pubkey from URI (remove query string if present)
static char pubkey_buffer[65];
const char* query_start = strchr(pubkey, '?');
size_t pubkey_len;
if (query_start) {
pubkey_len = query_start - pubkey;
} else {
pubkey_len = strlen(pubkey);
}
if (pubkey_len == 64) { // Valid pubkey length
strncpy(pubkey_buffer, pubkey, 64);
pubkey_buffer[64] = '\0';
handle_list_request(pubkey_buffer);
} else {
send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must be 64 hex characters");
log_request("GET", request_uri, "none", 400);
}
} else {
// Other methods not implemented yet
printf("Status: 501 Not Implemented\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Method %s not implemented\n", request_method);
log_request(request_method, request_uri, "none", 501);
}
}