package blossom import ( "net/http" "strings" "next.orly.dev/pkg/acl" "next.orly.dev/pkg/database" ) // Server provides a Blossom server implementation type Server struct { db *database.D storage *Storage acl *acl.S baseURL string // Configuration maxBlobSize int64 allowedMimeTypes map[string]bool requireAuth bool } // Config holds configuration for the Blossom server type Config struct { BaseURL string MaxBlobSize int64 AllowedMimeTypes []string RequireAuth bool BlobDir string // Directory for storing blob files } // NewServer creates a new Blossom server instance func NewServer(db *database.D, aclRegistry *acl.S, cfg *Config) *Server { if cfg == nil { cfg = &Config{ MaxBlobSize: 100 * 1024 * 1024, // 100MB default RequireAuth: false, } } storage := NewStorage(db, cfg.BlobDir) // Build allowed MIME types map allowedMap := make(map[string]bool) if len(cfg.AllowedMimeTypes) > 0 { for _, mime := range cfg.AllowedMimeTypes { allowedMap[mime] = true } } return &Server{ db: db, storage: storage, acl: aclRegistry, baseURL: cfg.BaseURL, maxBlobSize: cfg.MaxBlobSize, allowedMimeTypes: allowedMap, requireAuth: cfg.RequireAuth, } } // Handler returns an http.Handler that can be attached to a router func (s *Server) Handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers (BUD-01 requirement) s.setCORSHeaders(w, r) // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } // Route based on path and method path := r.URL.Path // Remove leading slash path = strings.TrimPrefix(path, "/") // Handle specific endpoints switch { case r.Method == http.MethodGet && path == "upload": // This shouldn't happen, but handle gracefully http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return case r.Method == http.MethodHead && path == "upload": s.handleUploadRequirements(w, r) return case r.Method == http.MethodPut && path == "upload": s.handleUpload(w, r) return case r.Method == http.MethodHead && path == "media": s.handleMediaHead(w, r) return case r.Method == http.MethodPut && path == "media": s.handleMediaUpload(w, r) return case r.Method == http.MethodPut && path == "mirror": s.handleMirror(w, r) return case r.Method == http.MethodPut && path == "report": s.handleReport(w, r) return case strings.HasPrefix(path, "list/"): if r.Method == http.MethodGet { s.handleListBlobs(w, r) return } http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return case r.Method == http.MethodGet: // Handle GET / s.handleGetBlob(w, r) return case r.Method == http.MethodHead: // Handle HEAD / s.handleHeadBlob(w, r) return case r.Method == http.MethodDelete: // Handle DELETE / s.handleDeleteBlob(w, r) return default: http.Error(w, "Not found", http.StatusNotFound) return } }) } // setCORSHeaders sets CORS headers as required by BUD-01 func (s *Server) setCORSHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Authorization, *") w.Header().Set("Access-Control-Max-Age", "86400") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers") } // setErrorResponse sets an error response with X-Reason header (BUD-01) func (s *Server) setErrorResponse(w http.ResponseWriter, status int, reason string) { w.Header().Set("X-Reason", reason) http.Error(w, reason, status) } // getRemoteAddr extracts the remote address from the request func (s *Server) getRemoteAddr(r *http.Request) string { // Check X-Forwarded-For header if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { parts := strings.Split(forwarded, ",") if len(parts) > 0 { return strings.TrimSpace(parts[0]) } } // Check X-Real-IP header if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP } // Fall back to RemoteAddr return r.RemoteAddr } // checkACL checks if the user has the required access level func (s *Server) checkACL( pubkey []byte, remoteAddr string, requiredLevel string, ) bool { if s.acl == nil { return true // No ACL configured, allow all } level := s.acl.GetAccessLevel(pubkey, remoteAddr) // Map ACL levels to permissions levelMap := map[string]int{ "none": 0, "read": 1, "write": 2, "admin": 3, "owner": 4, } required := levelMap[requiredLevel] actual := levelMap[level] return actual >= required }