Reticulum-Go/pkg/controlapi/exploratory_test.go

122 lines
3.5 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package controlapi
import (
"bufio"
"bytes"
"encoding/binary"
"io"
"net"
"testing"
"time"
"quad4/reticulum-go/pkg/identity"
)
// FuzzWSReadMessageExploratory feeds masked client frames into readMessage.
// Oversize lengths must error, and successful payloads must stay within
// wsMaxMessageBytes.
func FuzzWSReadMessageExploratory(f *testing.F) {
f.Add(maskedTextFrame([]byte(`{"type":"subscribe_announces"}`)))
f.Add(maskedTextFrame([]byte(`{}`)))
f.Add([]byte{0x81, 0x80})
f.Add([]byte{0x81, 0xfe, 0xff, 0xff})
f.Add([]byte{0x81, 0xff, 0, 0, 0, 0, 0, 0, 0, 1})
f.Add([]byte{0x89, 0x80, 0, 0, 0, 0})
f.Add([]byte{0x88, 0x80, 0, 0, 0, 0})
f.Add([]byte{0x01, 0x81, 0, 0, 0, 0, 'x'})
f.Fuzz(func(t *testing.T, raw []byte) {
if len(raw) > 1<<16 {
t.Skip()
}
conn := &wsConn{conn: discardConn{}, reader: bufio.NewReader(bytes.NewReader(raw))}
payload, err := conn.readMessage()
if err != nil {
return
}
if len(payload) > wsMaxMessageBytes {
t.Fatalf("payload len=%d exceeds cap", len(payload))
}
})
}
func TestWSOutboxDropWhenFull(t *testing.T) {
srv, _ := newTestServer(t)
ident, err := identity.NewIdentity()
if err != nil {
t.Fatal(err)
}
sess := newSession("exploratory-outbox", ident)
c := newWSClient(srv, sess, &wsConn{conn: discardConn{}, reader: bufio.NewReader(bytes.NewReader(nil))})
for i := range wsClientOutboxSize {
c.send(map[string]any{"type": "fill", "i": i})
}
c.send(map[string]any{"type": "overflow"})
if len(c.outbox) != wsClientOutboxSize {
t.Fatalf("outbox len=%d want %d after drop", len(c.outbox), wsClientOutboxSize)
}
}
func TestWSWriteLoopDoneBeforeEnable(t *testing.T) {
srv, _ := newTestServer(t)
ident, err := identity.NewIdentity()
if err != nil {
t.Fatal(err)
}
sess := newSession("exploratory-done", ident)
rc := newRecordConn()
c := newWSClient(srv, sess, &wsConn{conn: rc, reader: bufio.NewReader(rc)})
c.startWriter()
payload := []byte(`{"type":"should-not-flush"}`)
select {
case c.outbox <- payload:
default:
t.Fatal("outbox full")
}
c.close()
time.Sleep(20 * time.Millisecond)
got := rc.snapshot()
if bytes.Contains(got, payload) {
t.Fatalf("queued payload flushed after close before enableWrites: %q", got)
}
}
func maskedTextFrame(payload []byte) []byte {
mask := [4]byte{1, 2, 3, 4}
masked := make([]byte, len(payload))
for i, b := range payload {
masked[i] = b ^ mask[i%4]
}
var hdr []byte
switch {
case len(payload) < 126:
hdr = []byte{0x81, byte(0x80 | len(payload))}
default:
hdr = make([]byte, 4)
hdr[0] = 0x81
hdr[1] = 0x80 | 126
binary.BigEndian.PutUint16(hdr[2:], uint16(len(payload)))
}
out := append(hdr, mask[:]...)
return append(out, masked...)
}
type discardConn struct{}
func (discardConn) Read([]byte) (int, error) { return 0, io.EOF }
func (discardConn) Write(b []byte) (int, error) { return len(b), nil }
func (discardConn) Close() error { return nil }
func (discardConn) LocalAddr() net.Addr { return discardAddr("local") }
func (discardConn) RemoteAddr() net.Addr { return discardAddr("remote") }
func (discardConn) SetDeadline(time.Time) error { return nil }
func (discardConn) SetReadDeadline(time.Time) error { return nil }
func (discardConn) SetWriteDeadline(time.Time) error { return nil }
type discardAddr string
func (a discardAddr) Network() string { return "discard" }
func (a discardAddr) String() string { return string(a) }