mirror of https://github.com/fiatjaf/nak.git
nak bunker --persist/--profile
This commit is contained in:
parent
6e4a546212
commit
0aef173e8b
247
bunker.go
247
bunker.go
|
@ -1,10 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -17,6 +20,8 @@ import (
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const PERSISTENCE = "PERSISTENCE"
|
||||||
|
|
||||||
var bunker = &cli.Command{
|
var bunker = &cli.Command{
|
||||||
Name: "bunker",
|
Name: "bunker",
|
||||||
Usage: "starts a nip46 signer daemon with the given --sec key",
|
Usage: "starts a nip46 signer daemon with the given --sec key",
|
||||||
|
@ -24,6 +29,18 @@ var bunker = &cli.Command{
|
||||||
Description: ``,
|
Description: ``,
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "persist",
|
||||||
|
Usage: "whether to read and store authorized keys from and to a config file",
|
||||||
|
Category: PERSISTENCE,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "profile",
|
||||||
|
Value: "default",
|
||||||
|
Usage: "config file name to use for --persist mode (implies that if provided) -- based on --config-path, i.e. ~/.config/nak/",
|
||||||
|
OnlyOnce: true,
|
||||||
|
Category: PERSISTENCE,
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event, as hex or nsec",
|
Usage: "secret key to sign the event, as hex or nsec",
|
||||||
|
@ -43,13 +60,134 @@ var bunker = &cli.Command{
|
||||||
Aliases: []string{"k"},
|
Aliases: []string{"k"},
|
||||||
Usage: "pubkeys for which we will always respond",
|
Usage: "pubkeys for which we will always respond",
|
||||||
},
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "relay",
|
||||||
|
Usage: "relays to connect to (can also be provided as naked arguments)",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
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),
|
||||||
|
}
|
||||||
|
baseRelaysUrls := appendUnique(c.Args().Slice(), c.StringSlice("relay")...)
|
||||||
|
for i, url := range baseRelaysUrls {
|
||||||
|
baseRelaysUrls[i] = nostr.NormalizeURL(url)
|
||||||
|
}
|
||||||
|
baseAuthorizedKeys := getPubKeySlice(c, "authorized-keys")
|
||||||
|
|
||||||
|
var baseSecret plainOrEncryptedKey
|
||||||
|
{
|
||||||
|
sec := c.String("sec")
|
||||||
|
if c.Bool("prompt-sec") {
|
||||||
|
var err error
|
||||||
|
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get secret key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(sec, "ncryptsec1") {
|
||||||
|
baseSecret.Encrypted = &sec
|
||||||
|
} else {
|
||||||
|
if sec == "" {
|
||||||
|
sec = os.Getenv("NOSTR_SECRET_KEY")
|
||||||
|
if sec == "" {
|
||||||
|
sec = defaultKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||||
|
sk := ski.(nostr.SecretKey)
|
||||||
|
baseSecret.Plain = &sk
|
||||||
|
} else if sk, err := nostr.SecretKeyFromHex(sec); err != nil {
|
||||||
|
return fmt.Errorf("invalid secret key: %w", err)
|
||||||
|
} else {
|
||||||
|
baseSecret.Plain = &sk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default case: persist() is nil
|
||||||
|
var persist func()
|
||||||
|
|
||||||
|
if c.Bool("persist") || c.IsSet("profile") {
|
||||||
|
path := filepath.Join(c.String("config-path"), "bunker")
|
||||||
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path = filepath.Join(path, c.String("profile"))
|
||||||
|
|
||||||
|
persist = func() {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
log(color.RedString("failed to persist: %w\n"), err)
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log(color.RedString("failed to persist: %w\n"), err)
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||||
|
log(color.RedString("failed to persist: %w\n"), err)
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(color.YellowString("reading config from %s\n"), path)
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
if err := json.Unmarshal(b, &config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, url := range config.Relays {
|
||||||
|
config.Relays[i] = nostr.NormalizeURL(url)
|
||||||
|
}
|
||||||
|
config.Relays = appendUnique(config.Relays, baseRelaysUrls...)
|
||||||
|
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, baseAuthorizedKeys...)
|
||||||
|
|
||||||
|
if config.Secret.Plain == nil && config.Secret.Encrypted == nil {
|
||||||
|
config.Secret = baseSecret
|
||||||
|
} else if !baseSecret.equals(config.Secret) {
|
||||||
|
return fmt.Errorf("--sec provided conflicts with stored, you should create a new --profile or omit the --sec flag")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.Secret = baseSecret
|
||||||
|
config.Relays = baseRelaysUrls
|
||||||
|
config.AuthorizedKeys = baseAuthorizedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Relays) == 0 {
|
||||||
|
return fmt.Errorf("no relays given")
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt key here if necessary
|
||||||
|
var sec nostr.SecretKey
|
||||||
|
if config.Secret.Plain != nil {
|
||||||
|
sec = *config.Secret.Plain
|
||||||
|
} else {
|
||||||
|
plain, err := promptDecrypt(*config.Secret.Encrypted)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decrypt: %w", err)
|
||||||
|
}
|
||||||
|
sec = plain
|
||||||
|
}
|
||||||
|
|
||||||
|
if persist != nil {
|
||||||
|
persist()
|
||||||
|
}
|
||||||
|
|
||||||
// try to connect to the relays here
|
// try to connect to the relays here
|
||||||
qs := url.Values{}
|
qs := url.Values{}
|
||||||
relayURLs := make([]string, 0, c.Args().Len())
|
relayURLs := make([]string, 0, len(config.Relays))
|
||||||
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
relays := connectToAllRelays(ctx, c, config.Relays, nil, nostr.PoolOptions{})
|
||||||
relays := connectToAllRelays(ctx, c, relayUrls, nil, nostr.PoolOptions{})
|
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
log("failed to connect to any of the given relays.\n")
|
log("failed to connect to any of the given relays.\n")
|
||||||
os.Exit(3)
|
os.Exit(3)
|
||||||
|
@ -58,19 +196,11 @@ var bunker = &cli.Command{
|
||||||
relayURLs = append(relayURLs, relay.URL)
|
relayURLs = append(relayURLs, relay.URL)
|
||||||
qs.Add("relay", relay.URL)
|
qs.Add("relay", relay.URL)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if len(relayURLs) == 0 {
|
if len(relayURLs) == 0 {
|
||||||
return fmt.Errorf("not connected to any relays: please specify at least one")
|
return fmt.Errorf("not connected to any relays: please specify at least one")
|
||||||
}
|
}
|
||||||
|
|
||||||
// gather the secret key
|
|
||||||
sec, _, err := gatherSecretKeyOrBunkerFromArguments(ctx, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// other arguments
|
// other arguments
|
||||||
authorizedKeys := getPubKeySlice(c, "authorized-keys")
|
|
||||||
authorizedSecrets := c.StringSlice("authorized-secrets")
|
authorizedSecrets := c.StringSlice("authorized-secrets")
|
||||||
|
|
||||||
// this will be used to auto-authorize the next person who connects who isn't pre-authorized
|
// this will be used to auto-authorize the next person who connects who isn't pre-authorized
|
||||||
|
@ -87,9 +217,9 @@ var bunker = &cli.Command{
|
||||||
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
|
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
|
||||||
|
|
||||||
authorizedKeysStr := ""
|
authorizedKeysStr := ""
|
||||||
if len(authorizedKeys) != 0 {
|
if len(config.AuthorizedKeys) != 0 {
|
||||||
authorizedKeysStr = "\n authorized keys:"
|
authorizedKeysStr = "\n authorized keys:"
|
||||||
for _, pubkey := range authorizedKeys {
|
for _, pubkey := range config.AuthorizedKeys {
|
||||||
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex())
|
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +230,7 @@ var bunker = &cli.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
preauthorizedFlags := ""
|
preauthorizedFlags := ""
|
||||||
for _, k := range authorizedKeys {
|
for _, k := range config.AuthorizedKeys {
|
||||||
preauthorizedFlags += " -k " + k.Hex()
|
preauthorizedFlags += " -k " + k.Hex()
|
||||||
}
|
}
|
||||||
for _, s := range authorizedSecrets {
|
for _, s := range authorizedSecrets {
|
||||||
|
@ -121,6 +251,8 @@ var bunker = &cli.Command{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only print the restart command if not persisting:
|
||||||
|
if persist == nil {
|
||||||
restartCommand := fmt.Sprintf("nak bunker %s%s %s",
|
restartCommand := fmt.Sprintf("nak bunker %s%s %s",
|
||||||
secretKeyFlag,
|
secretKeyFlag,
|
||||||
preauthorizedFlags,
|
preauthorizedFlags,
|
||||||
|
@ -136,6 +268,17 @@ var bunker = &cli.Command{
|
||||||
color.CyanString(restartCommand),
|
color.CyanString(restartCommand),
|
||||||
colors.bold(bunkerURI),
|
colors.bold(bunkerURI),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// otherwise just print the data
|
||||||
|
log("listening at %v:\n pubkey: %s \n npub: %s%s%s\n bunker: %s\n\n",
|
||||||
|
colors.bold(relayURLs),
|
||||||
|
colors.bold(pubkey.Hex()),
|
||||||
|
colors.bold(npub),
|
||||||
|
authorizedKeysStr,
|
||||||
|
authorizedSecretsStr,
|
||||||
|
colors.bold(bunkerURI),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
|
|
||||||
|
@ -162,7 +305,7 @@ var bunker = &cli.Command{
|
||||||
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||||
if secret == newSecret {
|
if secret == newSecret {
|
||||||
// store this key
|
// store this key
|
||||||
authorizedKeys = append(authorizedKeys, from)
|
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from)
|
||||||
// discard this and generate a new secret
|
// discard this and generate a new secret
|
||||||
newSecret = randString(12)
|
newSecret = randString(12)
|
||||||
// print bunker info again after this
|
// print bunker info again after this
|
||||||
|
@ -170,9 +313,13 @@ var bunker = &cli.Command{
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
printBunkerInfo()
|
printBunkerInfo()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if persist != nil {
|
||||||
|
persist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices.Contains(authorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
return slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
for ie := range events {
|
for ie := range events {
|
||||||
|
@ -248,3 +395,71 @@ var bunker = &cli.Command{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type plainOrEncryptedKey struct {
|
||||||
|
Plain *nostr.SecretKey
|
||||||
|
Encrypted *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe plainOrEncryptedKey) MarshalJSON() ([]byte, error) {
|
||||||
|
if pe.Plain != nil {
|
||||||
|
res := make([]byte, 66)
|
||||||
|
hex.Encode(res[1:], (*pe.Plain)[:])
|
||||||
|
res[0] = '"'
|
||||||
|
res[65] = '"'
|
||||||
|
return res, nil
|
||||||
|
} else if pe.Encrypted != nil {
|
||||||
|
return json.Marshal(*pe.Encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no key to marshal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe *plainOrEncryptedKey) UnmarshalJSON(buf []byte) error {
|
||||||
|
if len(buf) == 66 {
|
||||||
|
sk, err := nostr.SecretKeyFromHex(string(buf[1 : 1+64]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pe.Plain = &sk
|
||||||
|
return nil
|
||||||
|
} else if bytes.HasPrefix(buf, []byte("\"nsec")) {
|
||||||
|
_, v, err := nip19.Decode(string(buf[1 : len(buf)-1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sk := v.(nostr.SecretKey)
|
||||||
|
pe.Plain = &sk
|
||||||
|
return nil
|
||||||
|
} else if bytes.HasPrefix(buf, []byte("\"ncryptsec1")) {
|
||||||
|
ncryptsec := string(buf[1 : len(buf)-1])
|
||||||
|
pe.Encrypted = &ncryptsec
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unrecognized key format '%s'", string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a plainOrEncryptedKey) equals(b plainOrEncryptedKey) bool {
|
||||||
|
if a.Plain == nil && b.Plain != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Plain != nil && b.Plain == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Plain != nil && b.Plain != nil && *a.Plain != *b.Plain {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Encrypted == nil && b.Encrypted != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Encrypted != nil && b.Encrypted == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.Encrypted != nil && b.Encrypted != nil && *a.Encrypted != *b.Encrypted {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -17,14 +17,16 @@ import (
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultKey = nostr.KeyOne.Hex()
|
||||||
|
|
||||||
var defaultKeyFlags = []cli.Flag{
|
var defaultKeyFlags = []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sec",
|
Name: "sec",
|
||||||
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
Usage: "secret key to sign the event, as nsec, ncryptsec or hex, or a bunker URL",
|
||||||
DefaultText: "the key '1'",
|
DefaultText: "the key '01'",
|
||||||
Category: CATEGORY_SIGNER,
|
Category: CATEGORY_SIGNER,
|
||||||
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
Sources: cli.EnvVars("NOSTR_SECRET_KEY"),
|
||||||
Value: nostr.KeyOne.Hex(),
|
Value: defaultKey,
|
||||||
HideDefault: true,
|
HideDefault: true,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
|
|
10
main.go
10
main.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/sdk"
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
@ -52,6 +53,13 @@ var app = &cli.Command{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "config-path",
|
Name: "config-path",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
|
Value: (func() string {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
return filepath.Join(home, ".config/nak")
|
||||||
|
} else {
|
||||||
|
return filepath.Join("/dev/null")
|
||||||
|
}
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "quiet",
|
Name: "quiet",
|
||||||
|
@ -125,7 +133,7 @@ func main() {
|
||||||
|
|
||||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("%s\n", color.YellowString(err.Error()))
|
log("%s\n", color.RedString(err.Error()))
|
||||||
}
|
}
|
||||||
colors.reset()
|
colors.reset()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -20,11 +20,6 @@ var (
|
||||||
|
|
||||||
func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
||||||
configPath := c.String("config-path")
|
configPath := c.String("config-path")
|
||||||
if configPath == "" {
|
|
||||||
if home, err := os.UserHomeDir(); err == nil {
|
|
||||||
configPath = filepath.Join(home, ".config/nak")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
hintsFilePath = filepath.Join(configPath, "outbox/hints.bg")
|
hintsFilePath = filepath.Join(configPath, "outbox/hints.bg")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue