453 lines
11 KiB
Go
453 lines
11 KiB
Go
package blossom
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"fiatjaf.com/nostr"
|
|
"github.com/liamg/magic"
|
|
)
|
|
|
|
type mirrorRequest struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (bs BlossomServer) handleUploadCheck(w http.ResponseWriter, r *http.Request) {
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, err.Error(), 400)
|
|
return
|
|
}
|
|
if auth == nil {
|
|
blossomError(w, "missing \"Authorization\" header", 401)
|
|
return
|
|
}
|
|
if auth.Tags.FindWithValue("t", "upload") == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
|
|
mimetype := r.Header.Get("X-Content-Type")
|
|
exts, _ := mime.ExtensionsByType(mimetype)
|
|
var ext string
|
|
if len(exts) > 0 {
|
|
ext = exts[0]
|
|
}
|
|
|
|
// get the file size from the incoming header
|
|
size, _ := strconv.Atoi(r.Header.Get("X-Content-Length"))
|
|
|
|
if bs.RejectUpload != nil {
|
|
reject, reason, code := bs.RejectUpload(r.Context(), auth, size, ext)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, "invalid \"Authorization\": "+err.Error(), 404)
|
|
return
|
|
}
|
|
if auth == nil {
|
|
blossomError(w, "missing \"Authorization\" header", 401)
|
|
return
|
|
}
|
|
if auth.Tags.FindWithValue("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, min(50, size), size)
|
|
if n, err := r.Body.Read(b); err != nil && n != size {
|
|
blossomError(w, "failed to read initial bytes of upload body: "+err.Error(), 400)
|
|
return
|
|
}
|
|
var ext string
|
|
if ft, _ := magic.Lookup(b); ft != nil {
|
|
ext = "." + ft.Extension
|
|
} else {
|
|
// if we can't find, use the filetype given by the upload header
|
|
mimetype := r.Header.Get("Content-Type")
|
|
ext = getExtension(mimetype)
|
|
}
|
|
|
|
// run the reject hooks
|
|
if nil != bs.RejectUpload {
|
|
reject, reason, code := bs.RejectUpload(r.Context(), auth, size, ext)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
// if it passes then we have to read the entire thing into memory so we can compute the sha256
|
|
for {
|
|
var n int
|
|
n, err = r.Body.Read(b[len(b):cap(b)])
|
|
b = b[:len(b)+n]
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
break
|
|
}
|
|
if len(b) == cap(b) {
|
|
// add more capacity (let append pick how much)
|
|
// if Content-Length was correct we shouldn't reach this
|
|
b = append(b, 0)[:len(b)]
|
|
}
|
|
}
|
|
if err != nil {
|
|
blossomError(w, "failed to read upload body: "+err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
hash := sha256.Sum256(b)
|
|
hhash := hex.EncodeToString(hash[:])
|
|
|
|
// keep track of the blob descriptor
|
|
bd := BlobDescriptor{
|
|
URL: bs.ServiceURL + "/" + hhash + ext,
|
|
SHA256: hhash,
|
|
Size: len(b),
|
|
Type: mime.TypeByExtension(ext),
|
|
Uploaded: nostr.Now(),
|
|
}
|
|
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
|
blossomError(w, "failed to save event: "+err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
// save actual blob
|
|
if nil != bs.StoreBlob {
|
|
if err := bs.StoreBlob(r.Context(), hhash, b); err != nil {
|
|
blossomError(w, "failed to save: "+err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
// return response
|
|
json.NewEncoder(w).Encode(bd)
|
|
}
|
|
|
|
func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
|
spl := strings.SplitN(r.URL.Path, ".", 2)
|
|
hhash := spl[0]
|
|
if len(hhash) != 65 {
|
|
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
|
return
|
|
}
|
|
hhash = hhash[1:]
|
|
|
|
// check for an authorization tag, if any
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
// if there is one, we check if it has the extra requirements
|
|
if auth != nil {
|
|
if auth.Tags.FindWithValue("t", "get") == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
|
|
if auth.Tags.FindWithValue("x", hhash) == nil &&
|
|
auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
if nil != bs.RejectGet {
|
|
reject, reason, code := bs.RejectGet(r.Context(), auth, hhash)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
var ext string
|
|
if len(spl) == 2 {
|
|
ext = "." + spl[1]
|
|
}
|
|
|
|
if nil != bs.LoadBlob {
|
|
reader, _ := bs.LoadBlob(r.Context(), hhash)
|
|
if reader != nil {
|
|
// use unix epoch as the time if we can't find the descriptor
|
|
// as described in the http.ServeContent documentation
|
|
t := time.Unix(0, 0)
|
|
descriptor, err := bs.Store.Get(r.Context(), hhash)
|
|
if err == nil && descriptor != nil {
|
|
t = descriptor.Uploaded.Time()
|
|
}
|
|
w.Header().Set("ETag", hhash)
|
|
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
|
http.ServeContent(w, r, hhash+ext, t, reader)
|
|
return
|
|
}
|
|
}
|
|
|
|
blossomError(w, "file not found", 404)
|
|
}
|
|
|
|
func (bs BlossomServer) handleHasBlob(w http.ResponseWriter, r *http.Request) {
|
|
spl := strings.SplitN(r.URL.Path, ".", 2)
|
|
hhash := spl[0]
|
|
if len(hhash) != 65 {
|
|
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
|
return
|
|
}
|
|
hhash = hhash[1:]
|
|
|
|
bd, err := bs.Store.Get(r.Context(), hhash)
|
|
if err != nil {
|
|
blossomError(w, "failed to query: "+err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
if bd == nil {
|
|
blossomError(w, "file not found", 404)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
|
|
// check for an authorization tag, if any
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
// if there is one, we check if it has the extra requirements
|
|
if auth != nil {
|
|
if auth.Tags.FindWithValue("t", "list") == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
pubkey, err := nostr.PubKeyFromHex(r.URL.Path[6:])
|
|
|
|
if nil != bs.RejectList {
|
|
reject, reason, code := bs.RejectList(r.Context(), auth, pubkey)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Write([]byte{'['})
|
|
enc := json.NewEncoder(w)
|
|
first := true
|
|
for bd := range bs.Store.List(r.Context(), pubkey) {
|
|
if !first {
|
|
w.Write([]byte{','})
|
|
} else {
|
|
first = false
|
|
}
|
|
enc.Encode(bd)
|
|
}
|
|
w.Write([]byte{']'})
|
|
}
|
|
|
|
func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
if auth != nil {
|
|
if auth.Tags.FindWithValue("t", "delete") == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
spl := strings.SplitN(r.URL.Path, ".", 2)
|
|
hhash := spl[0]
|
|
if len(hhash) != 65 {
|
|
blossomError(w, "invalid /<sha256>[.ext] path", 400)
|
|
return
|
|
}
|
|
hhash = hhash[1:]
|
|
if auth.Tags.FindWithValue("x", hhash) == nil &&
|
|
auth.Tags.FindWithValue("server", bs.ServiceURL) == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
|
return
|
|
}
|
|
|
|
// should we accept this delete?
|
|
if nil != bs.RejectDelete {
|
|
reject, reason, code := bs.RejectDelete(r.Context(), auth, hhash)
|
|
if reject {
|
|
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 {
|
|
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 {
|
|
if nil != bs.DeleteBlob {
|
|
if err := bs.DeleteBlob(r.Context(), hhash); err != nil {
|
|
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
|
var body []byte
|
|
_, err := r.Body.Read(body)
|
|
if err != nil {
|
|
blossomError(w, "can't read request body", 400)
|
|
return
|
|
}
|
|
|
|
var evt nostr.Event
|
|
if err := json.Unmarshal(body, &evt); err != nil {
|
|
blossomError(w, "can't parse event", 400)
|
|
return
|
|
}
|
|
|
|
if !evt.VerifySignature() {
|
|
blossomError(w, "invalid report event is provided", 400)
|
|
return
|
|
}
|
|
|
|
if evt.Kind != nostr.KindReporting {
|
|
blossomError(w, "invalid report event is provided", 400)
|
|
return
|
|
}
|
|
|
|
if bs.ReceiveReport != nil {
|
|
if err := bs.ReceiveReport(r.Context(), evt); err != nil {
|
|
blossomError(w, "failed to receive report: "+err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) {
|
|
auth, err := readAuthorization(r)
|
|
if err != nil {
|
|
blossomError(w, "invalid \"Authorization\": "+err.Error(), 400)
|
|
return
|
|
}
|
|
if auth == nil {
|
|
blossomError(w, "missing \"Authorization\" header", 401)
|
|
return
|
|
}
|
|
if auth.Tags.FindWithValue("t", "upload") == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
|
|
var req mirrorRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
blossomError(w, "invalid request body: "+err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
// download the blob
|
|
resp, err := http.Get(req.URL)
|
|
if err != nil {
|
|
blossomError(w, "failed to download from url: "+err.Error(), 503)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
blossomError(w, "failed to read response body: "+err.Error(), 503)
|
|
return
|
|
}
|
|
|
|
// calculate sha256
|
|
hash := sha256.Sum256(body)
|
|
hhash := hex.EncodeToString(hash[:])
|
|
|
|
// verify hash against x tag
|
|
if auth.Tags.FindWithValue("x", hhash) == nil {
|
|
blossomError(w, "blob hash does not match any \"x\" tag in authorization event", 403)
|
|
return
|
|
}
|
|
|
|
// determine content type and extension
|
|
var ext string
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType != "" {
|
|
ext = getExtension(contentType)
|
|
} else {
|
|
// try to detect from url
|
|
ext = filepath.Ext(req.URL)
|
|
}
|
|
|
|
// run reject hook if defined
|
|
if bs.RejectUpload != nil {
|
|
reject, reason, code := bs.RejectUpload(r.Context(), auth, len(body), ext)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
// keep track of the blob descriptor
|
|
bd := BlobDescriptor{
|
|
URL: bs.ServiceURL + "/" + hhash + ext,
|
|
SHA256: hhash,
|
|
Size: len(body),
|
|
Type: contentType,
|
|
Uploaded: nostr.Now(),
|
|
}
|
|
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
|
blossomError(w, "failed to save event: "+err.Error(), 400)
|
|
return
|
|
}
|
|
|
|
// save actual blob
|
|
if bs.StoreBlob != nil {
|
|
if err := bs.StoreBlob(r.Context(), hhash, body); err != nil {
|
|
blossomError(w, "failed to save: "+err.Error(), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
// return response
|
|
json.NewEncoder(w).Encode(bd)
|
|
}
|
|
|
|
func (bs BlossomServer) handleNegentropy(w http.ResponseWriter, r *http.Request) {
|
|
}
|