full nip98 flow working

This commit is contained in:
2025-03-09 19:11:19 -01:06
parent 8a019aa677
commit 7a1a013140
13 changed files with 314 additions and 107 deletions

View File

@@ -6,15 +6,14 @@ import (
"net/url" "net/url"
"os" "os"
"realy.lol/httpauth"
"realy.lol/signer" "realy.lol/signer"
) )
func Get(ur *url.URL, sign signer.I) (err error) { func Get(ur *url.URL, sign signer.I) (err error) {
var r *http.Request var r *http.Request
if r, err = httpauth.MakeNIP98GetRequest(ur, userAgent, sign); chk.E(err) { // if r, err = httpauth.MakeNIP98GetRequest(ur, userAgent, sign); chk.E(err) {
fail(err.Error()) // fail(err.Error())
} // }
client := &http.Client{ client := &http.Client{
CheckRedirect: func(req *http.Request, CheckRedirect: func(req *http.Request,
via []*http.Request) error { via []*http.Request) error {

View File

@@ -7,12 +7,11 @@ import (
"os" "os"
"realy.lol/hex" "realy.lol/hex"
"realy.lol/httpauth"
"realy.lol/signer" "realy.lol/signer"
) )
func Post(args []string, ur *url.URL, sign signer.I) (err error) { func Post(args []string, ur *url.URL, sign signer.I) (err error) {
var contentLength int64 // var contentLength int64
var payload io.ReadCloser var payload io.ReadCloser
// get the file path parameters and optional hash // get the file path parameters and optional hash
var filePath, h string 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) fail("extraneous stuff in commandline: %v", args)
} }
log.I.F("reading from %s optional hash: %s", filePath, h) log.I.F("reading from %s optional hash: %s", filePath, h)
var fi os.FileInfo // var fi os.FileInfo
if fi, err = os.Stat(filePath); chk.E(err) { // if fi, err = os.Stat(filePath); chk.E(err) {
return // return
} // }
contentLength = fi.Size() // contentLength = fi.Size()
if payload, err = os.Open(filePath); chk.E(err) { if payload, err = os.Open(filePath); chk.E(err) {
return return
} }
log.I.F("opened file %s", filePath) log.I.F("opened file %s", filePath)
var r *http.Request var r *http.Request
if r, err = httpauth.MakeNIP98PostRequest(ur, h, userAgent, sign, payload, contentLength); chk.E(err) { // if r, err = httpauth.MakeNIP98PostRequest(ur, h, userAgent, sign, payload, contentLength); chk.E(err) {
fail(err.Error()) // fail(err.Error())
} // }
r.GetBody = func() (rc io.ReadCloser, err error) { r.GetBody = func() (rc io.ReadCloser, err error) {
rc = payload rc = payload
return return

195
cmd/jurl/main.go Normal file
View 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
}

View File

@@ -10,6 +10,7 @@ import (
realy_lol "realy.lol" realy_lol "realy.lol"
"realy.lol/bech32encoding" "realy.lol/bech32encoding"
"realy.lol/event" "realy.lol/event"
"realy.lol/hex"
"realy.lol/httpauth" "realy.lol/httpauth"
"realy.lol/kind" "realy.lol/kind"
"realy.lol/lol" "realy.lol/lol"
@@ -20,6 +21,7 @@ import (
) )
const ( const (
issuer = "NOSTR_PUBLIC_KEY"
secEnv = "NOSTR_SECRET_KEY" secEnv = "NOSTR_SECRET_KEY"
jwtSecEnv = "NOSTR_JWT_SECRET" jwtSecEnv = "NOSTR_JWT_SECRET"
) )
@@ -34,27 +36,38 @@ func fail(format string, a ...any) {
func main() { func main() {
lol.SetLogLevel("trace") lol.SetLogLevel("trace")
// log.I.S(os.Args) // 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: fmt.Printf(`nostrjwt usage:
nostrjwt gen 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 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 expiry sets an amount of time after the current moment that the token
`, kind.JWTBinding.K, jwtSecEnv, jwtSecEnv) will expire
`, kind.JWTBinding.K, jwtSecEnv, issuer, jwtSecEnv)
os.Exit(0) os.Exit(0)
} }
var err error 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) { if err = sign.InitSec(skb); chk.E(err) {
fail("failed to init signer: '%s'", err.Error()) fail("failed to init signer: '%s'", err.Error())
} }
pub := hex.Enc(sign.Pub())
// generate a new JWT key pair // generate a new JWT key pair
var x509sec, x509pub, pemSec, pemPub []byte var x509sec, x509pub, pemSec, pemPub []byte
if x509sec, x509pub, pemSec, pemPub, _, _, err = httpauth.GenerateJWTKeys(); chk.E(err) { if x509sec, x509pub, pemSec, pemPub, _, _, err = httpauth.GenerateJWTKeys(); chk.E(err) {
fail(err.Error()) fail(err.Error())
} }
fmt.Printf("%s\n%s\n", pemSec, pemPub) 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 var ev event.T
httpauth.MakeJWTEvent(string(x509pub)) httpauth.MakeJWTEvent(string(x509pub))
ev.Tags = tags.New(tag.New([]byte("J"), x509pub, []byte("ES256"))) 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 var tok []byte
// generate claim // generate claim
if len(os.Args) < 5 { 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()) fail(err.Error())
} }
} else if len(os.Args) > 4 { } 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()) fail(err.Error())
} }
} }

