You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
781 lines
21 KiB
781 lines
21 KiB
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.baseURL, 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.baseURL, 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) { |
|
// Check ACL |
|
pubkey, _ := GetPubkeyFromRequest(r) |
|
remoteAddr := s.getRemoteAddr(r) |
|
|
|
if !s.checkACL(pubkey, remoteAddr, "write") { |
|
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions") |
|
return |
|
} |
|
|
|
// 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) |
|
|
|
// 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 |
|
} |
|
|
|
// Optional authorization validation |
|
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 |
|
} |
|
} |
|
|
|
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 |
|
} |
|
|
|
// 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.baseURL, 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.baseURL, 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) { |
|
// Check ACL |
|
pubkey, _ := GetPubkeyFromRequest(r) |
|
remoteAddr := s.getRemoteAddr(r) |
|
|
|
if !s.checkACL(pubkey, remoteAddr, "write") { |
|
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions") |
|
return |
|
} |
|
|
|
// 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 |
|
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 |
|
} |
|
} |
|
|
|
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.baseURL, 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) { |
|
// Check ACL |
|
pubkey, _ := GetPubkeyFromRequest(r) |
|
remoteAddr := s.getRemoteAddr(r) |
|
|
|
if !s.checkACL(pubkey, remoteAddr, "write") { |
|
s.setErrorResponse(w, http.StatusForbidden, "insufficient permissions") |
|
return |
|
} |
|
|
|
// 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 |
|
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 |
|
} |
|
} |
|
|
|
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) |
|
|
|
// 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) |
|
}
|
|
|