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

@@ -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);
}
}