mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-08 22:46:48 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97bc365996 | ||
|
|
44e4b86955 | ||
|
|
932825080e | ||
|
|
aa81e17e22 | ||
|
|
034902c3e3 | ||
|
|
5705647c6b | ||
|
|
b2607e787f | ||
|
|
011efe0dd2 | ||
|
|
16eee5cffb | ||
|
|
c5076b297d | ||
|
|
1a5b498fb8 | ||
|
|
b6da555807 | ||
|
|
1ab44ab897 | ||
|
|
3da898cec7 | ||
|
|
cfbe484784 | ||
|
|
583f712fe4 | ||
|
|
28b1061166 | ||
|
|
25f19ce46e |
44
adding.go
44
adding.go
@@ -33,6 +33,23 @@ func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadc
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if the event has been deleted by ID
|
||||
for _, query := range rl.QueryEvents {
|
||||
ch, err := query(ctx, nostr.Filter{
|
||||
Kinds: []int{5},
|
||||
Tags: nostr.TagMap{"#e": []string{evt.ID}},
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
target := <-ch
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, errors.New("blocked: this event has been deleted")
|
||||
}
|
||||
|
||||
// will store
|
||||
// regular kinds are just saved directly
|
||||
if nostr.IsRegularKind(evt.Kind) {
|
||||
@@ -47,6 +64,33 @@ func (rl *Relay) handleNormal(ctx context.Context, evt *nostr.Event) (skipBroadc
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check to see if the event has been deleted by address
|
||||
for _, query := range rl.QueryEvents {
|
||||
dTagValue := ""
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) > 0 && tag[0] == "d" {
|
||||
dTagValue = tag[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
address := fmt.Sprintf("%d:%s:%s", evt.Kind, evt.PubKey, dTagValue)
|
||||
ch, err := query(ctx, nostr.Filter{
|
||||
Kinds: []int{5},
|
||||
Since: &evt.CreatedAt,
|
||||
Tags: nostr.TagMap{"#a": []string{address}},
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
target := <-ch
|
||||
if target == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, errors.New("blocked: this event has been deleted")
|
||||
}
|
||||
|
||||
// otherwise it's a replaceable -- so we'll use the replacer functions if we have any
|
||||
if len(rl.ReplaceEvent) > 0 {
|
||||
for _, repl := range rl.ReplaceEvent {
|
||||
|
||||
@@ -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)
|
||||
@@ -142,6 +148,7 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// return response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(bd)
|
||||
}
|
||||
|
||||
@@ -185,7 +192,26 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var ext string
|
||||
if len(spl) == 2 {
|
||||
ext = "." + spl[1]
|
||||
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 {
|
||||
@@ -200,7 +226,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 +257,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) {
|
||||
@@ -336,8 +369,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 +386,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 +394,101 @@ 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 != "" {
|
||||
ext = getExtension(contentType)
|
||||
} else {
|
||||
// Try to detect from URL extension
|
||||
if idx := strings.LastIndex(body.URL, "."); idx >= 0 {
|
||||
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, 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) {
|
||||
|
||||
@@ -18,6 +18,7 @@ type BlossomServer struct {
|
||||
LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error)
|
||||
DeleteBlob []func(ctx context.Context, sha256 string) error
|
||||
ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) error
|
||||
RedirectGet []func(ctx context.Context, sha256 string, fileExtension 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)
|
||||
@@ -25,6 +26,11 @@ type BlossomServer struct {
|
||||
RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
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=
|
||||
|
||||
18
handlers.go
18
handlers.go
@@ -217,12 +217,16 @@ func (rl *Relay) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
if env.Event.Kind == 5 {
|
||||
// this always returns "blocked: " whenever it returns an error
|
||||
writeErr = srl.handleDeleteRequest(ctx, &env.Event)
|
||||
} else if nostr.IsEphemeralKind(env.Event.Kind) {
|
||||
// this will also always return a prefixed reason
|
||||
writeErr = srl.handleEphemeral(ctx, &env.Event)
|
||||
} else {
|
||||
// this will also always return a prefixed reason
|
||||
skipBroadcast, writeErr = srl.handleNormal(ctx, &env.Event)
|
||||
}
|
||||
|
||||
if writeErr == nil {
|
||||
if nostr.IsEphemeralKind(env.Event.Kind) {
|
||||
// this will also always return a prefixed reason
|
||||
writeErr = srl.handleEphemeral(ctx, &env.Event)
|
||||
} else {
|
||||
// this will also always return a prefixed reason
|
||||
skipBroadcast, writeErr = srl.handleNormal(ctx, &env.Event)
|
||||
}
|
||||
}
|
||||
|
||||
var reason string
|
||||
@@ -236,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 {
|
||||
|
||||
14
nip86.go
14
nip86.go
@@ -86,8 +86,9 @@ func (rl *Relay) HandleNIP86(w http.ResponseWriter, r *http.Request) {
|
||||
goto respond
|
||||
}
|
||||
|
||||
if uTag := evt.Tags.Find("u"); uTag == nil || rl.getBaseURL(r) != uTag[1] {
|
||||
resp.Error = "invalid 'u' tag"
|
||||
if uTag := evt.Tags.Find("u"); uTag == nil || nostr.NormalizeURL(rl.getBaseURL(r)) != nostr.NormalizeURL(uTag[1]) {
|
||||
resp.Error = fmt.Sprintf("invalid 'u' tag, got '%s', expected '%s'",
|
||||
nostr.NormalizeURL(rl.getBaseURL(r)), nostr.NormalizeURL(uTag[1]))
|
||||
goto respond
|
||||
} else if pht := evt.Tags.FindWithValue("payload", hex.EncodeToString(payloadHash[:])); pht == nil {
|
||||
resp.Error = "invalid auth event payload hash"
|
||||
@@ -124,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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -151,33 +151,64 @@ func TestBasicRelayFunctionality(t *testing.T) {
|
||||
t.Fatalf("failed to publish deletion event: %v", err)
|
||||
}
|
||||
|
||||
// Try to query the deleted event
|
||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
||||
IDs: []string{evt3.ID},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to subscribe: %v", err)
|
||||
}
|
||||
defer sub.Unsub()
|
||||
{
|
||||
// Try to query the deleted event
|
||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
||||
IDs: []string{evt3.ID},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to subscribe: %v", err)
|
||||
}
|
||||
defer sub.Unsub()
|
||||
|
||||
// Should get EOSE without receiving the deleted event
|
||||
gotEvent := false
|
||||
for {
|
||||
select {
|
||||
case <-sub.Events:
|
||||
gotEvent = true
|
||||
case <-sub.EndOfStoredEvents:
|
||||
if gotEvent {
|
||||
t.Error("should not have received deleted event")
|
||||
// Should get EOSE without receiving the deleted event
|
||||
gotEvent := false
|
||||
DeletedLoop:
|
||||
for {
|
||||
select {
|
||||
case <-sub.Events:
|
||||
gotEvent = true
|
||||
case <-sub.EndOfStoredEvents:
|
||||
if gotEvent {
|
||||
t.Error("should not have received deleted event")
|
||||
}
|
||||
break DeletedLoop
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for EOSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Try to query the deletion itself
|
||||
sub, err := client2.Subscribe(ctx, []nostr.Filter{{
|
||||
Kinds: []int{5},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to subscribe: %v", err)
|
||||
}
|
||||
defer sub.Unsub()
|
||||
|
||||
// Should get EOSE without receiving the deleted event
|
||||
gotEvent := false
|
||||
DeletionLoop:
|
||||
for {
|
||||
select {
|
||||
case <-sub.Events:
|
||||
gotEvent = true
|
||||
case <-sub.EndOfStoredEvents:
|
||||
if !gotEvent {
|
||||
t.Error("should have received deletion event")
|
||||
}
|
||||
break DeletionLoop
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for EOSE")
|
||||
}
|
||||
return
|
||||
case <-ctx.Done():
|
||||
t.Fatal("timeout waiting for EOSE")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// test 4: teplaceable events
|
||||
// test 4: replaceable events
|
||||
t.Run("replaceable events", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
Reference in New Issue
Block a user