131 lines
3.5 KiB
Go
131 lines
3.5 KiB
Go
package nip46
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
mrand "math/rand"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"fiatjaf.com/nostr"
|
|
"fiatjaf.com/nostr/nip44"
|
|
"github.com/puzpuzpuz/xsync/v3"
|
|
)
|
|
|
|
var NoConnectionReceived = errors.New("relay connections ended without a bunker connection established")
|
|
|
|
// GenerateNostrConnectURL generates a nostrconnect:// URL that can be passed to the remote-signer
|
|
// The remote-signer will then send a connect response to the client-pubkey via the specified relays
|
|
func GenerateNostrConnectURL(
|
|
ctx context.Context,
|
|
clientSecretKey [32]byte,
|
|
relayURLs []string,
|
|
permissions []string,
|
|
clientName, clientURL, clientImage string,
|
|
) (string, error) {
|
|
if len(relayURLs) == 0 {
|
|
return "", fmt.Errorf("at least one relay URL is required")
|
|
}
|
|
|
|
// generate random secret
|
|
secretBytes := make([]byte, 16)
|
|
if _, err := rand.Read(secretBytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate secret: %w", err)
|
|
}
|
|
secret := fmt.Sprintf("%x", secretBytes)
|
|
|
|
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
|
params := url.Values{}
|
|
for _, relay := range relayURLs {
|
|
params.Add("relay", relay)
|
|
}
|
|
params.Set("secret", secret)
|
|
|
|
if len(permissions) > 0 {
|
|
params.Set("perms", strings.Join(permissions, ","))
|
|
}
|
|
if clientName != "" {
|
|
params.Set("name", clientName)
|
|
}
|
|
if clientURL != "" {
|
|
params.Set("url", clientURL)
|
|
}
|
|
if clientImage != "" {
|
|
params.Set("image", clientImage)
|
|
}
|
|
|
|
connectURL := fmt.Sprintf("nostrconnect://%s?%s", clientPublicKey.Hex(), params.Encode())
|
|
return connectURL, nil
|
|
}
|
|
|
|
// NewBunkerFromNostrConnect waits for a client to connect through our nostrconnect:// URL and returns a BunkerClient on success
|
|
func NewBunkerFromNostrConnect(
|
|
ctx context.Context,
|
|
clientSecretKey [32]byte,
|
|
relayURLs []string,
|
|
secret string,
|
|
pool *nostr.Pool,
|
|
) (*BunkerClient, error) {
|
|
if pool == nil {
|
|
pool = nostr.NewPool(nostr.PoolOptions{})
|
|
}
|
|
|
|
if len(relayURLs) == 0 {
|
|
return nil, fmt.Errorf("at least one relay URL is required")
|
|
}
|
|
|
|
clientPublicKey := nostr.GetPublicKey(clientSecretKey)
|
|
|
|
// subscribe to events addressed to our client public key
|
|
for ie := range pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
|
Tags: nostr.TagMap{"p": []string{clientPublicKey.Hex()}},
|
|
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
|
Since: nostr.Now(),
|
|
LimitZero: true,
|
|
}, nostr.SubscriptionOptions{Label: "nostrconnect-client"}) {
|
|
if ie.Kind != nostr.KindNostrConnect {
|
|
continue
|
|
}
|
|
if ie.PubKey.String() == clientPublicKey.Hex() {
|
|
continue
|
|
}
|
|
|
|
// decrypt and process the connect response
|
|
targetPublicKey := ie.PubKey
|
|
conversationKey, err := nip44.GenerateConversationKey(targetPublicKey, clientSecretKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
plain, err := nip44.Decrypt(ie.Content, conversationKey)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
var req Response // contradictorily the thing starts with a response
|
|
if err := json.Unmarshal([]byte(plain), &req); err != nil {
|
|
continue
|
|
}
|
|
|
|
if req.Result != "" {
|
|
if req.Result == secret {
|
|
// secret validation passed - connection established
|
|
return &BunkerClient{
|
|
pool: pool,
|
|
clientSecretKey: clientSecretKey,
|
|
target: targetPublicKey,
|
|
relays: relayURLs,
|
|
conversationKey: conversationKey,
|
|
listeners: xsync.NewMapOf[string, chan Response](),
|
|
onAuth: func(string) {},
|
|
idPrefix: "nl-" + strconv.Itoa(mrand.Intn(65536)),
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, NoConnectionReceived
|
|
}
|