diff --git a/Accept: b/Accept: new file mode 100644 index 0000000..e69de29 diff --git a/GET b/GET new file mode 100644 index 0000000..e69de29 diff --git a/Host: b/Host: new file mode 100644 index 0000000..e69de29 diff --git a/Origin: b/Origin: new file mode 100644 index 0000000..e69de29 diff --git a/User-Agent: b/User-Agent: new file mode 100644 index 0000000..e69de29 diff --git a/build/ginxsom-fcgi b/build/ginxsom-fcgi index 09bd5c7..a951d80 100755 Binary files a/build/ginxsom-fcgi and b/build/ginxsom-fcgi differ diff --git a/build/main.o b/build/main.o index 0b1b3d8..f73f019 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/config/local-nginx.conf b/config/local-nginx.conf index f087802..5231a85 100644 --- a/config/local-nginx.conf +++ b/config/local-nginx.conf @@ -18,6 +18,9 @@ http { keepalive_timeout 65; types_hash_max_size 2048; + # Temporary file paths (relative to nginx prefix) + client_body_temp_path tmp/nginx_temp; + # MIME types (local) include mime.types; default_type application/octet-stream; @@ -30,7 +33,19 @@ http { server unix:/tmp/ginxsom-fcgi.sock; } - # Local development server + # Map endpoints to their allowed methods for CORS + map $uri $cors_methods { + default "GET, HEAD, OPTIONS"; + /upload "PUT, HEAD, OPTIONS"; + /mirror "PUT, OPTIONS"; + /report "PUT, OPTIONS"; + /auth "GET, OPTIONS"; + ~^/list/ "GET, OPTIONS"; + ~^/api/ "GET, PUT, OPTIONS"; + ~^/[a-f0-9]\{64\} "GET, HEAD, DELETE, OPTIONS"; + } + + # Local development server - HTTP server { listen 9001; server_name localhost; @@ -41,10 +56,21 @@ http { # Maximum upload size (adjust as needed) client_max_body_size 100M; - # Security headers (applied to all responses) - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options DENY; - add_header X-XSS-Protection "1; mode=block"; + # Security headers + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + + # Universal CORS headers for all responses + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + + # Universal OPTIONS preflight handler + if ($request_method = OPTIONS) { + return 204; + } # 1. SPECIFIC ENDPOINTS (most specific first) @@ -53,6 +79,7 @@ http { if ($request_method !~ ^(PUT|HEAD)$) { return 405; } + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -73,7 +100,7 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } # GET /list/ (BUD-02) - List user's blobs @@ -81,6 +108,7 @@ http { if ($request_method !~ ^(GET)$) { return 405; } + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -101,7 +129,7 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } # PUT /mirror (BUD-04) - Mirror content @@ -109,6 +137,7 @@ http { if ($request_method !~ ^(PUT)$) { return 405; } + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -129,35 +158,7 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; - } - - # HEAD/PUT /media (BUD-05) - Media operations - location = /media { - if ($request_method !~ ^(HEAD|PUT)$) { - return 405; - } - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - fastcgi_param SCRIPT_NAME $fastcgi_script_name; - fastcgi_param REQUEST_URI $request_uri; - fastcgi_param DOCUMENT_URI $document_uri; - fastcgi_param DOCUMENT_ROOT $document_root; - fastcgi_param SERVER_PROTOCOL $server_protocol; - fastcgi_param REQUEST_SCHEME $scheme; - fastcgi_param HTTPS $https if_not_empty; - fastcgi_param GATEWAY_INTERFACE CGI/1.1; - fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; - fastcgi_param REMOTE_ADDR $remote_addr; - fastcgi_param REMOTE_PORT $remote_port; - fastcgi_param SERVER_ADDR $server_addr; - fastcgi_param SERVER_PORT $server_port; - fastcgi_param SERVER_NAME $server_name; - fastcgi_param REDIRECT_STATUS 200; - fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } # PUT /report (BUD-09) - Report content @@ -165,6 +166,7 @@ http { if ($request_method !~ ^(PUT)$) { return 405; } + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -185,24 +187,15 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } # GET /auth (NIP-42) - Challenge generation location = /auth { - if ($request_method !~ ^(GET|OPTIONS)$) { + if ($request_method !~ ^(GET)$) { return 405; } - - # Handle preflight OPTIONS requests for CORS - if ($request_method = OPTIONS) { - add_header Access-Control-Allow-Origin *; - add_header Access-Control-Allow-Methods "GET, OPTIONS"; - add_header Access-Control-Allow-Headers "Content-Type, Authorization"; - add_header Access-Control-Max-Age 86400; - return 204; - } - + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -223,29 +216,15 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - fastcgi_pass fastcgi_backend; - - # CORS headers for NIP-42 access - add_header Access-Control-Allow-Origin *; - add_header Access-Control-Allow-Methods "GET, OPTIONS"; - add_header Access-Control-Allow-Headers "Content-Type, Authorization"; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } # Admin API endpoints (/api/*) location /api/ { - # Handle preflight OPTIONS requests for CORS - if ($request_method = OPTIONS) { - add_header Access-Control-Allow-Origin *; - add_header Access-Control-Allow-Methods "GET, PUT, OPTIONS"; - add_header Access-Control-Allow-Headers "Content-Type, Authorization"; - add_header Access-Control-Max-Age 86400; - return 204; - } - if ($request_method !~ ^(GET|PUT)$) { return 405; } - + fastcgi_pass fastcgi_backend; fastcgi_param QUERY_STRING $query_string; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; @@ -266,17 +245,7 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; - - # Support for Authorization header fastcgi_param HTTP_AUTHORIZATION $http_authorization; - - fastcgi_pass fastcgi_backend; - - # CORS headers for API access - add_header Access-Control-Allow-Origin *; - add_header Access-Control-Allow-Methods "GET, PUT, OPTIONS"; - add_header Access-Control-Allow-Headers "Content-Type, Authorization"; - add_header Access-Control-Max-Age 86400; } # 2. BLOB OPERATIONS (SHA256 patterns) @@ -303,6 +272,10 @@ http { # Cache headers for blob content add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; } # Internal handler for DELETE operations @@ -329,36 +302,47 @@ http { fastcgi_param SERVER_NAME $server_name; fastcgi_param REDIRECT_STATUS 200; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } - # 3. INTERNAL HANDLERS - # FastCGI handler for HEAD requests location ~ "^/fcgi-head/([a-f0-9]{64}).*$" { internal; fastcgi_pass fastcgi_backend; - fastcgi_param REQUEST_METHOD HEAD; - fastcgi_param REQUEST_URI /$1; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD HEAD; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI /$1; + fastcgi_param DOCUMENT_URI /$1; + fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; } - # 4. UTILITY ENDPOINTS + # 3. UTILITY ENDPOINTS # Health check endpoint location /health { access_log off; return 200 "OK\n"; add_header Content-Type text/plain; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; } # List files endpoint for debugging @@ -371,6 +355,342 @@ http { location = / { return 200 "Ginxsom Blossom Server\nEndpoints: GET /, PUT /upload, GET /list/\nHealth: GET /health\n"; add_header Content-Type text/plain; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + } + } + + # Local development server - HTTPS + server { + listen 9443 ssl; + server_name localhost; + + # SSL Configuration using existing certificates + ssl_certificate /home/teknari/.ssl_for_local_servers/cert.pem; + ssl_certificate_key /home/teknari/.ssl_for_local_servers/key.pem; + + # SSL Security settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; + + # Root directory for blossom files (local blobs directory) + root blobs; + + # Maximum upload size (adjust as needed) + client_max_body_size 100M; + + # Security headers + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + + # Universal CORS headers for all responses + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + + # Universal OPTIONS preflight handler + if ($request_method = OPTIONS) { + return 204; + } + + # 1. SPECIFIC ENDPOINTS (most specific first) + + # PUT /upload (BUD-02) - File uploads + location = /upload { + if ($request_method !~ ^(PUT|HEAD)$) { + return 405; + } + # CORS headers + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # GET /list/ (BUD-02) - List user's blobs + location ~ "^/list/([a-f0-9]{64})$" { + if ($request_method !~ ^(GET)$) { + return 405; + } + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # PUT /mirror (BUD-04) - Mirror content + location = /mirror { + if ($request_method !~ ^(PUT)$) { + return 405; + } + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # PUT /report (BUD-09) - Report content + location = /report { + if ($request_method !~ ^(PUT)$) { + return 405; + } + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # GET /auth (NIP-42) - Challenge generation + location = /auth { + if ($request_method !~ ^(GET)$) { + return 405; + } + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # Admin API endpoints (/api/*) + location /api/ { + if ($request_method !~ ^(GET|PUT)$) { + return 405; + } + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # 2. BLOB OPERATIONS (SHA256 patterns) + + # GET/HEAD/DELETE / (BUD-01) - Blob operations with optional file extensions + location ~ "^/([a-f0-9]{64})(\.[a-zA-Z0-9]+)?$" { + # Handle DELETE method via rewrite to avoid fastcgi_param in if block + if ($request_method = DELETE) { + rewrite ^/(.*)$ /fcgi-delete/$1 last; + } + + # Route HEAD requests to FastCGI for metadata + if ($request_method = HEAD) { + rewrite ^/(.*)$ /fcgi-head/$1 last; + } + + # Only allow GET for file serving at this point + if ($request_method != GET) { + return 405; + } + + # GET requests - serve files directly with extension fallback + try_files /$1.txt /$1.jpg /$1.jpeg /$1.png /$1.webp /$1.gif /$1.pdf /$1.mp4 /$1.mp3 /$1.md =404; + + # Cache headers for blob content + add_header Cache-Control "public, max-age=31536000, immutable"; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + } + + # Internal handler for DELETE operations + location ~ "^/fcgi-delete/([a-f0-9]{64}).*$" { + internal; + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD DELETE; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI /$1; + fastcgi_param DOCUMENT_URI /$1; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # FastCGI handler for HEAD requests + location ~ "^/fcgi-head/([a-f0-9]{64}).*$" { + internal; + fastcgi_pass fastcgi_backend; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD HEAD; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param REQUEST_URI /$1; + fastcgi_param DOCUMENT_URI /$1; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; + fastcgi_param HTTP_AUTHORIZATION $http_authorization; + } + + # 3. UTILITY ENDPOINTS + + # Health check endpoint + location /health { + access_log off; + return 200 "OK (HTTPS)\n"; + add_header Content-Type text/plain; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; + } + + # List files endpoint for debugging + location /debug/list { + autoindex on; + autoindex_format json; + } + + # Root redirect + location = / { + return 200 "Ginxsom Blossom Server (HTTPS)\nEndpoints: GET /, PUT /upload, GET /list/\nHealth: GET /health\n"; + add_header Content-Type text/plain; + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always; + add_header Access-Control-Max-Age 86400 always; } } } diff --git a/db/ginxsom.db b/db/ginxsom.db index 4738486..d9b3687 100644 Binary files a/db/ginxsom.db and b/db/ginxsom.db differ diff --git a/src/main.c b/src/main.c index 2da2980..d4a1424 100644 --- a/src/main.c +++ b/src/main.c @@ -306,6 +306,7 @@ int run_interactive_setup(const char *config_path) { */ // Function declarations +void handle_options_request(void); 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, @@ -452,6 +453,7 @@ void handle_head_request(const char *sha256) { // Validate SHA-256 format (64 hex characters) if (strlen(sha256) != 64) { printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash format\n"); return; @@ -460,6 +462,7 @@ void handle_head_request(const char *sha256) { // Check if blob exists in database - this is the single source of truth if (!get_blob_metadata(sha256, &metadata)) { printf("Status: 404 Not Found\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Blob not found\n"); return; @@ -467,6 +470,7 @@ void handle_head_request(const char *sha256) { // Return successful HEAD response with metadata from database printf("Status: 200 OK\r\n"); + printf("Content-Type: %s\r\n", metadata.type); printf("Content-Length: %ld\r\n", metadata.size); printf("Cache-Control: public, max-age=31536000, immutable\r\n"); @@ -533,6 +537,12 @@ const char *extract_sha256_from_uri(const char *uri) { ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +// BUD-01 CORS Compliance System (Now handled by nginx) +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + // Enhanced error response helper functions void send_error_response(int status_code, const char *error_type, const char *message, const char *details) { @@ -696,6 +706,7 @@ void handle_list_request(const char *pubkey) { // Start JSON response printf("Status: 200 OK\r\n"); + printf("Content-Type: application/json\r\n\r\n"); printf("[\n"); @@ -911,6 +922,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res // Return success response printf("Status: 200 OK\r\n"); + printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"message\": \"Blob deleted successfully\",\n"); @@ -973,6 +985,7 @@ void handle_upload_request(void) { unsigned char *file_data = malloc(content_length); if (!file_data) { printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Memory allocation failed\n"); return; @@ -983,6 +996,7 @@ void handle_upload_request(void) { free(file_data); printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to read complete file data\n"); return; @@ -994,6 +1008,7 @@ void handle_upload_request(void) { if (nostr_sha256(file_data, content_length, hash) != NOSTR_SUCCESS) { free(file_data); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Hash calculation failed\n"); return; @@ -1022,6 +1037,7 @@ void handle_upload_request(void) { if (!outfile) { free(file_data); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to create file\n"); return; @@ -1044,6 +1060,7 @@ void handle_upload_request(void) { // Clean up partial file unlink(filepath); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to write complete file\n"); return; @@ -1113,6 +1130,7 @@ void handle_upload_request(void) { unlink(filepath); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to store blob metadata\n"); return; @@ -1129,6 +1147,7 @@ void handle_upload_request(void) { // 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); @@ -1214,6 +1233,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re file_data = malloc(content_length); if (!file_data) { printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Memory allocation failed\n"); return; @@ -1223,6 +1243,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re if (bytes_read != (size_t)content_length) { free(file_data); printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to read complete file data\n"); return; @@ -1238,6 +1259,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re if (nostr_sha256(file_data, file_size, hash) != NOSTR_SUCCESS) { if (should_free_file_data) free(file_data); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Hash calculation failed\n"); return; @@ -1266,6 +1288,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re if (!outfile) { if (should_free_file_data) free(file_data); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to create file\n"); return; @@ -1291,6 +1314,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re // Clean up partial file unlink(filepath); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to write complete file\n"); return; @@ -1348,6 +1372,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re // consistency unlink(filepath); printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Failed to store blob metadata\n"); return; @@ -1364,6 +1389,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re // 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); @@ -1388,6 +1414,25 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +// OPTIONS Preflight Request Handler (BUD-01 CORS) +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +// Handle OPTIONS requests for CORS preflight +void handle_options_request(void) { + log_request("OPTIONS", "*", "cors_preflight", 0); + + printf("Status: 200 OK\r\n"); + + printf("Content-Length: 0\r\n"); + printf("\r\n"); + // No body for OPTIONS response + + log_request("OPTIONS", "*", "cors_preflight", 200); +} + // Handle GET /auth requests to provide NIP-42 challenges void handle_auth_challenge_request(void) { // Log the incoming request @@ -1402,6 +1447,7 @@ void handle_auth_challenge_request(void) { if (result != NOSTR_SUCCESS) { printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"error\": \"challenge_generation_failed\",\n"); @@ -1417,11 +1463,8 @@ void handle_auth_challenge_request(void) { // Return the challenge as JSON printf("Status: 200 OK\r\n"); - printf("Content-Type: application/json\r\n"); - printf("Access-Control-Allow-Origin: *\r\n"); - printf("Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"); - printf("Access-Control-Allow-Headers: Content-Type, Authorization\r\n"); - printf("\r\n"); + + printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"challenge\": \"%s\",\n", challenge_buffer); printf(" \"relay\": \"ginxsom\",\n"); @@ -1490,11 +1533,21 @@ if (!config_loaded /* && !initialize_server_config() */) { if (!request_method || !request_uri) { printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid request\n"); continue; } + ///////////////////////////////////////////////////////////////////// + // HANDLE OPTIONS PREFLIGHT REQUESTS (BUD-01 CORS) + ///////////////////////////////////////////////////////////////////// + + if (strcmp(request_method, "OPTIONS") == 0) { + handle_options_request(); + continue; + } + ///////////////////////////////////////////////////////////////////// // CENTRALIZED REQUEST VALIDATION SYSTEM ///////////////////////////////////////////////////////////////////// @@ -1607,6 +1660,7 @@ if (!config_loaded /* && !initialize_server_config() */) { log_request("HEAD", request_uri, "public", 200); } else { printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash in URI\n"); log_request("HEAD", request_uri, "none", 400); @@ -1687,6 +1741,7 @@ if (!config_loaded /* && !initialize_server_config() */) { } else { // Other methods not implemented yet printf("Status: 501 Not Implemented\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); printf("Method %s not implemented\n", request_method); log_request(request_method, request_uri, "none", 501); diff --git a/tests/auth_test_tmp/test_upload.txt b/tests/auth_test_tmp/test_upload.txt new file mode 100644 index 0000000..786ccf7 --- /dev/null +++ b/tests/auth_test_tmp/test_upload.txt @@ -0,0 +1 @@ +test data for upload diff --git a/tests/cors_test.sh b/tests/cors_test.sh new file mode 100755 index 0000000..dba98ba --- /dev/null +++ b/tests/cors_test.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +# CORS Test Suite for Ginxsom Blossom Server +# Tests all endpoints for both HTTP and HTTPS protocols +# Validates presence of required CORS headers per BUD-01 specification + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test configuration +HTTP_BASE="http://localhost:9001" +HTTPS_BASE="https://localhost:9443" +SAMPLE_SHA256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" # empty string hash +SAMPLE_PUBKEY="0000000000000000000000000000000000000000000000000000000000000001" + +# Required CORS headers for BUD-01 compliance +REQUIRED_HEADERS=( + "Access-Control-Allow-Origin" + "Access-Control-Allow-Methods" + "Access-Control-Allow-Headers" + "Access-Control-Max-Age" +) + +# Test counters +TOTAL_TESTS=0 +PASSED_TESTS=0 +FAILED_TESTS=0 + +echo -e "${BLUE}===========================================${NC}" +echo -e "${BLUE} GINXSOM CORS TEST SUITE (BUD-01) ${NC}" +echo -e "${BLUE}===========================================${NC}" +echo "" + +# Function to check CORS headers +check_cors_headers() { + local endpoint="$1" + local method="$2" + local protocol="$3" + local extra_args="$4" + + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + echo -e "${YELLOW}Testing:${NC} $method $endpoint ($protocol)" + + # Make request and capture headers + if [ "$protocol" = "HTTPS" ]; then + headers=$(curl -k -s -I -X "$method" $extra_args "$endpoint" 2>/dev/null || echo "CURL_FAILED") + else + headers=$(curl -s -I -X "$method" $extra_args "$endpoint" 2>/dev/null || echo "CURL_FAILED") + fi + + if [ "$headers" = "CURL_FAILED" ]; then + echo -e " ${RED}✗ FAIL:${NC} Could not connect to $endpoint" + FAILED_TESTS=$((FAILED_TESTS + 1)) + return 1 + fi + + # Check HTTP response is valid + if ! echo "$headers" | grep -q "HTTP/"; then + echo -e " ${RED}✗ FAIL:${NC} Invalid HTTP response" + FAILED_TESTS=$((FAILED_TESTS + 1)) + return 1 + fi + + local all_headers_present=true + local missing_headers=() + + # Check each required header + for header in "${REQUIRED_HEADERS[@]}"; do + if ! echo "$headers" | grep -qi "^$header:"; then + all_headers_present=false + missing_headers+=("$header") + fi + done + + if [ "$all_headers_present" = true ]; then + echo -e " ${GREEN}✓ PASS:${NC} All CORS headers present" + + # Verify specific header values + local origin_header=$(echo "$headers" | grep -i "Access-Control-Allow-Origin:" | head -1 | sed 's/.*: *//') + if [[ "$origin_header" == *"*"* ]]; then + echo -e " ${GREEN}✓${NC} Access-Control-Allow-Origin: $origin_header" + else + echo -e " ${RED}✗${NC} Access-Control-Allow-Origin should be '*', got: $origin_header" + all_headers_present=false + fi + + # Check for duplicate headers (common CORS issue) + local origin_count=$(echo "$headers" | grep -ci "Access-Control-Allow-Origin:" || echo "0") + if [ "$origin_count" -gt 1 ]; then + echo -e " ${RED}✗${NC} WARNING: Multiple Access-Control-Allow-Origin headers detected ($origin_count)" + all_headers_present=false + fi + + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + echo -e " ${RED}✗ FAIL:${NC} Missing CORS headers: ${missing_headers[*]}" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi + + echo "" + return 0 +} + +# Test endpoints for both HTTP and HTTPS +test_endpoint() { + local path="$1" + local method="$2" + local extra_args="$3" + + # Test HTTP + check_cors_headers "$HTTP_BASE$path" "$method" "HTTP" "$extra_args" + + # Test HTTPS + check_cors_headers "$HTTPS_BASE$path" "$method" "HTTPS" "$extra_args" +} + +echo -e "${BLUE}=== TESTING CORE BLOSSOM ENDPOINTS ===${NC}" +echo "" + +# 1. OPTIONS preflight tests (most critical for CORS) +echo -e "${YELLOW}--- OPTIONS Preflight Requests ---${NC}" +test_endpoint "/" "OPTIONS" +test_endpoint "/upload" "OPTIONS" +test_endpoint "/auth" "OPTIONS" +test_endpoint "/health" "OPTIONS" +test_endpoint "/mirror" "OPTIONS" +test_endpoint "/report" "OPTIONS" +test_endpoint "/$SAMPLE_SHA256" "OPTIONS" +test_endpoint "/list/$SAMPLE_PUBKEY" "OPTIONS" +test_endpoint "/api/config" "OPTIONS" + +# 2. GET requests +echo -e "${YELLOW}--- GET Requests ---${NC}" +test_endpoint "/" "GET" +test_endpoint "/health" "GET" +test_endpoint "/auth" "GET" +test_endpoint "/$SAMPLE_SHA256" "GET" +test_endpoint "/list/$SAMPLE_PUBKEY" "GET" +test_endpoint "/api/config" "GET" + +# 3. HEAD requests +echo -e "${YELLOW}--- HEAD Requests ---${NC}" +test_endpoint "/" "HEAD" +test_endpoint "/health" "HEAD" +test_endpoint "/$SAMPLE_SHA256" "HEAD" +test_endpoint "/upload" "HEAD" + +# 4. PUT requests (will likely fail with 400/401 but should still have CORS) +echo -e "${YELLOW}--- PUT Requests (CORS on Errors) ---${NC}" +test_endpoint "/upload" "PUT" "-H 'Content-Type: text/plain'" +test_endpoint "/mirror" "PUT" "-H 'Content-Type: application/json'" +test_endpoint "/report" "PUT" "-H 'Content-Type: application/json'" +test_endpoint "/api/config" "PUT" "-H 'Content-Type: application/json'" + +# 5. DELETE requests +echo -e "${YELLOW}--- DELETE Requests ---${NC}" +test_endpoint "/$SAMPLE_SHA256" "DELETE" + +# 6. Edge cases and variations +echo -e "${YELLOW}--- Edge Cases ---${NC}" +test_endpoint "/nonexistent" "GET" +test_endpoint "/$SAMPLE_SHA256.txt" "GET" +test_endpoint "/$SAMPLE_SHA256.jpg" "GET" +test_endpoint "/list/invalid_pubkey" "GET" + +echo -e "${BLUE}===========================================${NC}" +echo -e "${BLUE} TEST RESULTS SUMMARY ${NC}" +echo -e "${BLUE}===========================================${NC}" +echo "" +echo -e "Total Tests: $TOTAL_TESTS" +echo -e "${GREEN}Passed: $PASSED_TESTS${NC}" +echo -e "${RED}Failed: $FAILED_TESTS${NC}" +echo "" + +if [ $FAILED_TESTS -eq 0 ]; then + echo -e "${GREEN}🎉 ALL TESTS PASSED! 🎉${NC}" + echo -e "${GREEN}✅ CORS implementation is BUD-01 compliant${NC}" + echo -e "${GREEN}✅ All endpoints support cross-origin requests${NC}" + echo -e "${GREEN}✅ No duplicate header issues detected${NC}" + echo "" + exit 0 +else + echo -e "${RED}❌ TESTS FAILED! ❌${NC}" + echo -e "${RED}⚠️ CORS implementation needs fixes${NC}" + echo "" + exit 1 +fi \ No newline at end of file