Compare commits

..

4 Commits

5 changed files with 212 additions and 49 deletions

131
curl.go Normal file
View File

@@ -0,0 +1,131 @@
package main
import (
"context"
"encoding/base64"
"fmt"
"os"
"os/exec"
"strings"
"github.com/fiatjaf/cli/v3"
"github.com/nbd-wtf/go-nostr"
"golang.org/x/exp/slices"
)
var curlFlags []string
var curl = &cli.Command{
Name: "curl",
Usage: "calls curl but with a nip98 header",
Description: "accepts all flags and arguments exactly as they would be passed to curl.",
Flags: defaultKeyFlags,
Action: func(ctx context.Context, c *cli.Command) error {
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
// cowboy parsing of curl flags to get the data we need for nip98
var url string
var method string
var presumedMethod string
curlBodyBuildingFlags := []string{
"-d",
"--data",
"--data-binary",
"--data-ascii",
"--data-raw",
"--data-urlencode",
"-F",
"--form",
"--form-string",
"--form-escape",
"--upload-file",
}
nextIsMethod := false
for _, f := range curlFlags {
if nextIsMethod {
method = f
method, _ = strings.CutPrefix(method, `"`)
method, _ = strings.CutSuffix(method, `"`)
method = strings.ToUpper(method)
} else if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") {
url = f
} else if f == "--request" || f == "-X" {
nextIsMethod = true
continue
} else if slices.Contains(curlBodyBuildingFlags, f) ||
slices.ContainsFunc(curlBodyBuildingFlags, func(s string) bool {
return strings.HasPrefix(f, s)
}) {
presumedMethod = "POST"
}
nextIsMethod = false
}
if url == "" {
return fmt.Errorf("can't create nip98 event: target url is empty")
}
if method == "" {
if presumedMethod != "" {
method = presumedMethod
} else {
method = "GET"
}
}
// make and sign event
evt := nostr.Event{
Kind: 27235,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
{"u", url},
{"method", method},
},
}
if err := kr.SignEvent(ctx, &evt); err != nil {
return err
}
// the first 2 indexes of curlFlags were reserved for this
curlFlags[0] = "-H"
curlFlags[1] = fmt.Sprintf("Authorization: Nostr %s", base64.StdEncoding.EncodeToString([]byte(evt.String())))
// call curl
cmd := exec.Command("curl", curlFlags...)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Run()
return nil
},
}
func realCurl() error {
curlFlags = make([]string, 2, max(len(os.Args)-4, 2))
keyFlags := make([]string, 0, 5)
for i := 0; i < len(os.Args[2:]); i++ {
arg := os.Args[i+2]
if slices.ContainsFunc(defaultKeyFlags, func(f cli.Flag) bool {
bareArg, _ := strings.CutPrefix(arg, "-")
bareArg, _ = strings.CutPrefix(bareArg, "-")
return slices.Contains(f.Names(), bareArg)
}) {
keyFlags = append(keyFlags, arg)
if arg != "--prompt-sec" {
i++
val := os.Args[i+2]
keyFlags = append(keyFlags, val)
}
} else {
curlFlags = append(curlFlags, arg)
}
}
return curl.Run(context.Background(), keyFlags)
}

View File

@@ -154,21 +154,20 @@ example:
doAuth := c.Bool("auth") doAuth := c.Bool("auth")
// then process input and generate events // then process input and generate events:
for stdinEvent := range getStdinLinesOrBlank() {
evt := nostr.Event{
Tags: make(nostr.Tags, 0, 3),
}
kindWasSupplied := false // will reuse this
var evt nostr.Event
// this is called when we have a valid json from stdin
handleEvent := func(stdinEvent string) error {
evt.Content = ""
kindWasSupplied := strings.Contains(stdinEvent, `"kind"`)
mustRehashAndResign := false mustRehashAndResign := false
if stdinEvent != "" { if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil {
if err := easyjson.Unmarshal([]byte(stdinEvent), &evt); err != nil { return fmt.Errorf("invalid event received from stdin: %s", err)
ctx = lineProcessingError(ctx, "invalid event received from stdin: %s", err)
continue
}
kindWasSupplied = strings.Contains(stdinEvent, `"kind"`)
} }
if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") { if kind := c.Uint("kind"); slices.Contains(c.FlagNames(), "kind") {
@@ -324,6 +323,14 @@ example:
log(nevent + "\n") log(nevent + "\n")
} }
} }
return nil
}
for stdinEvent := range getJsonsOrBlank() {
if err := handleEvent(stdinEvent); err != nil {
ctx = lineProcessingError(ctx, err.Error())
}
} }
exitIfLineProcessingError(ctx) exitIfLineProcessingError(ctx)

