Skip to content

Commit

Permalink
store() on relay object and replace third parameter with conn object …
Browse files Browse the repository at this point in the history
…{store(), pubkey, ip, getOpenSubscriptions()}
  • Loading branch information
fiatjaf committed Dec 9, 2023
1 parent bde95dd commit 904d790
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 40 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 6 additions & 34 deletions reject.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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'
Expand All @@ -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'
Expand All @@ -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) },
)
}

Expand All @@ -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()
Expand Down
129 changes: 129 additions & 0 deletions session.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package main

import (
"hash/maphash"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"unsafe"
)

func getServiceBaseURL(r *http.Request) string {
Expand Down Expand Up @@ -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)))
}

0 comments on commit 904d790

Please sign in to comment.