relay_test.go raw

   1  package relay
   2  
   3  import (
   4  	"bytes"
   5  	"crypto/sha256"
   6  	"encoding/base64"
   7  	"encoding/hex"
   8  	"encoding/json"
   9  	"fmt"
  10  	"io"
  11  	"math/rand"
  12  	"net/http"
  13  	"os"
  14  	"path/filepath"
  15  	"strings"
  16  	"sync"
  17  	"testing"
  18  	"time"
  19  
  20  	"git.smesh.lol/torly/pkg/blossom"
  21  	"git.smesh.lol/torly/pkg/config"
  22  	"git.smesh.lol/torly/pkg/nostr"
  23  )
  24  
  25  type testRelay struct {
  26  	addr string
  27  	cfg  config.Config
  28  	tree *Tree
  29  }
  30  
  31  func startTestRelay(t *testing.T, authRequired bool) *testRelay {
  32  	t.Helper()
  33  	return startTestRelayWith(t, authRequired, nil)
  34  }
  35  
  36  func startTestRelayWith(t *testing.T, authRequired bool, mod func(*config.Config)) *testRelay {
  37  	t.Helper()
  38  	dir := t.TempDir()
  39  	cfg := config.Config{
  40  		AppName:        "torly-test",
  41  		Description:    "test relay",
  42  		ListenHost:     "127.0.0.1",
  43  		DataDir:        dir,
  44  		AuthRequired:   authRequired,
  45  		BlossomEnabled: true,
  46  		BlossomMaxBlob: 4 << 20,
  47  		MaxEventBytes:  128 << 10,
  48  		MaxSubsPerConn: 16,
  49  		MaxFilters:     8,
  50  		MaxLimit:       1000,
  51  		QueryLimit:     1000,
  52  		MaxFutureSecs:  900,
  53  		ConnWorkers:    8,
  54  		VerifyWorkers:  2,
  55  		ExpiryInterval: 60,
  56  	}
  57  	if mod != nil {
  58  		mod(&cfg)
  59  	}
  60  	var tree *Tree
  61  	var err error
  62  	for attempt := 0; attempt < 40; attempt++ {
  63  		cfg.Port = 23000 + rand.Intn(7000)
  64  		cfg.RelayURL = fmt.Sprintf("ws://127.0.0.1:%d/", cfg.Port)
  65  		tree, err = Start(cfg)
  66  		if err == nil {
  67  			break
  68  		}
  69  	}
  70  	if tree == nil {
  71  		t.Fatalf("could not start relay: %v", err)
  72  	}
  73  	t.Cleanup(tree.Close)
  74  	return &testRelay{addr: tree.Addr(), cfg: cfg, tree: tree}
  75  }
  76  
  77  func countFilter(t *testing.T, addr, filter string) (int, bool) {
  78  	t.Helper()
  79  	c := dialWS(t, addr)
  80  	defer c.close()
  81  	c.expect(`["AUTH"`)
  82  	c.writeText([]byte(`["COUNT","c",` + filter + `]`))
  83  	msg := c.expect(`["COUNT","c"`)
  84  	var parts []json.RawMessage
  85  	if err := json.Unmarshal([]byte(msg), &parts); err != nil {
  86  		t.Fatal(err)
  87  	}
  88  	var payload struct {
  89  		Count       int  `json:"count"`
  90  		Approximate bool `json:"approximate"`
  91  	}
  92  	if err := json.Unmarshal(parts[2], &payload); err != nil {
  93  		t.Fatal(err)
  94  	}
  95  	return payload.Count, payload.Approximate
  96  }
  97  
  98  func testEvent(t *testing.T, seed byte, kind int, tags nostr.Tags, content string) *nostr.Event {
  99  	t.Helper()
 100  	var sk [32]byte
 101  	sk[31] = seed
 102  	ev := &nostr.Event{Kind: kind, CreatedAt: time.Now().Unix(), Tags: tags, Content: content}
 103  	if err := ev.Sign(sk); err != nil {
 104  		t.Fatal(err)
 105  	}
 106  	return ev
 107  }
 108  
 109  func TestNIP11AndHealth(t *testing.T) {
 110  	tr := startTestRelay(t, false)
 111  	req, _ := http.NewRequest("GET", "http://"+tr.addr+"/", nil)
 112  	req.Header.Set("Accept", "application/nostr+json")
 113  	resp, err := http.DefaultClient.Do(req)
 114  	if err != nil {
 115  		t.Fatal(err)
 116  	}
 117  	defer resp.Body.Close()
 118  	if resp.StatusCode != 200 {
 119  		t.Fatalf("nip11 status: %d", resp.StatusCode)
 120  	}
 121  	var doc config.NIP11
 122  	if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
 123  		t.Fatal(err)
 124  	}
 125  	if doc.Name != "torly-test" || len(doc.SupportedNIPs) == 0 || doc.Software == "" || doc.PubKey == "" {
 126  		t.Fatalf("unexpected NIP-11: %+v", doc)
 127  	}
 128  
 129  	resp2, err := http.Get("http://" + tr.addr + "/health")
 130  	if err != nil {
 131  		t.Fatal(err)
 132  	}
 133  	defer resp2.Body.Close()
 134  	body, _ := io.ReadAll(resp2.Body)
 135  	if !strings.Contains(string(body), `"status":"ok"`) {
 136  		t.Fatalf("health: %s", body)
 137  	}
 138  }
 139  
 140  func TestPublishQueryCount(t *testing.T) {
 141  	tr := startTestRelay(t, false)
 142  	c := dialWS(t, tr.addr)
 143  	defer c.close()
 144  	c.expect(`["AUTH"`)
 145  
 146  	ev := testEvent(t, 1, 1, nostr.Tags{{"t", "integration"}}, "hello integration world")
 147  	raw := ev.AppendJSON(nil)
 148  	msg := append([]byte(`["EVENT",`), raw...)
 149  	msg = append(msg, ']')
 150  	if err := c.writeText(msg); err != nil {
 151  		t.Fatal(err)
 152  	}
 153  	ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 154  	if !strings.Contains(ok, "true") {
 155  		t.Fatalf("publish rejected: %s", ok)
 156  	}
 157  
 158  	if err := c.writeText([]byte(`["REQ","sub1",{"kinds":[1],"#t":["integration"]}]`)); err != nil {
 159  		t.Fatal(err)
 160  	}
 161  	got := c.expect(`["EVENT","sub1"`)
 162  	if !strings.Contains(got, nostr.Hex(ev.ID[:])) {
 163  		t.Fatalf("wrong event: %s", got)
 164  	}
 165  	c.expect(`["EOSE","sub1"`)
 166  
 167  	if err := c.writeText([]byte(`["COUNT","c1",{"kinds":[1],"#t":["integration"]}]`)); err != nil {
 168  		t.Fatal(err)
 169  	}
 170  	count := c.expect(`["COUNT","c1"`)
 171  	if !strings.Contains(count, `"count":1`) {
 172  		t.Fatalf("count: %s", count)
 173  	}
 174  
 175  	// A tampered signature must be rejected.
 176  	bad := testEvent(t, 1, 1, nil, "bad signature")
 177  	badSig := bad.Sig
 178  	badSig[0] ^= 1
 179  	bad.Sig = badSig
 180  	raw = bad.AppendJSON(nil)
 181  	msg = append([]byte(`["EVENT",`), raw...)
 182  	msg = append(msg, ']')
 183  	c.writeText(msg)
 184  	rejected := c.expect(`["OK","` + nostr.Hex(bad.ID[:]) + `"`)
 185  	if !strings.Contains(rejected, "false") || !strings.Contains(rejected, "invalid") {
 186  		t.Fatalf("bad signature accepted: %s", rejected)
 187  	}
 188  }
 189  
 190  func TestGraphQuery(t *testing.T) {
 191  	tr := startTestRelay(t, false)
 192  	c := dialWS(t, tr.addr)
 193  	defer c.close()
 194  	c.expect(`["AUTH"`)
 195  
 196  	pubOf := func(seed byte) string {
 197  		var sk [32]byte
 198  		sk[31] = seed
 199  		pub, err := nostr.PubKeyHex(sk)
 200  		if err != nil {
 201  			t.Fatal(err)
 202  		}
 203  		return pub
 204  	}
 205  	alice, bob, carol := pubOf(11), pubOf(12), pubOf(13)
 206  	contacts := testEvent(t, 11, 3, nostr.Tags{{"p", bob}, {"p", carol}}, "")
 207  	msg := append([]byte(`["EVENT",`), contacts.AppendJSON(nil)...)
 208  	msg = append(msg, ']')
 209  	c.writeText(msg)
 210  	if ok := c.expect(`["OK","` + nostr.Hex(contacts.ID[:]) + `"`); !strings.Contains(ok, "true") {
 211  		t.Fatalf("publish: %s", ok)
 212  	}
 213  
 214  	c.writeText([]byte(`["REQ","g",{"_graph":["` + alice + `",1,"PP","OUT"]}]`))
 215  	raw := c.expect(`["EVENT","g"`)
 216  	parts, err := nostr.SplitArray([]byte(raw))
 217  	if err != nil {
 218  		t.Fatal(err)
 219  	}
 220  	result, err := nostr.ParseEvent(parts[2])
 221  	if err != nil {
 222  		t.Fatal(err)
 223  	}
 224  	if err := result.Verify(); err != nil {
 225  		t.Fatalf("graph result signature: %v", err)
 226  	}
 227  	if result.Kind != nostr.KindGraphResult {
 228  		t.Fatalf("kind = %d", result.Kind)
 229  	}
 230  	if edge, _ := result.Tags.First("edge"); edge != "pp" {
 231  		t.Fatalf("edge tag = %q", edge)
 232  	}
 233  	if seed, _ := result.Tags.First("seed"); seed != alice {
 234  		t.Fatalf("seed tag = %q", seed)
 235  	}
 236  	var content struct {
 237  		PubkeysByDepth [][]string `json:"pubkeys_by_depth"`
 238  		TotalPubkeys   int        `json:"total_pubkeys"`
 239  	}
 240  	if err := json.Unmarshal([]byte(result.Content), &content); err != nil {
 241  		t.Fatalf("content %q: %v", result.Content, err)
 242  	}
 243  	if content.TotalPubkeys != 2 || len(content.PubkeysByDepth) != 1 {
 244  		t.Fatalf("content: %+v", content)
 245  	}
 246  	got := map[string]bool{content.PubkeysByDepth[0][0]: true, content.PubkeysByDepth[0][1]: true}
 247  	if !got[bob] || !got[carol] {
 248  		t.Fatalf("follows = %v", content.PubkeysByDepth)
 249  	}
 250  	c.expect(`["EOSE","g"`)
 251  
 252  	c.writeText([]byte(`["REQ","bad",{"_graph":["short",1,"pp","out"]}]`))
 253  	if closed := c.expect(`["CLOSED","bad"`); !strings.Contains(closed, "invalid") {
 254  		t.Fatalf("invalid graph query: %s", closed)
 255  	}
 256  	c.writeText([]byte(`["COUNT","cg",{"_graph":["` + alice + `",1,"pp","out"]}]`))
 257  	if closed := c.expect(`["CLOSED","cg"`); !strings.Contains(closed, "_graph") {
 258  		t.Fatalf("graph count: %s", closed)
 259  	}
 260  }
 261  
 262  func TestLiveDelivery(t *testing.T) {
 263  	tr := startTestRelay(t, false)
 264  	sub := dialWS(t, tr.addr)
 265  	defer sub.close()
 266  	sub.expect(`["AUTH"`)
 267  	sub.writeText([]byte(`["REQ","live",{"kinds":[1],"#t":["live"]}]`))
 268  	sub.expect(`["EOSE","live"`)
 269  
 270  	pub := dialWS(t, tr.addr)
 271  	defer pub.close()
 272  	pub.expect(`["AUTH"`)
 273  	ev := testEvent(t, 2, 1, nostr.Tags{{"t", "live"}}, "broadcast me")
 274  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 275  	msg = append(msg, ']')
 276  	pub.writeText(msg)
 277  	pub.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 278  	got := sub.expect(`["EVENT","live"`)
 279  	if !strings.Contains(got, nostr.Hex(ev.ID[:])) {
 280  		t.Fatalf("live event missing: %s", got)
 281  	}
 282  }
 283  
 284  func TestAuthRequired(t *testing.T) {
 285  	tr := startTestRelay(t, true)
 286  	c := dialWS(t, tr.addr)
 287  	defer c.close()
 288  	challengeMsg := c.expect(`["AUTH"`)
 289  	var parts []json.RawMessage
 290  	if err := json.Unmarshal([]byte(challengeMsg), &parts); err != nil {
 291  		t.Fatal(err)
 292  	}
 293  	var challenge string
 294  	json.Unmarshal(parts[1], &challenge)
 295  
 296  	ev := testEvent(t, 3, 1, nil, "needs auth")
 297  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 298  	msg = append(msg, ']')
 299  	c.writeText(msg)
 300  	rejected := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 301  	if !strings.Contains(rejected, "auth-required") {
 302  		t.Fatalf("expected auth-required, got %s", rejected)
 303  	}
 304  
 305  	auth := testEvent(t, 3, 22242, nostr.Tags{
 306  		{"relay", tr.cfg.RelayURL},
 307  		{"challenge", challenge},
 308  	}, "")
 309  	authMsg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
 310  	authMsg = append(authMsg, ']')
 311  	c.writeText(authMsg)
 312  	if ok := c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`); !strings.Contains(ok, "true") {
 313  		t.Fatalf("auth rejected: %s", ok)
 314  	}
 315  
 316  	c.writeText(msg)
 317  	if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
 318  		t.Fatalf("authenticated publish rejected: %s", ok)
 319  	}
 320  }
 321  
 322  func TestBlossom(t *testing.T) {
 323  	tr := startTestRelay(t, false)
 324  	body := []byte("blossom integration payload")
 325  	sum := sha256.Sum256(body)
 326  	hash := hex.EncodeToString(sum[:])
 327  
 328  	var sk [32]byte
 329  	sk[31] = 9
 330  	pub, _ := nostr.PubKeyHex(sk)
 331  
 332  	authEvent := func(action string) string {
 333  		ev := &nostr.Event{
 334  			Kind:      24242,
 335  			CreatedAt: time.Now().Unix(),
 336  			Tags: nostr.Tags{
 337  				{"t", action},
 338  				{"x", hash},
 339  				{"expiration", fmt.Sprint(time.Now().Unix() + 300)},
 340  			},
 341  		}
 342  		if err := ev.Sign(sk); err != nil {
 343  			t.Fatal(err)
 344  		}
 345  		return "Nostr " + base64.StdEncoding.EncodeToString(ev.AppendJSON(nil))
 346  	}
 347  
 348  	req, _ := http.NewRequest("PUT", "http://"+tr.addr+"/upload", bytes.NewReader(body))
 349  	req.Header.Set("Authorization", authEvent("upload"))
 350  	req.Header.Set("Content-Type", "text/plain")
 351  	resp, err := http.DefaultClient.Do(req)
 352  	if err != nil {
 353  		t.Fatal(err)
 354  	}
 355  	raw, _ := io.ReadAll(resp.Body)
 356  	resp.Body.Close()
 357  	if resp.StatusCode != 200 {
 358  		t.Fatalf("upload status %d: %s", resp.StatusCode, raw)
 359  	}
 360  	var desc blossom.Descriptor
 361  	if err := json.Unmarshal(raw, &desc); err != nil {
 362  		t.Fatal(err)
 363  	}
 364  	if desc.SHA256 != hash || desc.Size != int64(len(body)) {
 365  		t.Fatalf("descriptor: %+v", desc)
 366  	}
 367  
 368  	resp, err = http.Get("http://" + tr.addr + "/" + hash)
 369  	if err != nil {
 370  		t.Fatal(err)
 371  	}
 372  	downloaded, _ := io.ReadAll(resp.Body)
 373  	resp.Body.Close()
 374  	if resp.StatusCode != 200 || !bytes.Equal(downloaded, body) {
 375  		t.Fatalf("download: status %d body %q", resp.StatusCode, downloaded)
 376  	}
 377  
 378  	resp, err = http.Get("http://" + tr.addr + "/list/" + pub)
 379  	if err != nil {
 380  		t.Fatal(err)
 381  	}
 382  	listBody, _ := io.ReadAll(resp.Body)
 383  	resp.Body.Close()
 384  	if !strings.Contains(string(listBody), hash) {
 385  		t.Fatalf("list: %s", listBody)
 386  	}
 387  
 388  	del, _ := http.NewRequest("DELETE", "http://"+tr.addr+"/"+hash, nil)
 389  	del.Header.Set("Authorization", authEvent("delete"))
 390  	resp, err = http.DefaultClient.Do(del)
 391  	if err != nil {
 392  		t.Fatal(err)
 393  	}
 394  	resp.Body.Close()
 395  	if resp.StatusCode != 204 {
 396  		t.Fatalf("delete status: %d", resp.StatusCode)
 397  	}
 398  	resp, err = http.Get("http://" + tr.addr + "/" + hash)
 399  	if err != nil {
 400  		t.Fatal(err)
 401  	}
 402  	resp.Body.Close()
 403  	if resp.StatusCode != 404 {
 404  		t.Fatalf("blob survived delete: %d", resp.StatusCode)
 405  	}
 406  }
 407  
 408  func TestUnauthenticatedBlossomUploadRejected(t *testing.T) {
 409  	tr := startTestRelay(t, false)
 410  	req, _ := http.NewRequest("PUT", "http://"+tr.addr+"/upload", strings.NewReader("nope"))
 411  	req.Header.Set("Content-Type", "text/plain")
 412  	resp, err := http.DefaultClient.Do(req)
 413  	if err != nil {
 414  		t.Fatal(err)
 415  	}
 416  	defer resp.Body.Close()
 417  	if resp.StatusCode != 401 {
 418  		t.Fatalf("upload without auth: status %d", resp.StatusCode)
 419  	}
 420  }
 421  
 422  func TestConcurrentPublish(t *testing.T) {
 423  	tr := startTestRelay(t, false)
 424  	const conns = 6
 425  	const perConn = 4
 426  
 427  	clients := make([]*testWS, conns)
 428  	for i := range clients {
 429  		clients[i] = dialWS(t, tr.addr)
 430  		defer clients[i].close()
 431  		clients[i].expect(`["AUTH"`)
 432  	}
 433  
 434  	var wg sync.WaitGroup
 435  	errs := make(chan error, conns)
 436  	for g := 0; g < conns; g++ {
 437  		wg.Add(1)
 438  		go func(g int) {
 439  			defer wg.Done()
 440  			c := clients[g]
 441  			for i := 0; i < perConn; i++ {
 442  				ev := testEvent(t, byte(g*perConn+i+1), 1, nostr.Tags{{"t", "concurrent"}}, "parallel publish")
 443  				msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 444  				msg = append(msg, ']')
 445  				if err := c.writeText(msg); err != nil {
 446  					errs <- err
 447  					return
 448  				}
 449  				c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 450  			}
 451  		}(g)
 452  	}
 453  	wg.Wait()
 454  	close(errs)
 455  	for err := range errs {
 456  		t.Fatalf("publish: %v", err)
 457  	}
 458  
 459  	n, approx := countFilter(t, tr.addr, `{"#t":["concurrent"]}`)
 460  	if n != conns*perConn || approx {
 461  		t.Fatalf("count = %d (approximate=%v), want %d", n, approx, conns*perConn)
 462  	}
 463  }
 464  
 465  func TestDuplicatePublish(t *testing.T) {
 466  	tr := startTestRelay(t, false)
 467  	c := dialWS(t, tr.addr)
 468  	defer c.close()
 469  	c.expect(`["AUTH"`)
 470  	ev := testEvent(t, 4, 1, nil, "duplicate")
 471  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 472  	msg = append(msg, ']')
 473  	c.writeText(msg)
 474  	if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
 475  		t.Fatalf("first publish: %s", ok)
 476  	}
 477  	c.writeText(msg)
 478  	dup := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 479  	if !strings.Contains(dup, "true") || !strings.Contains(dup, "duplicate") {
 480  		t.Fatalf("duplicate response: %s", dup)
 481  	}
 482  }
 483  
 484  func TestEphemeralNotStored(t *testing.T) {
 485  	tr := startTestRelay(t, false)
 486  	sub := dialWS(t, tr.addr)
 487  	defer sub.close()
 488  	sub.expect(`["AUTH"`)
 489  	sub.writeText([]byte(`["REQ","eph",{"kinds":[20001]}]`))
 490  	sub.expect(`["EOSE","eph"`)
 491  
 492  	pub := dialWS(t, tr.addr)
 493  	defer pub.close()
 494  	pub.expect(`["AUTH"`)
 495  	ev := testEvent(t, 5, 20001, nostr.Tags{{"t", "ephemeral"}}, "do not store")
 496  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 497  	msg = append(msg, ']')
 498  	pub.writeText(msg)
 499  	if ok := pub.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
 500  		t.Fatalf("ephemeral rejected: %s", ok)
 501  	}
 502  	sub.expect(`["EVENT","eph"`)
 503  	if n, _ := countFilter(t, tr.addr, `{"kinds":[20001]}`); n != 0 {
 504  		t.Fatalf("ephemeral stored: n=%d", n)
 505  	}
 506  }
 507  
 508  func TestProtectedEventRequiresAuth(t *testing.T) {
 509  	tr := startTestRelay(t, false)
 510  	c := dialWS(t, tr.addr)
 511  	defer c.close()
 512  	challenge := challengeFrom(t, c.expect(`["AUTH"`))
 513  
 514  	ev := testEvent(t, 6, 1, nostr.Tags{{"-"}}, "protected")
 515  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 516  	msg = append(msg, ']')
 517  	c.writeText(msg)
 518  	if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "auth-required") {
 519  		t.Fatalf("protected event accepted without auth: %s", ok)
 520  	}
 521  
 522  	auth := testEvent(t, 6, 22242, nostr.Tags{{"relay", tr.cfg.RelayURL}, {"challenge", challenge}}, "")
 523  	authMsg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
 524  	authMsg = append(authMsg, ']')
 525  	c.writeText(authMsg)
 526  	c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`)
 527  
 528  	c.writeText(msg)
 529  	if ok := c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`); !strings.Contains(ok, "true") {
 530  		t.Fatalf("protected event rejected for its author: %s", ok)
 531  	}
 532  }
 533  
 534  func TestAuthWrongChallenge(t *testing.T) {
 535  	tr := startTestRelay(t, true)
 536  	c := dialWS(t, tr.addr)
 537  	defer c.close()
 538  	c.expect(`["AUTH"`)
 539  	auth := testEvent(t, 7, 22242, nostr.Tags{{"relay", tr.cfg.RelayURL}, {"challenge", "wrong"}}, "")
 540  	msg := append([]byte(`["AUTH",`), auth.AppendJSON(nil)...)
 541  	msg = append(msg, ']')
 542  	c.writeText(msg)
 543  	got := c.expect(`["OK","` + nostr.Hex(auth.ID[:]) + `"`)
 544  	if !strings.Contains(got, "false") || !strings.Contains(got, "auth-required") {
 545  		t.Fatalf("wrong challenge accepted: %s", got)
 546  	}
 547  }
 548  
 549  func challengeFrom(t *testing.T, msg string) string {
 550  	t.Helper()
 551  	var parts []json.RawMessage
 552  	if err := json.Unmarshal([]byte(msg), &parts); err != nil {
 553  		t.Fatal(err)
 554  	}
 555  	var challenge string
 556  	if err := json.Unmarshal(parts[1], &challenge); err != nil {
 557  		t.Fatal(err)
 558  	}
 559  	return challenge
 560  }
 561  
 562  func TestMalformedMessages(t *testing.T) {
 563  	tr := startTestRelay(t, false)
 564  	c := dialWS(t, tr.addr)
 565  	defer c.close()
 566  	c.expect(`["AUTH"`)
 567  
 568  	c.writeText([]byte(`this is not json`))
 569  	c.expect(`["NOTICE"`)
 570  
 571  	c.writeText([]byte(`["NOPE"]`))
 572  	notice := c.expect(`["NOTICE"`)
 573  	if !strings.Contains(notice, "unknown command") {
 574  		t.Fatalf("unknown command notice: %s", notice)
 575  	}
 576  
 577  	c.writeText([]byte(`["REQ","onlyid"]`))
 578  	c.expect(`["NOTICE"`)
 579  
 580  	c.writeText([]byte(`["REQ","badsub",{"kinds":"nope"}]`))
 581  	closed := c.expect(`["CLOSED","badsub"`)
 582  	if !strings.Contains(closed, "invalid") {
 583  		t.Fatalf("bad filter closed: %s", closed)
 584  	}
 585  
 586  	c.writeText([]byte(`["REQ",""]`))
 587  	c.expect(`["NOTICE"`)
 588  
 589  	longID := strings.Repeat("x", 65)
 590  	c.writeText([]byte(`["REQ","` + longID + `",{}]`))
 591  	c.expect(`["NOTICE"`)
 592  }
 593  
 594  func TestFilterAndSubscriptionLimits(t *testing.T) {
 595  	tr := startTestRelayWith(t, false, func(c *config.Config) {
 596  		c.MaxFilters = 2
 597  		c.MaxSubsPerConn = 2
 598  	})
 599  	c := dialWS(t, tr.addr)
 600  	defer c.close()
 601  	c.expect(`["AUTH"`)
 602  
 603  	c.writeText([]byte(`["REQ","many",{},{},{}]`))
 604  	closed := c.expect(`["CLOSED","many"`)
 605  	if !strings.Contains(closed, "too many filters") {
 606  		t.Fatalf("filter limit: %s", closed)
 607  	}
 608  
 609  	c.writeText([]byte(`["REQ","one",{}]`))
 610  	c.expect(`["EOSE","one"`)
 611  	c.writeText([]byte(`["REQ","two",{}]`))
 612  	c.expect(`["EOSE","two"`)
 613  	c.writeText([]byte(`["REQ","three",{}]`))
 614  	closed = c.expect(`["CLOSED","three"`)
 615  	if !strings.Contains(closed, "too many subscriptions") {
 616  		t.Fatalf("subscription limit: %s", closed)
 617  	}
 618  }
 619  
 620  func TestCountMultipleFiltersIsApproximate(t *testing.T) {
 621  	tr := startTestRelay(t, false)
 622  	c := dialWS(t, tr.addr)
 623  	defer c.close()
 624  	c.expect(`["AUTH"`)
 625  	ev := testEvent(t, 8, 1, nostr.Tags{{"t", "countmulti"}}, "counted twice")
 626  	msg := append([]byte(`["EVENT",`), ev.AppendJSON(nil)...)
 627  	msg = append(msg, ']')
 628  	c.writeText(msg)
 629  	c.expect(`["OK","` + nostr.Hex(ev.ID[:]) + `"`)
 630  
 631  	c.writeText([]byte(`["COUNT","cm",{"kinds":[1]},{"kinds":[1]}]`))
 632  	got := c.expect(`["COUNT","cm"`)
 633  	if !strings.Contains(got, `"count":2`) || !strings.Contains(got, `"approximate":true`) {
 634  		t.Fatalf("multi-filter count: %s", got)
 635  	}
 636  }
 637  
 638  func TestStaticFilesAndNotFound(t *testing.T) {
 639  	dir := t.TempDir()
 640  	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<html>static</html>"), 0o644); err != nil {
 641  		t.Fatal(err)
 642  	}
 643  	tr := startTestRelayWith(t, false, func(c *config.Config) { c.StaticDir = dir })
 644  
 645  	resp, err := http.Get("http://" + tr.addr + "/")
 646  	if err != nil {
 647  		t.Fatal(err)
 648  	}
 649  	body, _ := io.ReadAll(resp.Body)
 650  	resp.Body.Close()
 651  	if resp.StatusCode != 200 || !strings.Contains(string(body), "static") {
 652  		t.Fatalf("static index: %d %q", resp.StatusCode, body)
 653  	}
 654  
 655  	resp, err = http.Get("http://" + tr.addr + "/missing.txt")
 656  	if err != nil {
 657  		t.Fatal(err)
 658  	}
 659  	resp.Body.Close()
 660  	if resp.StatusCode != 404 {
 661  		t.Fatalf("missing static: %d", resp.StatusCode)
 662  	}
 663  
 664  	resp, err = http.Get("http://" + tr.addr + "/../server_test.go")
 665  	if err != nil {
 666  		t.Fatal(err)
 667  	}
 668  	resp.Body.Close()
 669  	if resp.StatusCode != 404 {
 670  		t.Fatalf("traversal: %d", resp.StatusCode)
 671  	}
 672  }
 673  
 674  func TestNIP11Head(t *testing.T) {
 675  	tr := startTestRelay(t, false)
 676  	req, _ := http.NewRequest("HEAD", "http://"+tr.addr+"/", nil)
 677  	req.Header.Set("Accept", "application/nostr+json")
 678  	resp, err := http.DefaultClient.Do(req)
 679  	if err != nil {
 680  		t.Fatal(err)
 681  	}
 682  	defer resp.Body.Close()
 683  	if resp.StatusCode != 200 || resp.Header.Get("Content-Length") == "" {
 684  		t.Fatalf("NIP-11 HEAD: %d %v", resp.StatusCode, resp.Header)
 685  	}
 686  	body, _ := io.ReadAll(resp.Body)
 687  	if len(body) != 0 {
 688  		t.Fatalf("HEAD returned a body: %q", body)
 689  	}
 690  }
 691