Refactored code by breaking the main.c up into BUD files.
This commit is contained in:
@@ -9,12 +9,26 @@
|
||||
#include <sys/mount.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include "admin_api.h"
|
||||
#include "ginxsom.h"
|
||||
#include "../nostr_core_lib/nostr_core/request_validator.h"
|
||||
|
||||
// Database path (consistent with main.c)
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
|
||||
// Function declarations (moved from admin_api.h)
|
||||
void handle_admin_api_request(const char* method, const char* uri);
|
||||
void handle_stats_api(void);
|
||||
void handle_config_get_api(void);
|
||||
void handle_config_put_api(void);
|
||||
void handle_files_api(void);
|
||||
void handle_health_api(void);
|
||||
int authenticate_admin_request(const char* auth_header);
|
||||
int is_admin_enabled(void);
|
||||
int verify_admin_pubkey(const char* event_pubkey);
|
||||
void send_json_response(int status, const char* json_content);
|
||||
void send_json_error(int status, const char* error, const char* message);
|
||||
int parse_query_params(const char* query_string, char params[][256], int max_params);
|
||||
|
||||
// Forward declarations for local utility functions
|
||||
static int admin_nip94_get_origin(char* out, size_t out_size);
|
||||
static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
|
||||
@@ -154,35 +168,32 @@ int authenticate_admin_request(const char* auth_header) {
|
||||
return 0; // No auth header
|
||||
}
|
||||
|
||||
// Use existing authentication system with "admin" method
|
||||
int auth_result = authenticate_request(auth_header, "admin", NULL);
|
||||
if (auth_result != NOSTR_SUCCESS) {
|
||||
return 0; // Invalid Nostr event
|
||||
// Use unified request validation system for admin operations
|
||||
nostr_request_t request = {
|
||||
.operation = "admin",
|
||||
.auth_header = auth_header,
|
||||
.event = NULL,
|
||||
.resource_hash = NULL,
|
||||
.mime_type = NULL,
|
||||
.file_size = 0,
|
||||
.client_ip = getenv("REMOTE_ADDR"),
|
||||
.app_context = NULL
|
||||
};
|
||||
|
||||
nostr_request_result_t result;
|
||||
int auth_result = nostr_validate_request(&request, &result);
|
||||
|
||||
if (auth_result != NOSTR_SUCCESS || !result.valid) {
|
||||
return 0; // Authentication failed
|
||||
}
|
||||
|
||||
// Extract pubkey from validated event using existing parser
|
||||
char event_json[4096];
|
||||
int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json));
|
||||
if (parse_result != NOSTR_SUCCESS) {
|
||||
return 0;
|
||||
// Extract pubkey from validation result and verify admin status
|
||||
const char* event_pubkey = result.pubkey[0] ? result.pubkey : NULL;
|
||||
if (!event_pubkey) {
|
||||
return 0; // No pubkey available
|
||||
}
|
||||
|
||||
cJSON* event = cJSON_Parse(event_json);
|
||||
if (!event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||||
if (!pubkey_json || !cJSON_IsString(pubkey_json)) {
|
||||
cJSON_Delete(event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* event_pubkey = cJSON_GetStringValue(pubkey_json);
|
||||
int is_admin = verify_admin_pubkey(event_pubkey);
|
||||
|
||||
cJSON_Delete(event);
|
||||
return is_admin;
|
||||
return verify_admin_pubkey(event_pubkey);
|
||||
}
|
||||
|
||||
int verify_admin_pubkey(const char* event_pubkey) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#ifndef ADMIN_API_H
|
||||
#define ADMIN_API_H
|
||||
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Main API request handler
|
||||
void handle_admin_api_request(const char* method, const char* uri);
|
||||
|
||||
// Individual endpoint handlers
|
||||
void handle_stats_api(void);
|
||||
void handle_config_get_api(void);
|
||||
void handle_config_put_api(void);
|
||||
void handle_files_api(void);
|
||||
void handle_health_api(void);
|
||||
|
||||
// Admin authentication functions
|
||||
int authenticate_admin_request(const char* auth_header);
|
||||
int is_admin_enabled(void);
|
||||
int verify_admin_pubkey(const char* event_pubkey);
|
||||
|
||||
// Utility functions
|
||||
void send_json_response(int status, const char* json_content);
|
||||
void send_json_error(int status, const char* error, const char* message);
|
||||
int parse_query_params(const char* query_string, char params[][256], int max_params);
|
||||
|
||||
#endif
|
||||
543
src/bud04.c
Normal file
543
src/bud04.c
Normal file
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* BUD-04 Mirroring Support
|
||||
* Handles PUT /mirror requests for remote blob downloading
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <curl/curl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "ginxsom.h"
|
||||
#include "../nostr_core_lib/nostr_core/request_validator.h"
|
||||
|
||||
// HTTP download response structure
|
||||
typedef struct {
|
||||
unsigned char* data;
|
||||
size_t size;
|
||||
char content_type[128];
|
||||
long http_code;
|
||||
size_t capacity;
|
||||
} mirror_download_t;
|
||||
|
||||
// CURL write callback for collecting response data
|
||||
static size_t write_callback(void* contents, size_t size, size_t nmemb, mirror_download_t* response) {
|
||||
size_t realsize = size * nmemb;
|
||||
|
||||
if (!response) return 0;
|
||||
|
||||
// Check if we need to expand buffer
|
||||
if (response->size + realsize >= response->capacity) {
|
||||
size_t new_capacity = response->capacity == 0 ? 8192 : response->capacity * 2;
|
||||
while (new_capacity < response->size + realsize + 1) {
|
||||
new_capacity *= 2;
|
||||
}
|
||||
|
||||
unsigned char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
memcpy(response->data + response->size, contents, realsize);
|
||||
response->size += realsize;
|
||||
response->data[response->size] = '\0'; // Null terminate for safety
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
// CURL header callback for collecting Content-Type
|
||||
static size_t header_callback(char* buffer, size_t size, size_t nitems, mirror_download_t* response) {
|
||||
size_t realsize = size * nitems;
|
||||
|
||||
if (!response) return realsize;
|
||||
|
||||
// Look for Content-Type header (case-insensitive)
|
||||
if (realsize > 14 && strncasecmp(buffer, "Content-Type:", 13) == 0) {
|
||||
// Skip "Content-Type:" and whitespace
|
||||
char* value = buffer + 13;
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
|
||||
// Find end of value (before \r\n)
|
||||
char* end = value;
|
||||
while (*end && *end != '\r' && *end != '\n') end++;
|
||||
|
||||
// Copy content type, limiting to buffer size
|
||||
size_t copy_len = end - value;
|
||||
if (copy_len >= sizeof(response->content_type)) {
|
||||
copy_len = sizeof(response->content_type) - 1;
|
||||
}
|
||||
|
||||
strncpy(response->content_type, value, copy_len);
|
||||
response->content_type[copy_len] = '\0';
|
||||
}
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
// Validate URL for security (prevent SSRF attacks)
|
||||
int validate_mirror_url(const char* url) {
|
||||
if (!url || strlen(url) == 0) {
|
||||
return 0; // Invalid URL
|
||||
}
|
||||
|
||||
// Must start with https:// (security requirement)
|
||||
if (strncmp(url, "https://", 8) != 0) {
|
||||
return 0; // Only HTTPS allowed
|
||||
}
|
||||
|
||||
// URL length check
|
||||
if (strlen(url) > 2048) {
|
||||
return 0; // URL too long
|
||||
}
|
||||
|
||||
// Check for prohibited hosts/IPs (basic SSRF protection)
|
||||
const char* host_start = url + 8; // Skip "https://"
|
||||
|
||||
// Block localhost and private IPs
|
||||
if (strncasecmp(host_start, "localhost", 9) == 0 ||
|
||||
strncasecmp(host_start, "127.", 4) == 0 ||
|
||||
strncasecmp(host_start, "192.168.", 8) == 0 ||
|
||||
strncasecmp(host_start, "10.", 3) == 0 ||
|
||||
strncmp(host_start, "172.16.", 7) == 0 ||
|
||||
strncmp(host_start, "172.17.", 7) == 0 ||
|
||||
strncmp(host_start, "172.18.", 7) == 0 ||
|
||||
strncmp(host_start, "172.19.", 7) == 0 ||
|
||||
strncmp(host_start, "172.2", 5) == 0 ||
|
||||
strncmp(host_start, "172.30.", 7) == 0 ||
|
||||
strncmp(host_start, "172.31.", 7) == 0) {
|
||||
return 0; // Private network blocked
|
||||
}
|
||||
|
||||
return 1; // URL appears valid
|
||||
}
|
||||
|
||||
// Detect/validate Content-Type
|
||||
const char* determine_blob_content_type(const char* url, const char* header_content_type,
|
||||
const unsigned char* data, size_t size) {
|
||||
// Priority 1: Use Content-Type header if present and valid
|
||||
if (header_content_type && strlen(header_content_type) > 0) {
|
||||
// Extract main MIME type (before semicolon)
|
||||
static char clean_type[128];
|
||||
const char* semicolon = strchr(header_content_type, ';');
|
||||
size_t len = semicolon ? (size_t)(semicolon - header_content_type) : strlen(header_content_type);
|
||||
|
||||
if (len < sizeof(clean_type)) {
|
||||
strncpy(clean_type, header_content_type, len);
|
||||
clean_type[len] = '\0';
|
||||
|
||||
// Remove trailing whitespace
|
||||
while (len > 0 && (clean_type[len-1] == ' ' || clean_type[len-1] == '\t')) {
|
||||
clean_type[--len] = '\0';
|
||||
}
|
||||
|
||||
return clean_type;
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Detect from URL extension
|
||||
if (url) {
|
||||
const char* dot = strrchr(url, '.');
|
||||
if (dot && dot[1]) {
|
||||
const char* ext = dot + 1;
|
||||
|
||||
// Remove query parameters
|
||||
const char* question = strchr(ext, '?');
|
||||
size_t ext_len = question ? (size_t)(question - ext) : strlen(ext);
|
||||
|
||||
if (ext_len > 0) {
|
||||
if (strncasecmp(ext, "png", ext_len) == 0) return "image/png";
|
||||
if (strncasecmp(ext, "jpg", ext_len) == 0) return "image/jpeg";
|
||||
if (strncasecmp(ext, "jpeg", ext_len) == 0) return "image/jpeg";
|
||||
if (strncasecmp(ext, "gif", ext_len) == 0) return "image/gif";
|
||||
if (strncasecmp(ext, "webp", ext_len) == 0) return "image/webp";
|
||||
if (strncasecmp(ext, "pdf", ext_len) == 0) return "application/pdf";
|
||||
if (strncasecmp(ext, "mp4", ext_len) == 0) return "video/mp4";
|
||||
if (strncasecmp(ext, "mp3", ext_len) == 0) return "audio/mpeg";
|
||||
if (strncasecmp(ext, "txt", ext_len) == 0) return "text/plain";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Basic content detection from data
|
||||
if (data && size >= 8) {
|
||||
// PNG signature
|
||||
if (memcmp(data, "\x89PNG\r\n\x1a\n", 8) == 0) {
|
||||
return "image/png";
|
||||
}
|
||||
// JPEG signature
|
||||
if (size >= 3 && memcmp(data, "\xff\xd8\xff", 3) == 0) {
|
||||
return "image/jpeg";
|
||||
}
|
||||
// GIF signature
|
||||
if (memcmp(data, "GIF87a", 6) == 0 || memcmp(data, "GIF89a", 6) == 0) {
|
||||
return "image/gif";
|
||||
}
|
||||
// PDF signature
|
||||
if (memcmp(data, "%PDF-", 5) == 0) {
|
||||
return "application/pdf";
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// Download blob from remote URL
|
||||
mirror_download_t* download_blob_from_url(const char* url, size_t max_size) {
|
||||
if (!url || !validate_mirror_url(url)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mirror_download_t* download = calloc(1, sizeof(mirror_download_t));
|
||||
if (!download) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Initialize download structure
|
||||
download->data = malloc(8192);
|
||||
if (!download->data) {
|
||||
free(download);
|
||||
curl_easy_cleanup(curl);
|
||||
return NULL;
|
||||
}
|
||||
download->capacity = 8192;
|
||||
download->size = 0;
|
||||
download->content_type[0] = '\0';
|
||||
|
||||
// Configure CURL
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, download);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, download);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Ginxsom-Blossom/1.0");
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
// Set maximum file size
|
||||
curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, (long)max_size);
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
// Get HTTP response code
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &download->http_code);
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
// Check for errors
|
||||
if (res != CURLE_OK || download->http_code != 200) {
|
||||
free(download->data);
|
||||
free(download);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return download;
|
||||
}
|
||||
|
||||
// Free download result memory
|
||||
void free_mirror_download(mirror_download_t* download) {
|
||||
if (download) {
|
||||
if (download->data) {
|
||||
free(download->data);
|
||||
}
|
||||
free(download);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON request body to extract URL
|
||||
int parse_mirror_request_body(const char* json_body, char* url_buffer, size_t url_buffer_size) {
|
||||
if (!json_body || !url_buffer || url_buffer_size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* json = cJSON_Parse(json_body);
|
||||
if (!json) {
|
||||
return 0; // Invalid JSON
|
||||
}
|
||||
|
||||
cJSON* url_item = cJSON_GetObjectItem(json, "url");
|
||||
if (!url_item || !cJSON_IsString(url_item)) {
|
||||
cJSON_Delete(json);
|
||||
return 0; // Missing or invalid URL field
|
||||
}
|
||||
|
||||
const char* url = cJSON_GetStringValue(url_item);
|
||||
if (!url || strlen(url) >= url_buffer_size) {
|
||||
cJSON_Delete(json);
|
||||
return 0; // URL too long or null
|
||||
}
|
||||
|
||||
strcpy(url_buffer, url);
|
||||
cJSON_Delete(json);
|
||||
|
||||
return 1; // Success
|
||||
}
|
||||
|
||||
// Handle PUT /mirror requests (BUD-04)
|
||||
void handle_mirror_request(void) {
|
||||
// Log the incoming request
|
||||
log_request("PUT", "/mirror", "pending", 0);
|
||||
|
||||
// Get HTTP headers
|
||||
const char* content_type = getenv("CONTENT_TYPE");
|
||||
const char* content_length_str = getenv("CONTENT_LENGTH");
|
||||
|
||||
// Validate Content-Type
|
||||
if (!content_type || strstr(content_type, "application/json") == NULL) {
|
||||
send_error_response(400, "invalid_content_type",
|
||||
"Content-Type must be application/json",
|
||||
"The mirror endpoint requires JSON request body");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate Content-Length
|
||||
if (!content_length_str) {
|
||||
send_error_response(400, "missing_header",
|
||||
"Content-Length header required",
|
||||
"The Content-Length header must be specified");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
long content_length = atol(content_length_str);
|
||||
if (content_length <= 0 || content_length > 4096) { // 4KB max for JSON
|
||||
send_error_response(400, "invalid_content_length",
|
||||
"Invalid content length",
|
||||
"JSON request body must be between 1 byte and 4KB");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read JSON request body
|
||||
char* json_body = malloc(content_length + 1);
|
||||
if (!json_body) {
|
||||
send_error_response(500, "memory_error",
|
||||
"Failed to allocate memory",
|
||||
"Internal server error");
|
||||
log_request("PUT", "/mirror", "none", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_read = fread(json_body, 1, content_length, stdin);
|
||||
if (bytes_read != (size_t)content_length) {
|
||||
free(json_body);
|
||||
send_error_response(400, "incomplete_body",
|
||||
"Failed to read complete request body",
|
||||
"The request body was incomplete");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
json_body[content_length] = '\0';
|
||||
|
||||
// Parse JSON to extract URL
|
||||
char url[2048];
|
||||
if (!parse_mirror_request_body(json_body, url, sizeof(url))) {
|
||||
free(json_body);
|
||||
send_error_response(400, "invalid_json",
|
||||
"Invalid JSON or missing URL field",
|
||||
"Request body must be valid JSON with 'url' field");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
free(json_body);
|
||||
|
||||
// Validate URL
|
||||
if (!validate_mirror_url(url)) {
|
||||
send_error_response(400, "invalid_url",
|
||||
"Invalid or prohibited URL",
|
||||
"URL must be HTTPS and not point to private networks");
|
||||
log_request("PUT", "/mirror", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for authorization
|
||||
const char* auth_header = getenv("HTTP_AUTHORIZATION");
|
||||
const char* expected_hash = NULL;
|
||||
const char* uploader_pubkey = NULL;
|
||||
static char pubkey_buffer[256];
|
||||
|
||||
if (auth_header) {
|
||||
// Use unified request validation system
|
||||
nostr_request_t request = {
|
||||
.operation = "upload",
|
||||
.auth_header = auth_header,
|
||||
.event = NULL,
|
||||
.resource_hash = NULL,
|
||||
.mime_type = NULL,
|
||||
.file_size = 0,
|
||||
.client_ip = getenv("REMOTE_ADDR"),
|
||||
.app_context = NULL
|
||||
};
|
||||
|
||||
nostr_request_result_t result;
|
||||
int auth_result = nostr_validate_request(&request, &result);
|
||||
|
||||
if (auth_result != NOSTR_SUCCESS || !result.valid) {
|
||||
const char* error_type = "authentication_failed";
|
||||
const char* message = "Invalid authentication";
|
||||
const char* details = result.reason[0] ? result.reason : "The provided authorization is invalid";
|
||||
|
||||
// Provide more specific error messages based on the reason
|
||||
if (strstr(result.reason, "whitelist")) {
|
||||
error_type = "pubkey_not_whitelisted";
|
||||
message = "Public key not authorized";
|
||||
} else if (strstr(result.reason, "blacklist")) {
|
||||
error_type = "access_denied";
|
||||
message = "Access denied by policy";
|
||||
}
|
||||
|
||||
send_error_response(401, error_type, message, details);
|
||||
log_request("PUT", "/mirror", "auth_failed", 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract uploader pubkey from validation result
|
||||
if (result.pubkey[0]) {
|
||||
strncpy(pubkey_buffer, result.pubkey, sizeof(pubkey_buffer)-1);
|
||||
pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0';
|
||||
uploader_pubkey = pubkey_buffer;
|
||||
}
|
||||
|
||||
// For mirror operations, we don't need to extract the expected hash here
|
||||
// The unified validation system handles hash validation internally
|
||||
// We just need the pubkey for metadata storage
|
||||
}
|
||||
|
||||
// Download the blob
|
||||
mirror_download_t* download = download_blob_from_url(url, 100 * 1024 * 1024); // 100MB limit
|
||||
if (!download) {
|
||||
send_error_response(400, "download_failed",
|
||||
"Failed to download blob from URL",
|
||||
"Could not fetch the specified URL or file too large");
|
||||
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate hash of downloaded content
|
||||
unsigned char hash[32];
|
||||
if (nostr_sha256(download->data, download->size, hash) != NOSTR_SUCCESS) {
|
||||
free_mirror_download(download);
|
||||
send_error_response(500, "hash_error",
|
||||
"Failed to calculate hash",
|
||||
"Internal server error during hash calculation");
|
||||
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert hash to hex string
|
||||
char sha256_hex[65];
|
||||
nostr_bytes_to_hex(hash, 32, sha256_hex);
|
||||
|
||||
// If authorization provided, verify hash matches
|
||||
if (expected_hash && strcmp(sha256_hex, expected_hash) != 0) {
|
||||
free_mirror_download(download);
|
||||
send_error_response(400, "hash_mismatch",
|
||||
"Downloaded content hash does not match authorization",
|
||||
"The file hash does not match the expected hash in the authorization event");
|
||||
log_request("PUT", "/mirror", "auth_mismatch", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine content type
|
||||
const char* content_type_final = determine_blob_content_type(url, download->content_type,
|
||||
download->data, download->size);
|
||||
|
||||
// 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[512];
|
||||
snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256_hex, extension);
|
||||
|
||||
FILE* outfile = fopen(filepath, "wb");
|
||||
if (!outfile) {
|
||||
free_mirror_download(download);
|
||||
send_error_response(500, "file_error",
|
||||
"Failed to create file",
|
||||
"Internal server error during file creation");
|
||||
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_written = fwrite(download->data, 1, download->size, outfile);
|
||||
fclose(outfile);
|
||||
|
||||
if (bytes_written != download->size) {
|
||||
unlink(filepath); // Clean up partial file
|
||||
free_mirror_download(download);
|
||||
send_error_response(500, "write_error",
|
||||
"Failed to write complete file",
|
||||
"Internal server error during file write");
|
||||
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set file permissions
|
||||
chmod(filepath, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
|
||||
|
||||
// Store metadata in database
|
||||
time_t uploaded_time = time(NULL);
|
||||
if (!insert_blob_metadata(sha256_hex, download->size, content_type_final,
|
||||
uploaded_time, uploader_pubkey, NULL)) {
|
||||
unlink(filepath); // Clean up file
|
||||
free_mirror_download(download);
|
||||
send_error_response(500, "database_error",
|
||||
"Failed to store blob metadata",
|
||||
"Internal server error during database operation");
|
||||
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500);
|
||||
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");
|
||||
printf("{\n");
|
||||
printf(" \"sha256\": \"%s\",\n", sha256_hex);
|
||||
printf(" \"size\": %zu,\n", download->size);
|
||||
printf(" \"type\": \"%s\",\n", content_type_final);
|
||||
printf(" \"uploaded\": %ld,\n", uploaded_time);
|
||||
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);
|
||||
}
|
||||
265
src/bud06.c
Normal file
265
src/bud06.c
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* BUD-06 Upload Requirements (Pre-flight Validation)
|
||||
* Handles HEAD /upload requests for upload requirement validation
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcgi_stdio.h>
|
||||
#include "ginxsom.h"
|
||||
#include "../nostr_core_lib/nostr_core/request_validator.h"
|
||||
|
||||
// BUD-06 X-Reason header constants
|
||||
#define XREASON_MISSING_SHA256 "Missing required X-SHA-256 header"
|
||||
#define XREASON_INVALID_SHA256 "X-SHA-256 must be 64 hex characters"
|
||||
#define XREASON_MISSING_LENGTH "Missing required X-Content-Length header"
|
||||
#define XREASON_INVALID_LENGTH "X-Content-Length must be a positive integer"
|
||||
#define XREASON_FILE_TOO_LARGE "File size exceeds maximum allowed (100MB)"
|
||||
#define XREASON_ZERO_LENGTH "File size cannot be zero"
|
||||
#define XREASON_BLOB_EXISTS "Blob with this hash already exists"
|
||||
#define XREASON_UNSUPPORTED_TYPE "Content type not supported by server policy"
|
||||
#define XREASON_AUTH_REQUIRED "Authorization required for upload"
|
||||
#define XREASON_AUTH_INVALID "Invalid or expired authorization"
|
||||
|
||||
// Enhanced error response with X-Reason header for BUD-06
|
||||
void send_upload_error_response(int status_code, const char* error_type,
|
||||
const char* message, const char* x_reason) {
|
||||
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 411: status_text = "Length Required"; break;
|
||||
case 413: status_text = "Content Too Large"; break;
|
||||
case 415: status_text = "Unsupported Media Type"; 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");
|
||||
if (x_reason) {
|
||||
printf("X-Reason: %s\r\n", x_reason);
|
||||
}
|
||||
printf("\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"%s\",\n", error_type);
|
||||
printf(" \"message\": \"%s\"", message);
|
||||
if (x_reason) {
|
||||
printf(",\n \"x_reason\": \"%s\"", x_reason);
|
||||
}
|
||||
printf("\n}\n");
|
||||
}
|
||||
|
||||
// Success response for validated upload requirements
|
||||
void send_upload_success_response(const char* sha256, const char* content_type, long content_length) {
|
||||
printf("Status: 200 OK\r\n");
|
||||
printf("Content-Type: application/json\r\n");
|
||||
printf("X-Upload-Status: Ready\r\n");
|
||||
printf("\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"message\": \"Upload requirements validated\",\n");
|
||||
printf(" \"sha256\": \"%s\",\n", sha256);
|
||||
printf(" \"content_type\": \"%s\",\n", content_type);
|
||||
printf(" \"content_length\": %ld\n", content_length);
|
||||
printf("}\n");
|
||||
}
|
||||
|
||||
// Validate SHA-256 format (64 hex characters)
|
||||
int validate_sha256_format(const char* sha256) {
|
||||
if (!sha256 || strlen(sha256) != 64) {
|
||||
return 0; // Invalid format
|
||||
}
|
||||
|
||||
// Check that all characters are hex
|
||||
for (int i = 0; i < 64; i++) {
|
||||
char c = sha256[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
return 0; // Invalid hex character
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // Valid format
|
||||
}
|
||||
|
||||
// Parse and validate X-Content-Length header
|
||||
int validate_content_length(const char* content_length_str, long* parsed_length) {
|
||||
if (!content_length_str || !parsed_length) {
|
||||
return 0; // Invalid input
|
||||
}
|
||||
|
||||
char* endptr;
|
||||
long length = strtol(content_length_str, &endptr, 10);
|
||||
|
||||
// Always set parsed_length so caller can check the actual value
|
||||
*parsed_length = length;
|
||||
|
||||
// Check if conversion was successful and no trailing characters
|
||||
if (*endptr != '\0') {
|
||||
return 0; // Invalid number format
|
||||
}
|
||||
|
||||
// Check for valid size range
|
||||
if (length <= 0) {
|
||||
return 0; // Zero or negative size not allowed
|
||||
}
|
||||
|
||||
if (length > 100 * 1024 * 1024) { // 100MB limit
|
||||
return -1; // File too large
|
||||
}
|
||||
|
||||
return 1; // Valid length
|
||||
}
|
||||
|
||||
// Check if blob already exists in database
|
||||
int check_blob_exists(const char* sha256) {
|
||||
if (!sha256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
blob_metadata_t metadata = {0};
|
||||
return get_blob_metadata(sha256, &metadata);
|
||||
}
|
||||
|
||||
// Validate upload headers and extract values
|
||||
int validate_upload_headers(const char** sha256, const char** content_type,
|
||||
long* content_length, char* error_reason, size_t reason_size) {
|
||||
// Get X-SHA-256 header
|
||||
const char* sha256_header = getenv("HTTP_X_SHA_256");
|
||||
if (!sha256_header) {
|
||||
strncpy(error_reason, XREASON_MISSING_SHA256, reason_size - 1);
|
||||
error_reason[reason_size - 1] = '\0';
|
||||
return 400; // Bad Request
|
||||
}
|
||||
|
||||
// Validate SHA-256 format
|
||||
if (!validate_sha256_format(sha256_header)) {
|
||||
strncpy(error_reason, XREASON_INVALID_SHA256, reason_size - 1);
|
||||
error_reason[reason_size - 1] = '\0';
|
||||
return 400; // Bad Request
|
||||
}
|
||||
|
||||
// Get X-Content-Length header
|
||||
const char* length_header = getenv("HTTP_X_CONTENT_LENGTH");
|
||||
if (!length_header) {
|
||||
strncpy(error_reason, XREASON_MISSING_LENGTH, reason_size - 1);
|
||||
error_reason[reason_size - 1] = '\0';
|
||||
return 411; // Length Required
|
||||
}
|
||||
|
||||
// Validate content length
|
||||
long parsed_length;
|
||||
int length_result = validate_content_length(length_header, &parsed_length);
|
||||
if (length_result == 0) {
|
||||
if (parsed_length == 0) {
|
||||
strncpy(error_reason, XREASON_ZERO_LENGTH, reason_size - 1);
|
||||
} else {
|
||||
strncpy(error_reason, XREASON_INVALID_LENGTH, reason_size - 1);
|
||||
}
|
||||
error_reason[reason_size - 1] = '\0';
|
||||
return 400; // Bad Request
|
||||
} else if (length_result == -1) {
|
||||
strncpy(error_reason, XREASON_FILE_TOO_LARGE, reason_size - 1);
|
||||
error_reason[reason_size - 1] = '\0';
|
||||
return 413; // Content Too Large
|
||||
}
|
||||
|
||||
// Get X-Content-Type header (optional)
|
||||
const char* type_header = getenv("HTTP_X_CONTENT_TYPE");
|
||||
|
||||
// Set output values
|
||||
*sha256 = sha256_header;
|
||||
*content_type = type_header ? type_header : "application/octet-stream";
|
||||
*content_length = parsed_length;
|
||||
|
||||
return 200; // Success
|
||||
}
|
||||
|
||||
// Main BUD-06 handler function
|
||||
void handle_head_upload_request(void) {
|
||||
// Log the incoming request
|
||||
log_request("HEAD", "/upload", "pending", 0);
|
||||
|
||||
// Validate upload headers
|
||||
const char* sha256 = NULL;
|
||||
const char* content_type = NULL;
|
||||
long content_length = 0;
|
||||
char error_reason[256];
|
||||
|
||||
int validation_result = validate_upload_headers(&sha256, &content_type,
|
||||
&content_length, error_reason, sizeof(error_reason));
|
||||
|
||||
if (validation_result != 200) {
|
||||
// Header validation failed
|
||||
const char* error_type;
|
||||
switch (validation_result) {
|
||||
case 400: error_type = "invalid_headers"; break;
|
||||
case 411: error_type = "length_required"; break;
|
||||
case 413: error_type = "payload_too_large"; break;
|
||||
default: error_type = "validation_error"; break;
|
||||
}
|
||||
|
||||
send_upload_error_response(validation_result, error_type, error_reason, error_reason);
|
||||
log_request("HEAD", "/upload", "none", validation_result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if blob already exists (duplicate detection)
|
||||
if (check_blob_exists(sha256)) {
|
||||
send_upload_error_response(409, "blob_exists", "Blob with this hash already exists", XREASON_BLOB_EXISTS);
|
||||
log_request("HEAD", "/upload", "none", 409);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for optional authorization
|
||||
const char* auth_header = getenv("HTTP_AUTHORIZATION");
|
||||
const char* auth_status = "none";
|
||||
|
||||
if (auth_header) {
|
||||
// Validate authorization if provided
|
||||
nostr_request_t request = {
|
||||
.operation = "upload",
|
||||
.auth_header = auth_header,
|
||||
.event = NULL,
|
||||
.resource_hash = sha256,
|
||||
.mime_type = content_type,
|
||||
.file_size = content_length,
|
||||
.client_ip = getenv("REMOTE_ADDR"),
|
||||
.app_context = NULL
|
||||
};
|
||||
|
||||
nostr_request_result_t result;
|
||||
int auth_result = nostr_validate_request(&request, &result);
|
||||
|
||||
if (auth_result != NOSTR_SUCCESS || !result.valid) {
|
||||
const char* error_type = "authentication_failed";
|
||||
const char* message = "Invalid or expired authentication";
|
||||
const char* details = result.reason[0] ? result.reason : "Authentication validation failed";
|
||||
|
||||
// Provide more specific error messages based on the reason
|
||||
if (strstr(result.reason, "whitelist")) {
|
||||
error_type = "pubkey_not_whitelisted";
|
||||
message = "Public key not authorized";
|
||||
details = result.reason;
|
||||
} else if (strstr(result.reason, "blacklist")) {
|
||||
error_type = "access_denied";
|
||||
message = "Access denied by policy";
|
||||
details = result.reason;
|
||||
} else if (strstr(result.reason, "size")) {
|
||||
error_type = "file_too_large";
|
||||
message = "File size exceeds policy limits";
|
||||
details = result.reason;
|
||||
}
|
||||
|
||||
send_upload_error_response(401, error_type, message, details);
|
||||
log_request("HEAD", "/upload", "auth_failed", 401);
|
||||
return;
|
||||
}
|
||||
auth_status = "authenticated";
|
||||
}
|
||||
|
||||
// All validations passed - return success
|
||||
send_upload_success_response(sha256, content_type, content_length);
|
||||
log_request("HEAD", "/upload", auth_status, 200);
|
||||
}
|
||||
280
src/bud08.c
Normal file
280
src/bud08.c
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* BUD-08 NIP-94 File Metadata Tags Implementation
|
||||
* Handles NIP-94 metadata generation for blob descriptors
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdint.h>
|
||||
#include "ginxsom.h"
|
||||
|
||||
// Database path
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
|
||||
// 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 ]");
|
||||
}
|
||||
329
src/bud09.c
Normal file
329
src/bud09.c
Normal file
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* BUD-09 - Blob Report (NIP-56 Report Events)
|
||||
* Handles reporting of inappropriate or harmful blob content
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sqlite3.h>
|
||||
#include <fcgi_stdio.h>
|
||||
#include <time.h>
|
||||
#include "ginxsom.h"
|
||||
#include "../nostr_core_lib/nostr_core/request_validator.h"
|
||||
|
||||
// Database path (should match main.c)
|
||||
#define DB_PATH "db/ginxsom.db"
|
||||
|
||||
// Forward declarations for helper functions
|
||||
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);
|
||||
int validate_sha256_format(const char* sha256);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BUD 09 - Blob Report (NIP-56 Report Events)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Validate NIP-56 report event structure
|
||||
int validate_report_event_structure(cJSON* event) {
|
||||
if (!event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Must be kind 1984
|
||||
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_json || !cJSON_IsNumber(kind_json)) {
|
||||
return 0;
|
||||
}
|
||||
if (cJSON_GetNumberValue(kind_json) != 1984) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Must have tags array
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Must have at least one 'x' tag
|
||||
int has_x_tag = 0;
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag)) continue;
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name)) {
|
||||
const char* tag_name_str = cJSON_GetStringValue(tag_name);
|
||||
if (tag_name_str && strcmp(tag_name_str, "x") == 0) {
|
||||
has_x_tag = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return has_x_tag;
|
||||
}
|
||||
|
||||
// Extract SHA-256 blob hashes from 'x' tags
|
||||
int extract_blob_hashes_from_report(cJSON* event, char blob_hashes[][65], int max_hashes) {
|
||||
if (!event || !blob_hashes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hash_count = 0;
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (hash_count >= max_hashes) break;
|
||||
if (!cJSON_IsArray(tag)) continue;
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (!tag_name || !cJSON_IsString(tag_name)) continue;
|
||||
|
||||
const char* tag_name_str = cJSON_GetStringValue(tag_name);
|
||||
if (tag_name_str && strcmp(tag_name_str, "x") == 0) {
|
||||
cJSON* hash_value = cJSON_GetArrayItem(tag, 1);
|
||||
if (hash_value && cJSON_IsString(hash_value)) {
|
||||
const char* hash = cJSON_GetStringValue(hash_value);
|
||||
if (hash && validate_sha256_format(hash)) {
|
||||
strncpy(blob_hashes[hash_count], hash, 64);
|
||||
blob_hashes[hash_count][64] = '\0';
|
||||
hash_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hash_count;
|
||||
}
|
||||
|
||||
// Validate NIP-56 report types in x tags
|
||||
int validate_report_types(cJSON* event) {
|
||||
if (!event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Valid NIP-56 report types
|
||||
const char* valid_types[] = {
|
||||
"nudity", "malware", "profanity", "illegal",
|
||||
"spam", "impersonation", "other", NULL
|
||||
};
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag)) continue;
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (!tag_name || !cJSON_IsString(tag_name)) continue;
|
||||
|
||||
const char* tag_name_str = cJSON_GetStringValue(tag_name);
|
||||
if (tag_name_str && strcmp(tag_name_str, "x") == 0) {
|
||||
// Check if report type is provided and valid (optional)
|
||||
cJSON* report_type = cJSON_GetArrayItem(tag, 2);
|
||||
if (report_type && cJSON_IsString(report_type)) {
|
||||
const char* type_str = cJSON_GetStringValue(report_type);
|
||||
if (type_str) {
|
||||
// Validate against known types (but allow unknown types per spec)
|
||||
for (int i = 0; valid_types[i] != NULL; i++) {
|
||||
if (strcmp(type_str, valid_types[i]) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Note: Allow unknown types as per NIP-56 spec flexibility
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // Always return success - report types are informational
|
||||
}
|
||||
|
||||
// Store blob report in database (optional server behavior)
|
||||
int store_blob_report(const char* event_json, const char* reporter_pubkey) {
|
||||
// Optional implementation - servers MAY store reports
|
||||
sqlite3* db;
|
||||
sqlite3_stmt* stmt;
|
||||
int rc;
|
||||
|
||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||
if (rc) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if blob_reports table exists, create if not
|
||||
const char* create_table_sql =
|
||||
"CREATE TABLE IF NOT EXISTS blob_reports ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"report_event TEXT NOT NULL, "
|
||||
"reporter_pubkey TEXT, "
|
||||
"reported_at INTEGER NOT NULL, "
|
||||
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
||||
")";
|
||||
|
||||
rc = sqlite3_exec(db, create_table_sql, NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* sql = "INSERT INTO blob_reports (report_event, reporter_pubkey, reported_at) VALUES (?, ?, strftime('%s', 'now'))";
|
||||
|
||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, event_json, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, reporter_pubkey, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
int success = (rc == SQLITE_DONE);
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Handle PUT /report requests (BUD-09)
|
||||
void handle_report_request(void) {
|
||||
// Log the incoming request
|
||||
log_request("PUT", "/report", "pending", 0);
|
||||
|
||||
// Validate HTTP method (only PUT allowed)
|
||||
const char* request_method = getenv("REQUEST_METHOD");
|
||||
if (!request_method || strcmp(request_method, "PUT") != 0) {
|
||||
send_error_response(405, "method_not_allowed", "Only PUT method allowed", "The /report endpoint only accepts PUT requests");
|
||||
log_request(request_method ? request_method : "NULL", "/report", "none", 405);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate Content-Type
|
||||
const char* content_type = getenv("CONTENT_TYPE");
|
||||
if (!content_type || strstr(content_type, "application/json") == NULL) {
|
||||
send_error_response(415, "unsupported_media_type", "Content-Type must be application/json", "Report requests must be JSON");
|
||||
log_request("PUT", "/report", "none", 415);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate Content-Length
|
||||
const char* content_length_str = getenv("CONTENT_LENGTH");
|
||||
if (!content_length_str) {
|
||||
send_error_response(400, "missing_content_length", "Content-Length header required", "Request body size must be specified");
|
||||
log_request("PUT", "/report", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
long content_length = atol(content_length_str);
|
||||
if (content_length <= 0 || content_length > 10240) { // 10KB limit for report events
|
||||
send_error_response(400, "invalid_content_length", "Invalid content length", "Report events must be between 1 byte and 10KB");
|
||||
log_request("PUT", "/report", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read JSON request body
|
||||
char* json_body = malloc(content_length + 1);
|
||||
if (!json_body) {
|
||||
send_error_response(500, "memory_error", "Failed to allocate memory", "Internal server error");
|
||||
log_request("PUT", "/report", "none", 500);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_read = fread(json_body, 1, content_length, stdin);
|
||||
if (bytes_read != (size_t)content_length) {
|
||||
free(json_body);
|
||||
send_error_response(400, "incomplete_body", "Failed to read complete request body", "The request body was incomplete");
|
||||
log_request("PUT", "/report", "none", 400);
|
||||
return;
|
||||
}
|
||||
json_body[content_length] = '\0';
|
||||
|
||||
// Parse JSON event
|
||||
cJSON* event = cJSON_Parse(json_body);
|
||||
if (!event) {
|
||||
free(json_body);
|
||||
send_error_response(400, "invalid_json", "Invalid JSON format", "Request body must be valid JSON");
|
||||
log_request("PUT", "/report", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate event structure (NIP-56 kind 1984 with x tags)
|
||||
if (!validate_report_event_structure(event)) {
|
||||
cJSON_Delete(event);
|
||||
free(json_body);
|
||||
send_error_response(400, "invalid_report_event", "Invalid report event structure", "Report must be NIP-56 kind 1984 event with x tags");
|
||||
log_request("PUT", "/report", "none", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate nostr event signature and structure
|
||||
int structure_result = nostr_validate_event_structure(event);
|
||||
if (structure_result != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(event);
|
||||
free(json_body);
|
||||
send_error_response(400, "invalid_event_structure", "Invalid nostr event structure", "Event does not conform to nostr event format");
|
||||
log_request("PUT", "/report", "structure_invalid", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
int crypto_result = nostr_verify_event_signature(event);
|
||||
if (crypto_result != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(event);
|
||||
free(json_body);
|
||||
send_error_response(400, "invalid_signature", "Invalid event signature", "Event signature verification failed");
|
||||
log_request("PUT", "/report", "signature_invalid", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract blob hashes from x tags
|
||||
char blob_hashes[10][65]; // Support up to 10 blob hashes per report
|
||||
int hash_count = extract_blob_hashes_from_report(event, blob_hashes, 10);
|
||||
if (hash_count == 0) {
|
||||
cJSON_Delete(event);
|
||||
free(json_body);
|
||||
send_error_response(400, "no_blob_hashes", "No valid blob hashes found", "Report must contain at least one valid SHA-256 hash in x tags");
|
||||
log_request("PUT", "/report", "no_hashes", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate report types (optional validation)
|
||||
validate_report_types(event);
|
||||
|
||||
// Extract reporter pubkey
|
||||
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* reporter_pubkey = NULL;
|
||||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||||
reporter_pubkey = cJSON_GetStringValue(pubkey_json);
|
||||
}
|
||||
|
||||
// Optional: Store report in database (server behavior)
|
||||
if (reporter_pubkey) {
|
||||
store_blob_report(json_body, reporter_pubkey);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
cJSON_Delete(event);
|
||||
free(json_body);
|
||||
|
||||
// Return success response
|
||||
printf("Status: 200 OK\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"message\": \"Report received\",\n");
|
||||
printf(" \"reported_blobs\": %d,\n", hash_count);
|
||||
printf(" \"reporter\": \"%s\"\n", reporter_pubkey ? reporter_pubkey : "anonymous");
|
||||
printf("}\n");
|
||||
|
||||
log_request("PUT", "/report", reporter_pubkey ? "authenticated" : "anonymous", 200);
|
||||
}
|
||||
121
src/ginxsom.h
121
src/ginxsom.h
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Ginxsom Blossom Server Header
|
||||
*
|
||||
*
|
||||
* This header contains all function declarations and type definitions
|
||||
* organized by BUD (Blossom Unified Draft) sections.
|
||||
*/
|
||||
@@ -43,18 +43,44 @@ void handle_head_request(const char* uri);
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Authorization header parsing
|
||||
int parse_authorization_header(const char* auth_header, char* event_json, size_t json_size);
|
||||
|
||||
// Blossom event validation (specific to kind 24242)
|
||||
int validate_blossom_event(cJSON* event, const char* expected_hash, const char* method);
|
||||
|
||||
// Main authentication orchestrator
|
||||
int authenticate_request(const char* auth_header, const char* method, const char* file_hash);
|
||||
// NOTE: Old authentication functions removed - now handled by nostr_core_lib unified system
|
||||
// Use nostr_validate_request() from request_validator.h for all authentication needs
|
||||
|
||||
// Upload handling
|
||||
void handle_upload_request(void);
|
||||
|
||||
// Blob metadata structure
|
||||
typedef struct {
|
||||
char sha256[65];
|
||||
long size;
|
||||
char type[128];
|
||||
long uploaded_at;
|
||||
char filename[256];
|
||||
int found;
|
||||
} blob_metadata_t;
|
||||
|
||||
// Blob metadata database operations
|
||||
int insert_blob_metadata(const char* sha256, long size, const char* type,
|
||||
long uploaded_at, const char* uploader_pubkey,
|
||||
const char* filename);
|
||||
int get_blob_metadata(const char* sha256, blob_metadata_t* metadata);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BUD 04 - Mirroring
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Mirror request handling
|
||||
void handle_mirror_request(void);
|
||||
|
||||
// URL validation for mirroring
|
||||
int validate_mirror_url(const char* url);
|
||||
|
||||
// Content type detection for mirrored blobs
|
||||
const char* determine_blob_content_type(const char* url, const char* header_content_type,
|
||||
const unsigned char* data, size_t size);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BUD 06 - Upload Requirements
|
||||
@@ -64,6 +90,54 @@ void handle_upload_request(void);
|
||||
// Upload policy management (for future implementation)
|
||||
void handle_upload_requirements_request(void);
|
||||
|
||||
// BUD-06 specific functions
|
||||
void handle_head_upload_request(void);
|
||||
int validate_upload_headers(const char** sha256, const char** content_type,
|
||||
long* content_length, char* error_reason, size_t reason_size);
|
||||
void send_upload_error_response(int status_code, const char* error_type,
|
||||
const char* message, const char* x_reason);
|
||||
void send_upload_success_response(const char* sha256, const char* content_type, long content_length);
|
||||
int validate_content_length(const char* content_length_str, long* parsed_length);
|
||||
int check_blob_exists(const char* sha256);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BUD 09 - Blob Report (NIP-56 Report Events)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Report event validation
|
||||
int validate_report_event_structure(cJSON* event);
|
||||
int extract_blob_hashes_from_report(cJSON* event, char blob_hashes[][65], int max_hashes);
|
||||
int validate_report_types(cJSON* event);
|
||||
|
||||
// Report storage and handling
|
||||
int store_blob_report(const char* event_json, const char* reporter_pubkey);
|
||||
void handle_report_request(void);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BUD 08 - NIP-94 File Metadata Tags
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// NIP-94 configuration and control
|
||||
int nip94_is_enabled(void);
|
||||
int nip94_get_origin(char* out, size_t out_size);
|
||||
|
||||
// MIME type and file extension handling
|
||||
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);
|
||||
|
||||
// Image dimension parsing
|
||||
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);
|
||||
|
||||
// NIP-94 metadata emission
|
||||
void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UTILITY FUNCTIONS
|
||||
@@ -77,6 +151,35 @@ void send_json_response(int status_code, const char* json_content);
|
||||
// Logging utilities
|
||||
void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
|
||||
|
||||
// SHA-256 validation helper (used by multiple BUDs)
|
||||
int validate_sha256_format(const char* sha256);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ADMIN API ENDPOINTS
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Admin API request handler
|
||||
void handle_admin_api_request(const char* method, const char* uri);
|
||||
|
||||
// Individual endpoint handlers
|
||||
void handle_stats_api(void);
|
||||
void handle_config_get_api(void);
|
||||
void handle_config_put_api(void);
|
||||
void handle_files_api(void);
|
||||
void handle_health_api(void);
|
||||
|
||||
// Admin authentication functions
|
||||
int authenticate_admin_request(const char* auth_header);
|
||||
int is_admin_enabled(void);
|
||||
int verify_admin_pubkey(const char* event_pubkey);
|
||||
|
||||
// Admin API utility functions
|
||||
void send_json_response(int status, const char* json_content);
|
||||
void send_json_error(int status, const char* error, const char* message);
|
||||
int parse_query_params(const char* query_string, char params[][256], int max_params);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
1773
src/main.c
1773
src/main.c
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user