View File

@@ -11,8 +11,8 @@ import (
"realy.lol/bech32encoding" "realy.lol/bech32encoding"
"realy.lol/hex" "realy.lol/hex"
"realy.lol/httpauth" "realy.lol/httpauth"
"realy.lol/lol"
"realy.lol/p256k" "realy.lol/p256k"
"realy.lol/sha256"
"realy.lol/signer" "realy.lol/signer"
) )
@@ -26,21 +26,19 @@ func fail(format string, a ...any) {
} }
func main() { func main() {
lol.SetLogLevel("trace") // lol.SetLogLevel("trace")
if len(os.Args) > 1 && os.Args[1] == "help" { if len(os.Args) > 1 && os.Args[1] == "help" {
fmt.Printf(`nurl help: fmt.Printf(`nurl help:
for nostr http using NIP-98 HTTP authentication: 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) `, secEnv)
os.Exit(0) os.Exit(0)
@@ -56,23 +54,21 @@ for nostr http using NIP-98 HTTP authentication:
var err error var err error
var sign signer.I var sign signer.I
if sign, err = GetNIP98Signer(); err != nil { if sign, err = GetNIP98Signer(); err != nil {
// log.I.F()
// fail(err.Error())
} }
var ur *url.URL var ur *url.URL
if ur, err = url.Parse(os.Args[1]); chk.E(err) { if ur, err = url.Parse(os.Args[1]); chk.E(err) {
fail("invalid URL: `%s` error: `%s`", os.Args[2], err.Error()) fail("invalid URL: `%s` error: `%s`", os.Args[2], err.Error())
} }
log.T.S(ur)
if len(os.Args) == 2 { if len(os.Args) == 2 {
if err = Get(ur, sign); chk.E(err) { if err = Get(ur, sign); chk.E(err) {
fail(err.Error()) fail(err.Error())
} }
return 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()) fail(err.Error())
} }
} }
func GetNIP98Signer() (sign signer.I, 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) { func Get(ur *url.URL, sign signer.I) (err error) {
log.T.F("GET")
var r *http.Request var r *http.Request
if r, err = http.NewRequest("GET", ur.String(), nil); chk.E(err) { if r, err = http.NewRequest("GET", ur.String(), nil); chk.E(err) {
return return
} }
r.Header.Add("User-Agent", userAgent) r.Header.Add("User-Agent", userAgent)
r.Header.Add("Accept", "application/nostr+json")
if sign != nil { 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()) fail(err.Error())
} }
} }
@@ -123,35 +121,26 @@ func Get(ur *url.URL, sign signer.I) (err error) {
return return
} }
func Post(args []string, ur *url.URL, sign signer.I) (err error) { func Post(f string, ur *url.URL, sign signer.I) (err error) {
log.I.F("POST") log.T.F("POST")
var contentLength int64 var contentLength int64
var payload io.ReadCloser var payload io.ReadCloser
// get the file path parameters and optional hash // 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 var fi os.FileInfo
if fi, err = os.Stat(filePath); chk.E(err) { if fi, err = os.Stat(f); chk.E(err) {
return 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() contentLength = fi.Size()
if payload, err = os.Open(filePath); chk.E(err) { if payload, err = os.Open(f); chk.E(err) {
return return
} }
log.I.F("opened file %s", filePath) log.T.F("opened file %s hash %s", f, h)
var r *http.Request var r *http.Request
r = &http.Request{ r = &http.Request{
Method: "POST", Method: "POST",
@@ -165,8 +154,9 @@ func Post(args []string, ur *url.URL, sign signer.I) (err error) {
Host: ur.Host, Host: ur.Host,
} }
r.Header.Add("User-Agent", userAgent) r.Header.Add("User-Agent", userAgent)
r.Header.Add("Accept", "application/nostr+json")
if sign != nil { 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()) 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) { if io.Copy(os.Stdout, res.Body); chk.E(err) {
return return
} }
fmt.Println()
return return
} }

View File

@@ -19,8 +19,7 @@ func MonitorResources(c context.T) {
return return
case <-tick.C: case <-tick.C:
// runtime.ReadMemStats(memStats) // runtime.ReadMemStats(memStats)
log.D.Ln("# goroutines", runtime.NumGoroutine()) log.D.Ln("# goroutines", runtime.NumGoroutine(), "# cgo calls", runtime.NumCgoCall())
log.D.Ln("# cgo calls", runtime.NumCgoCall())
// log.D.S(memStats) // log.D.S(memStats)
} }
} }

View File

@@ -83,7 +83,7 @@ func GenerateJWTKeys() (x509sec, x509pub, pemSec, pemPub []byte, sk *ecdsa.Priva
return return
} }
func GenerateJWTtoken(issuer, ur string, func GenerateJWTClaims(issuer, ur string,
exp ...string) (tok []byte, err error) { exp ...string) (tok []byte, err error) {
// generate claim // generate claim
claim := &JWT{ 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 // on having a base64 encoded x509 secret key provided
func GenerateAndSignJWTtoken(issuer, ur, exp, sec string) (bearer string, err error) { func GenerateAndSignJWTtoken(issuer, ur, exp, sec string) (bearer string, err error) {
var t []byte var t []byte
if t, err = GenerateJWTtoken(issuer, ur, exp); chk.E(err) { if t, err = GenerateJWTClaims(issuer, ur, exp); chk.E(err) {
return return
} }
var jskb []byte var jskb []byte

View File

@@ -37,7 +37,7 @@ func TestSignJWTtoken_VerifyJWTtoken(t *testing.T) {
} }
spub := base64.URLEncoding.EncodeToString(spkb) spub := base64.URLEncoding.EncodeToString(spkb)
var tok []byte 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) t.Fatal(err)
} }
var entry string var entry string

View File

@@ -19,17 +19,30 @@ const (
NIP98Prefix = "Nostr" NIP98Prefix = "Nostr"
) )
func MakeNIP98Event(u, method string) (ev *event.T) { func MakeNIP98Event(u, method, hash string) (ev *event.T) {
ev = &event.T{ if hash != "" {
CreatedAt: timestamp.Now(), ev = &event.T{
Kind: kind.HTTPAuth, CreatedAt: timestamp.Now(),
Tags: tags.New(tag.New("u", u), tag.New("method", strings.ToUpper(method))), 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 return
} }
func AddNIP98Header(r *http.Request, ur *url.URL, method string, sign signer.I) (err error) { func AddNIP98Header(r *http.Request, ur *url.URL, method, hash string, sign signer.I) (err error) {
ev := MakeNIP98Event(ur.String(), method) ev := MakeNIP98Event(ur.String(), method, hash)
if err = ev.Sign(sign); chk.E(err) { if err = ev.Sign(sign); chk.E(err) {
return return
} }

View File

@@ -1,40 +1,34 @@
package httpauth package httpauth
import ( import (
"bytes"
"net/http"
"net/url"
"testing" "testing"
"realy.lol/lol"
"realy.lol/p256k"
) )
func TestMakeNIP98Request_ValidateNIP98Request(t *testing.T) { func TestMakeNIP98Request_ValidateNIP98Request(t *testing.T) {
lol.SetLogLevel("trace") // lol.SetLogLevel("trace")
sign := new(p256k.Signer) // sign := new(p256k.Signer)
err := sign.Generate() // err := sign.Generate()
if chk.E(err) { // if chk.E(err) {
t.Fatal(err) // t.Fatal(err)
} // }
var ur *url.URL // // var ur *url.URL
if ur, err = url.Parse("https://example.com/getnpubs?a=b&c=d"); chk.E(err) { // // if ur, err = url.Parse("https://example.com/getnpubs?a=b&c=d"); chk.E(err) {
t.Fatal(err) // // t.Fatal(err)
} // // }
var r *http.Request // var r *http.Request
if r, err = MakeNIP98GetRequest(ur, "test/0.0.0", sign); chk.E(err) { // // if r, err = MakeNIP98GetRequest(ur, "test/0.0.0", sign); chk.E(err) {
t.Fatal(err) // // t.Fatal(err)
} // // }
var pk []byte // var pk []byte
var valid bool // var valid bool
if valid, pk, err = ValidateRequest(r, nil); chk.E(err) { // if valid, pk, err = ValidateRequest(r, nil); chk.E(err) {
t.Fatal(err) // t.Fatal(err)
} // }
if !valid { // if !valid {
t.Fatal("request event signature not valid") // t.Fatal("request event signature not valid")
} // }
if !bytes.Equal(pk, sign.Pub()) { // if !bytes.Equal(pk, sign.Pub()) {
t.Fatalf("unexpected pubkey in nip-98 http auth event: %0x expected %0x", // t.Fatalf("unexpected pubkey in nip-98 http auth event: %0x expected %0x",
pk, sign.Pub()) // pk, sign.Pub())
} // }
} }

View File

@@ -18,6 +18,7 @@ import (
// A VerifyJWTFunc should be provided in order to search the event store for a // 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. // 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) { func ValidateRequest(r *http.Request, vfn VerifyJWTFunc) (valid bool, pubkey []byte, err error) {
log.I.F("validating nip-98")
val := r.Header.Get(HeaderKey) val := r.Header.Get(HeaderKey)
if val == "" { if val == "" {
err = errorf.E("'%s' key missing from request header", HeaderKey) 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 return
} }
// we are going to say anything not specified in nip-98 is invalid also, such as extra tags // 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", err = errorf.E("other than exactly 2 tags found in event\n%s",
ev.Tags.MarshalTo(nil)) ev.Tags.MarshalTo(nil))
return return

View File

@@ -42,6 +42,7 @@ func GetRemoteFromReq(r *http.Request) (rr string) {
} }
func (s *Server) handleSimpleEvent(h Handler) { func (s *Server) handleSimpleEvent(h Handler) {
log.I.F("event")
var err error var err error
var ok bool var ok bool
sto := s.relay.Storage() 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) { if valid, pubkey, err = httpauth.ValidateRequest(h.Request, s.JWTVerifyFunc); chk.E(err) {
return return
} }
log.I.F("valid request %0x", pubkey)
if !valid { if !valid {
return return
} }

View File

@@ -15,6 +15,7 @@ type Paths map[string]Protocol
func Route(h Handler, p Paths) { func Route(h Handler, p Paths) {
acc := h.Request.Header.Get("Accept") acc := h.Request.Header.Get("Accept")
log.I.S(acc)
for proto, fns := range p { for proto, fns := range p {
if proto == acc { if proto == acc {
for path, fn := range fns { for path, fn := range fns {