bud 08 implemented

This commit is contained in:
Your Name
2025-09-03 14:41:55 -04:00
parent d845f7822f
commit 17bb57505e
12 changed files with 7610 additions and 147 deletions

View File

@@ -51,6 +51,17 @@ int check_blob_exists(const char* sha256);
int validate_upload_headers(const char** sha256, const char** content_type, long* content_length, char* error_reason, size_t reason_size);
void handle_head_upload_request(void);
// BUD-08 NIP-94 function declarations
int nip94_is_enabled(void);
int nip94_get_origin(char* out, size_t out_size);
const char* mime_to_extension(const char* mime_type);
void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
int parse_png_dimensions(const unsigned char* data, size_t size, int* width, int* height);
int parse_jpeg_dimensions(const unsigned char* data, size_t size, int* width, int* height);
int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, int* height);
int nip94_get_dimensions(const unsigned char* data, size_t size, const char* mime_type, int* width, int* height);
void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height);
// Blob metadata structure
typedef struct {
char sha256[MAX_SHA256_LEN];
@@ -170,28 +181,7 @@ int get_blob_metadata(const char* sha256, blob_metadata_t* metadata) {
// Check if physical file exists (with extension based on MIME type)
int file_exists_with_type(const char* sha256, const char* mime_type) {
char filepath[MAX_PATH_LEN];
const char* extension = "";
// Determine file extension based on MIME type
if (strstr(mime_type, "image/jpeg")) {
extension = ".jpg";
} else if (strstr(mime_type, "image/webp")) {
extension = ".webp";
} else if (strstr(mime_type, "image/png")) {
extension = ".png";
} else if (strstr(mime_type, "image/gif")) {
extension = ".gif";
} else if (strstr(mime_type, "video/mp4")) {
extension = ".mp4";
} else if (strstr(mime_type, "video/webm")) {
extension = ".webm";
} else if (strstr(mime_type, "audio/mpeg")) {
extension = ".mp3";
} else if (strstr(mime_type, "audio/ogg")) {
extension = ".ogg";
} else if (strstr(mime_type, "text/plain")) {
extension = ".txt";
}
const char* extension = mime_to_extension(mime_type);
snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256, extension);
@@ -1635,31 +1625,8 @@ void handle_mirror_request(void) {
const char* content_type_final = determine_blob_content_type(url, download->content_type,
download->data, download->size);
// Determine file extension from Content-Type
const char* extension = "";
if (strstr(content_type_final, "image/jpeg")) {
extension = ".jpg";
} else if (strstr(content_type_final, "image/webp")) {
extension = ".webp";
} else if (strstr(content_type_final, "image/png")) {
extension = ".png";
} else if (strstr(content_type_final, "image/gif")) {
extension = ".gif";
} else if (strstr(content_type_final, "video/mp4")) {
extension = ".mp4";
} else if (strstr(content_type_final, "video/webm")) {
extension = ".webm";
} else if (strstr(content_type_final, "audio/mpeg")) {
extension = ".mp3";
} else if (strstr(content_type_final, "audio/ogg")) {
extension = ".ogg";
} else if (strstr(content_type_final, "text/plain")) {
extension = ".txt";
} else if (strstr(content_type_final, "application/pdf")) {
extension = ".pdf";
} else {
extension = ".bin";
}
// Determine file extension from Content-Type using centralized mapping
const char* extension = mime_to_extension(content_type_final);
// Save file to blobs directory
char filepath[MAX_PATH_LEN];
@@ -1704,6 +1671,18 @@ void handle_mirror_request(void) {
return;
}
// Get origin from config
char origin[256];
nip94_get_origin(origin, sizeof(origin));
// Build canonical blob URL
char blob_url[512];
nip94_build_blob_url(origin, sha256_hex, content_type_final, blob_url, sizeof(blob_url));
// Get dimensions for NIP-94 metadata
int width = 0, height = 0;
nip94_get_dimensions(download->data, download->size, content_type_final, &width, &height);
// Return success response with blob descriptor
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
@@ -1712,8 +1691,15 @@ void handle_mirror_request(void) {
printf(" \"size\": %zu,\n", download->size);
printf(" \"type\": \"%s\",\n", content_type_final);
printf(" \"uploaded\": %ld,\n", uploaded_time);
printf(" \"url\": \"http://localhost:9001/%s%s\"\n", sha256_hex, extension);
printf("}\n");
printf(" \"url\": \"%s\"", blob_url);
// Add NIP-94 metadata if enabled
if (nip94_is_enabled()) {
printf(",\n");
nip94_emit_field(blob_url, content_type_final, sha256_hex, download->size, width, height);
}
printf("\n}\n");
free_mirror_download(download);
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 200);
@@ -1970,6 +1956,278 @@ void log_request(const char* method, const char* uri, const char* auth_status, i
auth_status ? auth_status : "none", status_code);
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// BUD 08 - Nip94 File Metadata Tags
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Check if NIP-94 metadata emission is enabled
int nip94_is_enabled(void) {
sqlite3* db;
sqlite3_stmt* stmt;
int rc, enabled = 1; // Default enabled
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
return 1; // Default enabled on DB error
}
const char* sql = "SELECT value FROM server_config WHERE key = 'nip94_enabled'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
enabled = (value && strcmp(value, "true") == 0) ? 1 : 0;
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return enabled;
}
// Get CDN origin for blob URLs
int nip94_get_origin(char* out, size_t out_size) {
if (!out || out_size == 0) {
return 0;
}
sqlite3* db;
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
// Default on DB error
strncpy(out, "http://localhost:9001", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
const char* sql = "SELECT value FROM server_config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Default fallback
strncpy(out, "http://localhost:9001", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
// Centralized MIME type to file extension mapping
const char* mime_to_extension(const char* mime_type) {
if (!mime_type) {
return ".bin";
}
if (strstr(mime_type, "image/jpeg")) {
return ".jpg";
} else if (strstr(mime_type, "image/webp")) {
return ".webp";
} else if (strstr(mime_type, "image/png")) {
return ".png";
} else if (strstr(mime_type, "image/gif")) {
return ".gif";
} else if (strstr(mime_type, "video/mp4")) {
return ".mp4";
} else if (strstr(mime_type, "video/webm")) {
return ".webm";
} else if (strstr(mime_type, "audio/mpeg")) {
return ".mp3";
} else if (strstr(mime_type, "audio/ogg")) {
return ".ogg";
} else if (strstr(mime_type, "text/plain")) {
return ".txt";
} else if (strstr(mime_type, "application/pdf")) {
return ".pdf";
} else {
return ".bin";
}
}
// Build canonical blob URL from origin + sha256 + extension
void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size) {
if (!origin || !sha256 || !out || out_size == 0) {
return;
}
const char* extension = mime_to_extension(mime_type);
snprintf(out, out_size, "%s/%s%s", origin, sha256, extension);
}
// Parse PNG dimensions from IHDR chunk
int parse_png_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
if (!data || size < 24 || !width || !height) {
return 0;
}
// Verify PNG signature
if (memcmp(data, "\x89PNG\r\n\x1a\n", 8) != 0) {
return 0;
}
// IHDR chunk should start at offset 8
// Skip chunk length (4 bytes) and chunk type "IHDR" (4 bytes)
// Width is at offset 16 (4 bytes, big-endian)
// Height is at offset 20 (4 bytes, big-endian)
if (size >= 24) {
*width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
*height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];
return 1;
}
return 0;
}
// Parse JPEG dimensions from SOF0 or SOF2 markers
int parse_jpeg_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
if (!data || size < 10 || !width || !height) {
return 0;
}
// Verify JPEG signature
if (size < 3 || memcmp(data, "\xff\xd8\xff", 3) != 0) {
return 0;
}
size_t pos = 2;
while (pos < size - 1) {
// Look for marker
if (data[pos] != 0xff) {
pos++;
continue;
}
unsigned char marker = data[pos + 1];
pos += 2;
// SOF0 (0xc0) or SOF2 (0xc2)
if (marker == 0xc0 || marker == 0xc2) {
// Skip length (2 bytes) and precision (1 byte)
if (pos + 5 < size) {
pos += 3;
// Height (2 bytes, big-endian)
*height = (data[pos] << 8) | data[pos + 1];
pos += 2;
// Width (2 bytes, big-endian)
*width = (data[pos] << 8) | data[pos + 1];
return 1;
}
return 0;
} else if ((marker >= 0xe0 && marker <= 0xef) ||
(marker >= 0xc4 && marker <= 0xdf && marker != 0xc8)) {
// Skip over other segments
if (pos + 1 < size) {
size_t seg_len = (data[pos] << 8) | data[pos + 1];
pos += seg_len;
} else {
break;
}
} else {
pos++;
}
}
return 0;
}
// Parse WebP dimensions from VP8/VP8L/VP8X chunks
int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, int* height) {
if (!data || size < 20 || !width || !height) {
return 0;
}
// Verify RIFF/WEBP header
if (memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WEBP", 4) != 0) {
return 0;
}
// Check chunk type at offset 12
if (memcmp(data + 12, "VP8 ", 4) == 0) {
// VP8 lossy format
if (size >= 30) {
// Skip to frame header (offset 26)
*width = ((data[28] | (data[29] << 8)) & 0x3fff);
*height = ((data[26] | (data[27] << 8)) & 0x3fff);
return 1;
}
} else if (memcmp(data + 12, "VP8L", 4) == 0) {
// VP8L lossless format
if (size >= 25) {
// Width and height are in bits 0-13 and 14-27 of a 32-bit value at offset 21
uint32_t dim_data = data[21] | (data[22] << 8) | (data[23] << 16) | (data[24] << 24);
*width = (dim_data & 0x3fff) + 1;
*height = ((dim_data >> 14) & 0x3fff) + 1;
return 1;
}
} else if (memcmp(data + 12, "VP8X", 4) == 0) {
// VP8X extended format
if (size >= 30) {
// Width (3 bytes, little-endian) at offset 24
// Height (3 bytes, little-endian) at offset 27
*width = (data[24] | (data[25] << 8) | (data[26] << 16)) + 1;
*height = (data[27] | (data[28] << 8) | (data[29] << 16)) + 1;
return 1;
}
}
return 0;
}
// Get file dimensions based on MIME type
int nip94_get_dimensions(const unsigned char* data, size_t size, const char* mime_type, int* width, int* height) {
if (!data || !mime_type || !width || !height) {
return 0;
}
if (strstr(mime_type, "image/png")) {
return parse_png_dimensions(data, size, width, height);
} else if (strstr(mime_type, "image/jpeg")) {
return parse_jpeg_dimensions(data, size, width, height);
} else if (strstr(mime_type, "image/webp")) {
return parse_webp_dimensions(data, size, width, height);
}
return 0;
}
// Emit NIP-94 metadata field to stdout
void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height) {
if (!url || !mime || !sha256) {
return;
}
printf(" \"nip94\": [\n");
printf(" [\"url\", \"%s\"],\n", url);
printf(" [\"m\", \"%s\"],\n", mime);
printf(" [\"x\", \"%s\"],\n", sha256);
printf(" [\"size\", \"%ld\"]", size);
// Add dim tag if dimensions are available
if (width > 0 && height > 0) {
printf(",\n [\"dim\", \"%dx%d\"]", width, height);
}
printf("\n ]");
}
// Handle GET /list/<pubkey> requests
void handle_list_request(const char* pubkey) {
@@ -2102,33 +2360,17 @@ void handle_list_request(const char* pubkey) {
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";
}
// Get origin from config for consistent URL generation
char origin[256];
nip94_get_origin(origin, sizeof(origin));
// Build canonical blob URL using centralized function
char blob_url[512];
nip94_build_blob_url(origin, sha256, type, blob_url, sizeof(blob_url));
// Output blob descriptor JSON
printf(" {\n");
printf(" \"url\": \"http://localhost:9001/%s%s\",\n", sha256, extension);
printf(" \"url\": \"%s\",\n", blob_url);
printf(" \"sha256\": \"%s\",\n", sha256);
printf(" \"size\": %ld,\n", size);
printf(" \"type\": \"%s\",\n", type);
@@ -2536,30 +2778,12 @@ void handle_upload_request(void) {
// Determine file extension from Content-Type
const char* extension = "";
if (strstr(content_type, "image/jpeg")) {
extension = ".jpg";
} else if (strstr(content_type, "image/webp")) {
extension = ".webp";
} else if (strstr(content_type, "image/png")) {
extension = ".png";
} else if (strstr(content_type, "image/gif")) {
extension = ".gif";
} else if (strstr(content_type, "video/mp4")) {
extension = ".mp4";
} else if (strstr(content_type, "video/webm")) {
extension = ".webm";
} else if (strstr(content_type, "audio/mpeg")) {
extension = ".mp3";
} else if (strstr(content_type, "audio/ogg")) {
extension = ".ogg";
} else if (strstr(content_type, "text/plain")) {
extension = ".txt";
} else {
// Default to binary extension for unknown types
extension = ".bin";
}
// Get dimensions from in-memory buffer before saving file
int width = 0, height = 0;
nip94_get_dimensions(file_data, content_length, content_type, &width, &height);
// Determine file extension from Content-Type using centralized mapping
const char* extension = mime_to_extension(content_type);
// Save file to blobs directory with SHA-256 + extension
char filepath[MAX_PATH_LEN];
@@ -2677,6 +2901,14 @@ void handle_upload_request(void) {
// Get origin from config
char origin[256];
nip94_get_origin(origin, sizeof(origin));
// Build canonical blob URL
char blob_url[512];
nip94_build_blob_url(origin, sha256_hex, content_type, blob_url, sizeof(blob_url));
// Return success response with blob descriptor
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
@@ -2685,12 +2917,26 @@ void handle_upload_request(void) {
printf(" \"size\": %ld,\n", content_length);
printf(" \"type\": \"%s\",\n", content_type);
printf(" \"uploaded\": %ld,\n", uploaded_time);
printf(" \"url\": \"http://localhost:9001/%s%s\"\n", sha256_hex, extension);
printf("}\n");
printf(" \"url\": \"%s\"", blob_url);
// Add NIP-94 metadata if enabled
if (nip94_is_enabled()) {
printf(",\n");
nip94_emit_field(blob_url, content_type, sha256_hex, content_length, width, height);
}
printf("\n}\n");
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// MAIN
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
int main(void) {
fprintf(stderr, "STARTUP: FastCGI application starting up\r\n");
fflush(stderr);