Files
next.orly.dev/pkg/blossom/handlers.go
mleku 9d13811f6b Add test files and enhance logging in various components
- Introduced test files for the blossom and database packages to improve test coverage and ensure functionality.
- Updated logging practices by suppressing unnecessary log outputs during tests to enhance clarity and focus on relevant information.
- Refactored error handling in the `handle-message` and `handle-req` functions to avoid logging expected context cancellation errors during shutdown.
- Bumped version to v0.25.2 to reflect these updates.
2025-11-05 08:15:02 +00:00

849 lines
23 KiB
Go

package blossom
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"lol.mleku.dev/log"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/utils"
)
// handleGetBlob handles GET /<sha256> requests (BUD-01)
func (s *Server) handleGetBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256 and extension
sha256Hex, ext, err := ExtractSHA256FromPath(path)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Convert hex to bytes
sha256Hash, err := hex.Dec(sha256Hex)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid SHA256 format")
return
}
// Check if blob exists
exists, err := s.storage.HasBlob(sha256Hash)
if err != nil {
log.E.F("error checking blob existence: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
if !exists {
s.setErrorResponse(w, http.StatusNotFound, "blob not found")
return
}
// Get blob metadata
metadata, err := s.storage.GetBlobMetadata(sha256Hash)
if err != nil {
log.E.F("error getting blob metadata: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Optional authorization check (BUD-01)
if s.requireAuth {
authEv, err := ValidateAuthEventForGet(r, s.getBaseURL(r), sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
if authEv == nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
}
// Get blob data
blobData, _, err := s.storage.GetBlob(sha256Hash)
if err != nil {
log.E.F("error getting blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Set headers
mimeType := DetectMimeType(metadata.MimeType, ext)
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(blobData)), 10))
w.Header().Set("Accept-Ranges", "bytes")
// Handle range requests (RFC 7233)
rangeHeader := r.Header.Get("Range")
if rangeHeader != "" {
start, end, valid, err := ParseRangeHeader(rangeHeader, int64(len(blobData)))
if err != nil {
s.setErrorResponse(w, http.StatusRequestedRangeNotSatisfiable, err.Error())
return
}
if valid {
WriteRangeResponse(w, blobData, start, end, int64(len(blobData)))
return
}
}
// Send full blob
w.WriteHeader(http.StatusOK)
_, _ = w.Write(blobData)
}
// handleHeadBlob handles HEAD /<sha256> requests (BUD-01)
func (s *Server) handleHeadBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256 and extension
sha256Hex, ext, err := ExtractSHA256FromPath(path)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Convert hex to bytes
sha256Hash, err := hex.Dec(sha256Hex)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid SHA256 format")
return
}
// Check if blob exists
exists, err := s.storage.HasBlob(sha256Hash)
if err != nil {
log.E.F("error checking blob existence: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
if !exists {
s.setErrorResponse(w, http.StatusNotFound, "blob not found")
return
}
// Get blob metadata
metadata, err := s.storage.GetBlobMetadata(sha256Hash)
if err != nil {
log.E.F("error getting blob metadata: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Optional authorization check
if s.requireAuth {
authEv, err := ValidateAuthEventForGet(r, s.getBaseURL(r), sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
if authEv == nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
}
// Set headers (same as GET but no body)
mimeType := DetectMimeType(metadata.MimeType, ext)
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Length", strconv.FormatInt(metadata.Size, 10))
w.Header().Set("Accept-Ranges", "bytes")
w.WriteHeader(http.StatusOK)
}
// handleUpload handles PUT /upload requests (BUD-02)
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
// Get initial pubkey from request (may be updated by auth validation)
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
// Read request body
body, err := io.ReadAll(io.LimitReader(r.Body, s.maxBlobSize+1))
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "error reading request body")
return
}
if int64(len(body)) > s.maxBlobSize {
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
fmt.Sprintf("blob too large: max %d bytes", s.maxBlobSize))
return
}
// Calculate SHA256
sha256Hash := CalculateSHA256(body)
sha256Hex := hex.Enc(sha256Hash)
// Optional authorization validation (do this BEFORE ACL check)
if r.Header.Get(AuthorizationHeader) != "" {
authEv, err := ValidateAuthEvent(r, "upload", sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv != nil {
pubkey = authEv.Pubkey
}
}
// Check ACL (do this AFTER getting pubkey from auth)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
// Check if blob already exists
exists, err := s.storage.HasBlob(sha256Hash)
if err != nil {
log.E.F("error checking blob existence: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
if len(pubkey) == 0 {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
// Detect MIME type
mimeType := DetectMimeType(
r.Header.Get("Content-Type"),
GetFileExtensionFromPath(r.URL.Path),
)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(r.URL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Check allowed MIME types
if len(s.allowedMimeTypes) > 0 && !s.allowedMimeTypes[mimeType] {
s.setErrorResponse(w, http.StatusUnsupportedMediaType,
fmt.Sprintf("MIME type %s not allowed", mimeType))
return
}
// Check storage quota if blob doesn't exist (new upload)
if !exists {
blobSizeMB := int64(len(body)) / (1024 * 1024)
if blobSizeMB == 0 && len(body) > 0 {
blobSizeMB = 1 // At least 1 MB for any non-zero blob
}
// Get storage quota from database
quotaMB, err := s.db.GetBlossomStorageQuota(pubkey)
if err != nil {
log.W.F("failed to get storage quota: %v", err)
} else if quotaMB > 0 {
// Get current storage used
usedMB, err := s.storage.GetTotalStorageUsed(pubkey)
if err != nil {
log.W.F("failed to calculate storage used: %v", err)
} else {
// Check if upload would exceed quota
if usedMB+blobSizeMB > quotaMB {
s.setErrorResponse(w, http.StatusPaymentRequired,
fmt.Sprintf("storage quota exceeded: %d/%d MB used, %d MB needed",
usedMB, quotaMB, blobSizeMB))
return
}
}
}
}
// Save blob if it doesn't exist
if !exists {
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType, ext); err != nil {
log.E.F("error saving blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
}
} else {
// Verify ownership
metadata, err := s.storage.GetBlobMetadata(sha256Hash)
if err != nil {
log.E.F("error getting blob metadata: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Allow if same pubkey or if ACL allows
if !utils.FastEqual(metadata.Pubkey, pubkey) && !s.checkACL(pubkey, remoteAddr, "admin") {
s.setErrorResponse(w, http.StatusConflict, "blob already exists")
return
}
}
// Build URL with extension
blobURL := BuildBlobURL(s.getBaseURL(r), sha256Hex, ext)
// Create descriptor
descriptor := NewBlobDescriptor(
blobURL,
sha256Hex,
int64(len(body)),
mimeType,
time.Now().Unix(),
)
// Return descriptor
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err = json.NewEncoder(w).Encode(descriptor); err != nil {
log.E.F("error encoding response: %v", err)
}
}
// handleUploadRequirements handles HEAD /upload requests (BUD-06)
func (s *Server) handleUploadRequirements(w http.ResponseWriter, r *http.Request) {
// Get headers
sha256Hex := r.Header.Get("X-SHA-256")
contentLengthStr := r.Header.Get("X-Content-Length")
contentType := r.Header.Get("X-Content-Type")
// Validate SHA256 header
if sha256Hex == "" {
s.setErrorResponse(w, http.StatusBadRequest, "missing X-SHA-256 header")
return
}
if !ValidateSHA256Hex(sha256Hex) {
s.setErrorResponse(w, http.StatusBadRequest, "invalid X-SHA-256 header format")
return
}
// Validate Content-Length header
if contentLengthStr == "" {
s.setErrorResponse(w, http.StatusLengthRequired, "missing X-Content-Length header")
return
}
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid X-Content-Length header")
return
}
if contentLength > s.maxBlobSize {
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
fmt.Sprintf("file too large: max %d bytes", s.maxBlobSize))
return
}
// Check MIME type if provided
if contentType != "" && len(s.allowedMimeTypes) > 0 {
if !s.allowedMimeTypes[contentType] {
s.setErrorResponse(w, http.StatusUnsupportedMediaType,
fmt.Sprintf("unsupported file type: %s", contentType))
return
}
}
// Check if blob already exists
sha256Hash, err := hex.Dec(sha256Hex)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid SHA256 format")
return
}
exists, err := s.storage.HasBlob(sha256Hash)
if err != nil {
log.E.F("error checking blob existence: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
if exists {
// Return 200 OK - blob already exists, upload can proceed
w.WriteHeader(http.StatusOK)
return
}
// Optional authorization check
if r.Header.Get(AuthorizationHeader) != "" {
authEv, err := ValidateAuthEvent(r, "upload", sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv == nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
// Check ACL
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(authEv.Pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
}
// All checks passed
w.WriteHeader(http.StatusOK)
}
// handleListBlobs handles GET /list/<pubkey> requests (BUD-02)
func (s *Server) handleListBlobs(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract pubkey from path: list/<pubkey>
if !strings.HasPrefix(path, "list/") {
s.setErrorResponse(w, http.StatusBadRequest, "invalid path")
return
}
pubkeyHex := strings.TrimPrefix(path, "list/")
if len(pubkeyHex) != 64 {
s.setErrorResponse(w, http.StatusBadRequest, "invalid pubkey format")
return
}
pubkey, err := hex.Dec(pubkeyHex)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid pubkey format")
return
}
// Parse query parameters
var since, until int64
if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
since, err = strconv.ParseInt(sinceStr, 10, 64)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid since parameter")
return
}
}
if untilStr := r.URL.Query().Get("until"); untilStr != "" {
until, err = strconv.ParseInt(untilStr, 10, 64)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid until parameter")
return
}
}
// Optional authorization check
requestPubkey, _ := GetPubkeyFromRequest(r)
if r.Header.Get(AuthorizationHeader) != "" {
authEv, err := ValidateAuthEvent(r, "list", nil)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv != nil {
requestPubkey = authEv.Pubkey
}
}
// Check if requesting own list or has admin access
if !utils.FastEqual(pubkey, requestPubkey) && !s.checkACL(requestPubkey, s.getRemoteAddr(r), "admin") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
// List blobs
descriptors, err := s.storage.ListBlobs(pubkey, since, until)
if err != nil {
log.E.F("error listing blobs: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Set URLs for descriptors
for _, desc := range descriptors {
desc.URL = BuildBlobURL(s.getBaseURL(r), desc.SHA256, "")
}
// Return JSON array
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err = json.NewEncoder(w).Encode(descriptors); err != nil {
log.E.F("error encoding response: %v", err)
}
}
// handleDeleteBlob handles DELETE /<sha256> requests (BUD-02)
func (s *Server) handleDeleteBlob(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// Extract SHA256
sha256Hex, _, err := ExtractSHA256FromPath(path)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
sha256Hash, err := hex.Dec(sha256Hex)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid SHA256 format")
return
}
// Authorization required for delete
authEv, err := ValidateAuthEvent(r, "delete", sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv == nil {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
// Check ACL
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(authEv.Pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
// Verify ownership
metadata, err := s.storage.GetBlobMetadata(sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusNotFound, "blob not found")
return
}
if !utils.FastEqual(metadata.Pubkey, authEv.Pubkey) && !s.checkACL(authEv.Pubkey, remoteAddr, "admin") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions to delete this blob")
return
}
// Delete blob
if err = s.storage.DeleteBlob(sha256Hash, authEv.Pubkey); err != nil {
log.E.F("error deleting blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error deleting blob")
return
}
w.WriteHeader(http.StatusOK)
}
// handleMirror handles PUT /mirror requests (BUD-04)
func (s *Server) handleMirror(w http.ResponseWriter, r *http.Request) {
// Get initial pubkey from request (may be updated by auth validation)
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
// Read request body (JSON with URL)
var req struct {
URL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid request body")
return
}
if req.URL == "" {
s.setErrorResponse(w, http.StatusBadRequest, "missing url field")
return
}
// Parse URL
mirrorURL, err := url.Parse(req.URL)
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid URL")
return
}
// Download blob from remote URL
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(mirrorURL.String())
if err != nil {
s.setErrorResponse(w, http.StatusBadGateway, "failed to fetch blob from remote URL")
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.setErrorResponse(w, http.StatusBadGateway,
fmt.Sprintf("remote server returned status %d", resp.StatusCode))
return
}
// Read blob data
body, err := io.ReadAll(io.LimitReader(resp.Body, s.maxBlobSize+1))
if err != nil {
s.setErrorResponse(w, http.StatusBadGateway, "error reading remote blob")
return
}
if int64(len(body)) > s.maxBlobSize {
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
fmt.Sprintf("blob too large: max %d bytes", s.maxBlobSize))
return
}
// Calculate SHA256
sha256Hash := CalculateSHA256(body)
sha256Hex := hex.Enc(sha256Hash)
// Optional authorization validation (do this BEFORE ACL check)
if r.Header.Get(AuthorizationHeader) != "" {
authEv, err := ValidateAuthEvent(r, "upload", sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv != nil {
pubkey = authEv.Pubkey
}
}
// Check ACL (do this AFTER getting pubkey from auth)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
if len(pubkey) == 0 {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
// Detect MIME type from remote response
mimeType := DetectMimeType(
resp.Header.Get("Content-Type"),
GetFileExtensionFromPath(mirrorURL.Path),
)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(mirrorURL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Save blob
if err = s.storage.SaveBlob(sha256Hash, body, pubkey, mimeType, ext); err != nil {
log.E.F("error saving mirrored blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
}
// Build URL
blobURL := BuildBlobURL(s.getBaseURL(r), sha256Hex, ext)
// Create descriptor
descriptor := NewBlobDescriptor(
blobURL,
sha256Hex,
int64(len(body)),
mimeType,
time.Now().Unix(),
)
// Return descriptor
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err = json.NewEncoder(w).Encode(descriptor); err != nil {
log.E.F("error encoding response: %v", err)
}
}
// handleMediaUpload handles PUT /media requests (BUD-05)
func (s *Server) handleMediaUpload(w http.ResponseWriter, r *http.Request) {
// Get initial pubkey from request (may be updated by auth validation)
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
// Read request body
body, err := io.ReadAll(io.LimitReader(r.Body, s.maxBlobSize+1))
if err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "error reading request body")
return
}
if int64(len(body)) > s.maxBlobSize {
s.setErrorResponse(w, http.StatusRequestEntityTooLarge,
fmt.Sprintf("blob too large: max %d bytes", s.maxBlobSize))
return
}
// Calculate SHA256 for authorization validation
sha256Hash := CalculateSHA256(body)
// Optional authorization validation (do this BEFORE ACL check)
if r.Header.Get(AuthorizationHeader) != "" {
authEv, err := ValidateAuthEvent(r, "media", sha256Hash)
if err != nil {
s.setErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if authEv != nil {
pubkey = authEv.Pubkey
}
}
// Check ACL (do this AFTER getting pubkey from auth)
if !s.checkACL(pubkey, remoteAddr, "write") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
if len(pubkey) == 0 {
s.setErrorResponse(w, http.StatusUnauthorized, "authorization required")
return
}
// Optimize media (placeholder - actual optimization would be implemented here)
originalMimeType := DetectMimeType(
r.Header.Get("Content-Type"),
GetFileExtensionFromPath(r.URL.Path),
)
optimizedData, mimeType := OptimizeMedia(body, originalMimeType)
// Extract extension from path or infer from MIME type
ext := GetFileExtensionFromPath(r.URL.Path)
if ext == "" {
ext = GetExtensionFromMimeType(mimeType)
}
// Calculate optimized blob SHA256
optimizedHash := CalculateSHA256(optimizedData)
optimizedHex := hex.Enc(optimizedHash)
// Check if optimized blob already exists
exists, err := s.storage.HasBlob(optimizedHash)
if err != nil {
log.E.F("error checking blob existence: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "internal server error")
return
}
// Check storage quota if optimized blob doesn't exist (new upload)
if !exists {
blobSizeMB := int64(len(optimizedData)) / (1024 * 1024)
if blobSizeMB == 0 && len(optimizedData) > 0 {
blobSizeMB = 1 // At least 1 MB for any non-zero blob
}
// Get storage quota from database
quotaMB, err := s.db.GetBlossomStorageQuota(pubkey)
if err != nil {
log.W.F("failed to get storage quota: %v", err)
} else if quotaMB > 0 {
// Get current storage used
usedMB, err := s.storage.GetTotalStorageUsed(pubkey)
if err != nil {
log.W.F("failed to calculate storage used: %v", err)
} else {
// Check if upload would exceed quota
if usedMB+blobSizeMB > quotaMB {
s.setErrorResponse(w, http.StatusPaymentRequired,
fmt.Sprintf("storage quota exceeded: %d/%d MB used, %d MB needed",
usedMB, quotaMB, blobSizeMB))
return
}
}
}
}
// Save optimized blob
if err = s.storage.SaveBlob(optimizedHash, optimizedData, pubkey, mimeType, ext); err != nil {
log.E.F("error saving optimized blob: %v", err)
s.setErrorResponse(w, http.StatusInternalServerError, "error saving blob")
return
}
// Build URL
blobURL := BuildBlobURL(s.baseURL, optimizedHex, ext)
// Create descriptor
descriptor := NewBlobDescriptor(
blobURL,
optimizedHex,
int64(len(optimizedData)),
mimeType,
time.Now().Unix(),
)
// Return descriptor
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err = json.NewEncoder(w).Encode(descriptor); err != nil {
log.E.F("error encoding response: %v", err)
}
}
// handleMediaHead handles HEAD /media requests (BUD-05)
func (s *Server) handleMediaHead(w http.ResponseWriter, r *http.Request) {
// Similar to handleUploadRequirements but for media
// Return 200 OK if media optimization is available
w.WriteHeader(http.StatusOK)
}
// handleReport handles PUT /report requests (BUD-09)
func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) {
// Check ACL
pubkey, _ := GetPubkeyFromRequest(r)
remoteAddr := s.getRemoteAddr(r)
if !s.checkACL(pubkey, remoteAddr, "read") {
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions")
return
}
// Read request body (NIP-56 report event)
var reportEv event.E
if err := json.NewDecoder(r.Body).Decode(&reportEv); err != nil {
s.setErrorResponse(w, http.StatusBadRequest, "invalid request body")
return
}
// Validate report event (kind 1984 per NIP-56)
if reportEv.Kind != 1984 {
s.setErrorResponse(w, http.StatusBadRequest, "invalid event kind, expected 1984")
return
}
// Verify signature
valid, err := reportEv.Verify()
if err != nil || !valid {
s.setErrorResponse(w, http.StatusUnauthorized, "invalid event signature")
return
}
// Extract x tags (blob hashes)
xTags := reportEv.Tags.GetAll([]byte("x"))
if len(xTags) == 0 {
s.setErrorResponse(w, http.StatusBadRequest, "report event missing 'x' tags")
return
}
// Serialize report event
reportData := reportEv.Serialize()
// Save report for each blob hash
for _, xTag := range xTags {
sha256Hex := string(xTag.Value())
if !ValidateSHA256Hex(sha256Hex) {
continue
}
sha256Hash, err := hex.Dec(sha256Hex)
if err != nil {
continue
}
if err = s.storage.SaveReport(sha256Hash, reportData); err != nil {
log.E.F("error saving report: %v", err)
}
}
w.WriteHeader(http.StatusOK)
}