feat(bunker): add NostrConnect `connect` command support to bunker

- Implement `connect` command to enable processing `nostrconnect://` URIs.
- Update configuration dynamically with new relays and authorized keys.
This commit is contained in:
Anthony Accioly 2025-07-04 17:51:20 +01:00
parent ea31694172
commit a4d4639e60
No known key found for this signature in database
GPG Key ID: 1BADB4682C8DDB3C
1 changed files with 192 additions and 15 deletions

207
bunker.go
View File

@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/hex"
"fiatjaf.com/nostr/nip44"
"fmt"
"net/url"
"os"
@ -230,10 +231,23 @@ var bunker = &cli.Command{
// static information
pubkey := sec.Public()
npub := nip19.EncodeNpub(pubkey)
signer := nip46.NewStaticKeySigner(sec)
printLock := sync.Mutex{}
exitChan := make(chan bool, 1)
// == SUB COMMANDS ==
// subscribe to relays
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(),
LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
})
handlerWg := sync.WaitGroup{}
// == SUBCOMMANDS ==
// printHelp displays available commands for the bunker interface
printHelp := func() {
@ -241,6 +255,7 @@ var bunker = &cli.Command{
log(" %s - Show this help message\n", color.GreenString("help, h, ?"))
log(" %s - Display current bunker information\n", color.GreenString("info, i"))
log(" %s - Generate and display QR code for the bunker URI\n", color.GreenString("qr"))
log(" %s - Connect to a remote client using nostrconnect:// URI\n", color.GreenString("connect, c <nostrconnect://uri>"))
log(" %s - Shutdown the bunker\n", color.GreenString("exit, quit, q"))
log("\n")
}
@ -331,15 +346,188 @@ var bunker = &cli.Command{
}
}
// handleConnect processes nostrconnect:// URIs for direct connection flow
handleConnect := func(connectURI string) {
if !strings.HasPrefix(connectURI, "nostrconnect://") {
log("Error: URI must start with nostrconnect://\n")
return
}
// Parse the nostrconnect URI
u, err := url.Parse(connectURI)
if err != nil {
log("Error: Invalid nostrconnect URI: %v\n", err)
return
}
// Extract client pubkey from the URI
clientPubkeyHex := u.Host
if clientPubkeyHex == "" {
log("Error: Missing client pubkey in URI\n")
return
}
clientPubkey, err := nostr.PubKeyFromHex(clientPubkeyHex)
if err != nil {
log("Error: Invalid client pubkey: %v\n", err)
return
}
// Extract relays from query parameters
queryParams := u.Query()
newRelayURLs := queryParams["relay"]
if len(newRelayURLs) == 0 {
log("Error: No relays specified in URI\n")
return
}
// Extract secret from query parameters (optional)
var secret string
secrets := queryParams["secret"]
if len(secrets) > 0 {
secret = secrets[0]
} else {
// log error and return if no secret is provided
log("Error: No secret provided in URI\n")
return
}
log("Parsed nostrconnect URI:\n")
log(" Client pubkey: %s\n", color.CyanString(clientPubkey.Hex()))
log(" Relays: %v\n", newRelayURLs)
log(" Secret: %s\n", color.YellowString(secret))
// Normalize relay URLs
for i, relayURL := range newRelayURLs {
newRelayURLs[i] = nostr.NormalizeURL(relayURL)
}
// Update relays in config (add new ones)
relaysAdded := false
for _, relayURL := range newRelayURLs {
if !slices.Contains(config.Relays, relayURL) {
config.Relays = append(config.Relays, relayURL)
log("Added new relay: %s\n", color.MagentaString(relayURL))
relaysAdded = true
}
}
// Add client key to authorized keys
keyAdded := false
if !slices.Contains(config.AuthorizedKeys, clientPubkey) {
config.AuthorizedKeys = append(config.AuthorizedKeys, clientPubkey)
log("Added client key to authorized keys: %s\n", color.GreenString(clientPubkey.Hex()))
keyAdded = true
}
// Persist config if needed and changes were made
if persist != nil && (relaysAdded || keyAdded) {
persist()
log(color.GreenString("Configuration saved\n"))
}
// Connect to new relays if any were added
if relaysAdded {
newRelays := connectToAllRelays(ctx, c, newRelayURLs, nil, nostr.PoolOptions{})
log("Connected to %d new relay(s)\n", len(newRelays))
// Update the relay URLs list with successfully connected relays
for _, relay := range newRelays {
if !slices.Contains(relayURLs, relay.URL) {
relayURLs = append(relayURLs, relay.URL)
}
}
log("Updated subscription to listen on %d relay(s)\n", len(relayURLs))
}
responsePayload := nip46.Response{
ID: secret,
Result: "ack",
}
// Marshal the response payload to JSON
responseJSON, err := json.MarshalIndent(responsePayload, "", " ")
if err != nil {
log("Error: Failed to marshal response payload: %v\n", err)
return
}
log("Sending connect response with:\n%s\n", string(responseJSON))
// Encrypt the response using NIP-44
conversationKey, err := nip44.GenerateConversationKey(clientPubkey, sec)
if err != nil {
log("Error: Failed to generate conversation key: %v\n", err)
return
}
encryptedContent, err := nip44.Encrypt(string(responseJSON), conversationKey)
if err != nil {
log("Error: Failed to encrypt response content: %v\n", err)
return
}
// Create the kind 24133 event
eventResponse := nostr.Event{
Kind: nostr.KindNostrConnect,
PubKey: pubkey,
Content: encryptedContent,
Tags: nostr.Tags{{"p", clientPubkey.Hex()}},
CreatedAt: nostr.Now(),
}
// Sign the event with the signer
if err := eventResponse.Sign(sec); err != nil {
log("Error: Failed to sign connect response: %v\n", err)
return
}
// Create a fake Kind 24133 connect request to process through the signer
targetRelays := newRelayURLs
if len(targetRelays) == 0 {
targetRelays = relayURLs
}
log("Sending connect response...\n")
successCount := 0
for _, relayURL := range targetRelays {
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
err := relay.Publish(ctx, eventResponse)
if err != nil {
log("Failed to publish to %s: %v\n", relayURL, err)
} else {
log("Published connect response to %s\n", color.GreenString(relayURL))
successCount++
}
}
}
if successCount == 0 {
log("Error: Failed to publish connect response to any relay\n")
} else {
log(color.GreenString("\nConnect response sent successfully to %d relay(s)!\n"), successCount)
}
}
// handleBunkerCommand processes user commands in the bunker interface
handleBunkerCommand := func(command string) {
switch strings.ToLower(command) {
parts := strings.Fields(command)
if len(parts) == 0 {
return
}
switch strings.ToLower(parts[0]) {
case "help", "h", "?":
printHelp()
case "info", "i":
printBunkerInfo()
case "qr":
printQR()
case "connect", "c":
if len(parts) < 2 {
log("Usage: connect <nostrconnect://uri>\n")
return
}
handleConnect(parts[1])
case "exit", "quit", "q":
log("Exit command received.\n")
exitChan <- true
@ -350,22 +538,11 @@ var bunker = &cli.Command{
}
}
// == END OF SUBCOMMANDS ==
// Print initial bunker information
printBunkerInfo()
// subscribe to relays
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(),
LimitZero: true,
}, nostr.SubscriptionOptions{
Label: "nak-bunker",
})
signer := nip46.NewStaticKeySigner(sec)
handlerWg := sync.WaitGroup{}
// just a gimmick
var cancelPreviousBunkerInfoPrint context.CancelFunc
_, cancel := context.WithCancel(ctx)