Files
orly/pkg/cmd/nurl/main.go
2025-07-17 13:18:55 +01:00

196 lines
4.4 KiB
Go

// 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 (
"fmt"
"io"
"net/http"
"net/url"
"orly.dev/pkg/crypto/p256k"
"orly.dev/pkg/crypto/sha256"
"orly.dev/pkg/encoders/bech32encoding"
"orly.dev/pkg/encoders/hex"
"orly.dev/pkg/interfaces/signer"
"orly.dev/pkg/protocol/httpauth"
"orly.dev/pkg/utils/chk"
"orly.dev/pkg/utils/errorf"
"orly.dev/pkg/utils/log"
realy_lol "orly.dev/pkg/version"
"os"
)
const secEnv = "NOSTR_SECRET_KEY"
var userAgent = fmt.Sprintf("nurl/%s", realy_lol.V)
func fail(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", a...)
os.Exit(1)
}
func main() {
// lol.SetLogLevel("trace")
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 (if relevant there can be request parameters).
* 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 {
}
var ur *url.URL
if ur, err = url.Parse(os.Args[1]); chk.E(err) {
fail("invalid URL: `%s` error: `%s`", os.Args[2], 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) {
nsex := os.Getenv(secEnv)
var sk []byte
if len(nsex) == 0 {
err = errorf.E(
"no bech32 secret key found in environment variable %s", secEnv,
)
return
} else if sk, err = bech32encoding.NsecToBytes([]byte(nsex)); chk.E(err) {
err = errorf.E("failed to decode nsec: '%s'", err.Error())
return
}
sign = &p256k.Signer{}
if err = sign.InitSec(sk); chk.E(err) {
err = errorf.E("failed to init signer: '%s'", err.Error())
return
}
return
}
func Get(ur *url.URL, sign signer.I) (err error) {
log.T.F("GET")
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 = errorf.E("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")
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", h, sign, 0,
); chk.E(err) {
fail(err.Error())
}
}
r.GetBody = func() (rc io.ReadCloser, err error) {
rc = payload
return
}
// log.I.S(r)
client := &http.Client{}
var res *http.Response
if res, err = client.Do(r); chk.E(err) {
return
}
// log.I.S(res)
defer res.Body.Close()
if io.Copy(os.Stdout, res.Body); chk.E(err) {
return
}
fmt.Println()
return
}