package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "math/big" "net" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" ) // The WebSocket listener the manifest declares (novox/hq research 036, gap G1), read by the server the // image runs: the module's own configuration, as declared, parsed by nats-server itself, with its TLS // paths and store pointed at this test's directories and a user list of its own beside it — and a // client connected over WebSocket to it, through the port the manifest opens from the mesh and the // container publishes. func TestTheDeclaredConfigurationListensForWebSocketFromTheMesh(t *testing.T) { m := manifest(t) // The three places the listener is declared agree on one port: the server's own configuration, the // opening from the mesh (the machine's filter admits it from the overlay only), and the container's // published port. var opening map[string]any for _, l := range m["listens"].([]any) { if entry := l.(map[string]any); entry["name"] == "bus-websocket" { opening = entry } } if opening == nil { t.Fatal("the manifest opens no bus-websocket port") } if opening["from"] != "mesh" { t.Fatalf("the WebSocket listener is open from %q; it is open from the mesh only, because it has no TLS", opening["from"]) } port := int(opening["port"].(float64)) published := false for _, r := range m["resources"].([]any) { if res := r.(map[string]any); res["id"] == "server" { for _, p := range res["ports"].([]any) { published = published || p.(string) == itoa(port)+":"+itoa(port) } } } if !published { t.Fatalf("the container does not publish %d:%d", port, port) } dir := t.TempDir() conf := serverConfOf(t, m) conf = strings.ReplaceAll(conf, "/tls/", selfSignedInto(t, dir)+"/") conf = strings.ReplaceAll(conf, `store_dir: "/data"`, `store_dir: "`+filepath.Join(dir, "data")+`"`) if err := os.WriteFile(filepath.Join(dir, "nats.conf"), []byte(conf), 0o600); err != nil { t.Fatal(err) } // A user list of this test's own, in the place the mesh writes it. users := "accounts { MESH { jetstream: enabled\n users = [ { user: \"view\", password: \"view\", permissions: {\n" + " publish: { allow: [\"probe\"] }\n subscribe: { allow: [\"probe\", \"_INBOX.view.>\"] } } } ]\n} }\n" if err := os.WriteFile(filepath.Join(dir, "accounts.conf"), []byte(users), 0o600); err != nil { t.Fatal(err) } opts, err := server.ProcessConfigFile(filepath.Join(dir, "nats.conf")) if err != nil { t.Fatalf("the server refuses the module's own configuration: %v", err) } if opts.Websocket.Port != port { t.Fatalf("the server's WebSocket listener is on %d and the opening from the mesh is %d", opts.Websocket.Port, port) } if !opts.Websocket.NoTLS { t.Fatal("the WebSocket listener wants TLS: a browser cannot be made to trust the mesh's own certificate, and the overlay is the tunnel") } if !opts.Websocket.Compression { t.Fatal("the WebSocket listener does not compress") } if opts.Websocket.Host != "0.0.0.0" { t.Fatalf("inside the container the listener binds %q; it binds every interface, and the machine publishes it from the mesh", opts.Websocket.Host) } // Run it, on ports the system chooses, so this runs beside a live bus. opts.Host, opts.Port = "127.0.0.1", -1 opts.HTTPHost, opts.HTTPPort = "127.0.0.1", 0 opts.Websocket.Host, opts.Websocket.Port = "127.0.0.1", -1 opts.NoLog, opts.NoSigs = true, true s, err := server.NewServer(opts) if err != nil { t.Fatal(err) } go s.Start() if !s.ReadyForConnections(10 * time.Second) { t.Fatal("the server did not start") } defer s.Shutdown() nc, err := nats.Connect(s.WebsocketURL(), nats.UserInfo("view", "view"), nats.CustomInboxPrefix("_INBOX.view"), nats.Compression(true)) if err != nil { t.Fatalf("a WebSocket client could not connect to %s: %v", s.WebsocketURL(), err) } defer nc.Close() if !strings.HasPrefix(nc.ConnectedUrl(), "ws://") { t.Fatalf("connected to %s, not over WebSocket", nc.ConnectedUrl()) } sub, err := nc.SubscribeSync("probe") if err != nil { t.Fatal(err) } if err := nc.Publish("probe", []byte("round trip")); err != nil { t.Fatal(err) } if msg, err := sub.NextMsg(5 * time.Second); err != nil || string(msg.Data) != "round trip" { t.Fatalf("no round trip over WebSocket: %v", err) } // And the client port beside it still requires TLS: a plain connection is refused. if _, err := nats.Connect(s.ClientURL(), nats.UserInfo("view", "view"), nats.Timeout(2*time.Second)); err == nil { t.Fatal("the client port accepted a connection without TLS") } } func itoa(n int) string { return strconv.Itoa(n) } // manifest is the nats module's own definition, beside this program. func manifest(t *testing.T) map[string]any { t.Helper() raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m map[string]any if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } return m } func serverConfOf(t *testing.T, m map[string]any) string { t.Helper() for _, r := range m["resources"].([]any) { if res := r.(map[string]any); res["id"] == "server-conf" { return res["content"].(string) } } t.Fatal("the nats module declares no server configuration") return "" } // selfSignedInto writes a throwaway certificate and key as tls.crt and tls.key into a directory, as the // mesh mounts the broker's at /tls, and returns that directory. func selfSignedInto(t *testing.T, dir string) string { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } template := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test bus"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) if err != nil { t.Fatal(err) } raw, err := x509.MarshalECPrivateKey(key) if err != nil { t.Fatal(err) } tlsDir := filepath.Join(dir, "tls") if err := os.MkdirAll(tlsDir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(tlsDir, "tls.crt"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(tlsDir, "tls.key"), pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: raw}), 0o600); err != nil { t.Fatal(err) } return tlsDir }