2023-12-02 10:18:55 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-02-17 15:56:57 -05:00
|
|
|
"context"
|
2023-12-02 10:18:55 -05:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
2024-02-16 09:08:48 -05:00
|
|
|
"sync"
|
2024-02-17 15:56:57 -05:00
|
|
|
"time"
|
2023-12-02 10:18:55 -05:00
|
|
|
|
2024-02-12 13:39:13 -05:00
|
|
|
"github.com/fatih/color"
|
2023-12-02 10:18:55 -05:00
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
|
|
"github.com/nbd-wtf/go-nostr/nip19"
|
|
|
|
"github.com/nbd-wtf/go-nostr/nip46"
|
|
|
|
"github.com/urfave/cli/v2"
|
2023-12-09 15:42:01 -05:00
|
|
|
"golang.org/x/exp/slices"
|
2023-12-02 10:18:55 -05:00
|
|
|
)
|
|
|
|
|
2023-12-09 14:37:47 -05:00
|
|
|
var bunker = &cli.Command{
|
|
|
|
Name: "bunker",
|
2023-12-02 10:18:55 -05:00
|
|
|
Usage: "starts a NIP-46 signer daemon with the given --sec key",
|
|
|
|
ArgsUsage: "[relay...]",
|
|
|
|
Description: ``,
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "sec",
|
|
|
|
Usage: "secret key to sign the event, as hex or nsec",
|
|
|
|
DefaultText: "the key '1'",
|
|
|
|
Value: "0000000000000000000000000000000000000000000000000000000000000001",
|
|
|
|
},
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "prompt-sec",
|
|
|
|
Usage: "prompt the user to paste a hex or nsec with which to sign the event",
|
|
|
|
},
|
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: "yes",
|
|
|
|
Aliases: []string{"y"},
|
|
|
|
Usage: "always respond to any NIP-46 requests from anyone",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
// try to connect to the relays here
|
|
|
|
qs := url.Values{}
|
|
|
|
relayURLs := make([]string, 0, c.Args().Len())
|
|
|
|
if relayUrls := c.Args().Slice(); len(relayUrls) > 0 {
|
|
|
|
_, relays := connectToAllRelays(c.Context, relayUrls)
|
|
|
|
if len(relays) == 0 {
|
|
|
|
log("failed to connect to any of the given relays.\n")
|
|
|
|
os.Exit(3)
|
|
|
|
}
|
|
|
|
for _, relay := range relays {
|
|
|
|
relayURLs = append(relayURLs, relay.URL)
|
|
|
|
qs.Add("relay", relay.URL)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(relayURLs) == 0 {
|
|
|
|
return fmt.Errorf("not connected to any relays: please specify at least one")
|
|
|
|
}
|
|
|
|
|
|
|
|
// gather the secret key
|
2024-02-05 22:58:26 -05:00
|
|
|
sec, _, err := gatherSecretKeyOrBunkerFromArguments(c)
|
2023-12-02 10:18:55 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
pubkey, err := nostr.GetPublicKey(sec)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
npub, _ := nip19.EncodePublicKey(pubkey)
|
2024-02-17 15:56:57 -05:00
|
|
|
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey, qs.Encode())
|
2024-02-12 13:39:13 -05:00
|
|
|
bold := color.New(color.Bold).Sprint
|
2024-02-17 15:56:57 -05:00
|
|
|
|
|
|
|
printBunkerInfo := func() {
|
|
|
|
log("listening at %v:\n pubkey: %s \n npub: %s\n bunker: %s\n\n",
|
|
|
|
bold(relayURLs),
|
|
|
|
bold(pubkey),
|
|
|
|
bold(npub),
|
|
|
|
bold(bunkerURI),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
printBunkerInfo()
|
2023-12-02 10:18:55 -05:00
|
|
|
|
|
|
|
alwaysYes := c.Bool("yes")
|
|
|
|
|
|
|
|
// subscribe to relays
|
|
|
|
pool := nostr.NewSimplePool(c.Context)
|
|
|
|
events := pool.SubMany(c.Context, relayURLs, nostr.Filters{
|
|
|
|
{
|
|
|
|
Kinds: []int{24133},
|
|
|
|
Tags: nostr.TagMap{"p": []string{pubkey}},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2024-01-11 19:41:50 -05:00
|
|
|
signer := nip46.NewStaticKeySigner(sec)
|
2024-02-16 09:08:48 -05:00
|
|
|
handlerWg := sync.WaitGroup{}
|
|
|
|
printLock := sync.Mutex{}
|
2024-02-17 15:56:57 -05:00
|
|
|
|
|
|
|
// just a gimmick
|
|
|
|
var cancelPreviousBunkerInfoPrint context.CancelFunc
|
|
|
|
_, cancel := context.WithCancel(c.Context)
|
|
|
|
cancelPreviousBunkerInfoPrint = cancel
|
|
|
|
|
2024-03-02 06:18:40 -05:00
|
|
|
// asking user for authorization
|
|
|
|
signer.AuthorizeRequest = func(harmless bool, from string) bool {
|
|
|
|
return alwaysYes || harmless || askProceed(from)
|
|
|
|
}
|
|
|
|
|
2023-12-02 10:18:55 -05:00
|
|
|
for ie := range events {
|
2024-02-17 15:56:57 -05:00
|
|
|
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
|
|
|
|
|
|
|
|
// handle the NIP-46 request event
|
2024-03-02 06:18:40 -05:00
|
|
|
req, resp, eventResponse, err := signer.HandleRequest(ie.Event)
|
2023-12-02 10:18:55 -05:00
|
|
|
if err != nil {
|
2024-02-16 09:08:48 -05:00
|
|
|
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error())
|
2023-12-02 10:18:55 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
jreq, _ := json.MarshalIndent(req, " ", " ")
|
2024-02-12 13:39:13 -05:00
|
|
|
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey), string(jreq))
|
2023-12-02 10:18:55 -05:00
|
|
|
jresp, _ := json.MarshalIndent(resp, " ", " ")
|
|
|
|
log("~ responding with %s\n", string(jresp))
|
|
|
|
|
2024-03-02 06:18:40 -05:00
|
|
|
handlerWg.Add(len(relayURLs))
|
|
|
|
for _, relayURL := range relayURLs {
|
|
|
|
go func(relayURL string) {
|
|
|
|
if relay, _ := pool.EnsureRelay(relayURL); relay != nil {
|
|
|
|
err := relay.Publish(c.Context, eventResponse)
|
|
|
|
printLock.Lock()
|
|
|
|
if err == nil {
|
|
|
|
log("* sent response through %s\n", relay.URL)
|
|
|
|
} else {
|
|
|
|
log("* failed to send response: %s\n", err)
|
2024-02-12 13:39:13 -05:00
|
|
|
}
|
2024-03-02 06:18:40 -05:00
|
|
|
printLock.Unlock()
|
|
|
|
handlerWg.Done()
|
|
|
|
}
|
|
|
|
}(relayURL)
|
2023-12-02 10:18:55 -05:00
|
|
|
}
|
2024-03-02 06:18:40 -05:00
|
|
|
handlerWg.Wait()
|
2024-02-17 15:56:57 -05:00
|
|
|
|
|
|
|
// just after handling one request we trigger this
|
|
|
|
go func() {
|
|
|
|
ctx, cancel := context.WithCancel(c.Context)
|
|
|
|
defer cancel()
|
|
|
|
cancelPreviousBunkerInfoPrint = cancel
|
|
|
|
// the idea is that we will print the bunker URL again so it is easier to copy-paste by users
|
|
|
|
// but we will only do if the bunker is inactive for more than 5 minutes
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
case <-time.After(time.Minute * 5):
|
|
|
|
fmt.Fprintf(os.Stderr, "\n")
|
|
|
|
printBunkerInfo()
|
|
|
|
}
|
|
|
|
}()
|
2023-12-02 10:18:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-12-09 15:42:01 -05:00
|
|
|
var allowedSources = make([]string, 0, 2)
|
|
|
|
|
|
|
|
func askProceed(source string) bool {
|
|
|
|
if slices.Contains(allowedSources, source) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2024-02-12 13:39:13 -05:00
|
|
|
fmt.Fprintf(os.Stderr, "request from %s:\n", color.New(color.Bold, color.FgBlue).Sprint(source))
|
|
|
|
res, err := ask(" proceed to fulfill this request? (yes/no/always from this) (y/n/a): ", "",
|
|
|
|
func(answer string) bool {
|
|
|
|
if answer != "y" && answer != "n" && answer != "a" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return false
|
2023-12-02 10:18:55 -05:00
|
|
|
}
|
2024-02-12 13:39:13 -05:00
|
|
|
switch res {
|
|
|
|
case "n":
|
2023-12-09 15:42:01 -05:00
|
|
|
return false
|
2024-02-12 13:39:13 -05:00
|
|
|
case "y":
|
2023-12-09 15:42:01 -05:00
|
|
|
return true
|
2024-02-12 13:39:13 -05:00
|
|
|
case "a":
|
2023-12-09 15:42:01 -05:00
|
|
|
allowedSources = append(allowedSources, source)
|
2023-12-02 10:18:55 -05:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-12-09 15:42:01 -05:00
|
|
|
return false
|
2023-12-02 10:18:55 -05:00
|
|
|
}
|