mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-04-09 06:56:55 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 {
|
||||
|
||||
@@ -227,6 +227,10 @@ 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 +340,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 +357,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 +365,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) {
|
||||
|
||||
@@ -43,6 +43,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
16
handlers.go
16
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
|
||||
|
||||
5
nip86.go
5
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"
|
||||
|
||||
@@ -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