diff --git a/README.md b/README.md index d795abc..36f30b7 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,22 @@ They both take 3 parameters, in the following order: - `event`: the event being written, for `reject-event.js`; or `filter`: the subscription filter, for `reject-filter.js`. - `relay`: an object with some fields: - `query()`, a function that can be called with any Nostr filter and will return an array of results with events (read from the local database) - - `authedUser`: either a string or `null`, if it's a string it will be the pubkey of a user who has performed `AUTH` with the relay + - `store`, an interface for storing ephemeral data (will be stored in memory and cleaned up when the server stops), provides these functions: + - `get(key)` + - `set(key, value)` + - `del(key)` + - `conn`: an object with some fields: + - `ip`, the IP address of the user, as a string + - `pubkey`, the public key of the user, as hex, if the user has performed authentication, otherwise `undefined` + - `getOpenSubscriptions()`, a function that returns an array of filters forall the subscriptions opened by this connection + - `store`, an interface for storing data associated with this connection, provides these functions: + - `get(key)` + - `set(key, value)` + - `del(key)` **Authentication requests** -The functions can prompt a client to authenticate using the NIP-42 flow anytime by return a string that starts with `"auth-required: "` (and then some human-readable message afterwards). If the client performs an authentication and make a new request, the next time the same request comes the third parameter, `authedUser`, will be set. +The functions can prompt a client to authenticate using the NIP-42 flow anytime by return a string that starts with `"auth-required: "` (and then some human-readable message afterwards). If the client performs an authentication and make a new request the `pubkey` will be set in the `conn` parameter. ### Other options diff --git a/go.mod b/go.mod index 282f880..7ef5f37 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,12 @@ toolchain go1.21.4 require ( github.com/fiatjaf/eventstore v0.2.14 - github.com/fiatjaf/khatru v0.2.0 + github.com/fiatjaf/khatru v0.2.1 github.com/fiatjaf/quickjs-go v0.3.1 github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 github.com/kelseyhightower/envconfig v1.4.0 github.com/nbd-wtf/go-nostr v0.26.4 + github.com/puzpuzpuz/xsync/v2 v2.5.1 github.com/rs/zerolog v1.31.0 github.com/urfave/cli/v2 v2.25.7 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 @@ -46,10 +47,10 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-sqlite3 v1.14.18 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rs/cors v1.7.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect + github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a // indirect github.com/tidwall/gjson v1.17.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index 97d38d4..74c15fe 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQt github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/fiatjaf/eventstore v0.2.14 h1:YhxhQaJweTIuIckfZQ4wTUZQ1IKPxX60/LV/1St+XaQ= github.com/fiatjaf/eventstore v0.2.14/go.mod h1:IpGfGcTBa0K7FUQEOJDBAxIhfgP7pf1TJDu9DShybMw= -github.com/fiatjaf/khatru v0.2.0 h1:aSBh2yl2fS/3+2aA/kmAXtmRJ2f5iLHmstsWNDERlxk= -github.com/fiatjaf/khatru v0.2.0/go.mod h1:reXIM06zBXmFWwM1qp9mW6jCWjxTkEbtObVEPm0jOXE= +github.com/fiatjaf/khatru v0.2.1 h1:NlgjBYH7iJpjFyOJVNEX/E2I1v4d5+KINhA+VxgDr4o= +github.com/fiatjaf/khatru v0.2.1/go.mod h1:DsiQEmQmb6/hTXV6/OMcF7C7h19u1tJG5zAgaQVjseY= github.com/fiatjaf/quickjs-go v0.3.1 h1:NZu3o/P3fGpwr1zfkwadjKm3EsWXEuSHJ4TV0FJ8Zas= github.com/fiatjaf/quickjs-go v0.3.1/go.mod h1:lYXCC+EmJ6YxXs128amkCmMXnimlO4dFCqY6fjwGC0M= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -133,6 +133,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= +github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a h1:iLcLb5Fwwz7g/DLK89F+uQBDeAhHhwdzB5fSlVdhGcM= +github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a/go.mod h1:wozgYq9WEBQBaIJe4YZ0qTSFAMxmcwBhQH0fO0R34Z0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/main.go b/main.go index 6ffbde3..a7fad36 100644 --- a/main.go +++ b/main.go @@ -216,6 +216,9 @@ func main() { relay.RejectFilter = append(relay.RejectFilter, rejectFilter, ) + relay.OnDisconnect = append(relay.OnDisconnect, + onDisconnect, + ) // other http handlers log.Info().Msgf("checking for html and assets under ./%s/", s.CustomDirectory) diff --git a/reject.go b/reject.go index af22c79..3336863 100644 --- a/reject.go +++ b/reject.go @@ -6,7 +6,6 @@ import ( "path/filepath" "runtime" - "github.com/fiatjaf/khatru" "github.com/fiatjaf/quickjs-go" "github.com/fiatjaf/quickjs-go/polyfill/pkg/console" "github.com/fiatjaf/quickjs-go/polyfill/pkg/fetch" @@ -22,9 +21,9 @@ const ( ) var defaultScripts = map[scriptPath]string{ - REJECT_EVENT: `export default function (event, relay, authedUser) { + REJECT_EVENT: `export default function (event, relay, conn) { if (event.kind === 0) { - if (authedUser) { + if (conn.pubkey) { return null } else { return 'auth-required: please auth before publishing metadata' @@ -42,8 +41,8 @@ var defaultScripts = map[scriptPath]string{ }) if (metadata.length === 0) return 'publish your metadata here first' }`, - REJECT_FILTER: `export default function (filter, relay, authedUser) { - if (!authedUser) return "auth-required: take a selfie and send it to the CIA" + REJECT_FILTER: `export default function (filter, relay, conn) { + if (!conn.pubkey) return "auth-required: take a selfie and send it to the CIA" return fetch( 'https://www.random.org/integers/?num=1&min=1&max=9&col=1&base=10&format=plain&rnd=new' @@ -64,7 +63,7 @@ func rejectEvent(ctx context.Context, event *nostr.Event) (reject bool, msg stri // second argument: the relay object with goodies func(qjs *quickjs.Context) quickjs.Value { return makeRelayObject(ctx, qjs) }, // third argument: the currently authenticated user - func(qjs *quickjs.Context) quickjs.Value { return makeAuthedUserString(ctx, qjs) }, + func(qjs *quickjs.Context) quickjs.Value { return makeConnectionObject(ctx, qjs) }, ) } @@ -75,37 +74,10 @@ func rejectFilter(ctx context.Context, filter nostr.Filter) (reject bool, msg st // second argument: the relay object with goodies func(qjs *quickjs.Context) quickjs.Value { return makeRelayObject(ctx, qjs) }, // third argument: the currently authenticated user - func(qjs *quickjs.Context) quickjs.Value { return makeAuthedUserString(ctx, qjs) }, + func(qjs *quickjs.Context) quickjs.Value { return makeConnectionObject(ctx, qjs) }, ) } -func makeRelayObject(ctx context.Context, qjs *quickjs.Context) quickjs.Value { - relayObject := qjs.Object() - queryFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { - filterjs := args[0] // this is expected to be a nostr filter object - filter := filterFromJs(qjs, filterjs) - events, err := wrapper.QuerySync(ctx, filter) - if err != nil { - qjs.ThrowError(err) - } - results := qjs.Array() - for _, event := range events { - results.Push(eventToJs(qjs, event)) - } - return results.ToValue() - }) - relayObject.Set("query", queryFunc) - return relayObject -} - -func makeAuthedUserString(ctx context.Context, qjs *quickjs.Context) quickjs.Value { - if pubkey := khatru.GetAuthed(ctx); pubkey != "" { - return qjs.String(pubkey) - } else { - return qjs.Null() - } -} - func runAndGetResult(scriptPath scriptPath, makeArgs ...func(qjs *quickjs.Context) quickjs.Value) (reject bool, msg string) { runtime.LockOSThread() defer runtime.UnlockOSThread() diff --git a/session.go b/session.go new file mode 100644 index 0000000..8c87aed --- /dev/null +++ b/session.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "sync" + + "github.com/fiatjaf/khatru" + "github.com/fiatjaf/quickjs-go" + "github.com/puzpuzpuz/xsync/v2" +) + +var sessionStorage = xsync.NewTypedMapOf[*khatru.WebSocket, store](pointerHasher) + +type store struct { + data map[string]string + mutex sync.Mutex +} + +var globalStore = store{data: make(map[string]string)} + +func onDisconnect(ctx context.Context) { + sessionStorage.Delete(khatru.GetConnection(ctx)) +} + +func makeRelayObject(ctx context.Context, qjs *quickjs.Context) quickjs.Value { + relayObject := qjs.Object() + + queryFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + filterjs := args[0] // this is expected to be a nostr filter object + filter := filterFromJs(qjs, filterjs) + events, err := wrapper.QuerySync(ctx, filter) + if err != nil { + qjs.ThrowError(err) + } + results := qjs.Array() + for _, event := range events { + results.Push(eventToJs(qjs, event)) + } + return results.ToValue() + }) + relayObject.Set("query", queryFunc) + + setFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + v := args[1].JSONStringify() + globalStore.mutex.Lock() + globalStore.data[k] = v + globalStore.mutex.Unlock() + return qjs.Undefined() + }) + getFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + globalStore.mutex.Lock() + v := qjs.ParseJSON(globalStore.data[k]) + globalStore.mutex.Unlock() + return v + }) + delFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + globalStore.mutex.Lock() + delete(globalStore.data, k) + globalStore.mutex.Unlock() + return qjs.Undefined() + }) + + store := qjs.Object() + store.Set("set", setFunc) + store.Set("get", getFunc) + store.Set("del", delFunc) + relayObject.Set("store", store) + + return relayObject +} + +func makeConnectionObject(ctx context.Context, qjs *quickjs.Context) quickjs.Value { + connObject := qjs.Object() + connObject.Set("ip", qjs.String(khatru.GetIP(ctx))) + if pubkey := khatru.GetAuthed(ctx); pubkey != "" { + connObject.Set("pubkey", qjs.String(pubkey)) + } + connObject.Set("getOpenSubscriptions", qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + subs := qjs.Array() + for _, filter := range khatru.GetOpenSubscriptions(ctx) { + subs.Push(filterToJs(qjs, filter)) + } + return subs.ToValue() + })) + + setFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + v := args[1].JSONStringify() + s, _ := sessionStorage.LoadOrCompute(khatru.GetConnection(ctx), func() store { + return store{data: make(map[string]string)} + }) + s.mutex.Lock() + s.data[k] = v + s.mutex.Unlock() + return qjs.Undefined() + }) + getFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + s, ok := sessionStorage.Load(khatru.GetConnection(ctx)) + if !ok { + return qjs.Undefined() + } + s.mutex.Lock() + v := qjs.ParseJSON(s.data[k]) + s.mutex.Unlock() + return v + }) + delFunc := qjs.Function(func(qjs *quickjs.Context, this quickjs.Value, args []quickjs.Value) quickjs.Value { + k := args[0].String() + s, ok := sessionStorage.Load(khatru.GetConnection(ctx)) + if ok { + s.mutex.Lock() + delete(s.data, k) + s.mutex.Unlock() + } + return qjs.Undefined() + }) + + store := qjs.Object() + store.Set("set", setFunc) + store.Set("get", getFunc) + store.Set("del", delFunc) + connObject.Set("store", store) + + return connObject +} diff --git a/utils.go b/utils.go index 29bad9f..8fea2dd 100644 --- a/utils.go +++ b/utils.go @@ -1,11 +1,13 @@ package main import ( + "hash/maphash" "net/http" "os" "path/filepath" "strconv" "strings" + "unsafe" ) func getServiceBaseURL(r *http.Request) string { @@ -41,3 +43,7 @@ func getIconURL(r *http.Request) string { } return "" } + +func pointerHasher[V any](_ maphash.Seed, k *V) uint64 { + return uint64(uintptr(unsafe.Pointer(k))) +}