mirror of
https://github.com/fiatjaf/khatru.git
synced 2025-03-17 13:22:56 +01:00
368 lines
8.7 KiB
Go
368 lines
8.7 KiB
Go
package blossom
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/liamg/magic"
|
|
"github.com/nbd-wtf/go-nostr"
|
|
)
|
|
|
|
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.GetFirst([]string{"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"))
|
|
|
|
for _, rb := range bs.RejectUpload {
|
|
reject, reason, code := rb(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.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, 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
|
|
for _, ru := range bs.RejectUpload {
|
|
reject, reason, code := ru(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
|
|
for _, sb := range bs.StoreBlob {
|
|
if err := sb(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.GetFirst([]string{"t", "get"}) == nil {
|
|
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 {
|
|
blossomError(w, "invalid \"Authorization\" event \"x\" or \"server\" tag", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, rg := range bs.RejectGet {
|
|
reject, reason, code := rg(r.Context(), auth, hhash)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
var ext string
|
|
if len(spl) == 2 {
|
|
ext = "." + spl[1]
|
|
}
|
|
|
|
for _, lb := range bs.LoadBlob {
|
|
reader, _ := lb(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.GetFirst([]string{"t", "list"}) == nil {
|
|
blossomError(w, "invalid \"Authorization\" event \"t\" tag", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
pubkey := r.URL.Path[6:]
|
|
|
|
for _, rl := range bs.RejectList {
|
|
reject, reason, code := rl(r.Context(), auth, pubkey)
|
|
if reject {
|
|
blossomError(w, reason, code)
|
|
return
|
|
}
|
|
}
|
|
|
|
ch, err := bs.Store.List(r.Context(), pubkey)
|
|
if err != nil {
|
|
blossomError(w, "failed to query: "+err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
w.Write([]byte{'['})
|
|
enc := json.NewEncoder(w)
|
|
first := true
|
|
for bd := range ch {
|
|
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.GetFirst([]string{"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.GetFirst([]string{"x", hhash}) == nil &&
|
|
auth.Tags.GetFirst([]string{"server", bs.ServiceURL}) == nil {
|
|
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 {
|
|
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 {
|
|
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) 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 isValid, _ := evt.CheckSignature(); !isValid {
|
|
blossomError(w, "invalid report event is provided", 400)
|
|
return
|
|
}
|
|
|
|
if evt.Kind != nostr.KindReporting {
|
|
blossomError(w, "invalid report event is provided", 400)
|
|
return
|
|
}
|
|
|
|
for _, rr := range bs.ReceiveReport {
|
|
if err := rr(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) {
|
|
}
|
|
|
|
func (bs BlossomServer) handleNegentropy(w http.ResponseWriter, r *http.Request) {
|
|
}
|