khatru: the accioly blossom redirect patch, reworked.

This commit is contained in:
fiatjaf
2025-06-10 16:43:05 -03:00
parent 50a753504d
commit 027d016d97
4 changed files with 84 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ import (
"mime"
"net/http"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -199,8 +200,21 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
ext = "." + spl[1]
}
if nil != bs.LoadBlob {
reader, _ := bs.LoadBlob(r.Context(), hhash)
if bs.LoadBlob != nil {
reader, redirectURL, err := bs.LoadBlob(r.Context(), hhash)
if err == nil && redirectURL != nil {
// check that the redirectURL contains the hash of the file
if ok, _ := regexp.MatchString(`\b`+hhash+`\b`, redirectURL.String()); !ok {
blossomError(w, "redirect url doesn't contain the file hash", 500)
return
}
w.Header().Set("ETag", hhash)
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
http.Redirect(w, r, redirectURL.String(), http.StatusTemporaryRedirect)
return
}
if reader != nil {
// use unix epoch as the time if we can't find the descriptor
// as described in the http.ServeContent documentation
@@ -211,7 +225,11 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("ETag", hhash)
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
http.ServeContent(w, r, hhash+ext, t, reader)
name := hhash
if ext != "" {
name += ext
}
http.ServeContent(w, r, name, t, reader)
return
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"io"
"net/http"
"net/url"
"strings"
"fiatjaf.com/nostr"
@@ -15,7 +16,7 @@ type BlossomServer struct {
Store BlobIndex
StoreBlob func(ctx context.Context, sha256 string, body []byte) error
LoadBlob func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
LoadBlob func(ctx context.Context, sha256 string) (io.ReadSeeker, *url.URL, error)
DeleteBlob func(ctx context.Context, sha256 string) error
ReceiveReport func(ctx context.Context, reportEvt nostr.Event) error

View File

@@ -26,9 +26,9 @@ func main() {
// store the blob data somewhere
return nil
}
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
// load and return the blob data
return nil, nil
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, *url.URL, error) {
// load and return the blob data, or a redirect URL
return nil, nil, nil
}
bl.DeleteBlob = func(ctx context.Context, sha256 string) error {
// delete the blob data
@@ -43,10 +43,34 @@ func main() {
You can integrate any storage backend by implementing the three core functions:
- `StoreBlob`: Save the blob data
- `LoadBlob`: Retrieve the blob data
- `StoreBlob`: Persist the blob data
- `LoadBlob`: Retrieve the blob data -- or a redirect URL
- `DeleteBlob`: Remove the blob data
## URL Redirection
Blossom supports redirection to external storage locations when retrieving blobs. This is useful when you want to serve files from a CDN or cloud storage service while keeping Blossom compatibility.
To implement this, your `LoadBlob` function should return a `*url.URL` as its second argument. If this URL is not `nil`, `khatru` will redirect the client to it.
Here's an example that redirects to a templated URL:
```go
import (
"net/url"
"net/http"
)
// ...
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, *url.URL, error) {
// generate a custom redirect URL
redirectURL, _ := url.Parse(fmt.Sprintf("https://my-cdn.com/%s", sha256))
return nil, redirectURL, nil
}
```
This URL must include the sha256 hash somewhere.
## Upload Restrictions
You can implement upload restrictions using the `RejectUpload` hook. Here's an example that limits file size and restricts uploads to whitelisted users:
@@ -91,3 +115,32 @@ bl.Store = blossom.EventStoreBlobIndexWrapper{
```
This will store blob metadata as special `kind:24242` events, but you shouldn't have to worry about it as the wrapper handles all the complexity of tracking ownership and managing blob lifecycle. Jut avoid reusing the same datastore that is used for the actual relay events unless you know what you're doing.
## Upload Restrictions
You can implement upload restrictions using the `RejectUpload` hook. Here's an example that limits file size and restricts uploads to whitelisted users:
```go
const maxFileSize = 10 * 1024 * 1024 // 10MB
var allowedUsers = map[string]bool{
"pubkey1": true,
"pubkey2": true,
}
bl.RejectUpload = func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) {
// check file size
if size > maxFileSize {
return true, "file too large", 413
}
// check if user is allowed
if auth == nil || !allowedUsers[auth.PubKey] {
return true, "unauthorized", 403
}
return false, "", 0
}
```
There are other `Reject*` hooks you can also implement, but this is the most important one.

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"fiatjaf.com/nostr/eventstore/badger"
@@ -32,10 +33,10 @@ func main() {
fmt.Println("storing", sha256, len(body))
return nil
}
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
bl.LoadBlob = func(ctx context.Context, sha256 string) (io.ReadSeeker, *url.URL, error) {
fmt.Println("loading", sha256)
blob := strings.NewReader("aaaaa")
return blob, nil
return blob, nil, nil
}
fmt.Println("running on :3334")