implement auth, closed and close envelopes
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
func (s *Server) HandleMessage() {
|
import (
|
||||||
|
"lol.mleku.dev/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) HandleMessage(msg []byte) {
|
||||||
|
log.I.F("received message:\n%s\n", msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
var err error
|
var err error
|
||||||
var conn *websocket.Conn
|
var conn *websocket.Conn
|
||||||
if conn, err = websocket.Accept(
|
if conn, err = websocket.Accept(
|
||||||
w, r, &websocket.AcceptOptions{},
|
w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}},
|
||||||
); chk.E(err) {
|
); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,8 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
var typ websocket.MessageType
|
var typ websocket.MessageType
|
||||||
var message []byte
|
var msg []byte
|
||||||
if typ, message, err = conn.Read(s.Ctx); err != nil {
|
if typ, msg, err = conn.Read(s.Ctx); err != nil {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
err.Error(), "use of closed network connection",
|
err.Error(), "use of closed network connection",
|
||||||
) {
|
) {
|
||||||
@@ -68,12 +68,12 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if typ == PingMessage {
|
if typ == PingMessage {
|
||||||
if err = conn.Write(s.Ctx, PongMessage, message); chk.E(err) {
|
if err = conn.Write(s.Ctx, PongMessage, msg); chk.E(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go s.HandleMessage()
|
go s.HandleMessage(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
"lol.mleku.dev/log"
|
"lol.mleku.dev/log"
|
||||||
"next.orly.dev/app/config"
|
"next.orly.dev/app/config"
|
||||||
)
|
)
|
||||||
@@ -20,11 +21,14 @@ func Run(ctx context.Context, cfg *config.C) (quit chan struct{}) {
|
|||||||
}()
|
}()
|
||||||
// start listener
|
// start listener
|
||||||
l := &Server{
|
l := &Server{
|
||||||
|
Ctx: ctx,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
addr := fmt.Sprintf("%s:%d", cfg.Listen, cfg.Port)
|
addr := fmt.Sprintf("%s:%d", cfg.Listen, cfg.Port)
|
||||||
log.I.F("starting listener on %s", addr)
|
log.I.F("starting listener on http://%s", addr)
|
||||||
go http.ListenAndServe(addr, l)
|
go func() {
|
||||||
|
chk.E(http.ListenAndServe(addr, l))
|
||||||
|
}()
|
||||||
quit = make(chan struct{})
|
quit = make(chan struct{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
||||||
s.HandleRelayInfo(w, r)
|
s.HandleRelayInfo(w, r)
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
|
if s.mux == nil {
|
||||||
|
http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
|
||||||
|
} else {
|
||||||
|
s.mux.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -20,8 +20,6 @@ require (
|
|||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/felixge/fgprof v0.9.3 // indirect
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
|
||||||
github.com/karrick/bufpool v1.2.0 // indirect
|
|
||||||
github.com/karrick/gopool v1.1.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
|||||||
227
pkg/encoders/envelopes/authenvelope/authenvelope.go
Normal file
227
pkg/encoders/envelopes/authenvelope/authenvelope.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// Package authenvelope defines the auth challenge (relay message) and response
|
||||||
|
// (client message) of the NIP-42 authentication protocol.
|
||||||
|
package authenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"lol.mleku.dev/errorf"
|
||||||
|
"lol.mleku.dev/log"
|
||||||
|
envs "next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/encoders/event"
|
||||||
|
text2 "next.orly.dev/pkg/encoders/text"
|
||||||
|
"next.orly.dev/pkg/interfaces/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// L is the label associated with this type of codec.Envelope.
|
||||||
|
const L = "AUTH"
|
||||||
|
|
||||||
|
// Challenge is the relay-sent message containing a relay-chosen random string
|
||||||
|
// to prevent replay attacks on NIP-42 authentication.
|
||||||
|
type Challenge struct {
|
||||||
|
Challenge []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ codec.Envelope = (*Challenge)(nil)
|
||||||
|
|
||||||
|
// NewChallenge creates a new empty authenvelope.Challenge.
|
||||||
|
func NewChallenge() *Challenge { return &Challenge{} }
|
||||||
|
|
||||||
|
// NewChallengeWith creates a new authenvelope.Challenge with provided bytes.
|
||||||
|
func NewChallengeWith[V string | []byte](challenge V) *Challenge {
|
||||||
|
return &Challenge{[]byte(challenge)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label returns the label of a authenvelope.Challenge.
|
||||||
|
func (en *Challenge) Label() string { return L }
|
||||||
|
|
||||||
|
// Write encodes and writes the Challenge instance to the provided writer.
|
||||||
|
//
|
||||||
|
// # Parameters
|
||||||
|
//
|
||||||
|
// - w (io.Writer): The destination where the encoded data will be written.
|
||||||
|
//
|
||||||
|
// # Return Values
|
||||||
|
//
|
||||||
|
// - err (error): An error if writing to the writer fails.
|
||||||
|
//
|
||||||
|
// # Expected behaviour
|
||||||
|
//
|
||||||
|
// Encodes the Challenge instance into a byte slice using Marshal, logs the
|
||||||
|
// encoded challenge, and writes it to the provided io.Writer.
|
||||||
|
func (en *Challenge) Write(w io.Writer) (err error) {
|
||||||
|
var b []byte
|
||||||
|
b = en.Marshal(b)
|
||||||
|
log.T.F("writing out challenge envelope: '%s'", b)
|
||||||
|
_, err = w.Write(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encodes the Challenge instance into a byte slice, formatting it as
|
||||||
|
// a JSON-like structure with a specific label and escaping rules applied to
|
||||||
|
// its content.
|
||||||
|
//
|
||||||
|
// # Parameters
|
||||||
|
//
|
||||||
|
// - dst ([]byte): The destination buffer where the encoded data will be written.
|
||||||
|
//
|
||||||
|
// # Return Values
|
||||||
|
//
|
||||||
|
// - b ([]byte): The byte slice containing the encoded Challenge data.
|
||||||
|
//
|
||||||
|
// # Expected behaviour
|
||||||
|
//
|
||||||
|
// - Prepares the destination buffer and applies a label to it.
|
||||||
|
//
|
||||||
|
// - Escapes the challenge content according to Nostr-specific rules before
|
||||||
|
// appending it to the output.
|
||||||
|
//
|
||||||
|
// - Returns the resulting byte slice with the complete encoded structure.
|
||||||
|
func (en *Challenge) Marshal(dst []byte) (b []byte) {
|
||||||
|
b = dst
|
||||||
|
var err error
|
||||||
|
b = envs.Marshal(
|
||||||
|
b, L,
|
||||||
|
func(bst []byte) (o []byte) {
|
||||||
|
o = bst
|
||||||
|
o = append(o, '"')
|
||||||
|
o = text2.NostrEscape(o, en.Challenge)
|
||||||
|
o = append(o, '"')
|
||||||
|
return
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_ = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the provided byte slice and extracts the challenge value,
|
||||||
|
// leaving any remaining bytes after parsing.
|
||||||
|
//
|
||||||
|
// # Parameters
|
||||||
|
//
|
||||||
|
// - b ([]byte): The byte slice containing the encoded challenge data.
|
||||||
|
//
|
||||||
|
// # Return Values
|
||||||
|
//
|
||||||
|
// - r ([]byte): Any remaining bytes after parsing the challenge.
|
||||||
|
//
|
||||||
|
// - err (error): An error if parsing fails.
|
||||||
|
//
|
||||||
|
// # Expected behaviour
|
||||||
|
//
|
||||||
|
// - Extracts the quoted challenge string from the input byte slice.
|
||||||
|
//
|
||||||
|
// - Trims any trailing characters following the closing quote.
|
||||||
|
func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
|
||||||
|
r = b
|
||||||
|
if en.Challenge, r, err = text2.UnmarshalQuoted(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for ; len(r) >= 0; r = r[1:] {
|
||||||
|
if r[0] == ']' {
|
||||||
|
r = r[:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseChallenge parses the provided byte slice into a new Challenge instance,
|
||||||
|
// extracting the challenge value and returning any remaining bytes after parsing.
|
||||||
|
//
|
||||||
|
// # Parameters
|
||||||
|
//
|
||||||
|
// - b ([]byte): The byte slice containing the encoded challenge data.
|
||||||
|
//
|
||||||
|
// # Return Values
|
||||||
|
//
|
||||||
|
// - t (*Challenge): A pointer to the newly created and populated Challenge
|
||||||
|
// instance.
|
||||||
|
//
|
||||||
|
// - rem ([]byte): Any remaining bytes in the input slice after parsing.
|
||||||
|
//
|
||||||
|
// - err (error): An error if parsing fails.
|
||||||
|
//
|
||||||
|
// # Expected behaviour
|
||||||
|
//
|
||||||
|
// Parses the byte slice into a new Challenge instance using Unmarshal,
|
||||||
|
// returning any remaining bytes and an error if parsing fails.
|
||||||
|
func ParseChallenge(b []byte) (t *Challenge, rem []byte, err error) {
|
||||||
|
t = NewChallenge()
|
||||||
|
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response is a client-side envelope containing the signed event bearing the
|
||||||
|
// relay's URL and Challenge string.
|
||||||
|
type Response struct {
|
||||||
|
Event *event.E
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ codec.Envelope = (*Response)(nil)
|
||||||
|
|
||||||
|
// NewResponse creates a new empty Response.
|
||||||
|
func NewResponse() *Response { return &Response{} }
|
||||||
|
|
||||||
|
// NewResponseWith creates a new Response with a provided event.E.
|
||||||
|
func NewResponseWith(event *event.E) *Response { return &Response{Event: event} }
|
||||||
|
|
||||||
|
// Label returns the label of a auth Response envelope.
|
||||||
|
func (en *Response) Label() string { return L }
|
||||||
|
|
||||||
|
func (en *Response) Id() []byte { return en.Event.ID }
|
||||||
|
|
||||||
|
// Write the Response to a provided io.Writer.
|
||||||
|
func (en *Response) Write(w io.Writer) (err error) {
|
||||||
|
var b []byte
|
||||||
|
b = en.Marshal(b)
|
||||||
|
_, err = w.Write(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a Response to minified JSON, appending to a provided destination
|
||||||
|
// slice. Note that this ensures correct string escaping on the challenge field.
|
||||||
|
func (en *Response) Marshal(dst []byte) (b []byte) {
|
||||||
|
var err error
|
||||||
|
if en == nil {
|
||||||
|
err = errorf.E("nil response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if en.Event == nil {
|
||||||
|
err = errorf.E("nil event in response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b = dst
|
||||||
|
b = envs.Marshal(b, L, en.Event.Marshal)
|
||||||
|
_ = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal a Response from minified JSON, returning the remainder after the en
|
||||||
|
// of the envelope. Note that this ensures the challenge string was correctly
|
||||||
|
// escaped by NIP-01 escaping rules.
|
||||||
|
func (en *Response) Unmarshal(b []byte) (r []byte, err error) {
|
||||||
|
r = b
|
||||||
|
// literally just unmarshal the event
|
||||||
|
en.Event = event.New()
|
||||||
|
if r, err = en.Event.Unmarshal(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r, err = envs.SkipToTheEnd(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResponse reads a Response encoded in minified JSON and unpacks it to
|
||||||
|
// the runtime format.
|
||||||
|
func ParseResponse(b []byte) (t *Response, rem []byte, err error) {
|
||||||
|
t = NewResponse()
|
||||||
|
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
86
pkg/encoders/envelopes/authenvelope/authenvelope_test.go
Normal file
86
pkg/encoders/envelopes/authenvelope/authenvelope_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package authenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/crypto/p256k"
|
||||||
|
"next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/protocol/auth"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
|
"next.orly.dev/pkg/utils/bufpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
const relayURL = "wss://example.com"
|
||||||
|
|
||||||
|
func TestAuth(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
signer := new(p256k.Signer)
|
||||||
|
if err = signer.Generate(); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _ = range 1000 {
|
||||||
|
b1, b2, b3, b4 := bufpool.Get(), bufpool.Get(), bufpool.Get(), bufpool.Get()
|
||||||
|
ch := auth.GenerateChallenge()
|
||||||
|
chal := Challenge{Challenge: ch}
|
||||||
|
b1 = chal.Marshal(b1)
|
||||||
|
oChal := make([]byte, len(b1))
|
||||||
|
copy(oChal, b1)
|
||||||
|
var rem []byte
|
||||||
|
var l string
|
||||||
|
if l, b1, err = envelopes.Identify(b1); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if l != L {
|
||||||
|
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||||
|
}
|
||||||
|
c2 := NewChallenge()
|
||||||
|
if rem, err = c2.Unmarshal(b1); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rem) != 0 {
|
||||||
|
t.Fatalf("remainder should be empty\n%s", rem)
|
||||||
|
}
|
||||||
|
if !utils.FastEqual(chal.Challenge, c2.Challenge) {
|
||||||
|
t.Fatalf(
|
||||||
|
"challenge mismatch\n%s\n%s",
|
||||||
|
chal.Challenge, c2.Challenge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
b2 = c2.Marshal(b2)
|
||||||
|
if !utils.FastEqual(oChal, b2) {
|
||||||
|
t.Fatalf("challenge mismatch\n%s\n%s", oChal, b2)
|
||||||
|
}
|
||||||
|
resp := Response{
|
||||||
|
Event: auth.CreateUnsigned(
|
||||||
|
signer.Pub(), ch,
|
||||||
|
relayURL,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if err = resp.Event.Sign(signer); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b3 = resp.Marshal(b3)
|
||||||
|
oResp := make([]byte, len(b3))
|
||||||
|
copy(oResp, b3)
|
||||||
|
if l, b3, err = envelopes.Identify(b3); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if l != L {
|
||||||
|
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||||
|
}
|
||||||
|
r2 := NewResponse()
|
||||||
|
if _, err = r2.Unmarshal(b3); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b4 = r2.Marshal(b4)
|
||||||
|
if !utils.FastEqual(oResp, b4) {
|
||||||
|
t.Fatalf("challenge mismatch\n%s\n%s", oResp, b4)
|
||||||
|
}
|
||||||
|
bufpool.Put(b1)
|
||||||
|
bufpool.Put(b2)
|
||||||
|
bufpool.Put(b3)
|
||||||
|
bufpool.Put(b4)
|
||||||
|
oChal, oResp = oChal[:0], oResp[:0]
|
||||||
|
}
|
||||||
|
}
|
||||||
100
pkg/encoders/envelopes/closedenvelope/closedenvelope.go
Normal file
100
pkg/encoders/envelopes/closedenvelope/closedenvelope.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Package closedenvelope defines the nostr message type CLOSED which is sent
|
||||||
|
// from a relay to indicate the relay-side termination of a subscription or the
|
||||||
|
// demand for authentication associated with a subscription.
|
||||||
|
package closedenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/encoders/text"
|
||||||
|
"next.orly.dev/pkg/interfaces/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// L is the label associated with this type of codec.Envelope.
|
||||||
|
const L = "CLOSED"
|
||||||
|
|
||||||
|
// T is a CLOSED envelope, which is a signal that a subscription has been
|
||||||
|
// stopped on the relay side for some reason. Primarily this is for auth and can
|
||||||
|
// be for other things like rate limiting.
|
||||||
|
type T struct {
|
||||||
|
Subscription []byte
|
||||||
|
Reason []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ codec.Envelope = (*T)(nil)
|
||||||
|
|
||||||
|
// New creates an empty new T.
|
||||||
|
func New() *T {
|
||||||
|
return new(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFrom creates a new closedenvelope.T populated with subscription ID and Reason.
|
||||||
|
func NewFrom(id, msg []byte) *T {
|
||||||
|
return &T{
|
||||||
|
Subscription: id, Reason: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label returns the label of a closedenvelope.T.
|
||||||
|
func (en *T) Label() string { return L }
|
||||||
|
|
||||||
|
// ReasonString returns the Reason in the form of a string.
|
||||||
|
func (en *T) ReasonString() string { return string(en.Reason) }
|
||||||
|
|
||||||
|
// Write the closedenvelope.T to a provided io.Writer.
|
||||||
|
func (en *T) Write(w io.Writer) (err error) {
|
||||||
|
var b []byte
|
||||||
|
b = en.Marshal(b)
|
||||||
|
_, err = w.Write(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a closedenvelope.T envelope in minified JSON, appending to a provided
|
||||||
|
// destination slice. Note that this ensures correct string escaping on the
|
||||||
|
// Reason field.
|
||||||
|
func (en *T) Marshal(dst []byte) (b []byte) {
|
||||||
|
b = dst
|
||||||
|
b = envelopes.Marshal(
|
||||||
|
b, L,
|
||||||
|
func(bst []byte) (o []byte) {
|
||||||
|
o = bst
|
||||||
|
o = append(o, '"')
|
||||||
|
o = append(o, en.Subscription...)
|
||||||
|
o = append(o, '"')
|
||||||
|
o = append(o, ',')
|
||||||
|
o = append(o, '"')
|
||||||
|
o = text.NostrEscape(o, en.Reason)
|
||||||
|
o = append(o, '"')
|
||||||
|
return
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal a closedenvelope.T from minified JSON, returning the remainder after the end
|
||||||
|
// of the envelope. Note that this ensures the Reason string is correctly
|
||||||
|
// unescaped by NIP-01 escaping rules.
|
||||||
|
func (en *T) Unmarshal(b []byte) (r []byte, err error) {
|
||||||
|
r = b
|
||||||
|
if en.Subscription, r, err = text.UnmarshalQuoted(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if en.Reason, r, err = text.UnmarshalQuoted(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r, err = envelopes.SkipToTheEnd(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads a closedenvelope.T from minified JSON into a newly allocated closedenvelope.T.
|
||||||
|
func Parse(b []byte) (t *T, rem []byte, err error) {
|
||||||
|
t = New()
|
||||||
|
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
85
pkg/encoders/envelopes/closedenvelope/closedenvelope_test.go
Normal file
85
pkg/encoders/envelopes/closedenvelope/closedenvelope_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package closedenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
|
"next.orly.dev/pkg/utils/bufpool"
|
||||||
|
|
||||||
|
"lukechampine.com/frand"
|
||||||
|
)
|
||||||
|
|
||||||
|
var messages = [][]byte{
|
||||||
|
[]byte(""),
|
||||||
|
[]byte("pow: difficulty 25>=24"),
|
||||||
|
[]byte("duplicate: already have this event"),
|
||||||
|
[]byte("blocked: you are banned from posting here"),
|
||||||
|
[]byte("blocked: please register your pubkey at https://my-expensive-realy.example.com"),
|
||||||
|
[]byte("rate-limited: slow down there chief"),
|
||||||
|
[]byte("invalid: event creation date is too far off from the current time"),
|
||||||
|
[]byte("pow: difficulty 26 is less than 30"),
|
||||||
|
[]byte("error: could not connect to the database"),
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomMessage() []byte {
|
||||||
|
return messages[frand.Intn(len(messages)-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalUnmarshal(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
for _ = range 1000 {
|
||||||
|
rb, rb1, rb2 := bufpool.Get(), bufpool.Get(), bufpool.Get()
|
||||||
|
s := []byte(fmt.Sprintf("sub:%d", frand.Intn(math.MaxInt64)))
|
||||||
|
req := NewFrom(s, RandomMessage())
|
||||||
|
rb = req.Marshal(rb)
|
||||||
|
rb1 = append(rb1, rb...)
|
||||||
|
var rem []byte
|
||||||
|
var l string
|
||||||
|
if l, rb, err = envelopes.Identify(rb); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if l != L {
|
||||||
|
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||||
|
}
|
||||||
|
req2 := New()
|
||||||
|
if rem, err = req2.Unmarshal(rb); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// log.I.Ln(req2.ID)
|
||||||
|
if len(rem) > 0 {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, remainder\n%d %s",
|
||||||
|
len(rem), rem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rb2 = req2.Marshal(rb2)
|
||||||
|
if !utils.FastEqual(rb1, rb2) {
|
||||||
|
if len(rb1) != len(rb2) {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, different lengths\n%d %s\n%d %s\n",
|
||||||
|
len(rb1), rb1, len(rb2), rb2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for i := range rb1 {
|
||||||
|
if rb1[i] != rb2[i] {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, difference at position %d\n%d %s\n%s\n%d %s\n%s\n",
|
||||||
|
i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i],
|
||||||
|
rb2[i:],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed\n%d %s\n%d %s\n",
|
||||||
|
len(rb1), rb1, len(rb2), rb2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
bufpool.Put(rb1)
|
||||||
|
bufpool.Put(rb2)
|
||||||
|
bufpool.Put(rb)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
pkg/encoders/envelopes/closeenvelope/closeenvelope.go
Normal file
78
pkg/encoders/envelopes/closeenvelope/closeenvelope.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Package closeenvelope provides the encoder for the client message CLOSE which
|
||||||
|
// is a request to terminate a subscription.
|
||||||
|
package closeenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/encoders/text"
|
||||||
|
"next.orly.dev/pkg/interfaces/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// L is the label associated with this type of codec.Envelope.
|
||||||
|
const L = "CLOSE"
|
||||||
|
|
||||||
|
// T is a CLOSE envelope, which is a signal from client to relay to stop a
|
||||||
|
// specified subscription.
|
||||||
|
type T struct {
|
||||||
|
ID []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ codec.Envelope = (*T)(nil)
|
||||||
|
|
||||||
|
// New creates an empty new standard formatted closeenvelope.T.
|
||||||
|
func New() *T { return new(T) }
|
||||||
|
|
||||||
|
// NewFrom creates a new closeenvelope.T populated with subscription ID.
|
||||||
|
func NewFrom(id []byte) *T { return &T{ID: id} }
|
||||||
|
|
||||||
|
// Label returns the label of a closeenvelope.T.
|
||||||
|
func (en *T) Label() string { return L }
|
||||||
|
|
||||||
|
// Write the closeenvelope.T to a provided io.Writer.
|
||||||
|
func (en *T) Write(w io.Writer) (err error) {
|
||||||
|
_, err = w.Write(en.Marshal(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal a closeenvelope.T envelope in minified JSON, appending to a provided
|
||||||
|
// destination slice.
|
||||||
|
func (en *T) Marshal(dst []byte) (b []byte) {
|
||||||
|
b = dst
|
||||||
|
b = envelopes.Marshal(
|
||||||
|
b, L,
|
||||||
|
func(bst []byte) (o []byte) {
|
||||||
|
o = bst
|
||||||
|
o = append(o, '"')
|
||||||
|
o = append(o, en.ID...)
|
||||||
|
o = append(o, '"')
|
||||||
|
return
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal a closeenvelope.T from minified JSON, returning the remainder after
|
||||||
|
// the end of the envelope.
|
||||||
|
func (en *T) Unmarshal(b []byte) (r []byte, err error) {
|
||||||
|
r = b
|
||||||
|
if en.ID, r, err = text.UnmarshalQuoted(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r, err = envelopes.SkipToTheEnd(r); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads a CLOSE envelope from minified JSON into a newly allocated
|
||||||
|
// closeenvelope.T.
|
||||||
|
func Parse(b []byte) (t *T, rem []byte, err error) {
|
||||||
|
t = New()
|
||||||
|
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
68
pkg/encoders/envelopes/closeenvelope/closeenvelope_test.go
Normal file
68
pkg/encoders/envelopes/closeenvelope/closeenvelope_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package closeenvelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"lukechampine.com/frand"
|
||||||
|
"next.orly.dev/pkg/encoders/envelopes"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
|
"next.orly.dev/pkg/utils/bufpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalUnmarshal(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
for _ = range 1000 {
|
||||||
|
rb, rb1, rb2 := bufpool.Get(), bufpool.Get(), bufpool.Get()
|
||||||
|
s := []byte(fmt.Sprintf("sub:%d", frand.Intn(math.MaxInt64)))
|
||||||
|
req := NewFrom(s)
|
||||||
|
rb = req.Marshal(rb)
|
||||||
|
rb1 = append(rb1, rb...)
|
||||||
|
var rem []byte
|
||||||
|
var l string
|
||||||
|
if l, rb, err = envelopes.Identify(rb); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if l != L {
|
||||||
|
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||||
|
}
|
||||||
|
req2 := New()
|
||||||
|
if rem, err = req2.Unmarshal(rb); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// log.I.Ln(req2.ID)
|
||||||
|
if len(rem) > 0 {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, remainder\n%d %s",
|
||||||
|
len(rem), rem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rb2 = req2.Marshal(rb2)
|
||||||
|
if !utils.FastEqual(rb1, rb2) {
|
||||||
|
if len(rb1) != len(rb2) {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, different lengths\n%d %s\n%d %s\n",
|
||||||
|
len(rb1), rb1, len(rb2), rb2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for i := range rb1 {
|
||||||
|
if rb1[i] != rb2[i] {
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed, difference at position %d\n%d %s\n%s\n%d %s\n%s\n",
|
||||||
|
i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i],
|
||||||
|
rb2[i:],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf(
|
||||||
|
"unmarshal failed\n%d %s\n%d %s\n",
|
||||||
|
len(rb1), rb1, len(rb2), rb2,
|
||||||
|
)
|
||||||
|
bufpool.Put(rb1)
|
||||||
|
bufpool.Put(rb2)
|
||||||
|
bufpool.Put(rb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
pkg/encoders/envelopes/doc.go
Normal file
3
pkg/encoders/envelopes/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package envelopes provides common functions for marshaling and identifying
|
||||||
|
// nostr envelopes (JSON arrays containing protocol messages).
|
||||||
|
package envelopes
|
||||||
38
pkg/encoders/envelopes/identify.go
Normal file
38
pkg/encoders/envelopes/identify.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package envelopes
|
||||||
|
|
||||||
|
// Identify handles determining what kind of codec.Envelope is, by the Label,
|
||||||
|
// the first step in identifying the structure of the message. This first step
|
||||||
|
// is not sufficient because the same labels are used on several codec.Envelope
|
||||||
|
// types in the nostr specification. The rest of the context is in whether this
|
||||||
|
// is a client or a relay receiving it.
|
||||||
|
func Identify(b []byte) (t string, rem []byte, err error) {
|
||||||
|
var openBrackets, openQuotes, afterQuotes bool
|
||||||
|
var label []byte
|
||||||
|
rem = b
|
||||||
|
for ; len(rem) > 0; rem = rem[1:] {
|
||||||
|
if !openBrackets && rem[0] == '[' {
|
||||||
|
openBrackets = true
|
||||||
|
} else if openBrackets {
|
||||||
|
if !openQuotes && rem[0] == '"' {
|
||||||
|
openQuotes = true
|
||||||
|
} else if afterQuotes {
|
||||||
|
// return the remainder after the comma
|
||||||
|
if rem[0] == ',' {
|
||||||
|
rem = rem[1:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if openQuotes {
|
||||||
|
for i := range rem {
|
||||||
|
if rem[i] == '"' {
|
||||||
|
label = rem[:i]
|
||||||
|
rem = rem[i:]
|
||||||
|
t = string(label)
|
||||||
|
afterQuotes = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
41
pkg/encoders/envelopes/process.go
Normal file
41
pkg/encoders/envelopes/process.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package envelopes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshaller is a function signature the same as the codec.JSON Marshal but
|
||||||
|
// without the requirement of there being a full implementation or declared
|
||||||
|
// receiver variable of this interface. Used here to encapsulate one or more
|
||||||
|
// other data structures into an envelope.
|
||||||
|
type Marshaller func(dst []byte) (b []byte)
|
||||||
|
|
||||||
|
// Marshal is a parser for dynamic typed arrays like nosttr codec.Envelope
|
||||||
|
// types.
|
||||||
|
func Marshal(dst []byte, label string, m Marshaller) (b []byte) {
|
||||||
|
b = dst
|
||||||
|
b = append(b, '[', '"')
|
||||||
|
b = append(b, label...)
|
||||||
|
b = append(b, '"', ',')
|
||||||
|
b = m(b)
|
||||||
|
b = append(b, ']')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipToTheEnd scans forward after all fields in an envelope have been read to
|
||||||
|
// find the closing bracket.
|
||||||
|
func SkipToTheEnd(dst []byte) (rem []byte, err error) {
|
||||||
|
if len(dst) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rem = dst
|
||||||
|
// we have everything, just need to snip the end
|
||||||
|
for ; len(rem) > 0; rem = rem[1:] {
|
||||||
|
if rem[0] == ']' {
|
||||||
|
rem = rem[:0]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
36
pkg/encoders/event/canonical.go
Normal file
36
pkg/encoders/event/canonical.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"next.orly.dev/pkg/crypto/sha256"
|
||||||
|
"next.orly.dev/pkg/encoders/hex"
|
||||||
|
"next.orly.dev/pkg/encoders/ints"
|
||||||
|
"next.orly.dev/pkg/encoders/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToCanonical converts the event to the canonical encoding used to derive the
|
||||||
|
// event ID.
|
||||||
|
func (ev *E) ToCanonical(dst []byte) (b []byte) {
|
||||||
|
b = dst
|
||||||
|
b = append(b, "[0,\""...)
|
||||||
|
b = hex.EncAppend(b, ev.Pubkey)
|
||||||
|
b = append(b, "\","...)
|
||||||
|
b = ints.New(ev.CreatedAt).Marshal(nil)
|
||||||
|
b = append(b, ',')
|
||||||
|
b = ints.New(ev.Kind).Marshal(nil)
|
||||||
|
b = append(b, ',')
|
||||||
|
b = ev.Tags.Marshal(b)
|
||||||
|
b = append(b, ',')
|
||||||
|
b = text.AppendQuote(b, ev.Content, text.NostrEscape)
|
||||||
|
b = append(b, ']')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDBytes returns the raw SHA256 hash of the canonical form of an event.E.
|
||||||
|
func (ev *E) GetIDBytes() []byte { return Hash(ev.ToCanonical(nil)) }
|
||||||
|
|
||||||
|
// Hash is a little helper generate a hash and return a slice instead of an
|
||||||
|
// array.
|
||||||
|
func Hash(in []byte) (out []byte) {
|
||||||
|
h := sha256.Sum256(in)
|
||||||
|
return h[:]
|
||||||
|
}
|
||||||
@@ -94,17 +94,8 @@ func (ev *E) Free() {
|
|||||||
ev.b = nil
|
ev.b = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON marshals an event.E into a JSON byte string.
|
func (ev *E) Marshal(dst []byte) (b []byte) {
|
||||||
//
|
b = dst
|
||||||
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
|
||||||
//
|
|
||||||
// WARNING: if json.Marshal is called in the hopes of invoking this function on
|
|
||||||
// an event, if it has <, > or * in the content or tags they are escaped into
|
|
||||||
// unicode escapes and break the event ID. Call this function directly in order
|
|
||||||
// to bypass this issue.
|
|
||||||
func (ev *E) MarshalJSON() (b []byte, err error) {
|
|
||||||
b = bufpool.Get()
|
|
||||||
b = b[:0]
|
|
||||||
b = append(b, '{')
|
b = append(b, '{')
|
||||||
b = append(b, '"')
|
b = append(b, '"')
|
||||||
b = append(b, jId...)
|
b = append(b, jId...)
|
||||||
@@ -159,10 +150,76 @@ func (ev *E) MarshalJSON() (b []byte, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshalls a JSON string into an event.E.
|
// MarshalJSON marshals an event.E into a JSON byte string.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
|
//
|
||||||
|
// WARNING: if json.Marshal is called in the hopes of invoking this function on
|
||||||
|
// an event, if it has <, > or * in the content or tags they are escaped into
|
||||||
|
// unicode escapes and break the event ID. Call this function directly in order
|
||||||
|
// to bypass this issue.
|
||||||
|
func (ev *E) MarshalJSON() (b []byte, err error) {
|
||||||
|
b = bufpool.Get()
|
||||||
|
b = ev.Marshal(b[:0])
|
||||||
|
// b = b[:0]
|
||||||
|
// b = append(b, '{')
|
||||||
|
// b = append(b, '"')
|
||||||
|
// b = append(b, jId...)
|
||||||
|
// b = append(b, `":"`...)
|
||||||
|
// b = b[:len(b)+2*sha256.Size]
|
||||||
|
// xhex.Encode(b[len(b)-2*sha256.Size:], ev.ID)
|
||||||
|
// b = append(b, `","`...)
|
||||||
|
// b = append(b, jPubkey...)
|
||||||
|
// b = append(b, `":"`...)
|
||||||
|
// b = b[:len(b)+2*schnorr.PubKeyBytesLen]
|
||||||
|
// xhex.Encode(b[len(b)-2*schnorr.PubKeyBytesLen:], ev.Pubkey)
|
||||||
|
// b = append(b, `","`...)
|
||||||
|
// b = append(b, jCreatedAt...)
|
||||||
|
// b = append(b, `":`...)
|
||||||
|
// b = ints.New(ev.CreatedAt).Marshal(b)
|
||||||
|
// b = append(b, `,"`...)
|
||||||
|
// b = append(b, jKind...)
|
||||||
|
// b = append(b, `":`...)
|
||||||
|
// b = ints.New(ev.Kind).Marshal(b)
|
||||||
|
// b = append(b, `,"`...)
|
||||||
|
// b = append(b, jTags...)
|
||||||
|
// b = append(b, `":`...)
|
||||||
|
// if ev.Tags != nil {
|
||||||
|
// b = ev.Tags.Marshal(b)
|
||||||
|
// }
|
||||||
|
// b = append(b, `,"`...)
|
||||||
|
// b = append(b, jContent...)
|
||||||
|
// b = append(b, `":"`...)
|
||||||
|
// // it can happen the slice has insufficient capacity to hold the content AND
|
||||||
|
// // the signature at this point, because the signature encoder must have
|
||||||
|
// // sufficient capacity pre-allocated as it does not append to the buffer.
|
||||||
|
// // unlike every other encoding function up to this point. This also ensures
|
||||||
|
// // that since the bufpool defaults to 1kb, most events won't have a
|
||||||
|
// // re-allocation required, but if they do, it will be this next one, and it
|
||||||
|
// // integrates properly with the buffer pool, reducing GC pressure and
|
||||||
|
// // avoiding new heap allocations.
|
||||||
|
// if cap(b) < len(b)+len(ev.Content)+7+256+2 {
|
||||||
|
// b2 := make([]byte, len(b)+len(ev.Content)*2+7+256+2)
|
||||||
|
// copy(b2, b)
|
||||||
|
// b2 = b2[:len(b)]
|
||||||
|
// // return the old buffer to the pool for reuse.
|
||||||
|
// bufpool.PutBytes(b)
|
||||||
|
// b = b2
|
||||||
|
// }
|
||||||
|
// b = text.NostrEscape(b, ev.Content)
|
||||||
|
// b = append(b, `","`...)
|
||||||
|
// b = append(b, jSig...)
|
||||||
|
// b = append(b, `":"`...)
|
||||||
|
// b = b[:len(b)+2*schnorr.SignatureSize]
|
||||||
|
// xhex.Encode(b[len(b)-2*schnorr.SignatureSize:], ev.Sig)
|
||||||
|
// b = append(b, `"}`...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshalls a JSON string into an event.E.
|
||||||
//
|
//
|
||||||
// Call ev.Free() to return the provided buffer to the bufpool afterwards.
|
// Call ev.Free() to return the provided buffer to the bufpool afterwards.
|
||||||
func (ev *E) UnmarshalJSON(b []byte) (err error) {
|
func (ev *E) Unmarshal(b []byte) (rem []byte, err error) {
|
||||||
key := make([]byte, 0, 9)
|
key := make([]byte, 0, 9)
|
||||||
for ; len(b) > 0; b = b[1:] {
|
for ; len(b) > 0; b = b[1:] {
|
||||||
// Skip whitespace
|
// Skip whitespace
|
||||||
@@ -337,10 +394,7 @@ BetweenKV:
|
|||||||
log.I.F("between kv")
|
log.I.F("between kv")
|
||||||
goto eof
|
goto eof
|
||||||
AfterClose:
|
AfterClose:
|
||||||
// Skip any trailing whitespace
|
rem = b
|
||||||
for len(b) > 0 && isWhitespace(b[0]) {
|
|
||||||
b = b[1:]
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
invalid:
|
invalid:
|
||||||
err = fmt.Errorf(
|
err = fmt.Errorf(
|
||||||
@@ -353,6 +407,14 @@ eof:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON unmarshalls a JSON string into an event.E.
|
||||||
|
//
|
||||||
|
// Call ev.Free() to return the provided buffer to the bufpool afterwards.
|
||||||
|
func (ev *E) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
_, err = ev.Unmarshal(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// isWhitespace returns true if the byte is a whitespace character (space, tab, newline, carriage return).
|
// isWhitespace returns true if the byte is a whitespace character (space, tab, newline, carriage return).
|
||||||
func isWhitespace(b byte) bool {
|
func isWhitespace(b byte) bool {
|
||||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||||
|
|||||||
48
pkg/encoders/event/signatures.go
Normal file
48
pkg/encoders/event/signatures.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"lol.mleku.dev/errorf"
|
||||||
|
"lol.mleku.dev/log"
|
||||||
|
"next.orly.dev/pkg/crypto/p256k"
|
||||||
|
"next.orly.dev/pkg/interfaces/signer"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sign the event using the signer.I. Uses github.com/bitcoin-core/secp256k1 if
|
||||||
|
// available for much faster signatures.
|
||||||
|
//
|
||||||
|
// Note that this only populates the Pubkey, ID and Sig. The caller must
|
||||||
|
// set the CreatedAt timestamp as intended.
|
||||||
|
func (ev *E) Sign(keys signer.I) (err error) {
|
||||||
|
ev.Pubkey = keys.Pub()
|
||||||
|
ev.ID = ev.GetIDBytes()
|
||||||
|
if ev.Sig, err = keys.Sign(ev.ID); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify an event is signed by the pubkey it contains. Uses
|
||||||
|
// github.com/bitcoin-core/secp256k1 if available for faster verification.
|
||||||
|
func (ev *E) Verify() (valid bool, err error) {
|
||||||
|
keys := p256k.Signer{}
|
||||||
|
if err = keys.InitPub(ev.Pubkey); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.T(err) {
|
||||||
|
// check that this isn't because of a bogus ID
|
||||||
|
id := ev.GetIDBytes()
|
||||||
|
if !utils.FastEqual(id, ev.ID) {
|
||||||
|
log.E.Ln("event ID incorrect")
|
||||||
|
ev.ID = id
|
||||||
|
err = nil
|
||||||
|
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.E(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errorf.W("event ID incorrect but signature is valid on correct ID")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ func (k *K) ToU64() uint64 {
|
|||||||
|
|
||||||
// Name returns the human readable string describing the semantics of the
|
// Name returns the human readable string describing the semantics of the
|
||||||
// kind.K.
|
// kind.K.
|
||||||
func (k *K) Name() string { return GetString(k) }
|
func (k *K) Name() string { return GetString(k.K) }
|
||||||
|
|
||||||
// Equal checks if
|
// Equal checks if
|
||||||
func (k *K) Equal(k2 *K) bool {
|
func (k *K) Equal(k2 *K) bool {
|
||||||
@@ -105,13 +105,10 @@ func (k *K) Unmarshal(b []byte) (r []byte, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetString returns a human-readable identifier for a kind.K.
|
// GetString returns a human-readable identifier for a kind.K.
|
||||||
func GetString(t *K) string {
|
func GetString(t uint16) string {
|
||||||
if t == nil {
|
MapMx.RLock()
|
||||||
return ""
|
defer MapMx.RUnlock()
|
||||||
}
|
return Map[t]
|
||||||
MapMx.Lock()
|
|
||||||
defer MapMx.Unlock()
|
|
||||||
return Map[t.K]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEphemeral returns true if the event kind is an ephemeral event. (not to be
|
// IsEphemeral returns true if the event kind is an ephemeral event. (not to be
|
||||||
@@ -340,7 +337,7 @@ var (
|
|||||||
ParameterizedReplaceableEnd = &K{39999}
|
ParameterizedReplaceableEnd = &K{39999}
|
||||||
)
|
)
|
||||||
|
|
||||||
var MapMx sync.Mutex
|
var MapMx sync.RWMutex
|
||||||
var Map = map[uint16]string{
|
var Map = map[uint16]string{
|
||||||
ProfileMetadata.K: "ProfileMetadata",
|
ProfileMetadata.K: "ProfileMetadata",
|
||||||
TextNote.K: "TextNote",
|
TextNote.K: "TextNote",
|
||||||
|
|||||||
@@ -23,8 +23,18 @@ type T struct {
|
|||||||
b bufpool.B
|
b bufpool.B
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(t ...[]byte) *T {
|
func New(t ...any) *T {
|
||||||
return &T{T: t, b: bufpool.Get()}
|
var bs [][]byte
|
||||||
|
for _, v := range t {
|
||||||
|
if vb, ok := v.([]byte); ok {
|
||||||
|
bs = append(bs, vb)
|
||||||
|
} else if vs, ok := v.(string); ok {
|
||||||
|
bs = append(bs, []byte(vs))
|
||||||
|
} else {
|
||||||
|
panic("programmer error: type of tag element is not []byte or string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &T{T: bs, b: bufpool.Get()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWithCap(c int) *T {
|
func NewWithCap(c int) *T {
|
||||||
@@ -45,8 +55,6 @@ func (t *T) Less(i, j int) bool {
|
|||||||
func (t *T) Swap(i, j int) { t.T[i], t.T[j] = t.T[j], t.T[i] }
|
func (t *T) Swap(i, j int) { t.T[i], t.T[j] = t.T[j], t.T[i] }
|
||||||
|
|
||||||
// Marshal encodes a tag.T as standard minified JSON array of strings.
|
// Marshal encodes a tag.T as standard minified JSON array of strings.
|
||||||
//
|
|
||||||
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
|
||||||
func (t *T) Marshal(dst []byte) (b []byte) {
|
func (t *T) Marshal(dst []byte) (b []byte) {
|
||||||
dst = append(dst, '[')
|
dst = append(dst, '[')
|
||||||
for i, s := range t.T {
|
for i, s := range t.T {
|
||||||
@@ -63,15 +71,21 @@ func (t *T) Marshal(dst []byte) (b []byte) {
|
|||||||
//
|
//
|
||||||
// Warning: this will mangle the output if the tag fields contain <, > or &
|
// Warning: this will mangle the output if the tag fields contain <, > or &
|
||||||
// characters. do not use json.Marshal in the hopes of rendering tags verbatim
|
// characters. do not use json.Marshal in the hopes of rendering tags verbatim
|
||||||
// in an event as you will have a bad time.
|
// in an event as you will have a bad time. Use the json.Marshal function in the
|
||||||
|
// pkg/encoders/json package instead, this has a fork of the json library that
|
||||||
|
// disables html escaping for json.Marshal.
|
||||||
|
//
|
||||||
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
func (t *T) MarshalJSON() (b []byte, err error) {
|
func (t *T) MarshalJSON() (b []byte, err error) {
|
||||||
b = t.Marshal(nil)
|
b = bufpool.Get()
|
||||||
|
b = t.Marshal(b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes a standard minified JSON array of strings to a tags.T.
|
// Unmarshal decodes a standard minified JSON array of strings to a tags.T.
|
||||||
//
|
//
|
||||||
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use if it
|
||||||
|
// was originally created using bufpool.Get().
|
||||||
func (t *T) Unmarshal(b []byte) (r []byte, err error) {
|
func (t *T) Unmarshal(b []byte) (r []byte, err error) {
|
||||||
var inQuotes, openedBracket bool
|
var inQuotes, openedBracket bool
|
||||||
var quoteStart int
|
var quoteStart int
|
||||||
@@ -101,3 +115,24 @@ func (t *T) UnmarshalJSON(b []byte) (err error) {
|
|||||||
_, err = t.Unmarshal(b)
|
_, err = t.Unmarshal(b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *T) Key() (key []byte) {
|
||||||
|
if len(t.T) > Key {
|
||||||
|
return t.T[Key]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Value() (key []byte) {
|
||||||
|
if len(t.T) > Value {
|
||||||
|
return t.T[Value]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Relay() (key []byte) {
|
||||||
|
if len(t.T) > Relay {
|
||||||
|
return t.T[Relay]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"lol.mleku.dev/chk"
|
"lol.mleku.dev/chk"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
"next.orly.dev/pkg/utils/bufpool"
|
"next.orly.dev/pkg/utils/bufpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +12,12 @@ import (
|
|||||||
// no uniqueness constraint (not a set).
|
// no uniqueness constraint (not a set).
|
||||||
type S []*T
|
type S []*T
|
||||||
|
|
||||||
|
func NewS(t ...*T) (s *S) {
|
||||||
|
s = new(S)
|
||||||
|
*s = append(*s, t...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func NewSWithCap(c int) (s *S) {
|
func NewSWithCap(c int) (s *S) {
|
||||||
ss := make([]*T, 0, c)
|
ss := make([]*T, 0, c)
|
||||||
return (*S)(&ss)
|
return (*S)(&ss)
|
||||||
@@ -31,6 +38,10 @@ func (s *S) Swap(i, j int) {
|
|||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *S) Append(t ...*T) {
|
||||||
|
*s = append(*s, t...)
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes a tags.T appended to a provided byte slice in JSON form.
|
// MarshalJSON encodes a tags.T appended to a provided byte slice in JSON form.
|
||||||
//
|
//
|
||||||
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
// Call bufpool.PutBytes(b) to return the buffer to the bufpool after use.
|
||||||
@@ -110,3 +121,13 @@ func (s *S) Unmarshal(b []byte) (r []byte, err error) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFirst returns the first tag.T that has the same Key as t.
|
||||||
|
func (s *S) GetFirst(t []byte) (first *T) {
|
||||||
|
for _, tt := range *s {
|
||||||
|
if utils.FastEqual(tt.T[0], t) {
|
||||||
|
return tt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
47
pkg/interfaces/codec/codec.go
Normal file
47
pkg/interfaces/codec/codec.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type I interface {
|
||||||
|
MarshalWrite(w io.Writer) (err error)
|
||||||
|
UnmarshalRead(r io.Reader) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envelope is an interface for the nostr "envelope" message formats, a JSON
|
||||||
|
// array with the first field an upper case string that provides type
|
||||||
|
// information, in combination with the context of the side sending it (relay or
|
||||||
|
// client).
|
||||||
|
type Envelope interface {
|
||||||
|
// Label returns the (uppercase) string that signifies the type of message.
|
||||||
|
Label() string
|
||||||
|
// Write outputs the envelope to an io.Writer
|
||||||
|
Write(w io.Writer) (err error)
|
||||||
|
// JSON is a somewhat simplified version of the
|
||||||
|
// json.Marshaler/json.Unmarshaler that has no error for the Marshal side of
|
||||||
|
// the operation.
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON is a somewhat simplified version of the json.Marshaler/json.Unmarshaler
|
||||||
|
// that has no error for the Marshal side of the operation.
|
||||||
|
type JSON interface {
|
||||||
|
// Marshal converts the data of the type into JSON, appending it to the provided
|
||||||
|
// slice and returning the extended slice.
|
||||||
|
Marshal(dst []byte) (b []byte)
|
||||||
|
// Unmarshal decodes a JSON form of a type back into the runtime form, and
|
||||||
|
// returns whatever remains after the type has been decoded out.
|
||||||
|
Unmarshal(b []byte) (r []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary is a similarly simplified form of the stdlib binary Marshal/Unmarshal
|
||||||
|
// server. Same as JSON it does not have an error for the MarshalBinary.
|
||||||
|
type Binary interface {
|
||||||
|
// MarshalBinary converts the data of the type into binary form, appending
|
||||||
|
// it to the provided slice.
|
||||||
|
MarshalBinary(dst []byte) (b []byte)
|
||||||
|
// UnmarshalBinary decodes a binary form of a type back into the runtime
|
||||||
|
// form, and returns whatever remains after the type has been decoded out.
|
||||||
|
UnmarshalBinary(b []byte) (r []byte, err error)
|
||||||
|
}
|
||||||
126
pkg/protocol/auth/nip42.go
Normal file
126
pkg/protocol/auth/nip42.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"lol.mleku.dev/errorf"
|
||||||
|
"next.orly.dev/pkg/encoders/event"
|
||||||
|
"next.orly.dev/pkg/encoders/kind"
|
||||||
|
"next.orly.dev/pkg/encoders/tag"
|
||||||
|
"next.orly.dev/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateChallenge creates a reasonable, 16-byte base64 challenge string
|
||||||
|
func GenerateChallenge() (b []byte) {
|
||||||
|
bb := make([]byte, 12)
|
||||||
|
b = make([]byte, 16)
|
||||||
|
_, _ = rand.Read(bb)
|
||||||
|
base64.URLEncoding.Encode(b, bb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUnsigned creates an event which should be sent via an "AUTH" command.
|
||||||
|
// If the authentication succeeds, the user will be authenticated as a pubkey.
|
||||||
|
func CreateUnsigned(pubkey, challenge []byte, relayURL string) (ev *event.E) {
|
||||||
|
return &event.E{
|
||||||
|
Pubkey: pubkey,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
Kind: kind.ClientAuthentication.K,
|
||||||
|
Tags: tag.NewS(
|
||||||
|
tag.New("relay", relayURL),
|
||||||
|
tag.New("challenge", string(challenge)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function for ValidateAuthEvent.
|
||||||
|
func parseURL(input string) (*url.URL, error) {
|
||||||
|
return url.Parse(
|
||||||
|
strings.ToLower(
|
||||||
|
strings.TrimSuffix(input, "/"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ChallengeTag is the tag for the challenge in a NIP-42 auth event
|
||||||
|
// (prevents relay attacks).
|
||||||
|
ChallengeTag = []byte("challenge")
|
||||||
|
// RelayTag is the relay tag for a NIP-42 auth event (prevents cross-server
|
||||||
|
// attacks).
|
||||||
|
RelayTag = []byte("relay")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate checks whether an event is a valid NIP-42 event for a given
|
||||||
|
// challenge and relayURL. The result of the validation is encoded in the ok
|
||||||
|
// bool.
|
||||||
|
func Validate(evt *event.E, challenge []byte, relayURL string) (
|
||||||
|
ok bool, err error,
|
||||||
|
) {
|
||||||
|
if evt.Kind != kind.ClientAuthentication.K {
|
||||||
|
err = errorf.E(
|
||||||
|
"event incorrect kind for auth: %d %s",
|
||||||
|
evt.Kind, kind.GetString(evt.Kind),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if evt.Tags.GetFirst(ChallengeTag) == nil {
|
||||||
|
err = errorf.E("challenge tag missing from auth response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utils.FastEqual(challenge, evt.Tags.GetFirst(ChallengeTag).Value()) {
|
||||||
|
err = errorf.E("challenge tag incorrect from auth response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var expected, found *url.URL
|
||||||
|
if expected, err = parseURL(relayURL); chk.D(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := evt.Tags.
|
||||||
|
GetFirst(RelayTag).Value()
|
||||||
|
if len(r) == 0 {
|
||||||
|
err = errorf.E("relay tag missing from auth response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if found, err = parseURL(string(r)); chk.D(err) {
|
||||||
|
err = errorf.E("error parsing relay url: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if expected.Scheme != found.Scheme {
|
||||||
|
err = errorf.E(
|
||||||
|
"HTTP Scheme incorrect: expected '%s' got '%s",
|
||||||
|
expected.Scheme, found.Scheme,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if expected.Host != found.Host {
|
||||||
|
err = errorf.E(
|
||||||
|
"HTTP Host incorrect: expected '%s' got '%s",
|
||||||
|
expected.Host, found.Host,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if expected.Path != found.Path {
|
||||||
|
err = errorf.E(
|
||||||
|
"HTTP Path incorrect: expected '%s' got '%s",
|
||||||
|
expected.Path, found.Path,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
ca := evt.CreatedAt
|
||||||
|
if ca > now+10*60 || ca < now-10*60 {
|
||||||
|
err = errorf.E(
|
||||||
|
"auth event more than 10 minutes before or after current time",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// save for last, as it is the most expensive operation
|
||||||
|
return evt.Verify()
|
||||||
|
}
|
||||||
34
pkg/protocol/auth/nip42_test.go
Normal file
34
pkg/protocol/auth/nip42_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"lol.mleku.dev/chk"
|
||||||
|
"lol.mleku.dev/log"
|
||||||
|
"next.orly.dev/pkg/crypto/p256k"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateUnsigned(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
signer := new(p256k.Signer)
|
||||||
|
if err = signer.Generate(); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
const relayURL = "wss://example.com"
|
||||||
|
for range 100 {
|
||||||
|
challenge := GenerateChallenge()
|
||||||
|
ev := CreateUnsigned(signer.Pub(), challenge, relayURL)
|
||||||
|
if err = ev.Sign(signer); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
log.I.S(ev)
|
||||||
|
if ok, err = Validate(ev, challenge, relayURL); chk.E(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
bb := ev.Marshal(nil)
|
||||||
|
t.Fatalf("failed to validate auth event\n%s", bb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user