mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-07 14:06:51 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5fc324f7 | ||
|
|
668c41b988 | ||
|
|
3c802caff5 | ||
|
|
e81416f41e | ||
|
|
e12d30f247 | ||
|
|
97bc365996 | ||
|
|
44e4b86955 | ||
|
|
932825080e | ||
|
|
aa81e17e22 | ||
|
|
034902c3e3 | ||
|
|
5705647c6b | ||
|
|
b2607e787f | ||
|
|
011efe0dd2 | ||
|
|
16eee5cffb | ||
|
|
c5076b297d | ||
|
|
1a5b498fb8 | ||
|
|
b6da555807 | ||
|
|
1ab44ab897 | ||
|
|
3da898cec7 | ||
|
|
cfbe484784 |
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -86,6 +87,11 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
ext = getExtension(mimetype)
|
||||
}
|
||||
|
||||
// special case of android apk -- if we see a .zip but they say it's .apk we trust them
|
||||
if ext == ".zip" && getExtension(r.Header.Get("Content-Type")) == ".apk" {
|
||||
ext = ".apk"
|
||||
}
|
||||
|
||||
// run the reject hooks
|
||||
for _, ru := range bs.RejectUpload {
|
||||
reject, reason, code := ru(r.Context(), auth, size, ext)
|
||||
@@ -119,13 +125,17 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
hash := sha256.Sum256(b)
|
||||
hhash := hex.EncodeToString(hash[:])
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// keep track of the blob descriptor
|
||||
bd := BlobDescriptor{
|
||||
URL: bs.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Size: len(b),
|
||||
Type: mime.TypeByExtension(ext),
|
||||
Type: mimeType,
|
||||
Uploaded: nostr.Now(),
|
||||
}
|
||||
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
||||
@@ -135,13 +145,14 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// save actual blob
|
||||
for _, sb := range bs.StoreBlob {
|
||||
if err := sb(r.Context(), hhash, b); err != nil {
|
||||
if err := sb(r.Context(), hhash, ext, b); err != nil {
|
||||
blossomError(w, "failed to save: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(bd)
|
||||
}
|
||||
|
||||
@@ -175,21 +186,46 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var ext string
|
||||
bd, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err != nil {
|
||||
// can't find the BlobDescriptor, try to get the extension from the URL
|
||||
if len(spl) == 2 {
|
||||
ext = spl[1]
|
||||
}
|
||||
} else if bd != nil {
|
||||
ext = getExtension(bd.Type)
|
||||
}
|
||||
|
||||
for _, rg := range bs.RejectGet {
|
||||
reject, reason, code := rg(r.Context(), auth, hhash)
|
||||
reject, reason, code := rg(r.Context(), auth, hhash, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ext string
|
||||
if len(spl) == 2 {
|
||||
ext = "." + spl[1]
|
||||
if len(bs.RedirectGet) > 0 {
|
||||
for _, redirect := range bs.RedirectGet {
|
||||
redirectURL, code, err := redirect(r.Context(), hhash, ext)
|
||||
if err == nil && redirectURL != "" {
|
||||
// check that the redirectURL contains the hash of the file
|
||||
if ok, _ := regexp.MatchString(`\b`+hhash+`\b`, redirectURL); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// not sure if browsers will cache redirects
|
||||
// but it doesn't hurt anyway
|
||||
w.Header().Set("ETag", hhash)
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
http.Redirect(w, r, redirectURL, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, lb := range bs.LoadBlob {
|
||||
reader, _ := lb(r.Context(), hhash)
|
||||
reader, _ := lb(r.Context(), hhash, ext)
|
||||
if reader != nil {
|
||||
// use unix epoch as the time if we can't find the descriptor
|
||||
// as described in the http.ServeContent documentation
|
||||
@@ -200,7 +236,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
|
||||
}
|
||||
}
|
||||
@@ -227,6 +267,9 @@ func (bs BlossomServer) handleHasBlob(w http.ResponseWriter, r *http.Request) {
|
||||
blossomError(w, "file not found", 404)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(bd.Size))
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Type", bd.Type)
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -302,9 +345,20 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var ext string
|
||||
bd, err := bs.Store.Get(r.Context(), hhash)
|
||||
if err != nil {
|
||||
// can't find the BlobDescriptor, try to get the extension from the URL
|
||||
if len(spl) == 2 {
|
||||
ext = spl[1]
|
||||
}
|
||||
} else if bd != nil {
|
||||
ext = getExtension(bd.Type)
|
||||
}
|
||||
|
||||
// should we accept this delete?
|
||||
for _, rd := range bs.RejectDelete {
|
||||
reject, reason, code := rd(r.Context(), auth, hhash)
|
||||
reject, reason, code := rd(r.Context(), auth, hhash, ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
@@ -320,7 +374,7 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
// 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 {
|
||||
if err := del(r.Context(), hhash, ext); err != nil {
|
||||
blossomError(w, "failed to delete blob: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -336,8 +390,8 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var evt *nostr.Event
|
||||
if err := json.Unmarshal(body, evt); err != nil {
|
||||
var evt nostr.Event
|
||||
if err := json.Unmarshal(body, &evt); err != nil {
|
||||
blossomError(w, "can't parse event", 400)
|
||||
return
|
||||
}
|
||||
@@ -353,7 +407,7 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
for _, rr := range bs.ReceiveReport {
|
||||
if err := rr(r.Context(), evt); err != nil {
|
||||
if err := rr(r.Context(), &evt); err != nil {
|
||||
blossomError(w, "failed to receive report: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -361,6 +415,100 @@ func (bs BlossomServer) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleMirror(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
|
||||
}
|
||||
|
||||
var body struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
blossomError(w, "invalid request body: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// download the blob
|
||||
resp, err := http.Get(body.URL)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to download blob: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
blossomError(w, "failed to read blob: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// calculate sha256 hash
|
||||
hash := sha256.Sum256(b)
|
||||
hhash := hex.EncodeToString(hash[:])
|
||||
|
||||
// verify hash matches x tag in auth event
|
||||
if auth.Tags.FindWithValue("x", hhash) == nil {
|
||||
blossomError(w, "blob hash does not match any \"x\" tag in authorization event", 403)
|
||||
return
|
||||
}
|
||||
|
||||
// get content type and extension
|
||||
var ext string
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType != "" { // First try to get the extension from the Content-Type header
|
||||
ext = getExtension(contentType)
|
||||
} else if ft, _ := magic.Lookup(b); ft != nil { // Else try to infer extension from the file content
|
||||
ext = "." + ft.Extension
|
||||
} else if idx := strings.LastIndex(body.URL, "."); idx >= 0 { // Else, try to get the extension from the URL
|
||||
ext = body.URL[idx:]
|
||||
}
|
||||
|
||||
// run reject hooks
|
||||
for _, ru := range bs.RejectUpload {
|
||||
reject, reason, code := ru(r.Context(), auth, len(b), ext)
|
||||
if reject {
|
||||
blossomError(w, reason, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// create blob descriptor
|
||||
bd := BlobDescriptor{
|
||||
URL: bs.ServiceURL + "/" + hhash + ext,
|
||||
SHA256: hhash,
|
||||
Size: len(b),
|
||||
Type: contentType,
|
||||
Uploaded: nostr.Now(),
|
||||
}
|
||||
|
||||
// store blob metadata
|
||||
if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil {
|
||||
blossomError(w, "failed to save metadata: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// store actual blob
|
||||
for _, sb := range bs.StoreBlob {
|
||||
if err := sb(r.Context(), hhash, ext, b); err != nil {
|
||||
blossomError(w, "failed to save blob: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(bd)
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleMedia(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/upload", 307)
|
||||
return
|
||||
}
|
||||
|
||||
func (bs BlossomServer) handleNegentropy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -14,17 +14,23 @@ type BlossomServer struct {
|
||||
ServiceURL string
|
||||
Store BlobIndex
|
||||
|
||||
StoreBlob []func(ctx context.Context, sha256 string, body []byte) error
|
||||
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
||||
DeleteBlob []func(ctx context.Context, sha256 string) error
|
||||
StoreBlob []func(ctx context.Context, sha256 string, ext string, body []byte) error
|
||||
LoadBlob []func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error)
|
||||
DeleteBlob []func(ctx context.Context, sha256 string, ext string) error
|
||||
ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) error
|
||||
RedirectGet []func(ctx context.Context, sha256 string, ext string) (url string, code int, err error)
|
||||
|
||||
RejectUpload []func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int)
|
||||
RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int)
|
||||
RejectList []func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int)
|
||||
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int)
|
||||
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int)
|
||||
}
|
||||
|
||||
// ServerOption represents a functional option for configuring a BlossomServer
|
||||
type ServerOption func(*BlossomServer)
|
||||
|
||||
// New creates a new BlossomServer with the given relay and service URL
|
||||
// Optional configuration can be provided via functional options
|
||||
func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
||||
bs := &BlossomServer{
|
||||
ServiceURL: serviceURL,
|
||||
@@ -43,6 +49,14 @@ func New(rl *khatru.Relay, serviceURL string) *BlossomServer {
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.URL.Path == "/media" {
|
||||
bs.handleMedia(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/mirror" && r.Method == "PUT" {
|
||||
bs.handleMirror(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/list/") && r.Method == "GET" {
|
||||
bs.handleList(w, r)
|
||||
|
||||
@@ -26,6 +26,8 @@ func getExtension(mimetype string) string {
|
||||
return ".webp"
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
case "application/vnd.android.package-archive":
|
||||
return ".apk"
|
||||
}
|
||||
|
||||
exts, _ := mime.ExtensionsByType(mimetype)
|
||||
|
||||
@@ -19,7 +19,7 @@ func (rl *Relay) handleDeleteRequest(ctx context.Context, evt *nostr.Event) erro
|
||||
case "e":
|
||||
f = nostr.Filter{IDs: []string{tag[1]}}
|
||||
case "a":
|
||||
spl := strings.Split(tag[1], ":")
|
||||
spl := strings.SplitN(tag[1], ":", 3)
|
||||
if len(spl) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ func main() {
|
||||
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: blobdb, ServiceURL: bl.ServiceURL}
|
||||
|
||||
// implement the required storage functions
|
||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error {
|
||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, ext string, body []byte) error {
|
||||
// store the blob data somewhere
|
||||
return nil
|
||||
})
|
||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
|
||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error) {
|
||||
// load and return the blob data
|
||||
return nil, nil
|
||||
})
|
||||
bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string) error {
|
||||
bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string, ext string) error {
|
||||
// delete the blob data
|
||||
return nil
|
||||
})
|
||||
@@ -47,6 +47,44 @@ You can integrate any storage backend by implementing the three core functions:
|
||||
- `LoadBlob`: Retrieve the blob data
|
||||
- `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.
|
||||
|
||||
You can implement a custom redirect function. This function should return a string with the redirect URL and an HTTP status code.
|
||||
|
||||
Here's an example that redirects to a templated URL:
|
||||
```go
|
||||
import "github.com/fiatjaf/khatru/policies"
|
||||
|
||||
// ...
|
||||
|
||||
bl.RedirectGet = append(bl.RedirectGet, policies.RedirectGet("https://blossom.example.com", http.StatusMovedPermanently))
|
||||
```
|
||||
|
||||
The `RedirectGet` hook will append the blob's SHA256 hash and file extension to the redirect URL.
|
||||
|
||||
For example, if the blob's SHA256 hash is `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553` and the file extension is `pdf`, the redirect URL will be `https://blossom.exampleserver.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`.
|
||||
|
||||
You can also customize the redirect URL by passing `{sha256}` and `{extension}` placeholders in the URL. For example:
|
||||
|
||||
```go
|
||||
bl.RedirectGet = append(bl.RedirectGet, policies.RedirectGet("https://mybucket.myblobstorage.com/{sha256}.{extension}?ref=xxxx", http.StatusFound))
|
||||
```
|
||||
|
||||
If you need more control over the redirect URL, you can implement a custom redirect function from scratch. This function should return a string with the redirect URL and an HTTP status code.
|
||||
|
||||
```go
|
||||
bl.RedirectGet = append(bl.RedirectGet, func(ctx context.Context, sha256 string, ext string) (string, int, error) {
|
||||
// generate a custom redirect URL
|
||||
cid := IPFSCID(sha256)
|
||||
redirectURL := fmt.Sprintf("https://ipfs.io/ipfs/%s/%s.%s", cid, sha256, ext)
|
||||
return redirectURL, http.StatusTemporaryRedirect, nil
|
||||
})
|
||||
```
|
||||
|
||||
This URL must include the sha256 hash somewhere. If you return an empty string `""` as the URL, your redirect call will be ignored and the next one in the chain (if any) will be called.
|
||||
|
||||
## 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:
|
||||
@@ -90,4 +128,4 @@ 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.
|
||||
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. Just avoid reusing the same datastore that is used for the actual relay events unless you know what you're doing.
|
||||
|
||||
@@ -4,6 +4,8 @@ layout: home
|
||||
hero:
|
||||
name: khatru
|
||||
text: a framework for making Nostr relays
|
||||
image:
|
||||
src: /logo.png
|
||||
tagline: write your custom relay with code over configuration
|
||||
actions:
|
||||
- theme: brand
|
||||
|
||||
@@ -31,11 +31,11 @@ func main() {
|
||||
}
|
||||
bl := blossom.New(relay, "http://localhost:3334")
|
||||
bl.Store = blossom.EventStoreBlobIndexWrapper{Store: bdb, ServiceURL: bl.ServiceURL}
|
||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error {
|
||||
fmt.Println("storing", sha256, len(body))
|
||||
bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, ext string, body []byte) error {
|
||||
fmt.Println("storing", sha256, ext, len(body))
|
||||
return nil
|
||||
})
|
||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) {
|
||||
bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error) {
|
||||
fmt.Println("loading", sha256)
|
||||
blob := strings.NewReader("aaaaa")
|
||||
return blob, nil
|
||||
|
||||
4
go.mod
4
go.mod
@@ -63,10 +63,10 @@ require (
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
15
go.sum
15
go.sum
@@ -14,13 +14,10 @@ github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiy
|
||||
github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
@@ -34,8 +31,6 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -134,8 +129,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nbd-wtf/go-nostr v0.51.7 h1:dGjtaaFQ1kA3H+vF8wt9a9WYl54K8C0JmVDf4cp+a4A=
|
||||
github.com/nbd-wtf/go-nostr v0.51.7/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
|
||||
github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE=
|
||||
github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -185,8 +178,8 @@ go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZ
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -211,8 +204,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -240,7 +240,7 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// the number of notified listeners matters in ephemeral events
|
||||
if nostr.IsEphemeralKind(env.Event.Kind) {
|
||||
if n == 0 {
|
||||
if n == 0 && len(rl.OnEphemeralEvent) == 0 {
|
||||
ok = false
|
||||
reason = "mute: no one was listening for this"
|
||||
} else {
|
||||
|
||||
11
nip86.go
11
nip86.go
@@ -125,13 +125,18 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
|
||||
methods := make([]string, 0, mat.NumField())
|
||||
for i := 0; i < mat.NumField(); i++ {
|
||||
field := mat.Field(i)
|
||||
value := mav.Field(i).Interface()
|
||||
|
||||
// danger: this assumes the struct fields are appropriately named
|
||||
methodName := strings.ToLower(field.Name)
|
||||
|
||||
if methodName == "rejectapicall" {
|
||||
continue
|
||||
}
|
||||
|
||||
// assign this only if the function was defined
|
||||
if mav.Field(i).Interface() != nil {
|
||||
methods[i] = methodName
|
||||
if !reflect.ValueOf(value).IsNil() {
|
||||
methods = append(methods, methodName)
|
||||
}
|
||||
}
|
||||
resp.Result = methods
|
||||
@@ -196,7 +201,7 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
|
||||
case nip86.ListBannedEvents:
|
||||
if rl.ManagementAPI.ListBannedEvents == nil {
|
||||
resp.Error = fmt.Sprintf("method %s not supported", thing.MethodName())
|
||||
} else if result, err := rl.ManagementAPI.ListEventsNeedingModeration(ctx); err != nil {
|
||||
} else if result, err := rl.ManagementAPI.ListBannedEvents(ctx); err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Result = result
|
||||
|
||||
43
policies/blossom.go
Normal file
43
policies/blossom.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package policies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RedirectGet returns a function that redirects to a specified URL template with the given status code.
|
||||
// The URL template can include {sha256} and/or {extension} placeholders that will be replaced
|
||||
// with the actual values. If neither placeholder is present, {sha256}.{extension} will be
|
||||
// appended to the URL with proper forward slash handling.
|
||||
func RedirectGet(urlTemplate string, statusCode int) func(context.Context, string, string) (url string, code int, err error) {
|
||||
return func(ctx context.Context, sha256 string, extension string) (string, int, error) {
|
||||
finalURL := urlTemplate
|
||||
|
||||
// Replace placeholders if they exist
|
||||
hasSHA256Placeholder := strings.Contains(finalURL, "{sha256}")
|
||||
hasExtensionPlaceholder := strings.Contains(finalURL, "{extension}")
|
||||
|
||||
if hasSHA256Placeholder {
|
||||
finalURL = strings.Replace(finalURL, "{sha256}", sha256, -1)
|
||||
}
|
||||
|
||||
if hasExtensionPlaceholder {
|
||||
finalURL = strings.Replace(finalURL, "{extension}", extension, -1)
|
||||
}
|
||||
|
||||
// If neither placeholder is present, append sha256.extension
|
||||
if !hasSHA256Placeholder && !hasExtensionPlaceholder {
|
||||
// Ensure URL ends with a forward slash
|
||||
if !strings.HasSuffix(finalURL, "/") {
|
||||
finalURL += "/"
|
||||
}
|
||||
|
||||
finalURL += sha256
|
||||
if extension != "" {
|
||||
finalURL += "." + extension
|
||||
}
|
||||
}
|
||||
|
||||
return finalURL, statusCode, nil
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,18 @@ func EventPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTo
|
||||
}
|
||||
}
|
||||
|
||||
func EventAuthedPubKeyRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||
|
||||
return func(ctx context.Context, _ *nostr.Event) (reject bool, msg string) {
|
||||
user := khatru.GetAuthed(ctx)
|
||||
if user == "" {
|
||||
return false, ""
|
||||
}
|
||||
return rl(user), "rate-limited: slow down, please"
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectionRateLimiter(tokensPerInterval int, interval time.Duration, maxTokens int) func(r *http.Request) bool {
|
||||
rl := startRateLimitSystem[string](tokensPerInterval, interval, maxTokens)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user