From b9d04f1496a9f97ac9a8e5df8ac10a9262ed37bd Mon Sep 17 00:00:00 2001 From: barkyq <122579762+barkyq@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:06:59 -0500 Subject: [PATCH 1/2] cleaning up marshaling/serialization (#1) * cleaning up marshaling/serialization * cleaning up marshaling/serialization --- event.go | 70 ++++++++------------------------------------------ event_aux.go | 72 +++++++++++++++++++++++++--------------------------- helpers.go | 40 +++++++++++++++++++++++++++++ relay.go | 10 +++++++- tags.go | 37 ++++++++++++++++++++++++--- 5 files changed, 129 insertions(+), 100 deletions(-) diff --git a/event.go b/event.go index 33055a4..a33d52f 100644 --- a/event.go +++ b/event.go @@ -4,10 +4,9 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "time" - "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "time" ) type Event struct { @@ -45,79 +44,32 @@ func (evt *Event) GetID() string { return hex.EncodeToString(h[:]) } -// Escaping strings for JSON encoding according to RFC4627. -// Also encloses result in quotation marks "". -func quoteEscapeString(dst []byte, s string) []byte { - dst = append(dst, '"') - for i := 0; i < len(s); i++ { - c := s[i] - switch { - case c == '"': - // quotation mark - dst = append(dst, []byte{'\\', '"'}...) - case c == '\\': - // reverse solidus - dst = append(dst, []byte{'\\', '\\'}...) - case c >= 0x20: - // default, rest below are control chars - dst = append(dst, c) - case c < 0x09: - dst = append(dst, []byte{'\\', 'u', '0', '0', '0', '0' + c}...) - case c == 0x09: - dst = append(dst, []byte{'\\', 't'}...) - case c == 0x0a: - dst = append(dst, []byte{'\\', 'n'}...) - case c == 0x0d: - dst = append(dst, []byte{'\\', 'r'}...) - case c < 0x10: - dst = append(dst, []byte{'\\', 'u', '0', '0', '0', 0x57 + c}...) - case c < 0x1a: - dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x20 + c}...) - case c < 0x20: - dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x47 + c}...) - } - } - dst = append(dst, '"') - return dst -} - // Serialize outputs a byte array that can be hashed/signed to identify/authenticate. // JSON encoding as defined in RFC4627. func (evt *Event) Serialize() []byte { // the serialization process is just putting everything into a JSON array // so the order is kept. See NIP-01 - ser := make([]byte, 0) + dst := make([]byte, 0) // the header portion is easy to serialize // [0,"pubkey",created_at,kind,[ - ser = append(ser, []byte( + dst = append(dst, []byte( fmt.Sprintf( - "[0,\"%s\",%d,%d,[", + "[0,\"%s\",%d,%d,", evt.PubKey, evt.CreatedAt.Unix(), evt.Kind, ))...) - // tags need to be escaped in general. - for i, tag := range evt.Tags { - if i > 0 { - ser = append(ser, ',') - } - ser = append(ser, '[') - for i, s := range tag { - if i > 0 { - ser = append(ser, ',') - } - ser = quoteEscapeString(ser, s) - } - ser = append(ser, ']') - } - ser = append(ser, []byte{']', ','}...) + + // tags + dst = evt.Tags.marshalTo(dst) + dst = append(dst, ',') // content needs to be escaped in general as it is user generated. - ser = quoteEscapeString(ser, evt.Content) - ser = append(ser, ']') + dst = escapeString(dst, evt.Content) + dst = append(dst, ']') - return ser + return dst } // CheckSignature checks if the signature is valid for the id diff --git a/event_aux.go b/event_aux.go index 8a7aa47..e0bfa37 100644 --- a/event_aux.go +++ b/event_aux.go @@ -1,11 +1,11 @@ package nostr import ( + "bytes" "encoding/json" "fmt" - "time" - "github.com/valyala/fastjson" + "time" ) func (evt *Event) UnmarshalJSON(payload []byte) error { @@ -74,35 +74,10 @@ func (evt *Event) UnmarshalJSON(payload []byte) error { evt.extra[key] = anyValue } }) - if visiterr != nil { - return visiterr - } - - return nil -} - -func (evt Event) MarshalJSON() ([]byte, error) { - var arena fastjson.Arena - - o := arena.NewObject() - o.Set("id", arena.NewString(evt.ID)) - o.Set("pubkey", arena.NewString(evt.PubKey)) - o.Set("created_at", arena.NewNumberInt(int(evt.CreatedAt.Unix()))) - o.Set("kind", arena.NewNumberInt(evt.Kind)) - o.Set("tags", tagsToFastjsonArray(&arena, evt.Tags)) - o.Set("content", arena.NewString(evt.Content)) - o.Set("sig", arena.NewString(evt.Sig)) - - for k, v := range evt.extra { - b, _ := json.Marshal(v) - if val, err := fastjson.ParseBytes(b); err == nil { - o.Set(k, val) - } - } - - return o.MarshalTo(nil), nil + return visiterr } +// unmarshaling helper func fastjsonArrayToTags(v *fastjson.Value) (Tags, error) { arr, err := v.Array() if err != nil { @@ -130,14 +105,37 @@ func fastjsonArrayToTags(v *fastjson.Value) (Tags, error) { return sll, nil } -func tagsToFastjsonArray(arena *fastjson.Arena, tags Tags) *fastjson.Value { - jtags := arena.NewArray() - for i, v := range tags { - arr := arena.NewArray() - for j, subv := range v { - arr.SetArrayItem(j, arena.NewString(subv)) +// MarshalJSON() returns the JSON byte encoding of the event, as in NIP-01. +func (evt *Event) MarshalJSON() ([]byte, error) { + dst := make([]byte, 0) + dst = append(dst, '{') + dst = append(dst, []byte(fmt.Sprintf("\"id\":\"%s\",\"pubkey\":\"%s\",\"created_at\":%d,\"kind\":%d,\"tags\":", + evt.ID, + evt.PubKey, + evt.CreatedAt.Unix(), + evt.Kind, + ))...) + dst = evt.Tags.marshalTo(dst) + dst = append(dst, []byte(",\"content\":")...) + dst = escapeString(dst, evt.Content) + dst = append(dst, []byte(fmt.Sprintf(",\"sig\":\"%s\"", + evt.Sig, + ))...) + // slower marshaling of "any" interface type + if evt.extra != nil { + buf := bytes.NewBuffer(nil) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + for k, v := range evt.extra { + if e := enc.Encode(v); e == nil { + dst = append(dst, ',') + dst = escapeString(dst, k) + dst = append(dst, ':') + dst = append(dst, buf.Bytes()[:buf.Len()-1]...) + } + buf.Reset() } - jtags.SetArrayItem(i, arr) } - return jtags + dst = append(dst, '}') + return dst, nil } diff --git a/helpers.go b/helpers.go index d28d4c7..1b94a8a 100644 --- a/helpers.go +++ b/helpers.go @@ -35,3 +35,43 @@ func ContainsPrefixOf(haystack []string, needle string) bool { } return false } + +// Escaping strings for JSON encoding according to RFC8259. +// Also encloses result in quotation marks "". +func escapeString(dst []byte, s string) []byte { + dst = append(dst, '"') + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c == '"': + // quotation mark + dst = append(dst, []byte{'\\', '"'}...) + case c == '\\': + // reverse solidus + dst = append(dst, []byte{'\\', '\\'}...) + case c >= 0x20: + // default, rest below are control chars + dst = append(dst, c) + case c == 0x08: + dst = append(dst, []byte{'\\', 'b'}...) + case c < 0x09: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', '0' + c}...) + case c == 0x09: + dst = append(dst, []byte{'\\', 't'}...) + case c == 0x0a: + dst = append(dst, []byte{'\\', 'n'}...) + case c == 0x0c: + dst = append(dst, []byte{'\\', 'f'}...) + case c == 0x0d: + dst = append(dst, []byte{'\\', 'r'}...) + case c < 0x10: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', 0x57 + c}...) + case c < 0x1a: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x20 + c}...) + case c < 0x20: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x47 + c}...) + } + } + dst = append(dst, '"') + return dst +} diff --git a/relay.go b/relay.go index dd8ca41..e632e6c 100644 --- a/relay.go +++ b/relay.go @@ -229,7 +229,15 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status { defer r.okCallbacks.Delete(event.ID) // publish event - if err := r.Connection.WriteJSON([]interface{}{"EVENT", event}); err != nil { + message := []byte("[\"EVENT\",") + if m, e := event.MarshalJSON(); e == nil { + message = append(message, m...) + message = append(message, ']') + } else { + return status + } + + if err := r.Connection.WriteMessage(websocket.TextMessage, message); err != nil { return status } diff --git a/tags.go b/tags.go index b38c8b2..a6e8f68 100644 --- a/tags.go +++ b/tags.go @@ -12,11 +12,16 @@ type Tag []string // StartsWith checks if a tag contains a prefix. // for example, -// ["p", "abcdef...", "wss://relay.com"] +// +// ["p", "abcdef...", "wss://relay.com"] +// // would match against -// ["p", "abcdef..."] +// +// ["p", "abcdef..."] +// // or even -// ["p", "abcdef...", "wss://"] +// +// ["p", "abcdef...", "wss://"] func (tag Tag) StartsWith(prefix []string) bool { prefixLen := len(prefix) @@ -141,3 +146,29 @@ func (tags Tags) ContainsAny(tagName string, values []string) bool { return false } + +// Marshal Tag. Used for Serialization so string escaping should be as in RFC8259. +func (tag Tag) marshalTo(dst []byte) []byte { + dst = append(dst, '[') + for i, s := range tag { + if i > 0 { + dst = append(dst, ',') + } + dst = escapeString(dst, s) + } + dst = append(dst, ']') + return dst +} + +// Marshal Tags. Used for Serialization so string escaping should be as in RFC8259. +func (tags Tags) marshalTo(dst []byte) []byte { + dst = append(dst, '[') + for i, tag := range tags { + if i > 0 { + dst = append(dst, ',') + } + dst = tag.marshalTo(dst) + } + dst = append(dst, ']') + return dst +} From afcfa2076316282639597de1e0831f5220be8178 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Wed, 18 Jan 2023 14:50:44 -0500 Subject: [PATCH 2/2] Changing (evt Event) MarshalJSON to avoid string escaping bug --- event_aux.go | 2 +- relay.go | 10 +--------- tags.go | 8 ++------ 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/event_aux.go b/event_aux.go index e0bfa37..3cac60f 100644 --- a/event_aux.go +++ b/event_aux.go @@ -106,7 +106,7 @@ func fastjsonArrayToTags(v *fastjson.Value) (Tags, error) { } // MarshalJSON() returns the JSON byte encoding of the event, as in NIP-01. -func (evt *Event) MarshalJSON() ([]byte, error) { +func (evt Event) MarshalJSON() ([]byte, error) { dst := make([]byte, 0) dst = append(dst, '{') dst = append(dst, []byte(fmt.Sprintf("\"id\":\"%s\",\"pubkey\":\"%s\",\"created_at\":%d,\"kind\":%d,\"tags\":", diff --git a/relay.go b/relay.go index e632e6c..dd8ca41 100644 --- a/relay.go +++ b/relay.go @@ -229,15 +229,7 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status { defer r.okCallbacks.Delete(event.ID) // publish event - message := []byte("[\"EVENT\",") - if m, e := event.MarshalJSON(); e == nil { - message = append(message, m...) - message = append(message, ']') - } else { - return status - } - - if err := r.Connection.WriteMessage(websocket.TextMessage, message); err != nil { + if err := r.Connection.WriteJSON([]interface{}{"EVENT", event}); err != nil { return status } diff --git a/tags.go b/tags.go index a6e8f68..2dd5ed8 100644 --- a/tags.go +++ b/tags.go @@ -12,15 +12,10 @@ type Tag []string // StartsWith checks if a tag contains a prefix. // for example, -// // ["p", "abcdef...", "wss://relay.com"] -// // would match against -// // ["p", "abcdef..."] -// // or even -// // ["p", "abcdef...", "wss://"] func (tag Tag) StartsWith(prefix []string) bool { prefixLen := len(prefix) @@ -160,7 +155,8 @@ func (tag Tag) marshalTo(dst []byte) []byte { return dst } -// Marshal Tags. Used for Serialization so string escaping should be as in RFC8259. +// MarshalTo appends the JSON encoded byte of Tags as [][]string to dst. +// String escaping is as described in RFC8259. func (tags Tags) marshalTo(dst []byte) []byte { dst = append(dst, '[') for i, tag := range tags {