View File

@@ -4,11 +4,13 @@ import (
"bufio" "bufio"
"context" "context"
"fmt" "fmt"
"iter"
"math/rand" "math/rand"
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url" "net/url"
"os" "os"
"slices"
"strings" "strings"
"time" "time"
@@ -43,59 +45,71 @@ func isPiped() bool {
return stat.Mode()&os.ModeCharDevice == 0 return stat.Mode()&os.ModeCharDevice == 0
} }
func getStdinLinesOrBlank() chan string { func getJsonsOrBlank() iter.Seq[string] {
multi := make(chan string) var curr strings.Builder
if hasStdinLines := writeStdinLinesOrNothing(multi); !hasStdinLines {
single := make(chan string, 1) return func(yield func(string) bool) {
single <- "" for stdinLine := range getStdinLinesOrBlank() {
close(single) // we're look for an event, but it may be in multiple lines, so if json parsing fails
return single // we'll try the next line until we're successful
} else { curr.WriteString(stdinLine)
return multi stdinEvent := curr.String()
var dummy any
if err := json.Unmarshal([]byte(stdinEvent), &dummy); err != nil {
continue
}
if !yield(stdinEvent) {
return
}
curr.Reset()
}
} }
} }
func getStdinLinesOrArguments(args cli.Args) chan string { func getStdinLinesOrBlank() iter.Seq[string] {
return func(yield func(string) bool) {
if hasStdinLines := writeStdinLinesOrNothing(yield); !hasStdinLines {
return
} else {
return
}
}
}
func getStdinLinesOrArguments(args cli.Args) iter.Seq[string] {
return getStdinLinesOrArgumentsFromSlice(args.Slice()) return getStdinLinesOrArgumentsFromSlice(args.Slice())
} }
func getStdinLinesOrArgumentsFromSlice(args []string) chan string { func getStdinLinesOrArgumentsFromSlice(args []string) iter.Seq[string] {
// try the first argument // try the first argument
if len(args) > 0 { if len(args) > 0 {
argsCh := make(chan string, 1) return slices.Values(args)
go func() {
for _, arg := range args {
argsCh <- arg
}
close(argsCh)
}()
return argsCh
} }
// try the stdin // try the stdin
multi := make(chan string) return func(yield func(string) bool) {
if !writeStdinLinesOrNothing(multi) { writeStdinLinesOrNothing(yield)
close(multi)
} }
return multi
} }
func writeStdinLinesOrNothing(ch chan string) (hasStdinLines bool) { func writeStdinLinesOrNothing(yield func(string) bool) (hasStdinLines bool) {
if isPiped() { if isPiped() {
// piped // piped
go func() { scanner := bufio.NewScanner(os.Stdin)
scanner := bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024) hasEmittedAtLeastOne := false
hasEmittedAtLeastOne := false for scanner.Scan() {
for scanner.Scan() { if !yield(strings.TrimSpace(scanner.Text())) {
ch <- strings.TrimSpace(scanner.Text()) return
hasEmittedAtLeastOne = true
} }
if !hasEmittedAtLeastOne { hasEmittedAtLeastOne = true
ch <- "" }
} if !hasEmittedAtLeastOne {
close(ch) yield("")
}() }
return true return true
} else { } else {
// not piped // not piped

11
main.go
View File

@@ -39,6 +39,7 @@ var app = &cli.Command{
outbox, outbox,
wallet, wallet,
mcpServer, mcpServer,
curl,
}, },
Version: version, Version: version,
Flags: []cli.Flag{ Flags: []cli.Flag{
@@ -140,6 +141,16 @@ func main() {
Usage: "prints the version", Usage: "prints the version",
} }
// a megahack to enable this curl command proxy
if len(os.Args) > 2 && os.Args[1] == "curl" {
if err := realCurl(); err != nil {
stdout(err)
colors.reset()
os.Exit(1)
}
return
}
if err := app.Run(context.Background(), os.Args); err != nil { if err := app.Run(context.Background(), os.Args); err != nil {
stdout(err) stdout(err)
colors.reset() colors.reset()

2
req.go
View File

@@ -107,7 +107,7 @@ example:
}() }()
} }
for stdinFilter := range getStdinLinesOrBlank() { for stdinFilter := range getJsonsOrBlank() {
filter := nostr.Filter{} filter := nostr.Filter{}
if stdinFilter != "" { if stdinFilter != "" {
if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil { if err := easyjson.Unmarshal([]byte(stdinFilter), &filter); err != nil {