mirror of https://github.com/fiatjaf/nak.git
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:
parent
ea31694172
commit
a4d4639e60
207
bunker.go
207
bunker.go
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue