// Package main is a simple implementation of a cURL like tool that can do // simple GET/POST operations on a HTTP server that understands NIP-98 // authentication, with the signing key found in an environment variable. package main import ( "crypto/sha256" "fmt" "io" "net/http" "net/url" "os" "strings" "lol.mleku.dev/chk" "lol.mleku.dev/log" "git.mleku.dev/mleku/nostr/encoders/bech32encoding" "git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/httpauth" "git.mleku.dev/mleku/nostr/interfaces/signer" "git.mleku.dev/mleku/nostr/interfaces/signer/p8k" "next.orly.dev/pkg/version" ) const secEnv = "NOSTR_SECRET_KEY" var userAgent = fmt.Sprintf("nurl/%s", strings.TrimSpace(version.V)) func fail(format string, a ...any) { _, _ = fmt.Fprintf(os.Stderr, format+"\n", a...) os.Exit(1) } func main() { if len(os.Args) > 1 && os.Args[1] == "help" { fmt.Printf( `nurl help: for nostr http using NIP-98 HTTP authentication: nurl [file] if no file is given, the request will be processed as a HTTP GET. * NIP-98 secret will be expected in the environment variable "%s" - if absent, will not be added to the header. - endpoint is assumed to not require it if absent. - an error will be returned if it was needed. output will be rendered to stdout `, secEnv, ) os.Exit(0) } if len(os.Args) < 2 { fail( `error: nurl requires minimum 1 arg: signing nsec (in bech32 format) is expected to be found in %s environment variable. use "help" to get usage information `, secEnv, ) } var err error var sign signer.I if sign, err = GetNIP98Signer(); err != nil { log.W.Ln("no signer available:", err) } var ur *url.URL if ur, err = url.Parse(os.Args[1]); chk.E(err) { fail("invalid URL: `%s` error: `%s`", os.Args[1], err.Error()) } log.T.S(ur) if len(os.Args) == 2 { if err = Get(ur, sign); chk.E(err) { fail(err.Error()) } return } if err = Post(os.Args[2], ur, sign); chk.E(err) { fail(err.Error()) } } func GetNIP98Signer() (sign signer.I, err error) { nsec := os.Getenv(secEnv) var sk []byte if len(nsec) == 0 { err = fmt.Errorf("no bech32 secret key found in environment variable %s", secEnv) return } else if sk, err = bech32encoding.NsecToBytes([]byte(nsec)); chk.E(err) { err = fmt.Errorf("failed to decode nsec: '%s'", err.Error()) return } var s *p8k.Signer if s, err = p8k.New(); chk.E(err) { err = fmt.Errorf("failed to create signer: '%s'", err.Error()) return } if err = s.InitSec(sk); chk.E(err) { err = fmt.Errorf("failed to init signer: '%s'", err.Error()) return } sign = s return } func Get(ur *url.URL, sign signer.I) (err error) { log.T.F("GET %s", ur.String()) var r *http.Request if r, err = http.NewRequest("GET", ur.String(), nil); chk.E(err) { return } r.Header.Add("User-Agent", userAgent) if sign != nil { if err = httpauth.AddNIP98Header( r, ur, "GET", "", sign, 0, ); chk.E(err) { fail(err.Error()) } } client := &http.Client{ CheckRedirect: func( req *http.Request, via []*http.Request, ) error { return http.ErrUseLastResponse }, } var res *http.Response if res, err = client.Do(r); chk.E(err) { err = fmt.Errorf("request failed: %w", err) return } if _, err = io.Copy(os.Stdout, res.Body); chk.E(err) { res.Body.Close() return } res.Body.Close() return } func Post(f string, ur *url.URL, sign signer.I) (err error) { log.T.F("POST %s", ur.String()) var contentLength int64 var payload io.ReadCloser // get the file path parameters and optional hash var fi os.FileInfo if fi, err = os.Stat(f); chk.E(err) { return } var b []byte if b, err = os.ReadFile(f); chk.E(err) { return } hb := sha256.Sum256(b) h := hex.Enc(hb[:]) contentLength = fi.Size() if payload, err = os.Open(f); chk.E(err) { return } log.T.F("opened file %s hash %s", f, h) var r *http.Request r = &http.Request{ Method: "POST", URL: ur, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: make(http.Header), Body: payload, ContentLength: contentLength, Host: ur.Host, } r.Header.Add("User-Agent", userAgent) if sign != nil { if err = httpauth.AddNIP98Header( r, ur, "POST", string(h), sign, 0, ); chk.E(err) { fail(err.Error()) } } r.GetBody = func() (rc io.ReadCloser, err error) { rc = payload return } client := &http.Client{} var res *http.Response if res, err = client.Do(r); chk.E(err) { return } defer res.Body.Close() if _, err = io.Copy(os.Stdout, res.Body); chk.E(err) { return } return }