diff --git a/blossom/blob.go b/blossom/blob.go index 8da2143..d17dd7b 100644 --- a/blossom/blob.go +++ b/blossom/blob.go @@ -13,7 +13,7 @@ type BlobDescriptor struct { Type string `json:"type"` Uploaded nostr.Timestamp `json:"uploaded"` - Owner string + Owner string `json:"-"` } type BlobIndex interface { diff --git a/blossom/eventstorewrapper.go b/blossom/eventstorewrapper.go index 16df375..d00d761 100644 --- a/blossom/eventstorewrapper.go +++ b/blossom/eventstorewrapper.go @@ -2,7 +2,6 @@ package blossom import ( "context" - "mime" "strconv" "github.com/fiatjaf/eventstore" @@ -53,6 +52,7 @@ func (es EventStoreBlobIndexWrapper) List(ctx context.Context, pubkey string) (c for evt := range ech { ch <- es.parseEvent(evt) } + close(ch) }() return ch, nil @@ -90,11 +90,7 @@ func (es EventStoreBlobIndexWrapper) Delete(ctx context.Context, sha256 string, func (es EventStoreBlobIndexWrapper) parseEvent(evt *nostr.Event) BlobDescriptor { hhash := evt.Tags[0][1] mimetype := evt.Tags[1][1] - exts, _ := mime.ExtensionsByType(mimetype) - var ext string - if exts != nil { - ext = exts[0] - } + ext := getExtension(mimetype) size, _ := strconv.Atoi(evt.Tags[2][1]) return BlobDescriptor{ diff --git a/blossom/handlers.go b/blossom/handlers.go index 05bbc1c..4ae34d4 100644 --- a/blossom/handlers.go +++ b/blossom/handlers.go @@ -17,15 +17,16 @@ import ( func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) { auth, err := readAuthorization(r) if err != nil { - http.Error(w, err.Error(), 400) + blossomError(w, err.Error(), 400) return } - - if auth != nil { - if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { - http.Error(w, "invalid Authorization event \"t\" tag", 403) - return - } + if auth == nil { + blossomError(w, "missing \"Authorization\" header", 400) + return + } + if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { + blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) + return } mimetype := r.Header.Get("X-Content-Type") @@ -41,7 +42,7 @@ func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request for _, rb := range bs.RejectUpload { reject, reason, code := rb(r.Context(), auth, size, ext) if reject { - http.Error(w, reason, code) + blossomError(w, reason, code) return } } @@ -50,24 +51,29 @@ func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { auth, err := readAuthorization(r) if err != nil { - http.Error(w, "invalid Authorization: "+err.Error(), 400) + blossomError(w, "invalid \"Authorization\": "+err.Error(), 400) return } - - if auth != nil { - if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { - http.Error(w, "invalid Authorization event \"t\" tag", 403) - return - } + if auth == nil { + blossomError(w, "missing \"Authorization\" header", 400) + return + } + if auth.Tags.GetFirst([]string{"t", "upload"}) == nil { + blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) + return } // get the file size from the incoming header size, _ := strconv.Atoi(r.Header.Get("Content-Length")) + if size == 0 { + blossomError(w, "missing \"Content-Length\" header", 400) + return + } // read first bytes of upload so we can find out the filetype - b := make([]byte, 50, size) + b := make([]byte, min(50, size), size) if _, err = r.Body.Read(b); err != nil { - http.Error(w, "failed to read initial bytes of upload body: "+err.Error(), 400) + blossomError(w, "failed to read initial bytes of upload body: "+err.Error(), 400) return } var ext string @@ -76,16 +82,14 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { } else { // if we can't find, use the filetype given by the upload header mimetype := r.Header.Get("Content-Type") - if exts, _ := mime.ExtensionsByType(mimetype); len(exts) > 0 { - ext = exts[0] - } + ext = getExtension(mimetype) } // run the reject hooks for _, ru := range bs.RejectUpload { reject, reason, code := ru(r.Context(), auth, size, ext) if reject { - http.Error(w, reason, code) + blossomError(w, reason, code) return } } @@ -108,7 +112,7 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { } } if err != nil { - http.Error(w, "failed to read upload body: "+err.Error(), 400) + blossomError(w, "failed to read upload body: "+err.Error(), 400) return } @@ -124,14 +128,14 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { Uploaded: nostr.Now(), } if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil { - http.Error(w, "failed to save event: "+err.Error(), 400) + blossomError(w, "failed to save event: "+err.Error(), 400) return } // save actual blob for _, sb := range bs.StoreBlob { if err := sb(r.Context(), hhash, b); err != nil { - http.Error(w, "failed to save: "+err.Error(), 500) + blossomError(w, "failed to save: "+err.Error(), 500) return } } @@ -144,7 +148,7 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { spl := strings.SplitN(r.URL.Path, ".", 2) hhash := spl[0] if len(hhash) != 65 { - http.Error(w, "invalid /[.ext] path", 400) + blossomError(w, "invalid /[.ext] path", 400) return } hhash = hhash[1:] @@ -152,20 +156,20 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { // check for an authorization tag, if any auth, err := readAuthorization(r) if err != nil { - http.Error(w, err.Error(), 400) + blossomError(w, err.Error(), 400) return } // if there is one, we check if it has the extra requirements if auth != nil { if auth.Tags.GetFirst([]string{"t", "get"}) == nil { - http.Error(w, "invalid Authorization event \"t\" tag", 403) + blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) return } if auth.Tags.GetFirst([]string{"x", hhash}) == nil && auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil { - http.Error(w, "invalid Authorization event \"x\" or \"server\" tag", 403) + blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403) return } } @@ -173,7 +177,7 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { for _, rg := range bs.RejectGet { reject, reason, code := rg(r.Context(), auth, hhash) if reject { - http.Error(w, reason, code) + blossomError(w, reason, code) return } } @@ -192,7 +196,7 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { } } - http.Error(w, "file not found", 404) + blossomError(w, "file not found", 404) return } @@ -200,19 +204,19 @@ func (bs BlossomServer) handleHasBlob(w http.ResponseWriter, r *http.Request) { spl := strings.SplitN(r.URL.Path, ".", 2) hhash := spl[0] if len(hhash) != 65 { - http.Error(w, "invalid /[.ext] path", 400) + blossomError(w, "invalid /[.ext] path", 400) return } hhash = hhash[1:] bd, err := bs.Store.Get(r.Context(), hhash) if err != nil { - http.Error(w, "failed to query: "+err.Error(), 500) + blossomError(w, "failed to query: "+err.Error(), 500) return } if bd == nil { - http.Error(w, "file not found", 404) + blossomError(w, "file not found", 404) return } @@ -223,14 +227,14 @@ func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) { // check for an authorization tag, if any auth, err := readAuthorization(r) if err != nil { - http.Error(w, err.Error(), 400) + blossomError(w, err.Error(), 400) return } // if there is one, we check if it has the extra requirements if auth != nil { if auth.Tags.GetFirst([]string{"t", "list"}) == nil { - http.Error(w, "invalid Authorization event \"t\" tag", 403) + blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) return } } @@ -240,14 +244,14 @@ func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) { for _, rl := range bs.RejectList { reject, reason, code := rl(r.Context(), auth, pubkey) if reject { - http.Error(w, reason, code) + blossomError(w, reason, code) return } } ch, err := bs.Store.List(r.Context(), pubkey) if err != nil { - http.Error(w, "failed to query: "+err.Error(), 500) + blossomError(w, "failed to query: "+err.Error(), 500) return } @@ -262,13 +266,13 @@ func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) { func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) { auth, err := readAuthorization(r) if err != nil { - http.Error(w, err.Error(), 400) + blossomError(w, err.Error(), 400) return } if auth != nil { if auth.Tags.GetFirst([]string{"t", "delete"}) == nil { - http.Error(w, "invalid Authorization event \"t\" tag", 403) + blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403) return } } @@ -276,35 +280,40 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) { spl := strings.SplitN(r.URL.Path, ".", 2) hhash := spl[0] if len(hhash) != 65 { - http.Error(w, "invalid /[.ext] path", 400) + blossomError(w, "invalid /[.ext] path", 400) return } hhash = hhash[1:] if auth.Tags.GetFirst([]string{"x", hhash}) == nil && auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil { - http.Error(w, "invalid Authorization event \"x\" or \"server\" tag", 403) + blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403) return } + // should we accept this delete? for _, rd := range bs.RejectDelete { reject, reason, code := rd(r.Context(), auth, hhash) if reject { - http.Error(w, reason, code) - return - } - } - - for _, del := range bs.DeleteBlob { - if err := del(r.Context(), hhash); err != nil { - http.Error(w, "failed to delete blob: "+err.Error(), 500) + blossomError(w, reason, code) return } } + // delete the entry that links this blob to this author if err := bs.Store.Delete(r.Context(), hhash, auth.PubKey); err != nil { - http.Error(w, "delete of blob entry failed: "+err.Error(), 500) + blossomError(w, "delete of blob entry failed: "+err.Error(), 500) return } + + // we will actually only delete the file if no one else owns it + if bd, err := bs.Store.Get(r.Context(), hhash); err == nil && bd == nil { + for _, del := range bs.DeleteBlob { + if err := del(r.Context(), hhash); err != nil { + blossomError(w, "failed to delete blob: "+err.Error(), 500) + return + } + } + } } func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) { diff --git a/blossom/utils.go b/blossom/utils.go index bb1adfd..a9ec9cc 100644 --- a/blossom/utils.go +++ b/blossom/utils.go @@ -1,9 +1,43 @@ package blossom -import "net/http" +import ( + "mime" + "net/http" +) func setCors(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Authorization") w.Header().Set("Access-Control-Allow-Methods", "GET PUT DELETE") } + +func blossomError(w http.ResponseWriter, msg string, code int) { + w.Header().Add("X-Reason", msg) + w.WriteHeader(code) +} + +func getExtension(mimetype string) string { + if mimetype == "" { + return "" + } + + switch mimetype { + case "image/jpeg": + return ".jpg" + case "image/gif": + return ".gif" + case "image/png": + return ".png" + case "image/webp": + return ".webp" + case "video/mp4": + return ".mp4" + } + + exts, _ := mime.ExtensionsByType(mimetype) + if len(exts) > 0 { + return exts[0] + } + + return "" +}