Files
orly/pkg/protocol/openapi/event.go

505 lines
13 KiB
Go

package openapi
import (
"bytes"
"fmt"
"github.com/danielgtaylor/huma/v2"
"net/http"
"orly.dev/pkg/app/relay/helpers"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/encoders/event"
"orly.dev/pkg/encoders/eventid"
"orly.dev/pkg/encoders/filter"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/encoders/ints"
"orly.dev/pkg/encoders/kind"
"orly.dev/pkg/encoders/tag"
"orly.dev/pkg/utils"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/context"
"orly.dev/pkg/utils/iptracker"
"orly.dev/pkg/utils/log"
"strings"
"time"
)
var EventBody = &huma.RequestBody{
Description: "a signed nostr event",
Content: map[string]*huma.MediaType{
"application/json": {
Schema: &huma.Schema{
Type: huma.TypeObject,
Properties: map[string]*huma.Schema{
"id": {
Type: huma.TypeString,
Description: "SHA256 hash of event in canonical (JSON array with fixed ordering) form, 64 characters hex",
},
"pubkey": {
Type: huma.TypeString,
Description: "BIP-340 Schnorr public key, 64 characters hex",
},
"created_at": {
Type: huma.TypeInteger,
Description: "unix timestamp of time of event creation",
},
"kind": {
Type: huma.TypeInteger,
Description: "kind number of event",
},
"tags": {
Type: huma.TypeArray,
Description: "array of arrays of strings",
Items: &huma.Schema{
Type: huma.TypeArray,
Items: &huma.Schema{
Type: huma.TypeString,
},
},
},
"content": {
Type: huma.TypeString,
Description: "content of event, escaped using NIP-01 standard escapes and UTF-8 encoding",
},
"sig": {
Type: huma.TypeString,
Description: "BIP-340 Schnorr signature, 128 characters hex",
},
},
},
},
},
}
// EventInput is the parameters for the Event HTTP API method.
type EventInput struct {
Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"`
Accept string `header:"Accept" default:"application/nostr+json"`
Body *event.J `doc:"event JSON"`
}
// EventOutput is the return parameters for the HTTP API Event method.
type EventOutput struct{}
// RegisterEvent is the implementation of the HTTP API Event method.
func (x *Operations) RegisterEvent(api huma.API) {
name := "Event"
description := "Submit an event"
path := x.path + "/event"
scopes := []string{"user", "write"}
method := http.MethodPost
huma.Register(
api, huma.Operation{
OperationID: name,
Summary: name,
Path: path,
Method: method,
Tags: []string{"events"},
RequestBody: EventBody,
Description: helpers.GenerateDescription(description, scopes),
Security: []map[string][]string{{"auth": scopes}},
}, func(ctx context.T, input *EventInput) (
output *EventOutput, err error,
) {
r := ctx.Value("http-request").(*http.Request)
remote := helpers.GetRemoteFromReq(r)
var authed, super bool
var pubkey []byte
if x.I.AuthRequired() {
// Check if the IP is blocked due to too many failed auth attempts
if iptracker.Global.IsBlocked(remote) {
blockedUntil := iptracker.Global.GetBlockedUntil(remote)
err = huma.Error403Forbidden(
fmt.Sprintf(
"Too many failed authentication attempts. Blocked until %s",
blockedUntil.Format(time.RFC3339),
),
)
return
}
authed, pubkey, super = x.UserAuth(r, remote)
if !authed {
// Record the failed authentication attempt
blocked := iptracker.Global.RecordFailedAttempt(remote)
if blocked {
blockedUntil := iptracker.Global.GetBlockedUntil(remote)
err = huma.Error403Forbidden(
fmt.Sprintf(
"Too many failed authentication attempts. Blocked until %s",
blockedUntil.Format(time.RFC3339),
),
)
} else {
err = huma.Error401Unauthorized("Not Authorized")
}
return
} else {
// If authentication is successful, remove any blocks for this IP
iptracker.Global.Authenticate(remote)
}
}
// get the other pubkeys from the header that will be sent forward
// by the other replicas to avoid repeatedly sending the message
// back to replicas that already received and forwarded it.
pubkeyHeader := r.Header.Get("X-Pubkeys")
pubkeys := [][]byte{pubkey}
if strings.Contains(pubkeyHeader, ":") {
split := strings.Split(pubkeyHeader, ":")
for _, pk := range split {
var pkb []byte
if pkb, err = hex.Dec(pk); chk.E(err) {
continue
}
pubkeys = append(pubkeys, pkb)
}
}
var ev *event.E
if ev, err = input.Body.ToEvent(); chk.E(err) {
err = huma.Error422UnprocessableEntity(
"Failed to convert event", err,
)
return
}
// these aliases make it so most of the following code can be copied
// verbatim from its counterpart in socketapi.HandleEvent, with the
// aid of a different implementation of the openapi.OK type.
a := x
env := ev
c := x.Context()
calculatedId := ev.GetIDBytes()
if !utils.FastEqual(calculatedId, ev.ID) {
err = huma.Error422UnprocessableEntity(
Ok.Invalid(
a, env, "event id is computed incorrectly, "+
"event has ID %0x, but when computed it is %0x",
ev.ID, calculatedId,
).Error(),
)
return
}
var ok bool
if ok, err = env.Verify(); chk.T(err) {
if err = Ok.Error(
a, env, fmt.Sprintf(
"failed to verify signature: %s",
err.Error(),
),
); chk.E(err) {
return
}
} else if !ok {
if err = Ok.Invalid(
a, env,
"signature is invalid",
); chk.E(err) {
return
}
return
}
// check that relay policy allows this event
accept, notice, _ := x.I.AcceptEvent(c, env, r, pubkey, remote)
if !accept && !super {
if err = Ok.Blocked(
a, env, notice,
); chk.E(err) {
return
}
return
}
// check for protected tag (NIP-70)
protectedTag := ev.Tags.GetFirst(tag.New("-"))
// if the super flag was set protected is ignored because the relay
// cluster replicas must replicate this event (and all events).
if protectedTag != nil && a.AuthRequired() && !super {
// check that the pubkey of the event matches the authed pubkey
if !utils.FastEqual(pubkey, ev.Pubkey) {
if err = Ok.Blocked(
a, env,
"protected tag may only be published by user authed to the same pubkey",
); chk.E(err) {
return
}
return
}
}
// check and process delete
sto := x.I.Storage()
if ev.Kind.K == kind.Deletion.K {
log.T.C(
func() string {
return fmt.Sprintf(
"delete event\n%s", ev.Serialize(),
)
},
)
for _, t := range ev.Tags.ToSliceOfTags() {
var res []*event.E
if t.Len() >= 2 {
switch {
case utils.FastEqual(t.Key(), []byte("e")):
// Process 'e' tag (event reference)
eventId := make([]byte, sha256.Size)
if _, err = hex.DecBytes(
eventId, t.Value(),
); chk.E(err) {
return
}
// Create a filter to find the referenced event
f := filter.New()
f.Ids = f.Ids.Append(eventId)
// Query for the referenced event
var referencedEvents []*event.E
referencedEvents, err = sto.QueryEvents(c, f)
if chk.E(err) {
if err = Ok.Error(
a, env,
"failed to query for referenced event",
); chk.E(err) {
return
}
return
}
// If we found the referenced event, check if the
// author matches
if len(referencedEvents) > 0 {
referencedEvent := referencedEvents[0]
// Check if the author of the deletion event
// matches the author of the referenced event
if !utils.FastEqual(
referencedEvent.Pubkey, env.Pubkey,
) {
if err = Ok.Blocked(
a, env,
"blocked: cannot delete events from other authors",
); chk.E(err) {
return
}
return
}
// Create eventid.T from the event ID bytes
var eid *eventid.T
if eid, err = eventid.NewFromBytes(eventId); chk.E(err) {
if err = Ok.Error(
a, env, "failed to create event ID",
); chk.E(err) {
return
}
return
}
// Use DeleteEvent to actually delete the
// referenced event
if err = sto.DeleteEvent(c, eid); chk.E(err) {
if err = Ok.Error(
a, env,
"failed to delete referenced event",
); chk.E(err) {
return
}
return
}
log.T.C(
func() string {
return fmt.Sprintf(
"successfully deleted event %x",
eventId,
)
},
)
}
case utils.FastEqual(t.Key(), []byte("a")):
split := bytes.Split(t.Value(), []byte{':'})
if len(split) != 3 {
continue
}
var pk []byte
if pk, err = hex.DecAppend(
nil, split[1],
); chk.E(err) {
if err = Ok.Invalid(
a, env,
"delete event a tag pubkey value invalid: %s",
t.Value(),
); chk.E(err) {
return
}
return
}
kin := ints.New(uint16(0))
if _, err = kin.Unmarshal(split[0]); chk.E(err) {
if err = Ok.Invalid(
a, env, "delete event a tag kind value "+
"invalid: %s",
t.Value(),
); chk.E(err) {
return
}
return
}
kk := kind.New(kin.Uint16())
if kk.Equal(kind.Deletion) {
if err = Ok.Blocked(
a, env, "delete event kind may not be "+
"deleted",
); chk.E(err) {
return
}
return
}
if !kk.IsParameterizedReplaceable() {
if err = Ok.Error(
a, env,
"delete tags with a tags containing "+
"non-parameterized-replaceable events can't be processed",
); chk.E(err) {
return
}
return
}
if !utils.FastEqual(pk, ev.Pubkey) {
if err = Ok.Blocked(
a, env,
"can't delete other users' events (delete by a tag)",
); chk.E(err) {
return
}
return
}
f := filter.New()
f.Kinds.K = []*kind.T{kk}
f.Authors.Append(pk)
f.Tags.AppendTags(
tag.New(
[]byte{'#', 'd'}, split[2],
),
)
res, err = sto.QueryEvents(c, f)
if chk.E(err) {
if err = Ok.Error(
a, env, "failed to query for target event",
); chk.E(err) {
return
}
return
}
}
}
if len(res) < 1 {
continue
}
var resTmp []*event.E
for _, v := range res {
if ev.CreatedAt.U64() >= v.CreatedAt.U64() {
resTmp = append(resTmp, v)
}
}
res = resTmp
for _, target := range res {
if target.Kind.K == kind.Deletion.K {
if err = Ok.Error(
a, env, "cannot delete delete event %s", ev.ID,
); chk.E(err) {
return
}
}
if target.CreatedAt.Int() > ev.CreatedAt.Int() {
continue
}
if !utils.FastEqual(target.Pubkey, env.Pubkey) {
if err = Ok.Error(
a, env, "only author can delete event",
); chk.E(err) {
return
}
return
}
// Create eventid.T from the target event ID bytes
var eid *eventid.T
eid, err = eventid.NewFromBytes(target.EventId().Bytes())
if chk.E(err) {
if err = Ok.Error(
a, env, "failed to create event ID",
); chk.E(err) {
return
}
return
}
// Use DeleteEvent to actually delete the target event
if err = sto.DeleteEvent(c, eid); chk.E(err) {
if err = Ok.Error(
a, env, "failed to delete target event",
); chk.E(err) {
return
}
return
}
log.T.C(
func() string {
return fmt.Sprintf(
"successfully deleted event %x",
target.EventId().Bytes(),
)
},
)
}
res = nil
}
// Send a success response after processing all deletions
if err = Ok.Ok(a, env, ""); chk.E(err) {
return
}
// Check if this event has been deleted before
if ev.Kind.K != kind.Deletion.K {
// Create a filter to check for deletion events that
// reference this event ID
f := filter.New()
f.Kinds.K = []*kind.T{kind.Deletion}
f.Tags.AppendTags(tag.New([]byte{'e'}, ev.ID))
// Query for deletion events
var deletionEvents []*event.E
deletionEvents, err = sto.QueryEvents(c, f)
if err == nil && len(deletionEvents) > 0 {
// Found deletion events for this ID, don't save it
if err = Ok.Blocked(
a, env,
"the event was deleted, not storing it again",
); chk.E(err) {
return
}
return
}
}
}
var reason []byte
ok, reason = x.I.AddEvent(
c, x.Relay(), ev, r, remote, pubkeys,
)
log.T.C(
func() string {
return fmt.Sprintf(
"http API event %0x added %v %s", ev.ID, ok, reason,
)
},
)
if !ok && err != nil {
if err = Ok.Error(
a, env, err.Error(),
); chk.E(err) {
return
}
}
return
},
)
}