From bf19f38996b49200ef8de7dffc32e72b322d62eb Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 21 Jan 2026 12:44:29 -0300 Subject: [PATCH] nak bunker connect 'nostrconnect://...' working. --- bunker.go | 258 ++++++++++++++++++++++++++++++++++++++++---------- bunkerconn.go | 94 ------------------ go.mod | 2 +- go.sum | 4 +- helpers.go | 22 +++-- 5 files changed, 224 insertions(+), 156 deletions(-) delete mode 100644 bunkerconn.go diff --git a/bunker.go b/bunker.go index 210eadb..f79a405 100644 --- a/bunker.go +++ b/bunker.go @@ -5,12 +5,12 @@ import ( "context" "encoding/hex" "fmt" + "net" "net/url" "os" "path/filepath" "slices" "strings" - "sync" "time" "fiatjaf.com/nostr" @@ -73,13 +73,7 @@ var bunker = &cli.Command{ }, Action: func(ctx context.Context, c *cli.Command) error { // read config from file - config := struct { - AuthorizedKeys []nostr.PubKey `json:"authorized-keys"` - Secret plainOrEncryptedKey `json:"sec"` - Relays []string `json:"relays"` - }{ - AuthorizedKeys: make([]nostr.PubKey, 0, 3), - } + config := BunkerConfig{} baseRelaysUrls := appendUnique(c.Args().Slice(), c.StringSlice("relay")...) for i, url := range baseRelaysUrls { baseRelaysUrls[i] = nostr.NormalizeURL(url) @@ -110,12 +104,6 @@ var bunker = &cli.Command{ } } - go func() { - for uri := range onSocketConnect(ctx, c) { - log("received nostrconnect URI: %s\n", uri) - } - }() - // default case: persist() is nil var persist func() @@ -148,6 +136,15 @@ var bunker = &cli.Command{ if err := json.Unmarshal(b, &config); err != nil { return err } + // convert from deprecated field + if len(config.AuthorizedKeys) > 0 { + config.Clients = make([]BunkerConfigClient, len(config.AuthorizedKeys)) + for i := range config.AuthorizedKeys { + config.Clients[i] = BunkerConfigClient{PubKey: config.AuthorizedKeys[i]} + } + config.AuthorizedKeys = nil + persist() + } } else if !os.IsNotExist(err) { return err } @@ -156,7 +153,11 @@ var bunker = &cli.Command{ config.Relays[i] = nostr.NormalizeURL(url) } config.Relays = appendUnique(config.Relays, baseRelaysUrls...) - config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, baseAuthorizedKeys...) + for _, bak := range baseAuthorizedKeys { + if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool { return c.PubKey == bak }) { + config.Clients = append(config.Clients, BunkerConfigClient{PubKey: bak}) + } + } if config.Secret.Plain == nil && config.Secret.Encrypted == nil { // we don't have any secret key stored, so just use whatever was given via flags @@ -173,7 +174,9 @@ var bunker = &cli.Command{ } else { config.Secret = baseSecret config.Relays = baseRelaysUrls - config.AuthorizedKeys = baseAuthorizedKeys + for _, bak := range baseAuthorizedKeys { + config.Clients = append(config.Clients, BunkerConfigClient{PubKey: bak}) + } } // if we got here without any keys set (no flags, first time using a profile), use the default @@ -211,8 +214,17 @@ var bunker = &cli.Command{ // try to connect to the relays here qs := url.Values{} - relayURLs := make([]string, 0, len(config.Relays)) - relays := connectToAllRelays(ctx, c, config.Relays, nil, nostr.PoolOptions{}) + allRelays := make([]string, len(config.Relays), len(config.Relays)+5) + copy(allRelays, config.Relays) + for _, c := range config.Clients { + for _, url := range c.CustomRelays { + if !slices.ContainsFunc(allRelays, func(u string) bool { return u == url }) { + allRelays = append(allRelays, url) + } + } + } + relayURLs := make([]string, 0, len(allRelays)) + relays := connectToAllRelays(ctx, c, allRelays, nil, nostr.PoolOptions{}) if len(relays) == 0 { log("failed to connect to any of the given relays.\n") os.Exit(3) @@ -242,10 +254,22 @@ var bunker = &cli.Command{ bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) authorizedKeysStr := "" - if len(config.AuthorizedKeys) != 0 { - authorizedKeysStr = "\n authorized keys:" - for _, pubkey := range config.AuthorizedKeys { - authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex()) + if len(config.Clients) != 0 { + authorizedKeysStr = "\n authorized clients:" + for _, c := range config.Clients { + authorizedKeysStr += "\n - " + colors.italic(c.PubKey.Hex()) + name := "" + if c.Name != "" { + name = c.Name + if c.URL != "" { + name += " " + colors.underline(c.URL) + } + } else if c.URL != "" { + name = colors.underline(c.URL) + } + if name != "" { + authorizedKeysStr += " (" + name + ")" + } } } @@ -255,8 +279,8 @@ var bunker = &cli.Command{ } preauthorizedFlags := "" - for _, k := range config.AuthorizedKeys { - preauthorizedFlags += " -k " + k.Hex() + for _, c := range config.Clients { + preauthorizedFlags += " -k " + c.PubKey.Hex() } for _, s := range authorizedSecrets { preauthorizedFlags += " -s " + s @@ -320,28 +344,84 @@ var bunker = &cli.Command{ Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, Since: nostr.Now(), LimitZero: true, - }, nostr.SubscriptionOptions{ - Label: "nak-bunker", - }) + }, nostr.SubscriptionOptions{Label: "nak-bunker"}) signer := nip46.NewStaticKeySigner(sec) - handlerWg := sync.WaitGroup{} - printLock := sync.Mutex{} + + // unix socket nostrconnect:// handling + go func() { + for uri := range onSocketConnect(ctx, c) { + clientPublicKey, err := nostr.PubKeyFromHex(uri.Host) + if err != nil { + continue + } + log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey), uri.String()) + + relays := uri.Query()["relay"] + + // pre-authorize this client since the user has explicitly added it + if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool { + return c.PubKey == clientPublicKey + }) { + config.Clients = append(config.Clients, BunkerConfigClient{ + PubKey: clientPublicKey, + Name: uri.Query().Get("name"), + URL: uri.Query().Get("url"), + Icon: uri.Query().Get("icon"), + CustomRelays: relays, + }) + } + + if persist != nil { + persist() + } + + resp, eventResponse, err := signer.HandleNostrConnectURI(ctx, uri) + if err != nil { + log("* failed to handle: %s\n", err) + continue + } + + go func() { + for event := range sys.Pool.SubscribeMany(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindNostrConnect}, + Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, + Since: nostr.Now(), + LimitZero: true, + }, nostr.SubscriptionOptions{Label: "nak-bunker"}) { + events <- event + } + }() + + time.Sleep(time.Millisecond * 25) + jresp, _ := json.MarshalIndent(resp, "", " ") + log("~ responding with %s\n", string(jresp)) + for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) { + if res.Error == nil { + log("* sent response through %s\n", res.Relay.URL) + } else { + log("* failed to send response: %s\n", err) + } + } + } + }() // just a gimmick var cancelPreviousBunkerInfoPrint context.CancelFunc _, cancel := context.WithCancel(ctx) cancelPreviousBunkerInfoPrint = cancel - // asking user for authorization signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool { - if slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) { + if slices.ContainsFunc(config.Clients, func(b BunkerConfigClient) bool { return b.PubKey == from }) { + return true + } + if slices.Contains(authorizedSecrets, secret) { return true } if secret == newSecret { // store this key - config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from) + config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from}) // discard this and generate a new secret newSecret = randString(12) // print bunker info again after this @@ -364,34 +444,35 @@ var bunker = &cli.Command{ cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks // handle the NIP-46 request event + from := ie.Event.PubKey req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) if err != nil { - log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) + log("< failed to handle request from %s: %s\n", from, err.Error()) continue } jreq, _ := json.MarshalIndent(req, "", " ") - log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) + log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq)) jresp, _ := json.MarshalIndent(resp, "", " ") log("~ responding with %s\n", string(jresp)) - handlerWg.Add(len(relayURLs)) - for _, relayURL := range relayURLs { - go func(relayURL string) { - defer handlerWg.Done() - if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { - err := relay.Publish(ctx, eventResponse) - printLock.Lock() - if err == nil { - log("* sent response through %s\n", relay.URL) - } else { - log("* failed to send response: %s\n", err) - } - printLock.Unlock() - } - }(relayURL) + // use custom relays if they are defined for this client + // (normally if the initial connection came from a nostrconnect:// URL) + relays := relayURLs + for _, c := range config.Clients { + if c.PubKey == from && len(c.CustomRelays) > 0 { + relays = c.CustomRelays + break + } + } + + for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) { + if res.Error == nil { + log("* sent response through %s\n", res.Relay.URL) + } else { + log("* failed to send response: %s\n", err) + } } - handlerWg.Wait() // just after handling one request we trigger this go func() { @@ -433,6 +514,23 @@ var bunker = &cli.Command{ }, } +type BunkerConfig struct { + Clients []BunkerConfigClient `json:"clients"` + Secret plainOrEncryptedKey `json:"sec"` + Relays []string `json:"relays"` + + // deprecated + AuthorizedKeys []nostr.PubKey `json:"authorized-keys,omitempty"` +} + +type BunkerConfigClient struct { + PubKey nostr.PubKey `json:"pubkey"` + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + Icon string `json:"icon,omitempty"` + CustomRelays []string `json:"custom_relays,omitempty"` +} + type plainOrEncryptedKey struct { Plain *nostr.SecretKey Encrypted *string @@ -500,3 +598,63 @@ func (a plainOrEncryptedKey) equals(b plainOrEncryptedKey) bool { return true } + +func onSocketConnect(ctx context.Context, c *cli.Command) chan *url.URL { + res := make(chan *url.URL) + + listener, err := net.Listen("tcp", ":22222") + if err != nil { + log(color.RedString("failed to listen on TCP port 22222: %w\n", err)) + return res + } + + go func() { + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + select { + case <-ctx.Done(): + return + default: + continue + } + } + + go func(conn net.Conn) { + defer conn.Close() + buf := make([]byte, 4096) + + for { + conn.SetReadDeadline(time.Now().Add(5 * time.Second)) + n, err := conn.Read(buf) + if err != nil { + break + } + + uri, err := url.Parse(string(buf[:n])) + if err == nil && uri.Scheme == "nostrconnect" { + res <- uri + } + } + }(conn) + } + }() + + return res +} + +func sendToSocket(c *cli.Command, value string) error { + conn, err := net.DialTimeout("tcp", "127.0.0.1:22222", 5*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to bunker TCP socket at 127.0.0.1:22222: %w", err) + } + defer conn.Close() + + _, err = conn.Write([]byte(value)) + if err != nil { + return fmt.Errorf("failed to send uri to bunker: %w", err) + } + return nil +} diff --git a/bunkerconn.go b/bunkerconn.go deleted file mode 100644 index 10cbcca..0000000 --- a/bunkerconn.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "context" - "fmt" - "net" - "net/url" - "os" - "path/filepath" - - "github.com/fatih/color" - "github.com/urfave/cli/v3" -) - -func onSocketConnect(ctx context.Context, c *cli.Command) chan *url.URL { - res := make(chan *url.URL) - - socketPath := getSocketPath(c) - if _, err := os.Stat(socketPath); err == nil { - // file exists, we must delete it (or not) - os.Remove(socketPath) - } else if !os.IsNotExist(err) { - log(color.RedString("failed to check on unix socket: %w\n", err)) - return res - } - - // start unix socket listener - os.MkdirAll(filepath.Dir(socketPath), 0755) - - listener, err := net.Listen("unix", socketPath) - if err != nil { - log(color.RedString("failed to listen on unix socket: %w\n", err)) - return res - } - - // handle unix socket connections in background - go func() { - defer listener.Close() - - // clean up socket file on exit - // (irrelevant, as we clean it on startup, but just to keep the user filesystem sane) - defer os.Remove(socketPath) - - for { - conn, err := listener.Accept() - if err != nil { - continue - } - - defer conn.Close() - buf := make([]byte, 4096) - - for { - n, err := conn.Read(buf) - if err != nil { - break - } - - uri, err := url.Parse(string(buf[:n])) - if err == nil && uri.Scheme == "nostrconnect" { - res <- uri - } - } - } - }() - - return res -} - -func sendToSocket(c *cli.Command, value string) error { - socketPath := getSocketPath(c) - - // connect to unix socket - conn, err := net.Dial("unix", socketPath) - if err != nil { - return fmt.Errorf("failed to connect to bunker unix socket at %s: %w", socketPath, err) - } - defer conn.Close() - - // send the uri - _, err = conn.Write([]byte(value)) - if err != nil { - return fmt.Errorf("failed to send uri to bunker: %w", err) - } - return nil -} - -func getSocketPath(c *cli.Command) string { - profile := "any" - if c.Bool("persist") || c.IsSet("profile") { - profile = c.String("profile") - } - return filepath.Join(c.String("config-path"), "bunkerconn", profile+".sock") -} diff --git a/go.mod b/go.mod index 6785167..bcded08 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/fiatjaf/nak go 1.25 require ( - fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4 + fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bep/debounce v1.2.1 github.com/btcsuite/btcd/btcec/v2 v2.3.6 diff --git a/go.sum b/go.sum index 8aac35e..6e9a334 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q= fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8= -fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4 h1:/6AVjHIbbgyuiilcUuoFPMXGNXqialKGQM7uskF0b/0= -fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= +fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4 h1:DF/4NSbCvXqIIRrwYp7L3S0SqC7/IhQl8mHkmYA5uXM= +fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc= diff --git a/helpers.go b/helpers.go index 6ae6e4f..69affa5 100644 --- a/helpers.go +++ b/helpers.go @@ -536,21 +536,25 @@ func decodeTagValue(value string) string { } var colors = struct { - reset func(...any) (int, error) - italic func(...any) string - italicf func(string, ...any) string - bold func(...any) string - boldf func(string, ...any) string - error func(...any) string - errorf func(string, ...any) string - success func(...any) string - successf func(string, ...any) string + reset func(...any) (int, error) + italic func(...any) string + italicf func(string, ...any) string + bold func(...any) string + boldf func(string, ...any) string + underline func(...any) string + underlinef func(string, ...any) string + error func(...any) string + errorf func(string, ...any) string + success func(...any) string + successf func(string, ...any) string }{ color.New(color.Reset).Print, color.New(color.Italic).Sprint, color.New(color.Italic).Sprintf, color.New(color.Bold).Sprint, color.New(color.Bold).Sprintf, + color.New(color.Underline).Sprint, + color.New(color.Underline).Sprintf, color.New(color.Bold, color.FgHiRed).Sprint, color.New(color.Bold, color.FgHiRed).Sprintf, color.New(color.Bold, color.FgHiGreen).Sprint,