handle-message.go raw
1 package app
2
3 import (
4 "fmt"
5 "strings"
6 "time"
7 "unicode/utf8"
8
9 "next.orly.dev/pkg/lol/chk"
10 "next.orly.dev/pkg/lol/log"
11 "next.orly.dev/pkg/nostr/encoders/envelopes"
12 "next.orly.dev/pkg/nostr/encoders/envelopes/authenvelope"
13 "next.orly.dev/pkg/nostr/encoders/envelopes/closeenvelope"
14 "next.orly.dev/pkg/nostr/encoders/envelopes/countenvelope"
15 "next.orly.dev/pkg/nostr/encoders/envelopes/eventenvelope"
16 "next.orly.dev/pkg/nostr/encoders/envelopes/noticeenvelope"
17 "next.orly.dev/pkg/nostr/encoders/envelopes/reqenvelope"
18 )
19
20 // validateJSONMessage checks if a message contains invalid control characters
21 // that would cause JSON parsing to fail. It also validates UTF-8 encoding.
22 func validateJSONMessage(msg []byte) (err error) {
23 // First, validate that the message is valid UTF-8
24 if !utf8.Valid(msg) {
25 return fmt.Errorf("invalid UTF-8 encoding")
26 }
27
28 // Check for invalid control characters in JSON strings
29 for i := 0; i < len(msg); i++ {
30 b := msg[i]
31
32 // Check for invalid control characters (< 32) except tab, newline, carriage return
33 if b < 32 && b != '\t' && b != '\n' && b != '\r' {
34 return fmt.Errorf(
35 "invalid control character 0x%02X at position %d", b, i,
36 )
37 }
38 }
39 return
40 }
41
42 func (l *Listener) HandleMessage(msg []byte, remote string) {
43 // Acquire read lock for message processing - allows concurrent processing
44 // but blocks during policy/follow list updates (which acquire write lock)
45 l.Server.AcquireMessageProcessingLock()
46 defer l.Server.ReleaseMessageProcessingLock()
47
48 // Handle blacklisted IPs - discard messages but keep connection open until timeout
49 if l.isBlacklisted {
50 // Check if timeout has been reached
51 if time.Now().After(l.blacklistTimeout) {
52 log.W.F(
53 "blacklisted IP %s timeout reached, closing connection", remote,
54 )
55 // Close the connection by cancelling the context
56 // The websocket handler will detect this and close the connection
57 return
58 }
59 log.D.F(
60 "discarding message from blacklisted IP %s (timeout in %v)", remote,
61 time.Until(l.blacklistTimeout),
62 )
63 return
64 }
65
66 msgPreview := string(msg)
67 if len(msgPreview) > 150 {
68 msgPreview = msgPreview[:150] + "..."
69 }
70 log.D.F("%s processing message (len=%d): %s", remote, len(msg), msgPreview)
71
72 // Validate message for invalid characters before processing
73 if err := validateJSONMessage(msg); err != nil {
74 log.E.F(
75 "%s message validation FAILED (len=%d): %v", remote, len(msg), err,
76 )
77 if noticeErr := noticeenvelope.NewFrom(
78 fmt.Sprintf(
79 "invalid message format: contains invalid characters: %s", msg,
80 ),
81 ).Write(l); noticeErr != nil {
82 log.E.F(
83 "%s failed to send validation error notice: %v", remote,
84 noticeErr,
85 )
86 }
87 return
88 }
89
90 l.msgCount++
91 var err error
92 var t string
93 var rem []byte
94
95 // Check for NIP-77 negentropy envelopes first (NEG-OPEN, NEG-MSG, NEG-CLOSE)
96 if IsNegentropyEnvelope(msg) {
97 negType, ok := IdentifyNegentropyEnvelope(msg)
98 if ok {
99 log.T.F("%s processing %s envelope", remote, negType)
100 switch negType {
101 case NegOpenLabel:
102 err = l.HandleNegOpen(msg)
103 case NegMsgLabel:
104 err = l.HandleNegMsg(msg)
105 case NegCloseLabel:
106 err = l.HandleNegClose(msg)
107 default:
108 err = fmt.Errorf("unknown negentropy envelope type: %s", negType)
109 }
110 if err != nil {
111 log.E.F("%s %s processing failed: %v", remote, negType, err)
112 }
113 return
114 }
115 }
116
117 // Attempt to identify the envelope type
118 if t, rem, err = envelopes.Identify(msg); err != nil {
119 log.E.F(
120 "%s envelope identification FAILED (len=%d): %v", remote, len(msg),
121 err,
122 )
123 // Don't log message preview as it may contain binary data
124 chk.E(err)
125 // Send error notice to client
126 if noticeErr := noticeenvelope.NewFrom("malformed message").Write(l); noticeErr != nil {
127 log.E.F(
128 "%s failed to send malformed message notice: %v", remote,
129 noticeErr,
130 )
131 }
132 return
133 }
134
135 log.T.F(
136 "%s identified envelope type: %s (payload_len=%d)", remote, t, len(rem),
137 )
138
139 // Process the identified envelope type
140 switch t {
141 case eventenvelope.L:
142 log.T.F("%s processing EVENT envelope", remote)
143 l.eventCount++
144 err = l.HandleEvent(rem)
145 case reqenvelope.L:
146 log.T.F("%s processing REQ envelope", remote)
147 l.reqCount++
148 err = l.HandleReq(rem)
149 case closeenvelope.L:
150 log.T.F("%s processing CLOSE envelope", remote)
151 err = l.HandleClose(rem)
152 case authenvelope.L:
153 log.T.F("%s processing AUTH envelope", remote)
154 err = l.HandleAuth(rem)
155 case countenvelope.L:
156 log.T.F("%s processing COUNT envelope", remote)
157 err = l.HandleCount(rem)
158 default:
159 err = fmt.Errorf("unknown envelope type %s", t)
160 log.E.F(
161 "%s unknown envelope type: %s (payload_len: %d)", remote, t,
162 len(rem),
163 )
164 }
165
166 // Handle any processing errors
167 if err != nil {
168 // Don't log context cancellation errors as they're expected during shutdown
169 if !strings.Contains(err.Error(), "context canceled") {
170 log.E.F(
171 "%s message processing FAILED (type=%s): %v", remote, t, err,
172 )
173 // Don't log message preview as it may contain binary data
174 // Send error notice to client (use generic message to avoid control chars in errors)
175 noticeMsg := fmt.Sprintf("%s processing failed", t)
176 if noticeErr := noticeenvelope.NewFrom(noticeMsg).Write(l); noticeErr != nil {
177 log.E.F(
178 "%s failed to send error notice after %s processing failure: %v",
179 remote, t, noticeErr,
180 )
181 return
182 }
183 log.T.F("%s sent error notice for %s processing failure", remote, t)
184 }
185 } else {
186 log.T.F("%s message processing SUCCESS (type=%s)", remote, t)
187 }
188 }
189