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