bud 08 implemented
This commit is contained in:
444
src/main.c
444
src/main.c
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user