Reticulum-Go/pkg/controlapi/fuzz_test.go

109 lines
3.2 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package controlapi
import (
"bytes"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"quad4/reticulum-go/pkg/identity"
)
func FuzzWSCommandDecode(f *testing.F) {
f.Add([]byte(`{}`))
f.Add([]byte(`{"type":"link.open"}`))
f.Add([]byte(`{"type":"link.send","link_id":"00","data":"YQ=="}`))
f.Add([]byte(`{"type":"link.request","link_id":"00","path":"/x"}`))
f.Add([]byte(`{"type":"link.send_resource","link_id":"00","data":"YQ=="}`))
f.Add([]byte(`{"type":"link.identify","link_id":"00"}`))
f.Add([]byte(`{"type":"request.respond","request_id":"00"}`))
f.Add([]byte(`{"type":"subscribe_announces","filter":"11111111111111111111111111111111"}`))
f.Add([]byte(`{not-json`))
f.Add([]byte{0xff, 0xfe, 0x00})
srv, _ := newTestServer(f)
ident, err := identity.NewIdentity()
if err != nil {
f.Fatal(err)
}
sess := newSession("fuzz", ident)
c := &wsClient{
session: sess,
server: srv,
outbox: make(chan []byte, 256),
done: make(chan struct{}),
writable: make(chan struct{}),
}
f.Fuzz(func(t *testing.T, raw []byte) {
if len(raw) > 8192 {
raw = raw[:8192]
}
// Drain outbox so a full buffer cannot block send forever.
for {
select {
case <-c.outbox:
default:
c.handleCommand(raw)
return
}
}
})
}
func FuzzHTTPRegisterBodies(f *testing.F) {
f.Add([]byte(`{}`))
f.Add([]byte(`{"app_name":"x"}`))
f.Add([]byte(`{"app_name":"x","aspects":["a"],"accepts_links":true}`))
f.Add([]byte(`{"path":"/ping"}`))
f.Add([]byte(`{"path":"/ping","allow":"list","allowed_identities":["00112233445566778899aabbccddeeff"]}`))
f.Add([]byte(`{"destination_hash":"00112233445566778899aabbccddeeff"}`))
f.Add([]byte(`{"app_data":"YQ=="}`))
f.Add([]byte(`{`))
f.Add([]byte{0x00, 0xff})
srv, key := newTestServer(f)
ts := httptest.NewServer(srv.httpServer.Handler)
f.Cleanup(ts.Close)
authKey := hex.EncodeToString(key)
_, sess := doJSON(f, http.MethodPost, ts.URL+"/v1/sessions", authKey, map[string]any{})
sessionID, _ := sess["session_id"].(string)
_, dest := doJSON(f, http.MethodPost, fmt.Sprintf("%s/v1/sessions/%s/destinations", ts.URL, sessionID), authKey, map[string]any{"app_name": "fuzz"})
destHash, _ := dest["destination_hash"].(string)
paths := []string{
fmt.Sprintf("%s/v1/sessions", ts.URL),
fmt.Sprintf("%s/v1/sessions/%s/destinations", ts.URL, sessionID),
fmt.Sprintf("%s/v1/sessions/%s/destinations/%s/announce", ts.URL, sessionID, destHash),
fmt.Sprintf("%s/v1/sessions/%s/destinations/%s/requests", ts.URL, sessionID, destHash),
fmt.Sprintf("%s/v1/sessions/%s/path/request", ts.URL, sessionID),
}
f.Fuzz(func(t *testing.T, body []byte) {
if len(body) > 8192 {
body = body[:8192]
}
for _, url := range paths {
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
continue
}
req.Header.Set("Authorization", "Bearer "+authKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
continue
}
_ = resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 600 {
t.Fatalf("unexpected status %d for %s", resp.StatusCode, url)
}
}
})
}