diff --git a/README.md b/README.md index 7d64ea8..7cade1f 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ type the password to decrypt your secret key: ********** ### talk to a relay's NIP-86 management API ```shell -nak relay allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com +nak admin allowpubkey --sec ncryptsec1qggx54cg270zy9y8krwmfz29jyypsuxken2fkk99gr52qhje968n6mwkrfstqaqhq9eq94pnzl4nff437l4lp4ur2cs4f9um8738s35l2esx2tas48thtfhrk5kq94pf9j2tpk54yuermra0xu6hl5ls --pubkey a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208 pyramid.fiatjaf.com type the password to decrypt your secret key: ********** calling 'allowpubkey' on https://pyramid.fiatjaf.com... { diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..f25fc71 --- /dev/null +++ b/admin.go @@ -0,0 +1,186 @@ +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "net/http" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip86" + "github.com/urfave/cli/v3" +) + +var admin = &cli.Command{ + Name: "admin", + Usage: "manage relays using the relay management API", + Description: `examples: + nak admin allowpubkey myrelay.com --pubkey 1234... --reason "good user" + nak admin banpubkey myrelay.com --pubkey 1234... --reason "spam" + nak admin listallowedpubkeys myrelay.com + nak admin changerelayname myrelay.com --name "My Relay"`, + ArgsUsage: "", + DisableSliceFlagSeparator: true, + Flags: defaultKeyFlags, + Commands: (func() []*cli.Command { + methods := []struct { + method string + args []string + }{ + {"allowpubkey", []string{"pubkey", "reason"}}, + {"banpubkey", []string{"pubkey", "reason"}}, + {"listallowedpubkeys", nil}, + {"listbannedpubkeys", nil}, + {"listeventsneedingmoderation", nil}, + {"allowevent", []string{"id", "reason"}}, + {"banevent", []string{"id", "reason"}}, + {"listbannedevents", nil}, + {"changerelayname", []string{"name"}}, + {"changerelaydescription", []string{"description"}}, + {"changerelayicon", []string{"icon"}}, + {"allowkind", []string{"kind"}}, + {"disallowkind", []string{"kind"}}, + {"listallowedkinds", nil}, + {"blockip", []string{"ip", "reason"}}, + {"unblockip", []string{"ip", "reason"}}, + {"listblockedips", nil}, + } + + commands := make([]*cli.Command, 0, len(methods)) + for _, def := range methods { + def := def + + flags := make([]cli.Flag, len(def.args), len(def.args)+4) + for i, argName := range def.args { + flags[i] = declareFlag(argName) + } + + cmd := &cli.Command{ + Name: def.method, + Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), + Description: fmt.Sprintf( + `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), + Flags: flags, + DisableSliceFlagSeparator: true, + Action: func(ctx context.Context, c *cli.Command) error { + params := make([]any, len(def.args)) + for i, argName := range def.args { + params[i] = getArgument(c, argName) + } + req := nip86.Request{Method: def.method, Params: params} + reqj, _ := json.Marshal(req) + + relayUrls := c.Args().Slice() + if len(relayUrls) == 0 { + stdout(string(reqj)) + return nil + } + + kr, _, err := gatherKeyerFromArguments(ctx, c) + if err != nil { + return err + } + + for _, relayUrl := range relayUrls { + httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] + log("calling '%s' on %s... ", def.method, httpUrl) + body := bytes.NewBuffer(nil) + body.Write(reqj) + req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Authorization + payloadHash := sha256.Sum256(reqj) + tokenEvent := nostr.Event{ + Kind: 27235, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + {"u", httpUrl}, + {"method", "POST"}, + {"payload", hex.EncodeToString(payloadHash[:])}, + }, + } + if err := kr.SignEvent(ctx, &tokenEvent); err != nil { + return fmt.Errorf("failed to sign token event: %w", err) + } + evtj, _ := json.Marshal(tokenEvent) + req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) + + // Content-Type + req.Header.Set("Content-Type", "application/nostr+json+rpc") + + // make request to relay + resp, err := http.DefaultClient.Do(req) + if err != nil { + log("failed: %s\n", err) + continue + } + b, err := io.ReadAll(resp.Body) + if err != nil { + log("failed to read response: %s\n", err) + continue + } + if resp.StatusCode >= 300 { + log("failed with status %d\n", resp.StatusCode) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + var response nip86.Response + if err := json.Unmarshal(b, &response); err != nil { + log("bad json response: %s\n", err) + bodyPrintable := string(b) + if len(bodyPrintable) > 300 { + bodyPrintable = bodyPrintable[0:297] + "..." + } + log(bodyPrintable) + continue + } + resp.Body.Close() + + // print the result + log("\n") + pretty, _ := json.MarshalIndent(response, "", " ") + stdout(string(pretty)) + } + + return nil + }, + } + + commands = append(commands, cmd) + } + + return commands + })(), +} + +func declareFlag(argName string) cli.Flag { + usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information." + switch argName { + case "kind": + return &cli.IntFlag{Name: argName, Required: true, Usage: usage} + case "reason": + return &cli.StringFlag{Name: argName, Usage: usage} + default: + return &cli.StringFlag{Name: argName, Required: true, Usage: usage} + } +} + +func getArgument(c *cli.Command, argName string) any { + switch argName { + case "kind": + return c.Int(argName) + default: + return c.String(argName) + } +} diff --git a/go.mod b/go.mod index 2cc9575..7cb2f34 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/coder/websocket v1.8.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect - github.com/dgraph-io/ristretto v1.0.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/pie/v2 v2.7.0 // indirect github.com/elnosh/gonuts v0.4.2 // indirect @@ -55,7 +55,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect @@ -72,7 +71,7 @@ require ( golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 45585c8..4f09f0b 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= -github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= +github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= +github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -222,8 +222,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 8e683ef..b1689b5 100644 --- a/main.go +++ b/main.go @@ -36,6 +36,7 @@ var app = &cli.Command{ key, verify, relay, + admin, bunker, serve, blossomCmd, diff --git a/relay.go b/relay.go index 450ca33..5b0ff9c 100644 --- a/relay.go +++ b/relay.go @@ -1,30 +1,19 @@ package main import ( - "bytes" "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" "fmt" - "io" - "net/http" - "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip11" - "fiatjaf.com/nostr/nip86" "github.com/urfave/cli/v3" ) var relay = &cli.Command{ Name: "relay", - Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.", - Description: `examples: - fetching relay information: + Usage: "gets the relay information document for the given relay, as JSON", + Description: ` nak relay nostr.wine - - managing a relay - nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`, +`, ArgsUsage: "", DisableSliceFlagSeparator: true, Action: func(ctx context.Context, c *cli.Command) error { @@ -44,164 +33,4 @@ var relay = &cli.Command{ } return nil }, - Commands: (func() []*cli.Command { - methods := []struct { - method string - args []string - }{ - {"allowpubkey", []string{"pubkey", "reason"}}, - {"banpubkey", []string{"pubkey", "reason"}}, - {"listallowedpubkeys", nil}, - {"allowpubkey", []string{"pubkey", "reason"}}, - {"listallowedpubkeys", nil}, - {"listeventsneedingmoderation", nil}, - {"allowevent", []string{"id", "reason"}}, - {"banevent", []string{"id", "reason"}}, - {"listbannedevents", nil}, - {"changerelayname", []string{"name"}}, - {"changerelaydescription", []string{"description"}}, - {"changerelayicon", []string{"icon"}}, - {"allowkind", []string{"kind"}}, - {"disallowkind", []string{"kind"}}, - {"listallowedkinds", nil}, - {"blockip", []string{"ip", "reason"}}, - {"unblockip", []string{"ip", "reason"}}, - {"listblockedips", nil}, - } - - commands := make([]*cli.Command, 0, len(methods)) - for _, def := range methods { - def := def - - flags := make([]cli.Flag, len(def.args), len(def.args)+4) - for i, argName := range def.args { - flags[i] = declareFlag(argName) - } - - flags = append(flags, defaultKeyFlags...) - - cmd := &cli.Command{ - Name: def.method, - Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method), - Description: fmt.Sprintf( - `the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method), - Flags: flags, - DisableSliceFlagSeparator: true, - Action: func(ctx context.Context, c *cli.Command) error { - params := make([]any, len(def.args)) - for i, argName := range def.args { - params[i] = getArgument(c, argName) - } - req := nip86.Request{Method: def.method, Params: params} - reqj, _ := json.Marshal(req) - - relayUrls := c.Args().Slice() - if len(relayUrls) == 0 { - stdout(string(reqj)) - return nil - } - - kr, _, err := gatherKeyerFromArguments(ctx, c) - if err != nil { - return err - } - - for _, relayUrl := range relayUrls { - httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:] - log("calling '%s' on %s... ", def.method, httpUrl) - body := bytes.NewBuffer(nil) - body.Write(reqj) - req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Authorization - payloadHash := sha256.Sum256(reqj) - tokenEvent := nostr.Event{ - Kind: 27235, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - {"u", httpUrl}, - {"method", "POST"}, - {"payload", hex.EncodeToString(payloadHash[:])}, - }, - } - if err := kr.SignEvent(ctx, &tokenEvent); err != nil { - return fmt.Errorf("failed to sign token event: %w", err) - } - evtj, _ := json.Marshal(tokenEvent) - req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj)) - - // Content-Type - req.Header.Set("Content-Type", "application/nostr+json+rpc") - - // make request to relay - resp, err := http.DefaultClient.Do(req) - if err != nil { - log("failed: %s\n", err) - continue - } - b, err := io.ReadAll(resp.Body) - if err != nil { - log("failed to read response: %s\n", err) - continue - } - if resp.StatusCode >= 300 { - log("failed with status %d\n", resp.StatusCode) - bodyPrintable := string(b) - if len(bodyPrintable) > 300 { - bodyPrintable = bodyPrintable[0:297] + "..." - } - log(bodyPrintable) - continue - } - var response nip86.Response - if err := json.Unmarshal(b, &response); err != nil { - log("bad json response: %s\n", err) - bodyPrintable := string(b) - if len(bodyPrintable) > 300 { - bodyPrintable = bodyPrintable[0:297] + "..." - } - log(bodyPrintable) - continue - } - resp.Body.Close() - - // print the result - log("\n") - pretty, _ := json.MarshalIndent(response, "", " ") - stdout(string(pretty)) - } - - return nil - }, - } - - commands = append(commands, cmd) - } - - return commands - })(), -} - -func declareFlag(argName string) cli.Flag { - usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information." - switch argName { - case "kind": - return &cli.IntFlag{Name: argName, Required: true, Usage: usage} - case "reason": - return &cli.StringFlag{Name: argName, Usage: usage} - default: - return &cli.StringFlag{Name: argName, Required: true, Usage: usage} - } -} - -func getArgument(c *cli.Command, argName string) any { - switch argName { - case "kind": - return c.Int(argName) - default: - return c.String(argName) - } }