Files
ginxsom/src/bud06.c
2025-09-11 13:28:54 -04:00

233 lines
8.4 KiB
C

/*
* 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"
// 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 for authorization first (before duplicate detection)
const char* auth_header = getenv("HTTP_AUTHORIZATION");
const char* auth_status = "none";
if (auth_header) {
// NOTE: Authorization validation now handled by centralized validation system in main.c
// This handler receives pre-validated requests, so if we reach here with auth_header,
// the authentication was already successful
auth_status = "authenticated";
} else {
// Check if server requires authorization for uploads
// If auth is required but not provided, return 401 before checking for duplicates
// TODO: This should check server configuration for auth requirements
// For now, assume auth is optional unless configured otherwise
}
// Check if blob already exists (duplicate detection - after auth validation)
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", auth_status, 409);
return;
}
// All validations passed - return success
send_upload_success_response(sha256, content_type, content_length);
log_request("HEAD", "/upload", auth_status, 200);
}