You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
4.5 KiB
201 lines
4.5 KiB
// 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 <url> [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: <url> |
|
|
|
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 |
|
}
|
|
|