Files
realy/cmd/realy/app/implementation.go
2025-02-08 13:51:56 -01:06

406 lines
12 KiB
Go

package app
import (
"bytes"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"realy.lol/context"
"realy.lol/ec/schnorr"
"realy.lol/event"
"realy.lol/filter"
"realy.lol/filters"
"realy.lol/hex"
"realy.lol/kind"
"realy.lol/kinds"
"realy.lol/realy/config"
"realy.lol/store"
"realy.lol/tag"
"realy.lol/tag/atag"
)
type List map[string]struct{}
type Relay struct {
sync.Mutex
*config.C
Store store.I
// Owners' pubkeys
Owners [][]byte
// Followed are the pubkeys that are in the Owners' follow lists and have full
// access permission.
Followed List
// OwnersFollowed are "guests" of the Followed and have full access but with
// rate limiting enabled.
OwnersFollowed List
// Muted are on Owners' mute lists and do not have write access to the relay,
// even if they would be in the OwnersFollowed list, they can only read.
Muted List
// OwnersFollowLists are the event IDs of owners follow lists, which must not be
// deleted, only replaced.
OwnersFollowLists [][]byte
// OwnersMuteLists are the event IDs of owners mute lists, which must not be
// deleted, only replaced.
OwnersMuteLists [][]byte
}
func (r *Relay) Name() string { return r.C.AppName }
func (r *Relay) Storage() store.I { return r.Store }
func (r *Relay) Init() (err error) {
for _, src := range r.C.Owners {
if len(src) < 1 {
continue
}
dst := make([]byte, len(src)/2)
if _, err = hex.DecBytes(dst, []byte(src)); chk.E(err) {
continue
}
r.Owners = append(r.Owners, dst)
}
if len(r.Owners) > 0 {
log.T.C(func() string {
ownerIds := make([]string, len(r.Owners))
for i, npub := range r.Owners {
ownerIds[i] = hex.Enc(npub)
}
owners := strings.Join(ownerIds, ",")
return fmt.Sprintf("owners %s", owners)
})
r.ZeroLists()
r.CheckOwnerLists(context.Bg())
}
return nil
}
func (r *Relay) NoLimiter(pubKey []byte) (ok bool) {
r.Lock()
defer r.Unlock()
_, ok = r.Followed[string(pubKey)]
return
}
func (r *Relay) ZeroLists() {
r.Followed = make(map[string]struct{})
r.OwnersFollowed = make(map[string]struct{})
r.OwnersFollowLists = r.OwnersFollowLists[:0]
r.Muted = make(map[string]struct{})
r.OwnersMuteLists = r.OwnersMuteLists[:0]
}
func (r *Relay) AcceptEvent(c context.T, evt *event.T, hr *http.Request, origin string,
authedPubkey []byte) (accept bool, notice string, afterSave func()) {
// if the authenticator is enabled we require auth to accept events
if !r.AuthEnabled() {
return true, "", nil
}
if evt.CreatedAt.I64()-10 > time.Now().Unix() {
return false,
"realy does not accept timestamps that are so obviously fake, fix your clock",
nil
}
if len(authedPubkey) != 32 && !r.PublicReadable {
return false, fmt.Sprintf("client not authed with auth required %s", origin), nil
}
if len(r.Owners) > 0 {
r.Lock()
defer r.Unlock()
if evt.Kind.Equal(kind.FollowList) {
// if owner or any of their follows lists are updated we need to regenerate the
// list this ensures that immediately a follow changes their list that newly
// followed can access the relay and upload DM events and such for owner
// followed users.
for o := range r.OwnersFollowed {
if bytes.Equal([]byte(o), evt.PubKey) {
return true, "", func() {
r.ZeroLists()
r.CheckOwnerLists(context.Bg())
}
}
}
}
if evt.Kind.Equal(kind.MuteList) {
// only owners control the mute list
for _, o := range r.Owners {
if bytes.Equal(o, evt.PubKey) {
return true, "", func() {
r.ZeroLists()
r.CheckOwnerLists(context.Bg())
}
}
}
}
for _, o := range r.Owners {
log.T.F("%0x,%0x", o, evt.PubKey)
if bytes.Equal(o, evt.PubKey) {
// prevent owners from deleting their own mute/follow lists in case of bad
// client implementation
if evt.Kind.Equal(kind.Deletion) {
// check all a tags present are not follow/mute lists of the owners
aTags := evt.Tags.GetAll(tag.New("a"))
for _, at := range aTags.F() {
a := &atag.T{}
var rem []byte
var err error
if rem, err = a.Unmarshal(at.Value()); chk.E(err) {
continue
}
if len(rem) > 0 {
log.I.S("remainder", evt, rem)
}
if a.Kind.Equal(kind.Deletion) {
// we don't delete delete events, period
return false, "delete event kind may not be deleted", nil
}
// if the kind is not parameterised replaceable, the tag is invalid and the
// delete event will not be saved.
if !a.Kind.IsParameterizedReplaceable() {
return false, "delete tags with a tags containing " +
"non-parameterized-replaceable events cannot be processed", nil
}
for _, own := range r.Owners {
// don't allow owners to delete their mute or follow lists because
// they should not want to, can simply replace it, and malicious
// clients may do this specifically to attack the owner's relay (s)
if bytes.Equal(own, a.PubKey) ||
a.Kind.Equal(kind.MuteList) ||
a.Kind.Equal(kind.FollowList) {
return false, "owners may not delete their own " +
"mute or follow lists, they can be replaced", nil
}
}
}
log.W.Ln("event is from owner")
return true, "", nil
}
}
// check the mute list, and reject events authored by muted pubkeys, even if
// they come from a pubkey that is on the follow list.
for pk := range r.Muted {
if bytes.Equal(evt.PubKey, []byte(pk)) {
return false, "rejecting event with pubkey " + hex.Enc(evt.PubKey) +
" because on owner mute list", nil
}
}
// for all else, check the authed pubkey is in the follow list
for pk := range r.Followed {
// allow all events from follows of owners
if bytes.Equal(authedPubkey, []byte(pk)) {
log.I.F("accepting event %0x because %0x on owner follow list",
evt.ID, []byte(pk))
return true, "", nil
}
}
}
}
// if auth is enabled and there is no moderators we just check that the pubkey
// has been loaded via the auth function.
accept = len(authedPubkey) == schnorr.PubKeyBytesLen
if !accept {
notice = "auth required but user not authed"
}
return
}
func (r *Relay) AcceptReq(c context.T, hr *http.Request, id []byte, ff *filters.T,
authedPubkey []byte) (allowed *filters.T, ok bool) {
if !r.AuthEnabled() || r.PublicReadable && len(authedPubkey) > 0 {
allowed = &filters.T{}
// non-authed users may not make filters that have too broad criteria, resource
// exhaustion attack mitigation.
for _, f := range ff.F {
// if f.Authors.Len() < 1 &&
// f.IDs.Len() < 1 &&
// f.Tags.Len() < 1 &&
// f.Kinds.Len() < 1 {
// // no criteria or only time criteria are not permitted
// } else {
allowed.F = append(allowed.F, f)
// }
}
ok = true
return
}
// if client isn't authed but there are kinds in the filters that are
// kind.Directory type then trim the filter down and only respond to the queries
// that blanket should deliver events in order to facilitate non-authorized users
// to interact with users, even just such as to see their profile metadata or
// learn about deleted events.
if len(authedPubkey) == 0 {
for _, f := range ff.F {
fk := f.Kinds.K
allowedKinds := kinds.New()
for _, fkk := range fk {
if fkk.IsDirectoryEvent() {
allowedKinds.K = append(allowedKinds.K, fkk)
}
}
// if none of the kinds in the req are permitted, continue to the next filter.
if len(allowedKinds.K) == 0 {
continue
}
// if no filters have yet been added, initialize one
if allowed == nil {
allowed = &filters.T{}
}
// overwrite the kinds that have been permitted
f.Kinds.K = allowedKinds.K
allowed.F = append(allowed.F, f)
}
if allowed != nil {
// request has been filtered and can be processed. note that the caller should
// still send out an auth request after the filter has been processed.
ok = true
return
}
}
// if the client hasn't authed, reject
if len(authedPubkey) == 0 {
return
}
// client is permitted, pass through the filter so request/count processing does
// not need logic and can just use the returned filter.
allowed = ff
// check that the client is authed to a pubkey in the owner follow list
r.Lock()
defer r.Unlock()
if len(r.Owners) > 0 {
for pk := range r.Followed {
if bytes.Equal(authedPubkey, []byte(pk)) {
ok = true
return
}
}
// if the authed pubkey was not found, reject the request.
return
}
// if auth is enabled and there is no moderators we just check that the pubkey
// has been loaded via the auth function.
ok = len(authedPubkey) == schnorr.PubKeyBytesLen
return
}
// CheckOwnerLists regenerates the owner follow and mute lists if they are empty.
//
// It also adds the followed npubs of the follows.
func (r *Relay) CheckOwnerLists(c context.T) {
if len(r.Owners) > 0 {
r.Lock()
defer r.Unlock()
var err error
var evs []*event.T
// need to search DB for moderator npub follow lists, followed npubs are allowed access.
if len(r.Followed) < 1 {
// add the owners themselves of course
for i := range r.Owners {
r.Followed[string(r.Owners[i])] = struct{}{}
}
log.D.Ln("regenerating owners follow lists")
if evs, err = r.Store.QueryEvents(c,
&filter.T{Authors: tag.New(r.Owners...),
Kinds: kinds.New(kind.FollowList)}); chk.E(err) {
}
for _, ev := range evs {
r.OwnersFollowLists = append(r.OwnersFollowLists, ev.ID)
for _, t := range ev.Tags.F() {
if bytes.Equal(t.Key(), []byte("p")) {
var p []byte
if p, err = hex.Dec(string(t.Value())); chk.E(err) {
continue
}
r.Followed[string(p)] = struct{}{}
r.OwnersFollowed[string(p)] = struct{}{}
}
}
}
evs = evs[:0]
// next, search for the follow lists of all on the follow list
log.D.Ln("searching for owners follows follow lists")
var followed []string
for f := range r.Followed {
followed = append(followed, f)
}
if evs, err = r.Store.QueryEvents(c,
&filter.T{Authors: tag.New(followed...),
Kinds: kinds.New(kind.FollowList)}); chk.E(err) {
}
for _, ev := range evs {
// we want to protect the follow lists of users as well so they also cannot be
// deleted, only replaced.
r.OwnersFollowLists = append(r.OwnersFollowLists, ev.ID)
for _, t := range ev.Tags.F() {
if bytes.Equal(t.Key(), []byte("p")) {
var p []byte
if p, err = hex.Dec(string(t.Value())); err != nil {
continue
}
r.Followed[string(p)] = struct{}{}
}
}
}
evs = evs[:0]
}
if len(r.Muted) < 1 {
log.D.Ln("regenerating owners mute lists")
r.Muted = make(map[string]struct{})
if evs, err = r.Store.QueryEvents(c,
&filter.T{Authors: tag.New(r.Owners...),
Kinds: kinds.New(kind.MuteList)}); chk.E(err) {
}
for _, ev := range evs {
r.OwnersMuteLists = append(r.OwnersMuteLists, ev.ID)
for _, t := range ev.Tags.F() {
if bytes.Equal(t.Key(), []byte("p")) {
var p []byte
if p, err = hex.Dec(string(t.Value())); chk.E(err) {
continue
}
r.Muted[string(p)] = struct{}{}
}
}
}
evs = evs[:0]
}
log.I.F("%d allowed npubs, %d blocked", len(r.Followed), len(r.Muted))
}
}
func (r *Relay) AuthEnabled() bool { return r.AuthRequired || len(r.Owners) > 0 }
// ServiceUrl returns the address of the relay to send back in auth responses.
// If auth is disabled this returns an empty string.
func (r *Relay) ServiceUrl(req *http.Request) (s string) {
if !r.AuthEnabled() {
return
}
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
host = req.Host
}
proto := req.Header.Get("X-Forwarded-Proto")
if proto == "" {
if host == "localhost" {
proto = "ws"
} else if strings.Contains(host, ":") {
// has a port number
proto = "ws"
} else if _, err := strconv.Atoi(strings.ReplaceAll(host, ".",
"")); chk.E(err) {
// it's a naked IP
proto = "ws"
} else {
proto = "wss"
}
} else if proto == "https" {
proto = "wss"
} else if proto == "http" {
proto = "ws"
}
return proto + "://" + host
}