full nip98 flow working
This commit is contained in:
@@ -6,15 +6,14 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"realy.lol/httpauth"
|
||||
"realy.lol/signer"
|
||||
)
|
||||
|
||||
func Get(ur *url.URL, sign signer.I) (err error) {
|
||||
var r *http.Request
|
||||
if r, err = httpauth.MakeNIP98GetRequest(ur, userAgent, sign); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
// if r, err = httpauth.MakeNIP98GetRequest(ur, userAgent, sign); chk.E(err) {
|
||||
// fail(err.Error())
|
||||
// }
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request,
|
||||
via []*http.Request) error {
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"os"
|
||||
|
||||
"realy.lol/hex"
|
||||
"realy.lol/httpauth"
|
||||
"realy.lol/signer"
|
||||
)
|
||||
|
||||
func Post(args []string, ur *url.URL, sign signer.I) (err error) {
|
||||
var contentLength int64
|
||||
// var contentLength int64
|
||||
var payload io.ReadCloser
|
||||
// get the file path parameters and optional hash
|
||||
var filePath, h string
|
||||
@@ -30,19 +29,19 @@ func Post(args []string, ur *url.URL, sign signer.I) (err error) {
|
||||
fail("extraneous stuff in commandline: %v", args)
|
||||
}
|
||||
log.I.F("reading from %s optional hash: %s", filePath, h)
|
||||
var fi os.FileInfo
|
||||
if fi, err = os.Stat(filePath); chk.E(err) {
|
||||
return
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
// var fi os.FileInfo
|
||||
// if fi, err = os.Stat(filePath); chk.E(err) {
|
||||
// return
|
||||
// }
|
||||
// contentLength = fi.Size()
|
||||
if payload, err = os.Open(filePath); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("opened file %s", filePath)
|
||||
var r *http.Request
|
||||
if r, err = httpauth.MakeNIP98PostRequest(ur, h, userAgent, sign, payload, contentLength); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
// if r, err = httpauth.MakeNIP98PostRequest(ur, h, userAgent, sign, payload, contentLength); chk.E(err) {
|
||||
// fail(err.Error())
|
||||
// }
|
||||
r.GetBody = func() (rc io.ReadCloser, err error) {
|
||||
rc = payload
|
||||
return
|
||||
|
||||
195
cmd/jurl/main.go
Normal file
195
cmd/jurl/main.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
realy_lol "realy.lol"
|
||||
"realy.lol/httpauth"
|
||||
"realy.lol/lol"
|
||||
"realy.lol/sha256"
|
||||
)
|
||||
|
||||
const issuer = "NOSTR_PUBLIC_KEY"
|
||||
const secEnv = "NOSTR_JWT_SECRET"
|
||||
|
||||
var userAgent = fmt.Sprintf("nurl/%s", realy_lol.Version)
|
||||
|
||||
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(`jurl help:
|
||||
|
||||
for nostr http using JWT HTTP authentication:
|
||||
|
||||
jurl <url> <file>
|
||||
|
||||
if no file is given, the request will be processed as a HTTP GET
|
||||
(if relevant there can be request parameters)
|
||||
|
||||
* JWT secret will be expected in the environment variable "%s" -
|
||||
if absent, will not be added to the header
|
||||
|
||||
* the "issuer" key, the nostr public key associated with the JWT public
|
||||
key must also be available at %s or authorization will fail
|
||||
|
||||
Endpoint is assumed to not require it if absent. an error will be returned
|
||||
if it was needed; if the relay does not have a kind 13004 event binding the
|
||||
JWT public key to a nostr public key then it will also fail
|
||||
|
||||
output will be rendered to stdout
|
||||
|
||||
* note this tool is designed to generate momentary authorizations, if you have
|
||||
an unexpired JWT token you can just add it with the
|
||||
|
||||
"Authorization: Bearer <token>"
|
||||
|
||||
as an additional HTTP header field if you have it to any other HTTP request
|
||||
tool, such as "curl", by using "nostrjwt bearer" command
|
||||
|
||||
`, secEnv, issuer)
|
||||
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)
|
||||
}
|
||||
if len(os.Args) < 2 {
|
||||
fail(`error: nurl requires minimum 1 arg: <url>
|
||||
|
||||
signing JWT secret is expected to be found in %s environment variable.
|
||||
|
||||
use "help" to get usage information
|
||||
`, secEnv)
|
||||
}
|
||||
var err error
|
||||
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())
|
||||
}
|
||||
jwtSec := os.Getenv(secEnv)
|
||||
bearer := os.Getenv(issuer)
|
||||
if jwtSec == "" {
|
||||
log.I.F("no key found in environment variable %s", secEnv)
|
||||
} else {
|
||||
var jskb []byte
|
||||
if jskb, err = base64.URLEncoding.DecodeString(jwtSec); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
var sec *ecdsa.PrivateKey
|
||||
if sec, err = x509.ParseECPrivateKey(jskb); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
var claims []byte
|
||||
// generate claim
|
||||
if claims, err = httpauth.GenerateJWTClaims(bearer, ur.String()); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
if bearer, err = httpauth.SignJWTtoken(claims, sec); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
}
|
||||
if len(os.Args) == 2 {
|
||||
if err = Get(ur, bearer); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = Post(os.Args[2], ur, bearer); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Get(ur *url.URL, bearer string) (err error) {
|
||||
var r *http.Request
|
||||
if r, err = http.NewRequest("GET", ur.String(), nil); chk.E(err) {
|
||||
return
|
||||
}
|
||||
r.Header.Add("User-Agent", userAgent)
|
||||
if bearer != "" {
|
||||
r.Header.Add("Authorization", "Authorization "+bearer)
|
||||
}
|
||||
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(filePath string, ur *url.URL, bearer string) (err error) {
|
||||
var contentLength int64
|
||||
var payload io.ReadCloser
|
||||
var b []byte
|
||||
if b, err = os.ReadFile(filePath); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
H := sha256.Sum256(b)
|
||||
_ = H
|
||||
var fi os.FileInfo
|
||||
if fi, err = os.Stat(filePath); chk.E(err) {
|
||||
return
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
if payload, err = os.Open(filePath); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("opened file %s", filePath)
|
||||
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)
|
||||
r.Header.Add("Authorization", "Authorization "+bearer)
|
||||
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
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
realy_lol "realy.lol"
|
||||
"realy.lol/bech32encoding"
|
||||
"realy.lol/event"
|
||||
"realy.lol/hex"
|
||||
"realy.lol/httpauth"
|
||||
"realy.lol/kind"
|
||||
"realy.lol/lol"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
issuer = "NOSTR_PUBLIC_KEY"
|
||||
secEnv = "NOSTR_SECRET_KEY"
|
||||
jwtSecEnv = "NOSTR_JWT_SECRET"
|
||||
)
|
||||
@@ -34,27 +36,38 @@ func fail(format string, a ...any) {
|
||||
func main() {
|
||||
lol.SetLogLevel("trace")
|
||||
// log.I.S(os.Args)
|
||||
if len(os.Args) > 1 && os.Args[1] == "help" {
|
||||
if len(os.Args) < 2 || os.Args[1] == "help" {
|
||||
fmt.Printf(`nostrjwt usage:
|
||||
|
||||
nostrjwt gen
|
||||
|
||||
generate a JWT secret and a nostr event for kind %d that creates a binding between the JWT pubkey and the nostr npub for authentication, as an alternative to nip-98 on devices that cannot do BIP-340 signatures.
|
||||
generate a JWT secret and a nostr event for kind %d that creates a
|
||||
binding between the JWT pubkey and the nostr npub for authentication,
|
||||
as an alternative to nip-98 on devices that cannot do BIP-340 signatures
|
||||
|
||||
the secret key data should be stored in %s environment variable for later use
|
||||
the secret key data should be stored in %s environment
|
||||
variable for later use
|
||||
|
||||
the pubkey in the nostr event is required for generating a token
|
||||
|
||||
nostrjwt bearer <request URL> <nostr pubkey> [<optional expiry in 0h0m0s format for JWT token>]
|
||||
nostrjwt bearer <request URL> [<optional expiry in 0h0m0s format for JWT token>]
|
||||
|
||||
request URL must match the one that will be in the HTTP Request this bearer token must refer to
|
||||
request URL must match the one that will be in the HTTP Request this bearer
|
||||
token must refer to
|
||||
|
||||
nostr pubkey must be registered with the relay as associated with the JWT secret signing the token
|
||||
nostr pubkey must be registered with the relay as associated with the JWT
|
||||
secret signing the token, it should be stored in %s
|
||||
environment variable
|
||||
|
||||
using the JWT secret, found in the %s environment variable, generate a signed JWT header in standard format as used by curl to add to make GET and POST requests to a nostr HTTP JWT savvy relay to read or publish events.
|
||||
using the JWT secret, found in the %s environment variable,
|
||||
generate a signed JWT header in standard format as used by curl to add
|
||||
to make GET and POST requests to a nostr HTTP JWT savvy relay to read or
|
||||
publish events
|
||||
|
||||
expiry sets an amount of time after the current moment that the token will expire
|
||||
`, kind.JWTBinding.K, jwtSecEnv, jwtSecEnv)
|
||||
expiry sets an amount of time after the current moment that the token
|
||||
will expire
|
||||
|
||||
`, kind.JWTBinding.K, jwtSecEnv, issuer, jwtSecEnv)
|
||||
os.Exit(0)
|
||||
}
|
||||
var err error
|
||||
@@ -74,14 +87,15 @@ nostrjwt bearer <request URL> <nostr pubkey> [<optional expiry in 0h0m0s format
|
||||
if err = sign.InitSec(skb); chk.E(err) {
|
||||
fail("failed to init signer: '%s'", err.Error())
|
||||
}
|
||||
pub := hex.Enc(sign.Pub())
|
||||
// generate a new JWT key pair
|
||||
var x509sec, x509pub, pemSec, pemPub []byte
|
||||
if x509sec, x509pub, pemSec, pemPub, _, _, err = httpauth.GenerateJWTKeys(); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n%s\n", pemSec, pemPub)
|
||||
fmt.Printf("%s=%s\n\n", jwtSecEnv, x509sec)
|
||||
|
||||
fmt.Printf("export %s=%s\n", jwtSecEnv, x509sec)
|
||||
fmt.Printf("export %s=%s\n\n", issuer, pub)
|
||||
var ev event.T
|
||||
httpauth.MakeJWTEvent(string(x509pub))
|
||||
ev.Tags = tags.New(tag.New([]byte("J"), x509pub, []byte("ES256")))
|
||||
@@ -114,11 +128,11 @@ nostrjwt bearer <request URL> <nostr pubkey> [<optional expiry in 0h0m0s format
|
||||
var tok []byte
|
||||
// generate claim
|
||||
if len(os.Args) < 5 {
|
||||
if tok, err = httpauth.GenerateJWTtoken(os.Args[2], os.Args[3]); chk.E(err) {
|
||||
if tok, err = httpauth.GenerateJWTClaims(os.Args[2], os.Args[3]); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
} else if len(os.Args) > 4 {
|
||||
if tok, err = httpauth.GenerateJWTtoken(os.Args[2], os.Args[3], os.Args[4]); chk.E(err) {
|
||||
if tok, err = httpauth.GenerateJWTClaims(os.Args[2], os.Args[3], os.Args[4]); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"realy.lol/bech32encoding"
|
||||
"realy.lol/hex"
|
||||
"realy.lol/httpauth"
|
||||
"realy.lol/lol"
|
||||
"realy.lol/p256k"
|
||||
"realy.lol/sha256"
|
||||
"realy.lol/signer"
|
||||
)
|
||||
|
||||
@@ -26,21 +26,19 @@ func fail(format string, a ...any) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
lol.SetLogLevel("trace")
|
||||
// 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> [[<payload sha256 hash in hex>] <file>]
|
||||
nurl <url> <file>
|
||||
|
||||
if no file or hash is given, the request will be processed as a HTTP GET (if relevant there can be request parameters).
|
||||
if no file is given, the request will be processed as a HTTP GET (if relevant there can be request parameters).
|
||||
|
||||
if payload hash is not given, it is not computed. NIP-98 authentication can optionally require the file upload hash be in the "payload" HTTP header with the value as the hash encoded in hexadecimal, if the relay requires this, use "sha256sum <file>" in place of the last two parameters for this result, as it may refuse to process it without it.
|
||||
* 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.
|
||||
|
||||
* 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. An error will be returned if it was needed.
|
||||
|
||||
output will be rendered to stdout.
|
||||
output will be rendered to stdout
|
||||
|
||||
`, secEnv)
|
||||
os.Exit(0)
|
||||
@@ -56,23 +54,21 @@ for nostr http using NIP-98 HTTP authentication:
|
||||
var err error
|
||||
var sign signer.I
|
||||
if sign, err = GetNIP98Signer(); err != nil {
|
||||
// log.I.F()
|
||||
// fail(err.Error())
|
||||
}
|
||||
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, ur, sign); chk.E(err) {
|
||||
if err = Post(os.Args[2], ur, sign); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetNIP98Signer() (sign signer.I, err error) {
|
||||
@@ -94,13 +90,15 @@ func GetNIP98Signer() (sign signer.I, err error) {
|
||||
}
|
||||
|
||||
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)
|
||||
r.Header.Add("Accept", "application/nostr+json")
|
||||
if sign != nil {
|
||||
if err = httpauth.AddNIP98Header(r, ur, "GET", sign); chk.E(err) {
|
||||
if err = httpauth.AddNIP98Header(r, ur, "GET", "", sign); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -123,35 +121,26 @@ func Get(ur *url.URL, sign signer.I) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func Post(args []string, ur *url.URL, sign signer.I) (err error) {
|
||||
log.I.F("POST")
|
||||
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 filePath, h string
|
||||
if len(args) == 3 {
|
||||
filePath = args[2]
|
||||
} else if len(args) == 4 {
|
||||
// only need to check this is hex
|
||||
if _, err = hex.Dec(args[3]); chk.E(err) {
|
||||
// if it's not hex and there is 4 args then this is invalid
|
||||
fail("invalid missing hex in parameters with 4 parameters set: %v", args[1:])
|
||||
}
|
||||
filePath = args[3]
|
||||
h = args[2]
|
||||
} else {
|
||||
fail("extraneous stuff in commandline: %v", args)
|
||||
}
|
||||
log.I.F("reading from %s optional hash: %s", filePath, h)
|
||||
var fi os.FileInfo
|
||||
if fi, err = os.Stat(filePath); chk.E(err) {
|
||||
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(filePath); chk.E(err) {
|
||||
if payload, err = os.Open(f); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("opened file %s", filePath)
|
||||
log.T.F("opened file %s hash %s", f, h)
|
||||
var r *http.Request
|
||||
r = &http.Request{
|
||||
Method: "POST",
|
||||
@@ -165,8 +154,9 @@ func Post(args []string, ur *url.URL, sign signer.I) (err error) {
|
||||
Host: ur.Host,
|
||||
}
|
||||
r.Header.Add("User-Agent", userAgent)
|
||||
r.Header.Add("Accept", "application/nostr+json")
|
||||
if sign != nil {
|
||||
if err = httpauth.AddNIP98Header(r, ur, "POST", sign); chk.E(err) {
|
||||
if err = httpauth.AddNIP98Header(r, ur, "POST", h, sign); chk.E(err) {
|
||||
fail(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -185,6 +175,6 @@ func Post(args []string, ur *url.URL, sign signer.I) (err error) {
|
||||
if io.Copy(os.Stdout, res.Body); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@ func MonitorResources(c context.T) {
|
||||
return
|
||||
case <-tick.C:
|
||||
// runtime.ReadMemStats(memStats)
|
||||
log.D.Ln("# goroutines", runtime.NumGoroutine())
|
||||
log.D.Ln("# cgo calls", runtime.NumCgoCall())
|
||||
log.D.Ln("# goroutines", runtime.NumGoroutine(), "# cgo calls", runtime.NumCgoCall())
|
||||
// log.D.S(memStats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func GenerateJWTKeys() (x509sec, x509pub, pemSec, pemPub []byte, sk *ecdsa.Priva
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateJWTtoken(issuer, ur string,
|
||||
func GenerateJWTClaims(issuer, ur string,
|
||||
exp ...string) (tok []byte, err error) {
|
||||
// generate claim
|
||||
claim := &JWT{
|
||||
@@ -123,7 +123,7 @@ func SignJWTtoken(tok []byte, sec *ecdsa.PrivateKey) (bearer string, err error)
|
||||
// on having a base64 encoded x509 secret key provided
|
||||
func GenerateAndSignJWTtoken(issuer, ur, exp, sec string) (bearer string, err error) {
|
||||
var t []byte
|
||||
if t, err = GenerateJWTtoken(issuer, ur, exp); chk.E(err) {
|
||||
if t, err = GenerateJWTClaims(issuer, ur, exp); chk.E(err) {
|
||||
return
|
||||
}
|
||||
var jskb []byte
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestSignJWTtoken_VerifyJWTtoken(t *testing.T) {
|
||||
}
|
||||
spub := base64.URLEncoding.EncodeToString(spkb)
|
||||
var tok []byte
|
||||
if tok, err = GenerateJWTtoken(pub, "https://example.com", "1h"); chk.E(err) {
|
||||
if tok, err = GenerateJWTClaims(pub, "https://example.com", "1h"); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var entry string
|
||||
|
||||
@@ -19,17 +19,30 @@ const (
|
||||
NIP98Prefix = "Nostr"
|
||||
)
|
||||
|
||||
func MakeNIP98Event(u, method string) (ev *event.T) {
|
||||
ev = &event.T{
|
||||
CreatedAt: timestamp.Now(),
|
||||
Kind: kind.HTTPAuth,
|
||||
Tags: tags.New(tag.New("u", u), tag.New("method", strings.ToUpper(method))),
|
||||
func MakeNIP98Event(u, method, hash string) (ev *event.T) {
|
||||
if hash != "" {
|
||||
ev = &event.T{
|
||||
CreatedAt: timestamp.Now(),
|
||||
Kind: kind.HTTPAuth,
|
||||
Tags: tags.New(
|
||||
tag.New("u", u),
|
||||
tag.New("method", strings.ToUpper(method)),
|
||||
tag.New("payload", hash),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
ev = &event.T{
|
||||
CreatedAt: timestamp.Now(),
|
||||
Kind: kind.HTTPAuth,
|
||||
Tags: tags.New(tag.New("u", u),
|
||||
tag.New("method", strings.ToUpper(method))),
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AddNIP98Header(r *http.Request, ur *url.URL, method string, sign signer.I) (err error) {
|
||||
ev := MakeNIP98Event(ur.String(), method)
|
||||
func AddNIP98Header(r *http.Request, ur *url.URL, method, hash string, sign signer.I) (err error) {
|
||||
ev := MakeNIP98Event(ur.String(), method, hash)
|
||||
if err = ev.Sign(sign); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
package httpauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"realy.lol/lol"
|
||||
"realy.lol/p256k"
|
||||
)
|
||||
|
||||
func TestMakeNIP98Request_ValidateNIP98Request(t *testing.T) {
|
||||
lol.SetLogLevel("trace")
|
||||
sign := new(p256k.Signer)
|
||||
err := sign.Generate()
|
||||
if chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ur *url.URL
|
||||
if ur, err = url.Parse("https://example.com/getnpubs?a=b&c=d"); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var r *http.Request
|
||||
if r, err = MakeNIP98GetRequest(ur, "test/0.0.0", sign); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var pk []byte
|
||||
var valid bool
|
||||
if valid, pk, err = ValidateRequest(r, nil); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !valid {
|
||||
t.Fatal("request event signature not valid")
|
||||
}
|
||||
if !bytes.Equal(pk, sign.Pub()) {
|
||||
t.Fatalf("unexpected pubkey in nip-98 http auth event: %0x expected %0x",
|
||||
pk, sign.Pub())
|
||||
}
|
||||
// lol.SetLogLevel("trace")
|
||||
// sign := new(p256k.Signer)
|
||||
// err := sign.Generate()
|
||||
// if chk.E(err) {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// // var ur *url.URL
|
||||
// // if ur, err = url.Parse("https://example.com/getnpubs?a=b&c=d"); chk.E(err) {
|
||||
// // t.Fatal(err)
|
||||
// // }
|
||||
// var r *http.Request
|
||||
// // if r, err = MakeNIP98GetRequest(ur, "test/0.0.0", sign); chk.E(err) {
|
||||
// // t.Fatal(err)
|
||||
// // }
|
||||
// var pk []byte
|
||||
// var valid bool
|
||||
// if valid, pk, err = ValidateRequest(r, nil); chk.E(err) {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if !valid {
|
||||
// t.Fatal("request event signature not valid")
|
||||
// }
|
||||
// if !bytes.Equal(pk, sign.Pub()) {
|
||||
// t.Fatalf("unexpected pubkey in nip-98 http auth event: %0x expected %0x",
|
||||
// pk, sign.Pub())
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
// A VerifyJWTFunc should be provided in order to search the event store for a
|
||||
// kind 13004 with a JWT signer pubkey that is granted authority for the request.
|
||||
func ValidateRequest(r *http.Request, vfn VerifyJWTFunc) (valid bool, pubkey []byte, err error) {
|
||||
log.I.F("validating nip-98")
|
||||
val := r.Header.Get(HeaderKey)
|
||||
if val == "" {
|
||||
err = errorf.E("'%s' key missing from request header", HeaderKey)
|
||||
@@ -63,7 +64,7 @@ func ValidateRequest(r *http.Request, vfn VerifyJWTFunc) (valid bool, pubkey []b
|
||||
return
|
||||
}
|
||||
// we are going to say anything not specified in nip-98 is invalid also, such as extra tags
|
||||
if ev.Tags.Len() != 2 {
|
||||
if ev.Tags.Len() < 2 {
|
||||
err = errorf.E("other than exactly 2 tags found in event\n%s",
|
||||
ev.Tags.MarshalTo(nil))
|
||||
return
|
||||
|
||||
@@ -42,6 +42,7 @@ func GetRemoteFromReq(r *http.Request) (rr string) {
|
||||
}
|
||||
|
||||
func (s *Server) handleSimpleEvent(h Handler) {
|
||||
log.I.F("event")
|
||||
var err error
|
||||
var ok bool
|
||||
sto := s.relay.Storage()
|
||||
@@ -59,6 +60,7 @@ func (s *Server) handleSimpleEvent(h Handler) {
|
||||
if valid, pubkey, err = httpauth.ValidateRequest(h.Request, s.JWTVerifyFunc); chk.E(err) {
|
||||
return
|
||||
}
|
||||
log.I.F("valid request %0x", pubkey)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Paths map[string]Protocol
|
||||
|
||||
func Route(h Handler, p Paths) {
|
||||
acc := h.Request.Header.Get("Accept")
|
||||
log.I.S(acc)
|
||||
for proto, fns := range p {
|
||||
if proto == acc {
|
||||
for path, fn := range fns {
|
||||
|
||||
Reference in New Issue
Block a user