mirror of
https://github.com/hanzoai/pubsub.git
synced 2026-10-30 15:06:26 +00:00
`go vet ./...` failed, so the gate never reached the tests. Six defects, none
of them subtle once vet names them:
mqtt_test.go — three tables declare wantErr and their bodies read it, but 18
rows across them stopped one value short, so the literals did not compile.
Every one of those cases asserts a specific message, so the value is false.
stree_test.go — IntersectGSL takes `func([]byte, *T) bool`. One callback in
LiteralMatchWithOverlappingPWCs returned nothing, unlike the three
identical callbacks beside it.
log_test.go, monitor_test.go — %q formats a string; both arguments are ints.
filestore_test.go, jetstream_consumer_test.go, raft_test.go — unsafe,
path/filepath and net/url imported and unused.
go vet ./... and go build ./... are both clean now.
9627 lines
300 KiB
Go
9627 lines
300 KiB
Go
// Copyright 2020-2026 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build !skip_mqtt_tests
|
|
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nkeys"
|
|
"github.com/nats-io/nuid"
|
|
)
|
|
|
|
var testMQTTTimeout = 10 * time.Second
|
|
|
|
var jsClusterTemplWithLeafAndMQTT = `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
{{leaf}}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
|
|
type mqttWrapAsWs struct {
|
|
net.Conn
|
|
t testing.TB
|
|
br *bufio.Reader
|
|
tmp []byte
|
|
}
|
|
|
|
func (c *mqttWrapAsWs) Write(p []byte) (int, error) {
|
|
proto := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, p)
|
|
return c.Conn.Write(proto)
|
|
}
|
|
|
|
func (c *mqttWrapAsWs) Read(p []byte) (int, error) {
|
|
for {
|
|
if n := len(c.tmp); n > 0 {
|
|
if len(p) < n {
|
|
n = len(p)
|
|
}
|
|
copy(p, c.tmp[:n])
|
|
c.tmp = c.tmp[n:]
|
|
return n, nil
|
|
}
|
|
c.tmp = testWSReadFrame(c.t, c.br)
|
|
}
|
|
}
|
|
|
|
func testMQTTReadPacket(t testing.TB, r *mqttReader) (byte, int) {
|
|
t.Helper()
|
|
|
|
var b byte
|
|
var pl int
|
|
var err error
|
|
|
|
rd := r.reader
|
|
|
|
fill := func() []byte {
|
|
t.Helper()
|
|
var buf [512]byte
|
|
|
|
n, err := rd.Read(buf[:])
|
|
if err != nil {
|
|
t.Fatalf("Error reading data: %v", err)
|
|
}
|
|
return copyBytes(buf[:n])
|
|
}
|
|
|
|
rd.SetReadDeadline(time.Now().Add(testMQTTTimeout))
|
|
for {
|
|
r.pstart = r.pos
|
|
if !r.hasMore() {
|
|
r.reset(fill())
|
|
continue
|
|
}
|
|
b, err = r.readByte("packet type")
|
|
if err != nil {
|
|
t.Fatalf("Error reading packet: %v", err)
|
|
}
|
|
var complete bool
|
|
pl, complete, err = r.readPacketLen()
|
|
if err != nil {
|
|
t.Fatalf("Error reading packet: %v", err)
|
|
}
|
|
if !complete {
|
|
r.reset(fill())
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
rd.SetReadDeadline(time.Time{})
|
|
return b, pl
|
|
}
|
|
|
|
func testMQTTReadPIPacket(expectedType byte, t testing.TB, r *mqttReader, expectedPI uint16) {
|
|
t.Helper()
|
|
b, _ := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != expectedType {
|
|
t.Fatalf("Expected packet %x, got %x", expectedType, pt)
|
|
}
|
|
rpi, err := r.readUint16("packet identifier")
|
|
if err != nil || rpi != expectedPI {
|
|
t.Fatalf("Expected PI %v got: %v, err=%v", expectedPI, rpi, err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTReader(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset([]byte{0, 2, 'a', 'b'})
|
|
bs, err := r.readBytes("", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sbs := string(bs)
|
|
if sbs != "ab" {
|
|
t.Fatalf(`expected "ab", got %q`, sbs)
|
|
}
|
|
|
|
r.reset([]byte{0, 2, 'a', 'b'})
|
|
bs, err = r.readBytes("", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bs[0], bs[1] = 'c', 'd'
|
|
if bytes.Equal(bs, r.buf[2:]) {
|
|
t.Fatal("readBytes should have returned a copy")
|
|
}
|
|
|
|
r.reset([]byte{'a', 'b'})
|
|
if b, err := r.readByte(""); err != nil || b != 'a' {
|
|
t.Fatalf("Error reading byte: b=%v err=%v", b, err)
|
|
}
|
|
if !r.hasMore() {
|
|
t.Fatal("expected to have more, did not")
|
|
}
|
|
if b, err := r.readByte(""); err != nil || b != 'b' {
|
|
t.Fatalf("Error reading byte: b=%v err=%v", b, err)
|
|
}
|
|
if r.hasMore() {
|
|
t.Fatal("expected to not have more")
|
|
}
|
|
if _, err := r.readByte("test"); err == nil || !strings.Contains(err.Error(), "error reading test") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
r.reset([]byte{0, 2, 'a', 'b'})
|
|
if s, err := r.readString(""); err != nil || s != "ab" {
|
|
t.Fatalf("Error reading string: s=%q err=%v", s, err)
|
|
}
|
|
|
|
r.reset([]byte{10})
|
|
if _, err := r.readUint16("uint16"); err == nil || !strings.Contains(err.Error(), "error reading uint16") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
r.reset([]byte{0x82, 0xff, 0x3})
|
|
l, _, err := r.readPacketLenWithCheck(false)
|
|
if err != nil {
|
|
t.Fatal("error getting packet len")
|
|
}
|
|
if l != 0xff82 {
|
|
t.Fatalf("expected length 0xff82 got 0x%x", l)
|
|
}
|
|
r.reset([]byte{0xff, 0xff, 0xff, 0xff, 0xff})
|
|
if _, _, err := r.readPacketLenWithCheck(false); err == nil || !strings.Contains(err.Error(), "malformed") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
r.reset([]byte{0, 2, 'a', 'b', mqttPacketPub, 0x82, 0xff, 0x3})
|
|
r.readString("")
|
|
for i := 0; i < 2; i++ {
|
|
r.pstart = r.pos
|
|
b, err := r.readByte("")
|
|
if err != nil {
|
|
t.Fatalf("Error reading byte: %v", err)
|
|
}
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Unexpected byte: %v", b)
|
|
}
|
|
pl, complete, err := r.readPacketLen()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if complete {
|
|
t.Fatal("Expected to be incomplete")
|
|
}
|
|
if pl != 0 {
|
|
t.Fatalf("Expected pl to be 0, got %v", pl)
|
|
}
|
|
if i > 0 {
|
|
break
|
|
}
|
|
if !bytes.Equal(r.pbuf, []byte{mqttPacketPub, 0x82, 0xff, 0x3}) {
|
|
t.Fatalf("Invalid recorded partial: %v", r.pbuf)
|
|
}
|
|
r.reset([]byte{'a', 'b', 'c'})
|
|
if !bytes.Equal(r.buf, []byte{mqttPacketPub, 0x82, 0xff, 0x3, 'a', 'b', 'c'}) {
|
|
t.Fatalf("Invalid buffer: %v", r.buf)
|
|
}
|
|
if r.pbuf != nil {
|
|
t.Fatalf("Partial buffer should have been reset, got %v", r.pbuf)
|
|
}
|
|
if r.pos != 0 {
|
|
t.Fatalf("Pos should have been reset, got %v", r.pos)
|
|
}
|
|
if r.pstart != 0 {
|
|
t.Fatalf("Pstart should have been reset, got %v", r.pstart)
|
|
}
|
|
}
|
|
// On second pass, the pbuf should have been extended with 'abc'
|
|
if !bytes.Equal(r.pbuf, []byte{mqttPacketPub, 0x82, 0xff, 0x3, 'a', 'b', 'c'}) {
|
|
t.Fatalf("Invalid recorded partial: %v", r.pbuf)
|
|
}
|
|
}
|
|
|
|
func TestMQTTWriter(t *testing.T) {
|
|
w := newMQTTWriter(0)
|
|
w.WriteUint16(1234)
|
|
|
|
r := &mqttReader{}
|
|
r.reset(w.Bytes())
|
|
if v, err := r.readUint16(""); err != nil || v != 1234 {
|
|
t.Fatalf("unexpected value: v=%v err=%v", v, err)
|
|
}
|
|
|
|
w.Reset()
|
|
w.WriteString("test")
|
|
r.reset(w.Bytes())
|
|
if len(r.buf) != 6 {
|
|
t.Fatalf("Expected 2 bytes size before string, got %v", r.buf)
|
|
}
|
|
|
|
w.Reset()
|
|
w.WriteBytes([]byte("test"))
|
|
r.reset(w.Bytes())
|
|
if len(r.buf) != 6 {
|
|
t.Fatalf("Expected 2 bytes size before bytes, got %v", r.buf)
|
|
}
|
|
|
|
ints := []int{
|
|
0, 1, 127, 128, 16383, 16384, 2097151, 2097152, 268435455,
|
|
}
|
|
lens := []int{
|
|
1, 1, 1, 2, 2, 3, 3, 4, 4,
|
|
}
|
|
|
|
tl := 0
|
|
w.Reset()
|
|
for i, v := range ints {
|
|
w.WriteVarInt(v)
|
|
tl += lens[i]
|
|
if tl != w.Len() {
|
|
t.Fatalf("expected len %d, got %d", tl, w.Len())
|
|
}
|
|
}
|
|
|
|
r.reset(w.Bytes())
|
|
for _, v := range ints {
|
|
x, _, _ := r.readPacketLenWithCheck(false)
|
|
if v != x {
|
|
t.Fatalf("expected %d, got %d", v, x)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testMQTTDefaultOptions() *Options {
|
|
o := DefaultOptions()
|
|
o.ServerName = nuid.Next()
|
|
o.Cluster.Port = 0
|
|
o.Gateway.Name = ""
|
|
o.Gateway.Port = 0
|
|
o.LeafNode.Port = 0
|
|
o.Websocket.Port = 0
|
|
o.MQTT.Host = "127.0.0.1"
|
|
o.MQTT.Port = -1
|
|
o.JetStream = true
|
|
return o
|
|
}
|
|
|
|
func testMQTTRunServer(t testing.TB, o *Options) *Server {
|
|
t.Helper()
|
|
o.NoLog = false
|
|
if o.StoreDir == _EMPTY_ {
|
|
o.StoreDir = t.TempDir()
|
|
}
|
|
s, err := NewServer(o)
|
|
if err != nil {
|
|
t.Fatalf("Error creating server: %v", err)
|
|
}
|
|
l := &DummyLogger{}
|
|
s.SetLogger(l, true, true)
|
|
s.Start()
|
|
if err := s.readyForConnections(3 * time.Second); err != nil {
|
|
testMQTTShutdownServer(s)
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func testMQTTShutdownRestartedServer(s **Server) {
|
|
srv := *s
|
|
testMQTTShutdownServer(srv)
|
|
*s = nil
|
|
}
|
|
|
|
func testMQTTShutdownServer(s *Server) {
|
|
if c := s.JetStreamConfig(); c != nil {
|
|
dir := strings.TrimSuffix(c.StoreDir, JetStreamStoreDir)
|
|
defer os.RemoveAll(dir)
|
|
}
|
|
s.Shutdown()
|
|
}
|
|
|
|
func testMQTTDefaultTLSOptions(t *testing.T, verify bool) *Options {
|
|
t.Helper()
|
|
o := testMQTTDefaultOptions()
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/server-cert.pem",
|
|
KeyFile: "../test/configs/certs/server-key.pem",
|
|
CaFile: "../test/configs/certs/ca.pem",
|
|
Verify: verify,
|
|
}
|
|
var err error
|
|
o.MQTT.TLSConfig, err = GenTLSConfig(tc)
|
|
o.MQTT.TLSTimeout = 2.0
|
|
if err != nil {
|
|
t.Fatalf("Error creating tls config: %v", err)
|
|
}
|
|
return o
|
|
}
|
|
|
|
func TestMQTTServerNameRequired(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
cluster {
|
|
port: -1
|
|
}
|
|
mqtt {
|
|
port: -1
|
|
}
|
|
`))
|
|
o, err := ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
if _, err := NewServer(o); err == nil || err.Error() != errMQTTServerNameMustBeSet.Error() {
|
|
t.Fatalf("Expected error about requiring server name to be set, got %v", err)
|
|
}
|
|
|
|
conf = createConfFile(t, []byte(`
|
|
mqtt {
|
|
port: -1
|
|
}
|
|
`))
|
|
o, err = ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
if _, err := NewServer(o); err == nil || err.Error() != errMQTTStandaloneNeedsJetStream.Error() {
|
|
t.Fatalf(`Expected errMQTTStandaloneNeedsJetStream ("next in line"), got %v`, err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTStandaloneRequiresJetStream(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
tls {
|
|
cert_file: "./configs/certs/server.pem"
|
|
key_file: "./configs/certs/key.pem"
|
|
}
|
|
}
|
|
`))
|
|
o, err := ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
if _, err := NewServer(o); err == nil || err.Error() != errMQTTStandaloneNeedsJetStream.Error() {
|
|
t.Fatalf("Expected error about requiring JetStream in standalone mode, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTConfig(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
tls {
|
|
cert_file: "./configs/certs/server.pem"
|
|
key_file: "./configs/certs/key.pem"
|
|
}
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
if o.MQTT.TLSConfig == nil {
|
|
t.Fatal("expected TLS config to be set")
|
|
}
|
|
}
|
|
|
|
func TestMQTTValidateOptions(t *testing.T) {
|
|
nmqtto := DefaultOptions()
|
|
mqtto := testMQTTDefaultOptions()
|
|
for _, test := range []struct {
|
|
name string
|
|
getOpts func() *Options
|
|
err error
|
|
}{
|
|
{"mqtt disabled", func() *Options { return nmqtto.Clone() }, nil},
|
|
{"mqtt username not allowed if users specified", func() *Options {
|
|
o := mqtto.Clone()
|
|
o.Users = []*User{{Username: "abc", Password: "pwd"}}
|
|
o.MQTT.Username = "b"
|
|
o.MQTT.Password = "pwd"
|
|
return o
|
|
}, errMQTTUserMixWithUsersNKeys},
|
|
{"mqtt token not allowed if users specified", func() *Options {
|
|
o := mqtto.Clone()
|
|
o.Nkeys = []*NkeyUser{{Nkey: "abc"}}
|
|
o.MQTT.Token = "mytoken"
|
|
return o
|
|
}, errMQTTTokenMixWIthUsersNKeys},
|
|
{"ack wait should be >=0", func() *Options {
|
|
o := mqtto.Clone()
|
|
o.MQTT.AckWait = -10 * time.Second
|
|
return o
|
|
}, errMQTTAckWaitMustBePositive},
|
|
{"js api timeout should be >=0", func() *Options {
|
|
o := mqtto.Clone()
|
|
o.MQTT.JSAPITimeout = -10 * time.Second
|
|
return o
|
|
}, errMQTTJSAPITimeoutMustBePositive},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := validateMQTTOptions(test.getOpts())
|
|
if test.err == nil && err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
} else if test.err != nil && (err == nil || err.Error() != test.err.Error()) {
|
|
t.Fatalf("Expected error to contain %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTParseOptions(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
content string
|
|
checkOpt func(*MQTTOpts) error
|
|
err string
|
|
}{
|
|
// Negative tests
|
|
{"bad type", "mqtt: []", nil, "to be a map"},
|
|
{"bad listen", "mqtt: { listen: [] }", nil, "port or host:port"},
|
|
{"bad port", `mqtt: { port: "abc" }`, nil, "not int64"},
|
|
{"bad host", `mqtt: { host: 123 }`, nil, "not string"},
|
|
{"bad tls", `mqtt: { tls: 123 }`, nil, "not map[string]interface {}"},
|
|
{"unknown field", `mqtt: { this_does_not_exist: 123 }`, nil, "unknown"},
|
|
{"ack wait", `mqtt: {ack_wait: abc}`, nil, "invalid duration"},
|
|
{"max ack pending", `mqtt: {max_ack_pending: abc}`, nil, "not int64"},
|
|
{"max ack pending too high", `mqtt: {max_ack_pending: 12345678}`, nil, "invalid value"},
|
|
{"js_api_timeout bad duration", `mqtt: {js_api_timeout: abc}`, nil, "invalid duration"},
|
|
// Positive tests
|
|
{"tls gen fails", `
|
|
mqtt {
|
|
tls {
|
|
cert_file: "./configs/certs/server.pem"
|
|
}
|
|
}`, nil, "missing 'key_file'"},
|
|
{"listen port only", `mqtt { listen: 1234 }`, func(o *MQTTOpts) error {
|
|
if o.Port != 1234 {
|
|
return fmt.Errorf("expected 1234, got %v", o.Port)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"listen host and port", `mqtt { listen: "localhost:1234" }`, func(o *MQTTOpts) error {
|
|
if o.Host != "localhost" || o.Port != 1234 {
|
|
return fmt.Errorf("expected localhost:1234, got %v:%v", o.Host, o.Port)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"host", `mqtt { host: "localhost" }`, func(o *MQTTOpts) error {
|
|
if o.Host != "localhost" {
|
|
return fmt.Errorf("expected localhost, got %v", o.Host)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"port", `mqtt { port: 1234 }`, func(o *MQTTOpts) error {
|
|
if o.Port != 1234 {
|
|
return fmt.Errorf("expected 1234, got %v", o.Port)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"tls config",
|
|
`
|
|
mqtt {
|
|
tls {
|
|
cert_file: "./configs/certs/server.pem"
|
|
key_file: "./configs/certs/key.pem"
|
|
}
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.TLSConfig == nil {
|
|
return fmt.Errorf("TLSConfig should have been set")
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"no auth user",
|
|
`
|
|
mqtt {
|
|
no_auth_user: "noauthuser"
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.NoAuthUser != "noauthuser" {
|
|
return fmt.Errorf("Invalid NoAuthUser value: %q", o.NoAuthUser)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"auth block",
|
|
`
|
|
mqtt {
|
|
authorization {
|
|
user: "mqttuser"
|
|
password: "pwd"
|
|
token: "token"
|
|
timeout: 2.0
|
|
}
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.Username != "mqttuser" || o.Password != "pwd" || o.Token != "token" || o.AuthTimeout != 2.0 {
|
|
return fmt.Errorf("Invalid auth block: %+v", o)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"auth timeout as int",
|
|
`
|
|
mqtt {
|
|
authorization {
|
|
timeout: 2
|
|
}
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.AuthTimeout != 2.0 {
|
|
return fmt.Errorf("Invalid auth timeout: %v", o.AuthTimeout)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"ack wait",
|
|
`
|
|
mqtt {
|
|
ack_wait: "10s"
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.AckWait != 10*time.Second {
|
|
return fmt.Errorf("Invalid ack wait: %v", o.AckWait)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"max ack pending",
|
|
`
|
|
mqtt {
|
|
max_ack_pending: 123
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.MaxAckPending != 123 {
|
|
return fmt.Errorf("Invalid max ack pending: %v", o.MaxAckPending)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"reject_qos2_publish",
|
|
`
|
|
mqtt {
|
|
reject_qos2_publish: true
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if !o.rejectQoS2Pub {
|
|
return fmt.Errorf("Invalid: expected rejectQoS2Pub to be set")
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"downgrade_qos2_subscribe",
|
|
`
|
|
mqtt {
|
|
downgrade_qos2_subscribe: true
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if !o.downgradeQoS2Sub {
|
|
return fmt.Errorf("Invalid: expected downgradeQoS2Sub to be set")
|
|
}
|
|
return nil
|
|
}, ""},
|
|
{"js_api_timeout",
|
|
`
|
|
mqtt {
|
|
js_api_timeout: "60s"
|
|
}
|
|
`, func(o *MQTTOpts) error {
|
|
if o.JSAPITimeout != 60*time.Second {
|
|
return fmt.Errorf("Invalid JS API timeout: %v", o.JSAPITimeout)
|
|
}
|
|
return nil
|
|
}, ""},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(test.content))
|
|
o, err := ProcessConfigFile(conf)
|
|
if test.err != _EMPTY_ {
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("For content: %q, expected error about %q, got %v", test.content, test.err, err)
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error for content %q: %v", test.content, err)
|
|
}
|
|
if err := test.checkOpt(&o.MQTT); err != nil {
|
|
t.Fatalf("Incorrect option for content %q: %v", test.content, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTStart(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc, err := net.Dial("tcp", net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port)))
|
|
if err != nil {
|
|
t.Fatalf("Unable to create tcp connection to mqtt port: %v", err)
|
|
}
|
|
nc.Close()
|
|
|
|
// Check failure to start due to port in use
|
|
o2 := testMQTTDefaultOptions()
|
|
o2.MQTT.Port = o.MQTT.Port
|
|
s2, err := NewServer(o2)
|
|
if err != nil {
|
|
t.Fatalf("Error creating server: %v", err)
|
|
}
|
|
defer s2.Shutdown()
|
|
l := &captureFatalLogger{fatalCh: make(chan string, 1)}
|
|
s2.SetLogger(l, false, false)
|
|
s2.Start()
|
|
|
|
select {
|
|
case e := <-l.fatalCh:
|
|
if !strings.Contains(e, "Unable to listen for MQTT connections") {
|
|
t.Fatalf("Unexpected error: %q", e)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Should have gotten a fatal error")
|
|
}
|
|
}
|
|
|
|
func TestMQTTTLS(t *testing.T) {
|
|
o := testMQTTDefaultTLSOptions(t, false)
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
c, _, err := testMQTTConnectRetryWithError(t, &mqttConnInfo{tls: true}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.Close()
|
|
c = nil
|
|
testMQTTShutdownServer(s)
|
|
|
|
// Force client cert verification
|
|
o = testMQTTDefaultTLSOptions(t, true)
|
|
s = testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
c, _, err = testMQTTConnectRetryWithError(t, &mqttConnInfo{tls: true}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if err == nil || c != nil {
|
|
if c != nil {
|
|
c.Close()
|
|
}
|
|
t.Fatal("Handshake expected to fail since client did not provide cert")
|
|
}
|
|
|
|
// Add client cert.
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/client-cert.pem",
|
|
KeyFile: "../test/configs/certs/client-key.pem",
|
|
}
|
|
tlsc, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating tls config: %v", err)
|
|
}
|
|
tlsc.InsecureSkipVerify = true
|
|
c, _, err = testMQTTConnectRetryWithError(t, &mqttConnInfo{
|
|
tls: true,
|
|
tlsc: tlsc,
|
|
}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.Close()
|
|
c = nil
|
|
testMQTTShutdownServer(s)
|
|
|
|
// Lower TLS timeout so low that we should fail
|
|
o.MQTT.TLSTimeout = 0.001
|
|
s = testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc, err := net.Dial("tcp", net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port)))
|
|
if err != nil {
|
|
t.Fatalf("Unable to create tcp connection to mqtt port: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
time.Sleep(100 * time.Millisecond)
|
|
tlsConn := tls.Client(nc, tlsc)
|
|
tlsConn.SetDeadline(time.Now().Add(time.Second))
|
|
if err := tlsConn.Handshake(); err == nil {
|
|
t.Fatal("Expected failure, did not get one")
|
|
}
|
|
}
|
|
|
|
type mqttConnInfo struct {
|
|
clientID string
|
|
cleanSess bool
|
|
keepAlive uint16
|
|
will *mqttWill
|
|
user string
|
|
pass string
|
|
ws bool
|
|
tls bool
|
|
tlsc *tls.Config
|
|
}
|
|
|
|
func testMQTTGetClient(t testing.TB, s *Server, clientID string) *client {
|
|
t.Helper()
|
|
var mc *client
|
|
s.mu.Lock()
|
|
for _, c := range s.clients {
|
|
c.mu.Lock()
|
|
if c.isMqtt() && c.mqtt.cid == clientID {
|
|
mc = c
|
|
}
|
|
c.mu.Unlock()
|
|
if mc != nil {
|
|
break
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
if mc == nil {
|
|
t.Fatalf("Did not find client %q", clientID)
|
|
}
|
|
return mc
|
|
}
|
|
|
|
func testMQTTGetAccountSessionManager(t *testing.T, s *Server, cid string) *mqttAccountSessionManager {
|
|
t.Helper()
|
|
c := testMQTTGetClient(t, s, cid)
|
|
require_NotNil(t, c)
|
|
asm := c.mqtt.asm
|
|
require_NotNil(t, asm)
|
|
return asm
|
|
}
|
|
|
|
func testMQTTRead(c net.Conn) ([]byte, error) {
|
|
var buf [512]byte
|
|
// Make sure that test does not block
|
|
c.SetReadDeadline(time.Now().Add(testMQTTTimeout))
|
|
n, err := c.Read(buf[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.SetReadDeadline(time.Time{})
|
|
return copyBytes(buf[:n]), nil
|
|
}
|
|
|
|
func testMQTTWrite(c net.Conn, buf []byte) (int, error) {
|
|
c.SetWriteDeadline(time.Now().Add(testMQTTTimeout))
|
|
n, err := c.Write(buf)
|
|
c.SetWriteDeadline(time.Time{})
|
|
return n, err
|
|
}
|
|
|
|
func testMQTTConnect(t testing.TB, ci *mqttConnInfo, host string, port int) (net.Conn, *mqttReader) {
|
|
t.Helper()
|
|
return testMQTTConnectRetry(t, ci, host, port, 0)
|
|
}
|
|
|
|
func testMQTTConnectRetry(t testing.TB, ci *mqttConnInfo, host string, port int, retryCount int) (net.Conn, *mqttReader) {
|
|
t.Helper()
|
|
c, r, err := testMQTTConnectRetryWithError(t, ci, host, port, retryCount)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c, r
|
|
}
|
|
|
|
func testMQTTConnectRetryWithError(t testing.TB, ci *mqttConnInfo, host string, port int, retryCount int) (net.Conn, *mqttReader, error) {
|
|
retry := func(c net.Conn) bool {
|
|
if c != nil {
|
|
c.Close()
|
|
}
|
|
if retryCount == 0 {
|
|
return false
|
|
}
|
|
time.Sleep(time.Second)
|
|
retryCount--
|
|
return true
|
|
}
|
|
|
|
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
|
var c net.Conn
|
|
var err error
|
|
RETRY:
|
|
if ci.ws {
|
|
var br *bufio.Reader
|
|
c, br, _, err = testNewWSClientWithError(t, testWSClientOptions{
|
|
host: host,
|
|
port: port,
|
|
noTLS: !ci.tls,
|
|
path: mqttWSPath,
|
|
})
|
|
if err == nil {
|
|
c = &mqttWrapAsWs{Conn: c, t: t, br: br}
|
|
}
|
|
} else {
|
|
c, err = net.Dial("tcp", addr)
|
|
if err == nil && ci.tls {
|
|
tc := ci.tlsc
|
|
if tc == nil {
|
|
tc = &tls.Config{InsecureSkipVerify: true}
|
|
}
|
|
c = tls.Client(c, tc)
|
|
c.SetDeadline(time.Now().Add(time.Second))
|
|
err = c.(*tls.Conn).Handshake()
|
|
c.SetDeadline(time.Time{})
|
|
}
|
|
}
|
|
if err != nil {
|
|
if retry(c) {
|
|
goto RETRY
|
|
}
|
|
return nil, nil, fmt.Errorf("Error creating mqtt connection: %v", err)
|
|
}
|
|
|
|
proto := mqttCreateConnectProto(ci)
|
|
if _, err := testMQTTWrite(c, proto); err != nil {
|
|
if retry(c) {
|
|
goto RETRY
|
|
}
|
|
return nil, nil, fmt.Errorf("Error writing connect: %v", err)
|
|
}
|
|
|
|
buf, err := testMQTTRead(c)
|
|
if err != nil {
|
|
if retry(c) {
|
|
goto RETRY
|
|
}
|
|
return nil, nil, fmt.Errorf("Error reading: %v", err)
|
|
}
|
|
mr := &mqttReader{reader: c}
|
|
mr.reset(buf)
|
|
|
|
return c, mr, nil
|
|
}
|
|
|
|
func mqttCreateConnectProto(ci *mqttConnInfo) []byte {
|
|
flags := byte(0)
|
|
if ci.cleanSess {
|
|
flags |= mqttConnFlagCleanSession
|
|
}
|
|
if ci.will != nil {
|
|
flags |= mqttConnFlagWillFlag | (ci.will.qos << 3)
|
|
if ci.will.retain {
|
|
flags |= mqttConnFlagWillRetain
|
|
}
|
|
}
|
|
if ci.user != _EMPTY_ {
|
|
flags |= mqttConnFlagUsernameFlag
|
|
}
|
|
if ci.pass != _EMPTY_ {
|
|
flags |= mqttConnFlagPasswordFlag
|
|
}
|
|
|
|
pkLen := 2 + len(mqttProtoName) +
|
|
1 + // proto level
|
|
1 + // flags
|
|
2 + // keepAlive
|
|
2 + len(ci.clientID)
|
|
|
|
if ci.will != nil {
|
|
pkLen += 2 + len(ci.will.topic)
|
|
pkLen += 2 + len(ci.will.message)
|
|
}
|
|
if ci.user != _EMPTY_ {
|
|
pkLen += 2 + len(ci.user)
|
|
}
|
|
if ci.pass != _EMPTY_ {
|
|
pkLen += 2 + len(ci.pass)
|
|
}
|
|
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(mqttPacketConnect)
|
|
w.WriteVarInt(pkLen)
|
|
w.WriteString(string(mqttProtoName))
|
|
w.WriteByte(0x4)
|
|
w.WriteByte(flags)
|
|
w.WriteUint16(ci.keepAlive)
|
|
w.WriteString(ci.clientID)
|
|
if ci.will != nil {
|
|
w.WriteBytes(ci.will.topic)
|
|
w.WriteBytes(ci.will.message)
|
|
}
|
|
if ci.user != _EMPTY_ {
|
|
w.WriteString(ci.user)
|
|
}
|
|
if ci.pass != _EMPTY_ {
|
|
w.WriteBytes([]byte(ci.pass))
|
|
}
|
|
return w.Bytes()
|
|
}
|
|
|
|
func testMQTTCheckConnAck(t testing.TB, r *mqttReader, rc byte, sessionPresent bool) {
|
|
t.Helper()
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
pt := b & mqttPacketMask
|
|
if pt != mqttPacketConnectAck {
|
|
t.Fatalf("Expected ConnAck (%x), got %x", mqttPacketConnectAck, pt)
|
|
}
|
|
if pl != 2 {
|
|
t.Fatalf("ConnAck packet length should be 2, got %v", pl)
|
|
}
|
|
caf, err := r.readByte("connack flags")
|
|
if err != nil {
|
|
t.Fatalf("Error reading packet length: %v", err)
|
|
}
|
|
if caf&0xfe != 0 {
|
|
t.Fatalf("ConnAck flag bits 7-1 should all be 0, got %x", caf>>1)
|
|
}
|
|
if sp := caf == 1; sp != sessionPresent {
|
|
t.Fatalf("Expected session present flag=%v got %v", sessionPresent, sp)
|
|
}
|
|
carc, err := r.readByte("connack return code")
|
|
if err != nil {
|
|
t.Fatalf("Error reading returned code: %v", err)
|
|
}
|
|
if carc != rc {
|
|
t.Fatalf("Expected return code to be %v, got %v", rc, carc)
|
|
}
|
|
}
|
|
|
|
func testMQTTCheckPubAck(t testing.TB, r *mqttReader, packetType byte) {
|
|
t.Helper()
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
pt := b & mqttPacketMask
|
|
if pt != packetType {
|
|
t.Fatalf("Expected %x, got %x", packetType, pt)
|
|
}
|
|
r.pos += pl
|
|
}
|
|
|
|
func TestMQTTRequiresJSEnabled(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
acc := NewAccount("mqtt")
|
|
o.Accounts = []*Account{acc}
|
|
o.Users = []*User{{Username: "mqtt", Account: acc}}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
addr := net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port))
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error creating mqtt connection: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
proto := mqttCreateConnectProto(&mqttConnInfo{cleanSess: true, user: "mqtt"})
|
|
if _, err := testMQTTWrite(c, proto); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
if _, err := testMQTTRead(c); err == nil {
|
|
t.Fatal("Expected failure, did not get one")
|
|
}
|
|
}
|
|
|
|
func testMQTTEnableJSForAccount(t *testing.T, s *Server, accName string) {
|
|
t.Helper()
|
|
acc, err := s.LookupAccount(accName)
|
|
if err != nil {
|
|
t.Fatalf("Error looking up account: %v", err)
|
|
}
|
|
limits := map[string]JetStreamAccountLimits{
|
|
_EMPTY_: {
|
|
MaxConsumers: -1,
|
|
MaxStreams: -1,
|
|
MaxMemory: 1024 * 1024,
|
|
MaxStore: 1024 * 1024,
|
|
},
|
|
}
|
|
if err := acc.EnableJetStream(limits, nil); err != nil {
|
|
t.Fatalf("Error enabling JS: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTTLSVerifyAndMap(t *testing.T) {
|
|
accName := "MyAccount"
|
|
acc := NewAccount(accName)
|
|
certUserName := "CN=example.com,OU=NATS.io"
|
|
users := []*User{{Username: certUserName, Account: acc}}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
filtering bool
|
|
provideCert bool
|
|
}{
|
|
{"no filtering, client provides cert", false, true},
|
|
{"no filtering, client does not provide cert", false, false},
|
|
{"filtering, client provides cert", true, true},
|
|
{"filtering, client does not provide cert", true, false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Host = "localhost"
|
|
o.Accounts = []*Account{acc}
|
|
o.Users = users
|
|
if test.filtering {
|
|
o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, jwt.ConnectionTypeMqtt})
|
|
}
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/tlsauth/server.pem",
|
|
KeyFile: "../test/configs/certs/tlsauth/server-key.pem",
|
|
CaFile: "../test/configs/certs/tlsauth/ca.pem",
|
|
Verify: true,
|
|
}
|
|
tlsc, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error creating tls config: %v", err)
|
|
}
|
|
o.MQTT.TLSConfig = tlsc
|
|
o.MQTT.TLSTimeout = 2.0
|
|
o.MQTT.TLSMap = true
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
testMQTTEnableJSForAccount(t, s, accName)
|
|
|
|
tlscc := &tls.Config{}
|
|
if test.provideCert {
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/tlsauth/client.pem",
|
|
KeyFile: "../test/configs/certs/tlsauth/client-key.pem",
|
|
}
|
|
var err error
|
|
tlscc, err = GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating tls config: %v", err)
|
|
}
|
|
}
|
|
tlscc.InsecureSkipVerify = true
|
|
if test.provideCert {
|
|
tlscc.MinVersion = tls.VersionTLS13
|
|
}
|
|
mc, r, err := testMQTTConnectRetryWithError(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
tls: true,
|
|
tlsc: tlscc,
|
|
}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if !test.provideCert {
|
|
if err == nil {
|
|
t.Fatal("Expected error, did not get one")
|
|
} else if !strings.Contains(err.Error(), "bad certificate") && !strings.Contains(err.Error(), "certificate required") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Error reading: %v", err)
|
|
}
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
var c *client
|
|
s.mu.Lock()
|
|
for _, sc := range s.clients {
|
|
sc.mu.Lock()
|
|
if sc.isMqtt() {
|
|
c = sc
|
|
}
|
|
sc.mu.Unlock()
|
|
if c != nil {
|
|
break
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
if c == nil {
|
|
t.Fatal("Client not found")
|
|
}
|
|
|
|
var uname string
|
|
var accname string
|
|
c.mu.Lock()
|
|
uname = c.opts.Username
|
|
if c.acc != nil {
|
|
accname = c.acc.GetName()
|
|
}
|
|
c.mu.Unlock()
|
|
if uname != certUserName {
|
|
t.Fatalf("Expected username %q, got %q", certUserName, uname)
|
|
}
|
|
if accname != accName {
|
|
t.Fatalf("Expected account %q, got %v", accName, accname)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTBasicAuth(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
opts func() *Options
|
|
user string
|
|
pass string
|
|
rc byte
|
|
}{
|
|
{
|
|
"top level auth, no override, wrong u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Username = "normal"
|
|
o.Password = "client"
|
|
return o
|
|
},
|
|
"mqtt", "client", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"top level auth, no override, correct u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Username = "normal"
|
|
o.Password = "client"
|
|
return o
|
|
},
|
|
"normal", "client", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"no top level auth, mqtt auth, wrong u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.Username = "mqtt"
|
|
o.MQTT.Password = "client"
|
|
return o
|
|
},
|
|
"normal", "client", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"no top level auth, mqtt auth, correct u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.Username = "mqtt"
|
|
o.MQTT.Password = "client"
|
|
return o
|
|
},
|
|
"mqtt", "client", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"top level auth, mqtt override, wrong u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Username = "normal"
|
|
o.Password = "client"
|
|
o.MQTT.Username = "mqtt"
|
|
o.MQTT.Password = "client"
|
|
return o
|
|
},
|
|
"normal", "client", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"top level auth, mqtt override, correct u/p",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Username = "normal"
|
|
o.Password = "client"
|
|
o.MQTT.Username = "mqtt"
|
|
o.MQTT.Password = "client"
|
|
return o
|
|
},
|
|
"mqtt", "client", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := test.opts()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: test.user,
|
|
pass: test.pass,
|
|
}
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, test.rc, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTAuthTimeout(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
at float64
|
|
mat float64
|
|
ok bool
|
|
}{
|
|
{"use top-level auth timeout", 0.5, 0.0, true},
|
|
{"use mqtt auth timeout", 0.5, 0.05, false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.AuthTimeout = test.at
|
|
o.MQTT.Username = "mqtt"
|
|
o.MQTT.Password = "client"
|
|
o.MQTT.AuthTimeout = test.mat
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, err := net.Dial("tcp", net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port)))
|
|
if err != nil {
|
|
t.Fatalf("Error connecting: %v", err)
|
|
}
|
|
defer mc.Close()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "mqtt",
|
|
pass: "client",
|
|
}
|
|
proto := mqttCreateConnectProto(ci)
|
|
if _, err := testMQTTWrite(mc, proto); err != nil {
|
|
if test.ok {
|
|
t.Fatalf("Error sending connect: %v", err)
|
|
}
|
|
// else it is ok since we got disconnected due to auth timeout
|
|
return
|
|
}
|
|
buf, err := testMQTTRead(mc)
|
|
if err != nil {
|
|
if test.ok {
|
|
t.Fatalf("Error reading: %v", err)
|
|
}
|
|
// else it is ok since we got disconnected due to auth timeout
|
|
return
|
|
}
|
|
r := &mqttReader{reader: mc}
|
|
r.reset(buf)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
testMQTTPublish(t, mc, r, 1, false, false, "foo", 1, []byte("msg"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTTokenAuth(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
opts func() *Options
|
|
token string
|
|
rc byte
|
|
}{
|
|
{
|
|
"top level auth, no override, wrong token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Authorization = "goodtoken"
|
|
return o
|
|
},
|
|
"badtoken", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"top level auth, no override, correct token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Authorization = "goodtoken"
|
|
return o
|
|
},
|
|
"goodtoken", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"no top level auth, mqtt auth, wrong token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.Token = "goodtoken"
|
|
return o
|
|
},
|
|
"badtoken", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"no top level auth, mqtt auth, correct token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.Token = "goodtoken"
|
|
return o
|
|
},
|
|
"goodtoken", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"top level auth, mqtt override, wrong token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Authorization = "clienttoken"
|
|
o.MQTT.Token = "mqtttoken"
|
|
return o
|
|
},
|
|
"clienttoken", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"top level auth, mqtt override, correct token",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Authorization = "clienttoken"
|
|
o.MQTT.Token = "mqtttoken"
|
|
return o
|
|
},
|
|
"mqtttoken", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := test.opts()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "ignore_use_token",
|
|
pass: test.token,
|
|
}
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, test.rc, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTJWTWithAllowedConnectionTypes(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
// Create System Account
|
|
syskp, _ := nkeys.CreateAccount()
|
|
syspub, _ := syskp.PublicKey()
|
|
sysAc := jwt.NewAccountClaims(syspub)
|
|
sysjwt, err := sysAc.Encode(oKp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
// Create memory resolver and store system account
|
|
mr := &MemAccResolver{}
|
|
mr.Store(syspub, sysjwt)
|
|
if err != nil {
|
|
t.Fatalf("Error saving system account JWT to memory resolver: %v", err)
|
|
}
|
|
// Add system account and memory resolver to server options
|
|
o.SystemAccount = syspub
|
|
o.AccountResolver = mr
|
|
setupAddTrusted(o)
|
|
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
connectionTypes []string
|
|
rc byte
|
|
}{
|
|
{"not allowed", []string{jwt.ConnectionTypeStandard}, mqttConnAckRCNotAuthorized},
|
|
{"allowed", []string{jwt.ConnectionTypeStandard, strings.ToLower(jwt.ConnectionTypeMqtt)}, mqttConnAckRCConnectionAccepted},
|
|
{"allowed with unknown", []string{jwt.ConnectionTypeMqtt, "SomeNewType"}, mqttConnAckRCConnectionAccepted},
|
|
{"not allowed with unknown", []string{"SomeNewType"}, mqttConnAckRCNotAuthorized},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
nuc := newJWTTestUserClaims()
|
|
nuc.AllowedConnectionTypes = test.connectionTypes
|
|
nuc.BearerToken = true
|
|
|
|
okp, _ := nkeys.FromSeed(oSeed)
|
|
|
|
akp, _ := nkeys.CreateAccount()
|
|
apub, _ := akp.PublicKey()
|
|
nac := jwt.NewAccountClaims(apub)
|
|
// Enable Jetstream on account with lax limitations
|
|
nac.Limits.JetStreamLimits.Consumer = -1
|
|
nac.Limits.JetStreamLimits.Streams = -1
|
|
nac.Limits.JetStreamLimits.MemoryStorage = 1024 * 1024
|
|
nac.Limits.JetStreamLimits.DiskStorage = 1024 * 1024
|
|
ajwt, err := nac.Encode(okp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating account JWT: %v", err)
|
|
}
|
|
|
|
nkp, _ := nkeys.CreateUser()
|
|
pub, _ := nkp.PublicKey()
|
|
nuc.Subject = pub
|
|
jwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
addAccountToMemResolver(s, apub, ajwt)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "ignore_use_token",
|
|
pass: jwt,
|
|
}
|
|
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, test.rc, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTUsersAuth(t *testing.T) {
|
|
users := []*User{{Username: "user", Password: "pwd"}}
|
|
for _, test := range []struct {
|
|
name string
|
|
opts func() *Options
|
|
user string
|
|
pass string
|
|
rc byte
|
|
}{
|
|
{
|
|
"no filtering, wrong user",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = users
|
|
return o
|
|
},
|
|
"wronguser", "pwd", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"no filtering, correct user",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = users
|
|
return o
|
|
},
|
|
"user", "pwd", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"filtering, user not allowed",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = users
|
|
// Only allowed for regular clients
|
|
o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard})
|
|
return o
|
|
},
|
|
"user", "pwd", mqttConnAckRCNotAuthorized,
|
|
},
|
|
{
|
|
"filtering, user allowed",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = users
|
|
o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, jwt.ConnectionTypeMqtt})
|
|
return o
|
|
},
|
|
"user", "pwd", mqttConnAckRCConnectionAccepted,
|
|
},
|
|
{
|
|
"filtering, wrong password",
|
|
func() *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = users
|
|
o.Users[0].AllowedConnectionTypes = testCreateAllowedConnectionTypes([]string{jwt.ConnectionTypeStandard, jwt.ConnectionTypeMqtt})
|
|
return o
|
|
},
|
|
"user", "badpassword", mqttConnAckRCNotAuthorized,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := test.opts()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: test.user,
|
|
pass: test.pass,
|
|
}
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, test.rc, false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTNoAuthUserValidation(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{{Username: "user", Password: "pwd"}}
|
|
// Should fail because it is not part of o.Users.
|
|
o.MQTT.NoAuthUser = "notfound"
|
|
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), "not present as user") {
|
|
t.Fatalf("Expected error saying not present as user, got %v", err)
|
|
}
|
|
|
|
// Set a valid no auth user for global options, but still should fail because
|
|
// of o.MQTT.NoAuthUser
|
|
o.NoAuthUser = "user"
|
|
o.MQTT.NoAuthUser = "notfound"
|
|
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), "not present as user") {
|
|
t.Fatalf("Expected error saying not present as user, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTNoAuthUser(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
override bool
|
|
useAuth bool
|
|
expectedUser string
|
|
expectedAcc string
|
|
}{
|
|
{"no override, no user provided", false, false, "noauth", "normal"},
|
|
{"no override, user povided", false, true, "user", "normal"},
|
|
{"override, no user provided", true, false, "mqttnoauth", "mqtt"},
|
|
{"override, user provided", true, true, "mqttuser", "mqtt"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
normalAcc := NewAccount("normal")
|
|
mqttAcc := NewAccount("mqtt")
|
|
o.Accounts = []*Account{normalAcc, mqttAcc}
|
|
o.Users = []*User{
|
|
{Username: "noauth", Password: "pwd", Account: normalAcc},
|
|
{Username: "user", Password: "pwd", Account: normalAcc},
|
|
{Username: "mqttnoauth", Password: "pwd", Account: mqttAcc},
|
|
{Username: "mqttuser", Password: "pwd", Account: mqttAcc},
|
|
}
|
|
o.NoAuthUser = "noauth"
|
|
if test.override {
|
|
o.MQTT.NoAuthUser = "mqttnoauth"
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
testMQTTEnableJSForAccount(t, s, "normal")
|
|
testMQTTEnableJSForAccount(t, s, "mqtt")
|
|
|
|
ci := &mqttConnInfo{clientID: "mqtt", cleanSess: true}
|
|
if test.useAuth {
|
|
ci.user = test.expectedUser
|
|
ci.pass = "pwd"
|
|
}
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
c := testMQTTGetClient(t, s, "mqtt")
|
|
c.mu.Lock()
|
|
uname := c.opts.Username
|
|
aname := c.acc.GetName()
|
|
c.mu.Unlock()
|
|
if uname != test.expectedUser {
|
|
t.Fatalf("Expected selected user to be %q, got %q", test.expectedUser, uname)
|
|
}
|
|
if aname != test.expectedAcc {
|
|
t.Fatalf("Expected selected account to be %q, got %q", test.expectedAcc, aname)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTConnectNotFirstPacket(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
c, err := net.Dial("tcp", net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port)))
|
|
if err != nil {
|
|
t.Fatalf("Error on dial: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
testMQTTSendPublishPacket(t, c, 0, false, false, "foo", 0, []byte("hello"))
|
|
testMQTTExpectDisconnect(t, c)
|
|
|
|
select {
|
|
case err := <-l.errCh:
|
|
if !strings.Contains(err, "should be a CONNECT") {
|
|
t.Fatalf("Expected error about first packet being a CONNECT, got %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not log any error")
|
|
}
|
|
}
|
|
|
|
func TestMQTTSecondConnect(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
proto := mqttCreateConnectProto(&mqttConnInfo{cleanSess: true})
|
|
if _, err := testMQTTWrite(mc, proto); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
testMQTTExpectDisconnect(t, mc)
|
|
}
|
|
|
|
func TestMQTTParseConnect(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
proto []byte
|
|
err string
|
|
wantErr bool
|
|
}{
|
|
{"packet in buffer error", []byte{0}, io.ErrUnexpectedEOF.Error(), false},
|
|
{"bad proto name", []byte{0, 4, 'B', 'A', 'D'}, "protocol name", false},
|
|
{"invalid proto name", []byte{0, 3, 'B', 'A', 'D'}, "expected connect packet with protocol name", false},
|
|
{"old proto not supported", []byte{0, 6, 'M', 'Q', 'I', 's', 'd', 'p'}, "older protocol", false},
|
|
{"error on protocol level", []byte{0, 4, 'M', 'Q', 'T', 'T'}, "protocol level", false},
|
|
{"unacceptable protocol version", []byte{0, 4, 'M', 'Q', 'T', 'T', 10}, "unacceptable protocol version", false},
|
|
{"error on flags", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel}, "flags", false},
|
|
{"reserved flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1}, errMQTTConnFlagReserved.Error(), false},
|
|
{"will qos without will flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1 << 3}, "if Will flag is set to 0, Will QoS must be 0 too", false},
|
|
{"will retain without will flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1 << 5}, errMQTTWillAndRetainFlag.Error(), false},
|
|
{"will qos", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 3<<3 | 1<<2}, "if Will flag is set to 1, Will QoS can be 0, 1 or 2", false},
|
|
{"no user but password", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagPasswordFlag}, errMQTTPasswordFlagAndNoUser.Error(), false},
|
|
{"missing keep alive", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0}, "keep alive", false},
|
|
{"missing client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1}, "client ID", false},
|
|
{"empty client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 0}, errMQTTCIDEmptyNeedsCleanFlag.Error(), false},
|
|
{"invalid utf8 client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 1, 241}, "invalid utf8 for client ID", false},
|
|
{"client ID with null byte", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 3, 'a', 0, 'b'}, "", true},
|
|
{"client ID with invalid character", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 1, '.'}, "", true},
|
|
{"missing will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0}, "Will topic", false},
|
|
{"empty will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 0}, errMQTTEmptyWillTopic.Error(), false},
|
|
{"invalid utf8 will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 241}, "invalid utf8 for Will topic", false},
|
|
{"invalid wildcard will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, '#'}, "wildcards not allowed", false},
|
|
{"error on will message", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 'a', 0, 3}, "Will message", false},
|
|
{"error on username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0}, "user name", false},
|
|
{"empty username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 0}, errMQTTEmptyUsername.Error(), false},
|
|
{"invalid utf8 username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 241}, "invalid utf8 for user name", false},
|
|
{"username with null byte", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 3, 'a', 0, 'b'}, "", true},
|
|
{"error on password", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagPasswordFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 'a'}, "password", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
_, _, err := c.mqttParseConnect(r, false)
|
|
if test.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected an error, got none")
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTConnectFailsOnParse(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
addr := net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port))
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error creating mqtt connection: %v", err)
|
|
}
|
|
|
|
pkLen := 2 + len(mqttProtoName) +
|
|
1 + // proto level
|
|
1 + // flags
|
|
2 + // keepAlive
|
|
2 + len("mqtt")
|
|
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(mqttPacketConnect)
|
|
w.WriteVarInt(pkLen)
|
|
w.WriteString(string(mqttProtoName))
|
|
w.WriteByte(0x7)
|
|
w.WriteByte(mqttConnFlagCleanSession)
|
|
w.WriteUint16(0)
|
|
w.WriteString("mqtt")
|
|
c.Write(w.Bytes())
|
|
|
|
buf, err := testMQTTRead(c)
|
|
if err != nil {
|
|
t.Fatalf("Error reading: %v", err)
|
|
}
|
|
r := &mqttReader{reader: c}
|
|
r.reset(buf)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCUnacceptableProtocolVersion, false)
|
|
}
|
|
|
|
func TestMQTTConnKeepAlive(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true, keepAlive: 1}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mc, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
|
|
time.Sleep(2 * time.Second)
|
|
testMQTTExpectDisconnect(t, mc)
|
|
}
|
|
|
|
func TestMQTTMalformedFixedHeaderFlagsCauseDisconnect(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
packet func(t *testing.T) (net.Conn, []byte)
|
|
}{
|
|
{
|
|
name: "connect",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
addr := net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port))
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error creating mqtt connection: %v", err)
|
|
}
|
|
proto := mqttCreateConnectProto(&mqttConnInfo{cleanSess: true})
|
|
proto[0] |= 0x1
|
|
return c, proto
|
|
},
|
|
},
|
|
{
|
|
name: "pingreq",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPing | 0x1, 0}
|
|
},
|
|
},
|
|
{
|
|
name: "puback",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubAck | 0x1, 2, 0, 1}
|
|
},
|
|
},
|
|
{
|
|
name: "pubrec",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubRec | 0x1, 2, 0, 1}
|
|
},
|
|
},
|
|
{
|
|
name: "pubrel",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubRel | 0x1, 2, 0, 1}
|
|
},
|
|
},
|
|
{
|
|
name: "pubcomp",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubComp | 0x1, 2, 0, 1}
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
c, packet := test.packet(t)
|
|
defer c.Close()
|
|
|
|
if _, err := testMQTTWrite(c, packet); err != nil {
|
|
t.Fatalf("Error writing malformed %s packet: %v", test.name, err)
|
|
}
|
|
testMQTTExpectDisconnect(t, c)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTMalformedRemainingLengthCausesDisconnect(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
packet func(t *testing.T) (net.Conn, []byte)
|
|
}{
|
|
{
|
|
name: "pingreq",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPing, 1, 0}
|
|
},
|
|
},
|
|
{
|
|
name: "puback",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubAck, 3, 0, 1, 0}
|
|
},
|
|
},
|
|
{
|
|
name: "pubrec",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubRec, 3, 0, 1, 0}
|
|
},
|
|
},
|
|
{
|
|
name: "pubrel",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubRel | 0x2, 3, 0, 1, 0}
|
|
},
|
|
},
|
|
{
|
|
name: "pubcomp",
|
|
packet: func(t *testing.T) (net.Conn, []byte) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
return mc, []byte{mqttPacketPubComp, 3, 0, 1, 0}
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
c, packet := test.packet(t)
|
|
defer c.Close()
|
|
|
|
if _, err := testMQTTWrite(c, packet); err != nil {
|
|
t.Fatalf("Error writing malformed %s packet: %v", test.name, err)
|
|
}
|
|
testMQTTExpectDisconnect(t, c)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTDontSetPinger(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.PingInterval = 15 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "mqtt", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
c := testMQTTGetClient(t, s, "mqtt")
|
|
c.mu.Lock()
|
|
timerSet := c.ping.tmr != nil
|
|
c.mu.Unlock()
|
|
if timerSet {
|
|
t.Fatalf("Ping timer should not be set for MQTT clients")
|
|
}
|
|
|
|
// Wait a bit and expect nothing (and connection should still be valid)
|
|
testMQTTExpectNothing(t, r)
|
|
testMQTTPublish(t, mc, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
}
|
|
|
|
func TestMQTTTopicAndSubjectConversion(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
mqttTopic string
|
|
natsSubject string
|
|
err string
|
|
}{
|
|
{"/", "/", "/./", ""},
|
|
{"//", "//", "/././", ""},
|
|
{"///", "///", "/./././", ""},
|
|
{"////", "////", "/././././", ""},
|
|
{"foo", "foo", "foo", ""},
|
|
{"/foo", "/foo", "/.foo", ""},
|
|
{"//foo", "//foo", "/./.foo", ""},
|
|
{"///foo", "///foo", "/././.foo", ""},
|
|
{"///foo/", "///foo/", "/././.foo./", ""},
|
|
{"///foo//", "///foo//", "/././.foo././", ""},
|
|
{"///foo///", "///foo///", "/././.foo./././", ""},
|
|
{"//.foo.//", "//.foo.//", "/././/foo//././", ""},
|
|
{"foo/bar", "foo/bar", "foo.bar", ""},
|
|
{"/foo/bar", "/foo/bar", "/.foo.bar", ""},
|
|
{"/foo/bar/", "/foo/bar/", "/.foo.bar./", ""},
|
|
{"foo/bar/baz", "foo/bar/baz", "foo.bar.baz", ""},
|
|
{"/foo/bar/baz", "/foo/bar/baz", "/.foo.bar.baz", ""},
|
|
{"/foo/bar/baz/", "/foo/bar/baz/", "/.foo.bar.baz./", ""},
|
|
{"bar", "bar/", "bar./", ""},
|
|
{"bar//", "bar//", "bar././", ""},
|
|
{"bar///", "bar///", "bar./././", ""},
|
|
{"foo//bar", "foo//bar", "foo./.bar", ""},
|
|
{"foo///bar", "foo///bar", "foo././.bar", ""},
|
|
{"foo////bar", "foo////bar", "foo./././.bar", ""},
|
|
{".", ".", "//", ""},
|
|
{"..", "..", "////", ""},
|
|
{"...", "...", "//////", ""},
|
|
{"./", "./", "//./", ""},
|
|
{".//.", ".//.", "//././/", ""},
|
|
{"././.", "././.", "//.//.//", ""},
|
|
{"././/.", "././/.", "//.//././/", ""},
|
|
{".foo", ".foo", "//foo", ""},
|
|
{"foo.", "foo.", "foo//", ""},
|
|
{".foo.", ".foo.", "//foo//", ""},
|
|
{"foo../bar/", "foo../bar/", "foo////.bar./", ""},
|
|
{"foo../bar/.", "foo../bar/.", "foo////.bar.//", ""},
|
|
{"/foo/", "/foo/", "/.foo./", ""},
|
|
{"./foo/.", "./foo/.", "//.foo.//", ""},
|
|
{"foo.bar/baz", "foo.bar/baz", "foo//bar.baz", ""},
|
|
// These should produce errors
|
|
{"foo/+", "foo/+", "", "wildcards not allowed in publish"},
|
|
{"foo/#", "foo/#", "", "wildcards not allowed in publish"},
|
|
{"foo bar", "foo bar", "", "not supported"},
|
|
{"foo\x7fbar", "foo\x7fbar", "", "not supported"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
res, err := mqttTopicToNATSPubSubject([]byte(test.mqttTopic))
|
|
if test.err != _EMPTY_ {
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
return
|
|
}
|
|
toNATS := string(res)
|
|
if toNATS != test.natsSubject {
|
|
t.Fatalf("Expected subject %q got %q", test.natsSubject, toNATS)
|
|
}
|
|
|
|
res = natsSubjectToMQTTTopic(res)
|
|
backToMQTT := string(res)
|
|
if backToMQTT != test.mqttTopic {
|
|
t.Fatalf("Expected topic %q got %q (NATS conversion was %q)", test.mqttTopic, backToMQTT, toNATS)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTFilterConversion(t *testing.T) {
|
|
// Similar to TopicConversion test except that wildcards are OK here.
|
|
// So testing only those.
|
|
for _, test := range []struct {
|
|
name string
|
|
mqttTopic string
|
|
natsSubject string
|
|
}{
|
|
{"single level wildcard", "+", "*"},
|
|
{"single level wildcard", "/+", "/.*"},
|
|
{"single level wildcard", "+/", "*./"},
|
|
{"single level wildcard", "/+/", "/.*./"},
|
|
{"single level wildcard", "foo/+", "foo.*"},
|
|
{"single level wildcard", "foo/+/", "foo.*./"},
|
|
{"single level wildcard", "foo/+/bar", "foo.*.bar"},
|
|
{"single level wildcard", "foo/+/+", "foo.*.*"},
|
|
{"single level wildcard", "foo/+/+/", "foo.*.*./"},
|
|
{"single level wildcard", "foo/+/+/bar", "foo.*.*.bar"},
|
|
{"single level wildcard", "foo//+", "foo./.*"},
|
|
{"single level wildcard", "foo//+/", "foo./.*./"},
|
|
{"single level wildcard", "foo//+//", "foo./.*././"},
|
|
{"single level wildcard", "foo//+//bar", "foo./.*./.bar"},
|
|
{"single level wildcard", "foo///+///bar", "foo././.*././.bar"},
|
|
{"single level wildcard", "foo.bar///+///baz", "foo//bar././.*././.baz"},
|
|
|
|
{"multi level wildcard", "#", ">"},
|
|
{"multi level wildcard", "/#", "/.>"},
|
|
{"multi level wildcard", "/foo/#", "/.foo.>"},
|
|
{"multi level wildcard", "foo/#", "foo.>"},
|
|
{"multi level wildcard", "foo//#", "foo./.>"},
|
|
{"multi level wildcard", "foo///#", "foo././.>"},
|
|
{"multi level wildcard", "foo/bar/#", "foo.bar.>"},
|
|
{"multi level wildcard", "foo/bar.baz/#", "foo.bar//baz.>"},
|
|
|
|
// NATS wildcards in MQTT filters are generally
|
|
// not escaped, and get interpreted as wildcards
|
|
// when converted to NATS subjects
|
|
{"* literal", "foo*/bar", "foo*.bar"},
|
|
{"> literal", "foo>/bar", "foo>.bar"},
|
|
{"* wildcard middle", "foo/*/bar", "foo.*.bar"},
|
|
{"* wildcard end", "foo/*/bar/*", "foo.*.bar.*"},
|
|
{"> wildcard", "foo/>", "foo.>"},
|
|
{"wildcard mix", "foo/*/bar/>", "foo.*.bar.>"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
res, err := mqttFilterToNATSSubject([]byte(test.mqttTopic))
|
|
if err != nil {
|
|
t.Fatalf("Error: %v", err)
|
|
}
|
|
if string(res) != test.natsSubject {
|
|
t.Fatalf("Expected subject %q got %q", test.natsSubject, res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTParseSub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
proto []byte
|
|
b byte
|
|
pl int
|
|
err string
|
|
wantErr bool
|
|
}{
|
|
{"reserved flag", nil, 3, 0, "wrong subscribe reserved flags", false},
|
|
{"ensure packet loaded", []byte{1, 2}, mqttSubscribeFlags, 10, io.ErrUnexpectedEOF.Error(), false},
|
|
{"error reading packet id", []byte{1}, mqttSubscribeFlags, 1, "reading packet identifier", false},
|
|
{"packet id cannot be zero", []byte{0, 0}, mqttSubscribeFlags, 2, errMQTTPacketIdentifierIsZero.Error(), false},
|
|
{"missing filters", []byte{0, 1}, mqttSubscribeFlags, 2, "subscribe protocol must contain at least 1 topic filter", false},
|
|
{"error reading topic", []byte{0, 1, 0, 2, 'a'}, mqttSubscribeFlags, 5, "topic filter", false},
|
|
{"empty topic", []byte{0, 1, 0, 0}, mqttSubscribeFlags, 4, errMQTTTopicFilterCannotBeEmpty.Error(), false},
|
|
{"invalid utf8 topic", []byte{0, 1, 0, 1, 241}, mqttSubscribeFlags, 5, "invalid utf8 for topic filter", false},
|
|
{"missing qos", []byte{0, 1, 0, 1, 'a'}, mqttSubscribeFlags, 5, "QoS", false},
|
|
{"invalid qos", []byte{0, 1, 0, 1, 'a', 3}, mqttSubscribeFlags, 6, "subscribe QoS value must be 0, 1 or 2", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
_, _, err := c.mqttParseSubsOrUnsubs(r, test.b, test.pl, true)
|
|
if test.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected an error, got none")
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMQTTSub(t testing.TB, pi uint16, c net.Conn, r *mqttReader, filters []*mqttFilter, expected []byte) {
|
|
t.Helper()
|
|
w := newMQTTWriter(0)
|
|
pkLen := 2 // for pi
|
|
for i := 0; i < len(filters); i++ {
|
|
f := filters[i]
|
|
pkLen += 2 + len(f.filter) + 1
|
|
}
|
|
w.WriteByte(mqttPacketSub | mqttSubscribeFlags)
|
|
w.WriteVarInt(pkLen)
|
|
w.WriteUint16(pi)
|
|
for i := 0; i < len(filters); i++ {
|
|
f := filters[i]
|
|
w.WriteBytes([]byte(f.filter))
|
|
w.WriteByte(f.qos)
|
|
}
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing SUBSCRIBE protocol: %v", err)
|
|
}
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketSubAck {
|
|
t.Fatalf("Expected SUBACK packet %x, got %x", mqttPacketSubAck, pt)
|
|
}
|
|
rpi, err := r.readUint16("packet identifier")
|
|
if err != nil || rpi != pi {
|
|
t.Fatalf("Error with packet identifier expected=%v got: %v err=%v", pi, rpi, err)
|
|
}
|
|
for i, rem := 0, pl-2; rem > 0; rem-- {
|
|
qos, err := r.readByte("filter qos")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if expected != nil && qos != expected[i] {
|
|
t.Fatalf("For topic filter %q expected qos of %v, got %v",
|
|
filters[i].filter, expected[i], qos)
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubAck(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{
|
|
{
|
|
Username: "user",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Subscribe: &SubjectPermission{Allow: []string{"foo", "bar", "baz", "foo.>"}},
|
|
},
|
|
},
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true, user: "user", pass: "pass"}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
subs := []*mqttFilter{
|
|
{filter: "foo", qos: 0},
|
|
{filter: "bar", qos: 1},
|
|
{filter: "baz", qos: 2},
|
|
{filter: "foo/#/bar", qos: 0}, // Invalid sub, so we should receive a result of mqttSubAckFailure
|
|
}
|
|
expected := []byte{
|
|
0,
|
|
1,
|
|
2,
|
|
mqttSubAckFailure,
|
|
}
|
|
testMQTTSub(t, 1, mc, r, subs, expected)
|
|
}
|
|
|
|
func TestMQTTQoS2SubDowngrade(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.downgradeQoS2Sub = true
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
subs := []*mqttFilter{
|
|
{filter: "bar", qos: 1},
|
|
{filter: "baz", qos: 2},
|
|
}
|
|
expected := []byte{
|
|
1,
|
|
1,
|
|
}
|
|
testMQTTSub(t, 1, mc, r, subs, expected)
|
|
}
|
|
|
|
func testMQTTFlush(t testing.TB, c net.Conn, bw *bufio.Writer, r *mqttReader) {
|
|
t.Helper()
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(mqttPacketPing)
|
|
w.WriteByte(0)
|
|
if bw != nil {
|
|
bw.Write(w.Bytes())
|
|
bw.Flush()
|
|
} else {
|
|
c.Write(w.Bytes())
|
|
}
|
|
ab, l := testMQTTReadPacket(t, r)
|
|
if pt := ab & mqttPacketMask; pt != mqttPacketPingResp {
|
|
t.Fatalf("Expected ping response got %x", pt)
|
|
}
|
|
if l != 0 {
|
|
t.Fatalf("Expected PINGRESP length to be 0, got %v", l)
|
|
}
|
|
}
|
|
|
|
func testMQTTExpectNothing(t testing.TB, r *mqttReader) {
|
|
t.Helper()
|
|
// First, check that we don't have buffered data.
|
|
if r.hasMore() {
|
|
t.Fatalf("Expected nothing, got %v", r.buf[r.pos:])
|
|
}
|
|
// Then, try to read from the reader with some timeout.
|
|
var buf [128]byte
|
|
r.reader.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
|
if n, err := r.reader.Read(buf[:]); err == nil {
|
|
t.Fatalf("Expected nothing, got %v", buf[:n])
|
|
}
|
|
r.reader.SetReadDeadline(time.Time{})
|
|
}
|
|
|
|
func testMQTTCheckPubMsg(t testing.TB, c net.Conn, r *mqttReader, topic string, expectedFlags byte, payload []byte) uint16 {
|
|
t.Helper()
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, topic, expectedFlags, payload)
|
|
if pi == 0 {
|
|
return 0
|
|
}
|
|
qos := mqttGetQoS(expectedFlags)
|
|
switch qos {
|
|
case 1:
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
case 2:
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, pi)
|
|
}
|
|
return pi
|
|
}
|
|
|
|
func testMQTTCheckPubMsgNoAck(t testing.TB, c net.Conn, r *mqttReader, topic string, expectedFlags byte, payload []byte) uint16 {
|
|
t.Helper()
|
|
pflags, pi := testMQTTGetPubMsg(t, c, r, topic, payload)
|
|
if pflags != expectedFlags {
|
|
t.Fatalf("Expected flags to be %x, got %x", expectedFlags, pflags)
|
|
}
|
|
return pi
|
|
}
|
|
|
|
func testMQTTGetPubMsg(t testing.TB, c net.Conn, r *mqttReader, topic string, payload []byte) (byte, uint16) {
|
|
t.Helper()
|
|
flags, pi, _, _ := testMQTTGetPubMsgEx(t, c, r, topic, payload)
|
|
return flags, pi
|
|
}
|
|
|
|
func testMQTTGetPubMsgEx(t testing.TB, _ net.Conn, r *mqttReader, topic string, payload []byte) (byte, uint16, string, []byte) {
|
|
t.Helper()
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Expected PUBLISH packet %x, got %x", mqttPacketPub, pt)
|
|
}
|
|
return testMQTTGetPubMsgExEx(t, nil, r, b, pl, topic, payload)
|
|
}
|
|
|
|
func testMQTTGetPubMsgExEx(t testing.TB, _ net.Conn, r *mqttReader, b byte, pl int, topic string, payload []byte) (byte, uint16, string, []byte) {
|
|
t.Helper()
|
|
pflags := b & mqttPacketFlagMask
|
|
qos := (pflags & mqttPubFlagQoS) >> 1
|
|
start := r.pos
|
|
ptopic, err := r.readString("topic name")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if topic != _EMPTY_ && ptopic != topic {
|
|
t.Fatalf("Expected topic %q, got %q", topic, ptopic)
|
|
}
|
|
var pi uint16
|
|
if qos > 0 {
|
|
pi, err = r.readUint16("packet identifier")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
msgLen := pl - (r.pos - start)
|
|
if r.pos+msgLen > len(r.buf) {
|
|
t.Fatalf("computed message length goes beyond buffer: ml=%v pos=%v lenBuf=%v",
|
|
msgLen, r.pos, len(r.buf))
|
|
}
|
|
ppayload := r.buf[r.pos : r.pos+msgLen]
|
|
if payload != nil && !bytes.Equal(payload, ppayload) {
|
|
t.Fatalf("Expected payload %q, got %q", payload, ppayload)
|
|
}
|
|
r.pos += msgLen
|
|
return pflags, pi, ptopic, ppayload
|
|
}
|
|
|
|
func testMQTTReadPubPacket(t testing.TB, r *mqttReader) (flags byte, pi uint16, topic string, payload []byte) {
|
|
t.Helper()
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Expected PUBLISH packet %x, got %x", mqttPacketPub, pt)
|
|
}
|
|
flags = b & mqttPacketFlagMask
|
|
start := r.pos
|
|
topic, err := r.readString("topic name")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
qos := (flags & mqttPubFlagQoS) >> 1
|
|
if qos > 0 {
|
|
pi, err = r.readUint16("packet identifier")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
msgLen := pl - (r.pos - start)
|
|
if r.pos+msgLen > len(r.buf) {
|
|
t.Fatalf("computed message length goes beyond buffer: ml=%v pos=%v lenBuf=%v",
|
|
msgLen, r.pos, len(r.buf))
|
|
}
|
|
payload = r.buf[r.pos : r.pos+msgLen]
|
|
r.pos += msgLen
|
|
return flags, pi, topic, payload
|
|
}
|
|
|
|
func testMQTTSendPIPacket(packetType byte, t testing.TB, c net.Conn, pi uint16) {
|
|
t.Helper()
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(packetType)
|
|
w.WriteVarInt(2)
|
|
w.WriteUint16(pi)
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing packet type %v: %v", packetType, err)
|
|
}
|
|
}
|
|
|
|
func testMQTTWritePublishPacket(t testing.TB, w *mqttWriter, qos byte, dup, retain bool, topic string, pi uint16, payload []byte) {
|
|
t.Helper()
|
|
w.WritePublishHeader(pi, qos, dup, retain, []byte(topic), len(payload))
|
|
if _, err := w.Write(payload); err != nil {
|
|
t.Fatalf("Error writing PUBLISH proto: %v", err)
|
|
}
|
|
}
|
|
|
|
func testMQTTSendPublishPacket(t testing.TB, c net.Conn, qos byte, dup, retain bool, topic string, pi uint16, payload []byte) {
|
|
t.Helper()
|
|
c.SetWriteDeadline(time.Now().Add(testMQTTTimeout))
|
|
_, header := mqttMakePublishHeader(pi, qos, dup, retain, []byte(topic), len(payload))
|
|
if _, err := c.Write(header); err != nil {
|
|
t.Fatalf("Error writing PUBLISH header: %v", err)
|
|
}
|
|
if _, err := c.Write(payload); err != nil {
|
|
t.Fatalf("Error writing PUBLISH payload: %v", err)
|
|
}
|
|
c.SetWriteDeadline(time.Time{})
|
|
}
|
|
|
|
func testMQTTPublish(t testing.TB, c net.Conn, r *mqttReader, qos byte, dup, retain bool, topic string, pi uint16, payload []byte) {
|
|
t.Helper()
|
|
testMQTTSendPublishPacket(t, c, qos, dup, retain, topic, pi, payload)
|
|
switch qos {
|
|
case 1:
|
|
b, _ := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPubAck {
|
|
t.Fatalf("Expected PUBACK packet %x, got %x", mqttPacketPubAck, pt)
|
|
}
|
|
rpi, err := r.readUint16("packet identifier")
|
|
if err != nil || rpi != pi {
|
|
t.Fatalf("Error with packet identifier expected=%v got: %v err=%v", pi, rpi, err)
|
|
}
|
|
|
|
case 2:
|
|
b, _ := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPubRec {
|
|
t.Fatalf("Expected PUBREC packet %x, got %x", mqttPacketPubRec, pt)
|
|
}
|
|
rpi, err := r.readUint16("packet identifier")
|
|
if err != nil || rpi != pi {
|
|
t.Fatalf("Error with packet identifier expected=%v got: %v err=%v", pi, rpi, err)
|
|
}
|
|
|
|
testMQTTSendPIPacket(mqttPacketPubRel|0x2, t, c, pi)
|
|
|
|
b, _ = testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPubComp {
|
|
t.Fatalf("Expected PUBCOMP packet %x, got %x", mqttPacketPubComp, pt)
|
|
}
|
|
rpi, err = r.readUint16("packet identifier")
|
|
if err != nil || rpi != pi {
|
|
t.Fatalf("Error with packet identifier expected=%v got: %v err=%v", pi, rpi, err)
|
|
}
|
|
|
|
testMQTTFlush(t, c, nil, r)
|
|
}
|
|
}
|
|
|
|
func TestMQTTParsePub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
flags byte
|
|
proto []byte
|
|
pl int
|
|
err string
|
|
wantErr bool
|
|
}{
|
|
{"qos not supported", (3 << 1), nil, 0, "QoS=3 is invalid in MQTT", false},
|
|
{"packet in buffer error", 0, nil, 10, io.ErrUnexpectedEOF.Error(), false},
|
|
{"error on topic", 0, []byte{0, 3, 'f', 'o'}, 4, "topic", false},
|
|
{"empty topic", 0, []byte{0, 0}, 2, errMQTTTopicIsEmpty.Error(), false},
|
|
{"wildcards topic", 0, []byte{0, 1, '#'}, 3, "wildcards not allowed", false},
|
|
{"invalid utf8 topic", 0, []byte{0, 1, 241}, 3, "", true},
|
|
{"topic with null byte", 0, []byte{0, 3, 'f', 0, 'o'}, 5, "", true},
|
|
{"error on packet identifier", mqttPubQos1, []byte{0, 3, 'f', 'o', 'o'}, 5, "packet identifier", false},
|
|
{"invalid packet identifier", mqttPubQos1, []byte{0, 3, 'f', 'o', 'o', 0, 0}, 7, errMQTTPacketIdentifierIsZero.Error(), false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
pp := &mqttPublish{flags: test.flags}
|
|
err := c.mqttParsePub(r, test.pl, pp, false)
|
|
if test.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected an error, got none")
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTParsePIMsg(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
proto []byte
|
|
err string
|
|
}{
|
|
{"packet in buffer error", nil, io.ErrUnexpectedEOF.Error()},
|
|
{"error reading packet identifier", []byte{0}, "packet identifier"},
|
|
{"invalid packet identifier", []byte{0, 0}, errMQTTPacketIdentifierIsZero.Error()},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
if _, err := mqttParsePIPacket(r); err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTPublish(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mcp, mpr, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTPublish(t, mcp, mpr, 1, false, false, "foo", 1, []byte("msg"))
|
|
testMQTTPublish(t, mcp, mpr, 2, false, false, "foo", 2, []byte("msg"))
|
|
}
|
|
|
|
func TestMQTTQoS2PubReject(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.rejectQoS2Pub = true
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mcp, mpr, 1, false, false, "foo", 1, []byte("msg"))
|
|
|
|
testMQTTSendPublishPacket(t, mcp, 2, false, false, "foo", 2, []byte("msg"))
|
|
testMQTTExpectDisconnect(t, mcp)
|
|
}
|
|
|
|
func TestMQTTSub(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
mqttSubTopic string
|
|
natsPubSubject string
|
|
mqttPubTopic string
|
|
ok bool
|
|
}{
|
|
{"1 level match", "foo", "foo", "foo", true},
|
|
{"1 level no match", "foo", "bar", "bar", false},
|
|
{"2 levels match", "foo/bar", "foo.bar", "foo/bar", true},
|
|
{"2 levels no match", "foo/bar", "foo.baz", "foo/baz", false},
|
|
{"3 levels match", "/foo/bar", "/.foo.bar", "/foo/bar", true},
|
|
{"3 levels no match", "/foo/bar", "/.foo.baz", "/foo/baz", false},
|
|
|
|
{"single level wc", "foo/+", "foo.bar.baz", "foo/bar/baz", false},
|
|
{"single level wc", "foo/+", "foo.bar./", "foo/bar/", false},
|
|
{"single level wc", "foo/+", "foo.bar", "foo/bar", true},
|
|
{"single level wc", "foo/+", "foo./", "foo/", true},
|
|
{"single level wc", "foo/+", "foo", "foo", false},
|
|
{"single level wc", "foo/+", "/.foo", "/foo", false},
|
|
|
|
{"multiple level wc", "foo/#", "foo.bar.baz./", "foo/bar/baz/", true},
|
|
{"multiple level wc", "foo/#", "foo.bar.baz", "foo/bar/baz", true},
|
|
{"multiple level wc", "foo/#", "foo.bar./", "foo/bar/", true},
|
|
{"multiple level wc", "foo/#", "foo.bar", "foo/bar", true},
|
|
{"multiple level wc", "foo/#", "foo./", "foo/", true},
|
|
{"multiple level wc", "foo/#", "foo", "foo", true},
|
|
{"multiple level wc", "foo/#", "/.foo", "/foo", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: test.mqttSubTopic, qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
natsPub(t, nc, test.natsPubSubject, []byte("msg"))
|
|
if test.ok {
|
|
testMQTTCheckPubMsg(t, mc, r, test.mqttPubTopic, 0, []byte("msg"))
|
|
} else {
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
testMQTTPublish(t, mcp, mpr, 0, false, false, test.mqttPubTopic, 0, []byte("msg"))
|
|
if test.ok {
|
|
testMQTTCheckPubMsg(t, mc, r, test.mqttPubTopic, 0, []byte("msg"))
|
|
} else {
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubQoS2(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
topic := "foo/bar/baz"
|
|
mqttTopic0 := "foo/#"
|
|
mqttTopic1 := "foo/bar/#"
|
|
mqttTopic2 := topic
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: mqttTopic0, qos: 0}}, []byte{0})
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: mqttTopic1, qos: 1}}, []byte{1})
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: mqttTopic2, qos: 2}}, []byte{2})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
for pubQoS, expectedCounts := range map[byte]map[byte]int{
|
|
0: {0: 3},
|
|
1: {0: 1, 1: 2},
|
|
2: {0: 1, 1: 1, 2: 1},
|
|
} {
|
|
t.Run(fmt.Sprintf("pubQoS %v", pubQoS), func(t *testing.T) {
|
|
pubPI := uint16(456)
|
|
|
|
testMQTTPublish(t, mcp, mpr, pubQoS, false, false, topic, pubPI, []byte("msg"))
|
|
|
|
qosCounts := map[byte]int{}
|
|
delivered := map[uint16]byte{}
|
|
|
|
// We have 3 subscriptions, each should receive the message, with the
|
|
// QoS that maybe "trimmed" to that of the subscription.
|
|
for i := 0; i < 3; i++ {
|
|
flags, pi := testMQTTGetPubMsg(t, mc, r, topic, []byte("msg"))
|
|
delivered[pi] = flags
|
|
qosCounts[mqttGetQoS(flags)]++
|
|
}
|
|
|
|
for pi, flags := range delivered {
|
|
switch mqttGetQoS(flags) {
|
|
case 1:
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, mc, pi)
|
|
|
|
case 2:
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, mc, pi)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, pi)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, mc, pi)
|
|
}
|
|
}
|
|
|
|
if !reflect.DeepEqual(qosCounts, expectedCounts) {
|
|
t.Fatalf("Expected QoS %#v, got %#v", expectedCounts, qosCounts)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubQoS2Restart(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
pubTopic := "foo/bar"
|
|
|
|
publish := func(msg string) {
|
|
t.Helper()
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mcp, mpr, 2, false, false, pubTopic, 1, []byte(msg))
|
|
testMQTTFlush(t, mcp, nil, mpr)
|
|
}
|
|
|
|
createSub := func(present bool) (net.Conn, *mqttReader) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, present)
|
|
if !present {
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo/bar", qos: 2}}, []byte{2})
|
|
testMQTTSub(t, 2, c, r, []*mqttFilter{{filter: "foo/+", qos: 2}}, []byte{2})
|
|
testMQTTFlush(t, c, nil, r)
|
|
}
|
|
return c, r
|
|
}
|
|
|
|
c, r := createSub(false)
|
|
defer c.Close()
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
checkSub := func(count int) {
|
|
t.Helper()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
for c := range js.Consumers(mqttOutStreamName) {
|
|
acc := s.GlobalAccount()
|
|
res := acc.sl.Match(c.Config.DeliverSubject)
|
|
if n := len(res.psubs); n != count {
|
|
return fmt.Errorf("Expected %v subscription on %q, got %v",
|
|
count, c.Config.DeliverSubject, n)
|
|
} else {
|
|
// We got the JS consumer and verified that it has the
|
|
// expected number of subscription, so we are done.
|
|
return nil
|
|
}
|
|
}
|
|
// No JS consumer was found.
|
|
return fmt.Errorf("Did not find a JS consumer for %q", mqttOutStreamName)
|
|
})
|
|
}
|
|
checkSub(1)
|
|
|
|
publish("msg1")
|
|
|
|
consume := func(msg string) {
|
|
t.Helper()
|
|
var pis []uint16
|
|
for range 2 {
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Expected PUBLISH packet %x, got %x", mqttPacketPub, pt)
|
|
}
|
|
flags, pi, _, _ := testMQTTGetPubMsgExEx(t, c, r, b, pl, pubTopic, []byte(msg))
|
|
require_Equal(t, flags, byte(mqttPubQoS2))
|
|
pis = append(pis, pi)
|
|
}
|
|
for _, pi := range pis {
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, pi)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, pi)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, pi)
|
|
}
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
consume("msg1")
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
checkSub(0)
|
|
|
|
publish("msg2")
|
|
|
|
c, r = createSub(true)
|
|
defer c.Close()
|
|
checkSub(1)
|
|
consume("msg2")
|
|
|
|
publish("msg3")
|
|
consume("msg3")
|
|
}
|
|
|
|
func TestMQTTSubQoS1(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mqttTopic := "foo/bar"
|
|
|
|
// Subscribe with QoS 1
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: mqttTopic, qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish from NATS, which means QoS 0
|
|
natsPub(t, nc, "foo.bar", []byte("NATS"))
|
|
// Will receive as QoS 0
|
|
testMQTTCheckPubMsg(t, mc, r, mqttTopic, 0, []byte("NATS"))
|
|
testMQTTCheckPubMsg(t, mc, r, mqttTopic, 0, []byte("NATS"))
|
|
|
|
// Publish from MQTT with QoS 0
|
|
testMQTTPublish(t, mcp, mpr, 0, false, false, mqttTopic, 0, []byte("msg"))
|
|
// Will receive as QoS 0
|
|
testMQTTCheckPubMsg(t, mc, r, mqttTopic, 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, mqttTopic, 0, []byte("msg"))
|
|
|
|
// Publish from MQTT with QoS 1
|
|
testMQTTPublish(t, mcp, mpr, 1, false, false, mqttTopic, 1, []byte("msg"))
|
|
pflags1, pi1 := testMQTTGetPubMsg(t, mc, r, mqttTopic, []byte("msg"))
|
|
if pflags1 != 0x2 {
|
|
t.Fatalf("Expected flags to be 0x2, got %v", pflags1)
|
|
}
|
|
pflags2, pi2 := testMQTTGetPubMsg(t, mc, r, mqttTopic, []byte("msg"))
|
|
if pflags2 != 0x2 {
|
|
t.Fatalf("Expected flags to be 0x2, got %v", pflags2)
|
|
}
|
|
if pi1 == pi2 {
|
|
t.Fatalf("packet identifier for message 1: %v should be different from message 2", pi1)
|
|
}
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, mc, pi1)
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, mc, pi2)
|
|
}
|
|
|
|
func getSubQoS(sub *subscription) int {
|
|
if sub.mqtt != nil {
|
|
return int(sub.mqtt.qos)
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func TestMQTTSubDups(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", user: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Test with single SUBSCRIBE protocol but multiple filters
|
|
filters := []*mqttFilter{
|
|
{filter: "foo", qos: 1},
|
|
{filter: "foo", qos: 0},
|
|
}
|
|
testMQTTSub(t, 1, mc, r, filters, []byte{1, 0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// And also with separate SUBSCRIBE protocols
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "bar", qos: 0}}, []byte{0})
|
|
// Ask for QoS 1 but server will downgrade to 1
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish and test msg received only once
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "bar", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "bar", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Check that the QoS for subscriptions have been updated to the latest received filter
|
|
var err error
|
|
subc := testMQTTGetClient(t, s, "sub")
|
|
subc.mu.Lock()
|
|
if subc.opts.Username != "sub" {
|
|
err = fmt.Errorf("wrong user name")
|
|
}
|
|
if err == nil {
|
|
if sub := subc.subs["foo"]; sub == nil || getSubQoS(sub) != 0 {
|
|
err = fmt.Errorf("subscription foo QoS should be 0, got %v", getSubQoS(sub))
|
|
}
|
|
}
|
|
if err == nil {
|
|
if sub := subc.subs["bar"]; sub == nil || getSubQoS(sub) != 1 {
|
|
err = fmt.Errorf("subscription bar QoS should be 1, got %v", getSubQoS(sub))
|
|
}
|
|
}
|
|
subc.mu.Unlock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Now subscribe on "foo/#" which means that a PUBLISH on "foo" will be received
|
|
// by this subscription and also the one on "foo".
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish and test msg received twice
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
|
|
checkWCSub := func(expectedQoS int) {
|
|
t.Helper()
|
|
|
|
subc.mu.Lock()
|
|
defer subc.mu.Unlock()
|
|
|
|
// When invoked with expectedQoS==1, we have the following subs:
|
|
// foo (QoS-0), bar (QoS-1), foo.> (QoS-1)
|
|
// which means (since QoS-1 have a JS consumer + sub for delivery
|
|
// and foo.> causes a "foo fwc") that we should have the following
|
|
// number of NATS subs: foo (1), bar (2), foo.> (2) and "foo fwc" (2),
|
|
// so total=7.
|
|
// When invoked with expectedQoS==0, it means that we have replaced
|
|
// foo/# QoS-1 to QoS-0, so we should have 2 less NATS subs,
|
|
// so total=5
|
|
expected := 7
|
|
if expectedQoS == 0 {
|
|
expected = 5
|
|
}
|
|
if lenmap := len(subc.subs); lenmap != expected {
|
|
t.Fatalf("Subs map should have %v entries, got %v", expected, lenmap)
|
|
}
|
|
if sub, ok := subc.subs["foo.>"]; !ok {
|
|
t.Fatal("Expected sub foo.> to be present but was not")
|
|
} else if getSubQoS(sub) != expectedQoS {
|
|
t.Fatalf("Expected sub foo.> QoS to be %v, got %v", expectedQoS, getSubQoS(sub))
|
|
}
|
|
if sub, ok := subc.subs["foo fwc"]; !ok {
|
|
t.Fatal("Expected sub foo fwc to be present but was not")
|
|
} else if getSubQoS(sub) != expectedQoS {
|
|
t.Fatalf("Expected sub foo fwc QoS to be %v, got %v", expectedQoS, getSubQoS(sub))
|
|
}
|
|
// Make sure existing sub on "foo" qos was not changed.
|
|
if sub, ok := subc.subs["foo"]; !ok {
|
|
t.Fatal("Expected sub foo to be present but was not")
|
|
} else if getSubQoS(sub) != 0 {
|
|
t.Fatalf("Expected sub foo QoS to be 0, got %v", getSubQoS(sub))
|
|
}
|
|
}
|
|
checkWCSub(1)
|
|
|
|
// Sub again on same subject with lower QoS
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish and test msg received twice
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
checkWCSub(0)
|
|
}
|
|
|
|
func TestMQTTSubWithSpaces(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{user: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo bar", qos: 0}}, []byte{mqttSubAckFailure})
|
|
}
|
|
|
|
// Filters containing characters that would corrupt the NATS wire protocol
|
|
// (\t, \n, \r, \f) must result in a per-filter SUBACK failure, not a
|
|
// connection drop. mqttFilterToNATSSubject rejects them; the SUBSCRIBE
|
|
// parser logs and continues so the failure surfaces as mqttSubAckFailure.
|
|
func TestMQTTSubWithControlChars(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
filter string
|
|
}{
|
|
{"tab", "foo\tbar"},
|
|
{"line feed", "foo\nbar"},
|
|
{"carriage return", "foo\rbar"},
|
|
{"form feed", "foo\fbar"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Mix a bad filter with a good one to prove the good one still
|
|
// succeeds (the connection is not torn down by the bad filter).
|
|
testMQTTSub(t, 1, mc, r,
|
|
[]*mqttFilter{{filter: test.filter, qos: 0}, {filter: "good/topic", qos: 0}},
|
|
[]byte{mqttSubAckFailure, 0})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubCaseSensitive(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{user: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "Foo/Bar", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "Foo/Bar", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "Foo/Bar", 0, []byte("msg"))
|
|
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo/bar", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "Foo.Bar", []byte("nats"))
|
|
testMQTTCheckPubMsg(t, mc, r, "Foo/Bar", 0, []byte("nats"))
|
|
|
|
natsPub(t, nc, "foo.bar", []byte("nats"))
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTPubSubMatrix(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
natsPub bool
|
|
mqttPub bool
|
|
mqttPubQoS byte
|
|
natsSub bool
|
|
mqttSubQoS0 bool
|
|
mqttSubQoS1 bool
|
|
}{
|
|
{"NATS to MQTT sub QoS-0", true, false, 0, false, true, false},
|
|
{"NATS to MQTT sub QoS-1", true, false, 0, false, false, true},
|
|
{"NATS to MQTT sub QoS-0 and QoS-1", true, false, 0, false, true, true},
|
|
|
|
{"MQTT QoS-0 to NATS sub", false, true, 0, true, false, false},
|
|
{"MQTT QoS-0 to MQTT sub QoS-0", false, true, 0, false, true, false},
|
|
{"MQTT QoS-0 to MQTT sub QoS-1", false, true, 0, false, false, true},
|
|
{"MQTT QoS-0 to NATS sub and MQTT sub QoS-0", false, true, 0, true, true, false},
|
|
{"MQTT QoS-0 to NATS sub and MQTT sub QoS-1", false, true, 0, true, false, true},
|
|
{"MQTT QoS-0 to all subs", false, true, 0, true, true, true},
|
|
|
|
{"MQTT QoS-1 to NATS sub", false, true, 1, true, false, false},
|
|
{"MQTT QoS-1 to MQTT sub QoS-0", false, true, 1, false, true, false},
|
|
{"MQTT QoS-1 to MQTT sub QoS-1", false, true, 1, false, false, true},
|
|
{"MQTT QoS-1 to NATS sub and MQTT sub QoS-0", false, true, 1, true, true, false},
|
|
{"MQTT QoS-1 to NATS sub and MQTT sub QoS-1", false, true, 1, true, false, true},
|
|
{"MQTT QoS-1 to all subs", false, true, 1, true, true, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc1, r1 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc2, r2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// First setup subscriptions based on test options.
|
|
var ns *nats.Subscription
|
|
if test.natsSub {
|
|
ns = natsSubSync(t, nc, "foo")
|
|
}
|
|
if test.mqttSubQoS0 {
|
|
testMQTTSub(t, 1, mc1, r1, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc1, nil, r1)
|
|
}
|
|
if test.mqttSubQoS1 {
|
|
testMQTTSub(t, 1, mc2, r2, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, r2)
|
|
}
|
|
|
|
// Just as a barrier
|
|
natsFlush(t, nc)
|
|
|
|
// Now publish
|
|
if test.natsPub {
|
|
natsPub(t, nc, "foo", []byte("msg"))
|
|
} else {
|
|
testMQTTPublish(t, mc, r, test.mqttPubQoS, false, false, "foo", 1, []byte("msg"))
|
|
}
|
|
|
|
// Check message received
|
|
if test.natsSub {
|
|
natsNexMsg(t, ns, time.Second)
|
|
// Make sure no other is received
|
|
if msg, err := ns.NextMsg(50 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have gotten a second message, got %v", msg)
|
|
}
|
|
}
|
|
if test.mqttSubQoS0 {
|
|
testMQTTCheckPubMsg(t, mc1, r1, "foo", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r1)
|
|
}
|
|
if test.mqttSubQoS1 {
|
|
var expectedFlag byte
|
|
if test.mqttPubQoS > 0 {
|
|
expectedFlag = test.mqttPubQoS << 1
|
|
}
|
|
testMQTTCheckPubMsg(t, mc2, r2, "foo", expectedFlag, []byte("msg"))
|
|
testMQTTExpectNothing(t, r2)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTPreventSubWithMQTTSubPrefix(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r,
|
|
[]*mqttFilter{{filter: strings.ReplaceAll(mqttSubPrefix, ".", "/") + "foo/bar", qos: 1}},
|
|
[]byte{mqttSubAckFailure})
|
|
}
|
|
|
|
func TestMQTTSubWithNATSStream(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
mcp, rp := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTFlush(t, mcp, nil, rp)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
sc := &StreamConfig{
|
|
Name: "test",
|
|
Storage: FileStorage,
|
|
Retention: InterestPolicy,
|
|
Subjects: []string{"foo.>"},
|
|
}
|
|
mset, err := s.GlobalAccount().addStream(sc)
|
|
if err != nil {
|
|
t.Fatalf("Unable to create stream: %v", err)
|
|
}
|
|
|
|
sub := natsSubSync(t, nc, "bar")
|
|
cc := &ConsumerConfig{
|
|
Durable: "dur",
|
|
AckPolicy: AckExplicit,
|
|
DeliverSubject: "bar",
|
|
}
|
|
if _, err := mset.addConsumer(cc); err != nil {
|
|
t.Fatalf("Unable to add consumer: %v", err)
|
|
}
|
|
|
|
// Now send message from NATS
|
|
resp, err := nc.Request("foo.bar", []byte("nats"), time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error publishing: %v", err)
|
|
}
|
|
ar := &ApiResponse{}
|
|
if err := json.Unmarshal(resp.Data, ar); err != nil || ar.Error != nil {
|
|
t.Fatalf("Unexpected response: err=%v resp=%+v", err, ar.Error)
|
|
}
|
|
|
|
// Check that message is received by both
|
|
checkRecv := func(content string, flags byte) {
|
|
t.Helper()
|
|
if msg := natsNexMsg(t, sub, time.Second); string(msg.Data) != content {
|
|
t.Fatalf("Expected %q, got %q", content, msg.Data)
|
|
}
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", flags, []byte(content))
|
|
}
|
|
checkRecv("nats", 0)
|
|
|
|
// Send from MQTT as a QoS0
|
|
testMQTTPublish(t, mcp, rp, 0, false, false, "foo/bar", 0, []byte("qos0"))
|
|
checkRecv("qos0", 0)
|
|
|
|
// Send from MQTT as a QoS1
|
|
testMQTTPublish(t, mcp, rp, 1, false, false, "foo/bar", 1, []byte("qos1"))
|
|
checkRecv("qos1", mqttPubQos1)
|
|
}
|
|
|
|
func TestMQTTTrackPendingOverrun(t *testing.T) {
|
|
sess := mqttSession{}
|
|
|
|
sess.last_pi = 0xFFFF
|
|
pi := sess.trackPublishRetained()
|
|
if pi != 1 {
|
|
t.Fatalf("Expected 1, got %v", pi)
|
|
}
|
|
|
|
p := &mqttPending{}
|
|
for i := 1; i <= 0xFFFF; i++ {
|
|
sess.pendingPublish[uint16(i)] = p
|
|
}
|
|
pi, _ = sess.trackPublish("test", "test")
|
|
if pi != 0 {
|
|
t.Fatalf("Expected 0, got %v", pi)
|
|
}
|
|
|
|
delete(sess.pendingPublish, 1234)
|
|
pi = sess.trackPublishRetained()
|
|
if pi != 1234 {
|
|
t.Fatalf("Expected 1234, got %v", pi)
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubRestart(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Start an MQTT subscription QoS=1 on "foo"
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Now start a NATS subscription on ">" (anything that would match the JS consumer delivery subject)
|
|
natsSubSync(t, nc, ">")
|
|
natsFlush(t, nc)
|
|
|
|
// Restart the MQTT client
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
mc, r = testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Restart an MQTT subscription QoS=1 on "foo"
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
pc, pr := testMQTTConnect(t, &mqttConnInfo{clientID: "pub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer pc.Close()
|
|
testMQTTCheckConnAck(t, pr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Publish a message QoS1
|
|
testMQTTPublish(t, pc, pr, 1, false, false, "foo", 1, []byte("msg1"))
|
|
// Make sure we receive it
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
|
|
// Now "restart" the subscription but as a Qos0
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish a message QoS
|
|
testMQTTPublish(t, pc, pr, 1, false, false, "foo", 1, []byte("msg2"))
|
|
// Make sure we receive but as a QoS0
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg2"))
|
|
}
|
|
|
|
func testMQTTGetClusterTemplaceNoLeaf() string {
|
|
return strings.Replace(jsClusterTemplWithLeafAndMQTT, "{{leaf}}", "", 1)
|
|
}
|
|
|
|
func TestMQTTSubPropagation(t *testing.T) {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 2)
|
|
defer cl.shutdown()
|
|
|
|
o := cl.opts[0]
|
|
s2 := cl.servers[1]
|
|
nc := natsConnect(t, s2.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Because in MQTT foo/# means foo.> but also foo, check that this is propagated
|
|
checkSubInterest(t, s2, globalAccountName, "foo", time.Second)
|
|
|
|
// Publish on foo.bar, foo./ and foo and we should receive them
|
|
natsPub(t, nc, "foo.bar", []byte("hello"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", 0, []byte("hello"))
|
|
|
|
natsPub(t, nc, "foo./", []byte("from"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/", 0, []byte("from"))
|
|
|
|
natsPub(t, nc, "foo", []byte("NATS"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("NATS"))
|
|
}
|
|
|
|
func TestMQTTCluster(t *testing.T) {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 2)
|
|
defer cl.shutdown()
|
|
|
|
for _, topTest := range []struct {
|
|
name string
|
|
restart bool
|
|
}{
|
|
{"first_start", true},
|
|
{"restart", false},
|
|
} {
|
|
t.Run(topTest.name, func(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
subQos byte
|
|
}{
|
|
{"qos_0", 0},
|
|
{"qos_1", 1},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
clientID := nuid.Next()
|
|
|
|
o := cl.opts[0]
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{clientID: clientID, cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#", qos: test.subQos}}, []byte{test.subQos})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
check := func(mc net.Conn, r *mqttReader, o *Options, s *Server) {
|
|
t.Helper()
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo.bar", []byte("fromNats"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", 0, []byte("fromNats"))
|
|
|
|
mpc, pr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mpc.Close()
|
|
testMQTTCheckConnAck(t, pr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mpc, pr, 0, false, false, "foo/baz", 0, []byte("mqtt_qos0"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/baz", 0, []byte("mqtt_qos0"))
|
|
|
|
testMQTTPublish(t, mpc, pr, 1, false, false, "foo/bat", 1, []byte("mqtt_qos1"))
|
|
expectedQoS := byte(0)
|
|
if test.subQos == 1 {
|
|
expectedQoS = mqttPubQos1
|
|
}
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bat", expectedQoS, []byte("mqtt_qos1"))
|
|
testMQTTDisconnect(t, mpc, nil)
|
|
}
|
|
check(mc, r, cl.opts[0], cl.servers[0])
|
|
check(mc, r, cl.opts[1], cl.servers[1])
|
|
|
|
// Start the same subscription from the other server. It should disconnect
|
|
// the one connected in the first server.
|
|
o = cl.opts[1]
|
|
|
|
mc2, r2 := testMQTTConnect(t, &mqttConnInfo{clientID: clientID, cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Expect first connection to be closed.
|
|
testMQTTExpectDisconnect(t, mc)
|
|
|
|
// Now re-run the checks
|
|
check(mc2, r2, cl.opts[0], cl.servers[0])
|
|
check(mc2, r2, cl.opts[1], cl.servers[1])
|
|
|
|
// Disconnect our sub and restart with clean session then disconnect again to clear the state.
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
mc2.Close()
|
|
mc2, r2 = testMQTTConnect(t, &mqttConnInfo{clientID: clientID, cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTFlush(t, mc2, nil, r2)
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
|
|
// Remove the session from the flappers so we can restart the test
|
|
// without failure and have to wait for 1sec before being able to reconnect.
|
|
s := cl.servers[0]
|
|
sm := &s.mqtt.sessmgr
|
|
sm.mu.Lock()
|
|
asm := sm.sessions[globalAccountName]
|
|
sm.mu.Unlock()
|
|
if asm != nil {
|
|
asm.mu.Lock()
|
|
delete(asm.flappers, clientID)
|
|
asm.mu.Unlock()
|
|
}
|
|
})
|
|
}
|
|
if !t.Failed() && topTest.restart {
|
|
cl.stopAll()
|
|
cl.restartAll()
|
|
|
|
streams := []string{mqttStreamName, mqttRetainedMsgsStreamName, mqttSessStreamName}
|
|
for _, sn := range streams {
|
|
cl.waitOnStreamLeader(globalAccountName, sn)
|
|
}
|
|
|
|
mset, err := cl.randomServer().GlobalAccount().lookupStream(mqttRetainedMsgsStreamName)
|
|
if err != nil {
|
|
t.Fatalf("Expected to find a stream for %q", mqttRetainedMsgsStreamName)
|
|
}
|
|
|
|
rmConsumerNames := []string{}
|
|
mset.mu.RLock()
|
|
for _, o := range mset.consumers {
|
|
rmConsumerNames = append(rmConsumerNames, o.name)
|
|
}
|
|
mset.mu.RUnlock()
|
|
|
|
for _, consumerName := range rmConsumerNames {
|
|
cl.waitOnConsumerLeader(globalAccountName, mqttRetainedMsgsStreamName, consumerName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMQTTConnectDisconnect(t *testing.T, o *Options, clientID string, clean bool, found bool) {
|
|
t.Helper()
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: clientID, cleanSess: clean}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, found)
|
|
testMQTTDisconnectEx(t, mc, nil, false)
|
|
mc.Close()
|
|
}
|
|
|
|
func TestMQTTClusterConnectDisconnectClean(t *testing.T) {
|
|
// The purpose of this test was to illustrate and verify
|
|
// https://github.com/nats-io/nats-server/pull/4734. Due to its timing
|
|
// sensitivity it has never been 100% reliable, prone to flaking on an
|
|
// occasional MQTT Connect failure (API timeout?). Skip for now, to reduce
|
|
// the flaky noise.
|
|
t.Skip()
|
|
|
|
nServers := 3
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", nServers)
|
|
defer cl.shutdown()
|
|
|
|
clientID := nuid.Next()
|
|
|
|
// test runs a connect/disconnect against a random server in the cluster, as
|
|
// specified.
|
|
N := 100
|
|
for n := 0; n < N; n++ {
|
|
testMQTTConnectDisconnect(t, cl.opts[rand.Intn(nServers)], clientID, true, false)
|
|
}
|
|
}
|
|
|
|
func TestMQTTClusterConnectDisconnectPersist(t *testing.T) {
|
|
// The purpose of this test was to illustrate and verify
|
|
// https://github.com/nats-io/nats-server/pull/4734. Due to its timing
|
|
// sensitivity it has never been 100% reliable, prone to flaking on an
|
|
// occasional MQTT Connect failure (API timeout?). Skip for now, to reduce
|
|
// the flaky noise.
|
|
t.Skip()
|
|
|
|
nServers := 3
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", nServers)
|
|
defer cl.shutdown()
|
|
|
|
clientID := nuid.Next()
|
|
|
|
// test runs a connect/disconnect against a random server in the cluster, as
|
|
// specified.
|
|
N := 20
|
|
for n := 0; n < N; n++ {
|
|
// First clean sessions on all servers
|
|
for i := 0; i < nServers; i++ {
|
|
testMQTTConnectDisconnect(t, cl.opts[i], clientID, true, false)
|
|
}
|
|
|
|
testMQTTConnectDisconnect(t, cl.opts[0], clientID, false, false)
|
|
testMQTTConnectDisconnect(t, cl.opts[1], clientID, false, true)
|
|
testMQTTConnectDisconnect(t, cl.opts[2], clientID, false, true)
|
|
testMQTTConnectDisconnect(t, cl.opts[0], clientID, false, true)
|
|
}
|
|
}
|
|
|
|
func TestMQTTClusterRetainedMsg(t *testing.T) {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 2)
|
|
defer cl.shutdown()
|
|
|
|
srv1Opts := cl.opts[0]
|
|
srv2Opts := cl.opts[1]
|
|
|
|
// Connect subscription on server 1.
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, srv1Opts.MQTT.Host, srv1Opts.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
|
|
// Create a publisher from server 2.
|
|
mp, rp := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, srv2Opts.MQTT.Host, srv2Opts.MQTT.Port)
|
|
defer mp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Send retained message.
|
|
testMQTTPublish(t, mp, rp, 1, false, true, "foo/bar", 1, []byte("retained"))
|
|
// Check it is received.
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo/bar", mqttPubQos1, []byte("retained"))
|
|
|
|
// Start a new subscription on server 1 and make sure we receive the retained message
|
|
mc2, rc2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, srv1Opts.MQTT.Host, srv1Opts.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rc2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc2, rc2, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "foo/bar", mqttPubQos1|mqttPubFlagRetain, []byte("retained"))
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
mc2.Close()
|
|
|
|
// Send an empty retained message which should remove it from storage, but still be delivered.
|
|
testMQTTPublish(t, mp, rp, 1, false, true, "foo/bar", 1, []byte(""))
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo/bar", mqttPubQos1, []byte(""))
|
|
|
|
// Now shutdown the consumer connection
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
// Reconnect to server where the retained message was published (server 2)
|
|
mc, rc = testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, srv2Opts.MQTT.Host, srv2Opts.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
// The retained message should not be delivered.
|
|
testMQTTExpectNothing(t, rc)
|
|
// Now disconnect and reconnect back to first server
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
// Now reconnect to the server 1, which is not where the messages were published, and check
|
|
// that we don't receive the message.
|
|
mc, rc = testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, srv1Opts.MQTT.Host, srv1Opts.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, rc)
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
// Will now test network deletes
|
|
|
|
// Create a subscription on server 1 and server 2
|
|
mc, rc = testMQTTConnect(t, &mqttConnInfo{clientID: "sub_one", cleanSess: false}, srv1Opts.MQTT.Host, srv1Opts.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
|
|
mc2, rc2 = testMQTTConnect(t, &mqttConnInfo{clientID: "sub_two", cleanSess: false}, srv2Opts.MQTT.Host, srv2Opts.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rc2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc2, rc2, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, rc2)
|
|
|
|
// Publish 1 retained message (producer is connected to server 2)
|
|
testMQTTPublish(t, mp, rp, 1, false, true, "bar", 1, []byte("msg1"))
|
|
|
|
// Make sure messages are received by both
|
|
testMQTTCheckPubMsg(t, mc, rc, "bar", mqttPubQos1, []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "bar", mqttPubQos1, []byte("msg1"))
|
|
|
|
// Now send an empty retained message that should delete it. For the one on server 1,
|
|
// this will be a network delete.
|
|
testMQTTPublish(t, mp, rp, 1, false, true, "bar", 1, []byte(""))
|
|
testMQTTCheckPubMsg(t, mc, rc, "bar", mqttPubQos1, []byte(""))
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "bar", mqttPubQos1, []byte(""))
|
|
|
|
// Now send a new retained message
|
|
testMQTTPublish(t, mp, rp, 1, false, true, "bar", 1, []byte("msg2"))
|
|
|
|
// Again, verify that they all receive it.
|
|
testMQTTCheckPubMsg(t, mc, rc, "bar", mqttPubQos1, []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "bar", mqttPubQos1, []byte("msg2"))
|
|
|
|
// But now, restart the consumer that was in the server that processed the
|
|
// original network delete.
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
mc, rc = testMQTTConnect(t, &mqttConnInfo{clientID: "sub_one", cleanSess: false}, srv1Opts.MQTT.Host, srv1Opts.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
// Expect the message to be delivered as retained
|
|
testMQTTCheckPubMsg(t, mc, rc, "bar", mqttPubQos1|mqttPubFlagRetain, []byte("msg2"))
|
|
}
|
|
|
|
func TestMQTTRetainedMsgMigration(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
// Create the retained messages stream to listen on the old subject first.
|
|
// The server will correct this when the migration takes place.
|
|
_, err := js.AddStream(&nats.StreamConfig{
|
|
Name: mqttRetainedMsgsStreamName,
|
|
Subjects: []string{`$MQTT.rmsgs`},
|
|
Storage: nats.FileStorage,
|
|
Retention: nats.LimitsPolicy,
|
|
Replicas: 1,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
// Publish some retained messages on the old "$MQTT.rmsgs" subject.
|
|
const N = 100
|
|
for i := 0; i < N; i++ {
|
|
msg := fmt.Sprintf(
|
|
`{"origin":"b5IQZNtG","subject":"test%d","topic":"test%d","msg":"YmFy","flags":1}`, i, i,
|
|
)
|
|
_, err := js.Publish(`$MQTT.rmsgs`, []byte(msg))
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Check that the old subject looks right.
|
|
si, err := js.StreamInfo(mqttRetainedMsgsStreamName, &nats.StreamInfoRequest{
|
|
SubjectsFilter: `$MQTT.>`,
|
|
})
|
|
require_NoError(t, err)
|
|
if si.State.NumSubjects != 1 {
|
|
t.Fatalf("expected 1 subject, got %d", si.State.NumSubjects)
|
|
}
|
|
if n := si.State.Subjects[`$MQTT.rmsgs`]; n != N {
|
|
t.Fatalf("expected to find %d messages on the original subject but found %d", N, n)
|
|
}
|
|
|
|
// Create an MQTT client, this will cause a migration to take place.
|
|
mc, rc := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
as := testMQTTGetAccountSessionManager(t, s, "sub")
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
as.mu.RLock()
|
|
defer as.mu.RUnlock()
|
|
if n := as.retmsgs.Size(); n != N {
|
|
return fmt.Errorf("Got only %v retained messages", n)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "+", qos: 0}}, []byte{0})
|
|
topics := map[string]struct{}{}
|
|
for i := 0; i < N; i++ {
|
|
_, _, topic, _ := testMQTTGetPubMsgEx(t, mc, rc, _EMPTY_, []byte("bar"))
|
|
topics[topic] = struct{}{}
|
|
}
|
|
if len(topics) != N {
|
|
t.Fatalf("Unexpected topics: %v", topics)
|
|
}
|
|
|
|
// Now look at the stream, there should be N messages on the new
|
|
// divided subjects and none on the old undivided subject.
|
|
si, err = js.StreamInfo(mqttRetainedMsgsStreamName, &nats.StreamInfoRequest{
|
|
SubjectsFilter: `$MQTT.>`,
|
|
})
|
|
require_NoError(t, err)
|
|
if si.State.NumSubjects != N {
|
|
t.Fatalf("expected %d subjects, got %d", N, si.State.NumSubjects)
|
|
}
|
|
if n := si.State.Subjects[`$MQTT.rmsgs`]; n > 0 {
|
|
t.Fatalf("expected to find no messages on the original subject but found %d", n)
|
|
}
|
|
|
|
// Check that the message counts look right. There should be one
|
|
// retained message per key.
|
|
for i := 0; i < N; i++ {
|
|
expected := fmt.Sprintf(`$MQTT.rmsgs.test%d`, i)
|
|
n, ok := si.State.Subjects[expected]
|
|
if !ok {
|
|
t.Fatalf("expected to find %q but didn't", expected)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("expected %q to have 1 message but had %d", expected, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMQTTRetainedNoMsgBodyCorruption(t *testing.T) {
|
|
f := func() {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Send a retained message.
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: "pub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, c, r, 0, false, true, "foo/bar", 0, []byte("retained 1"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
checkRetained := func(msg string) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo/#", qos: 0}}, []byte{0})
|
|
testMQTTCheckPubMsg(t, c, r, "foo/bar", mqttPubFlagRetain, []byte(msg))
|
|
}
|
|
// Subscribe to make it load into the cache.
|
|
checkRetained("retained 1")
|
|
|
|
// Now send another one.
|
|
testMQTTPublish(t, c, r, 0, false, true, "foo/bar", 0, []byte("retained 2"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Check it is updated
|
|
checkRetained("retained 2")
|
|
|
|
// Now we will simulate an update coming from another server
|
|
// if we were in cluster mode.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
msg := nats.NewMsg("$MQTT.rmsgs.foo.bar")
|
|
msg.Header.Set(mqttNatsRetainedMessageOrigin, "XXXXXXXX")
|
|
msg.Header.Set(mqttNatsRetainedMessageTopic, "foo/bar")
|
|
msg.Header.Set(mqttNatsRetainedMessageFlags, "1")
|
|
msg.Data = []byte("retained 3")
|
|
nc.PublishMsg(msg)
|
|
natsFlush(t, nc)
|
|
|
|
// Have a continuous flow of updates coming in
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
ch := make(chan struct{})
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
nc.PublishMsg(msg)
|
|
select {
|
|
case <-ch:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Retrieve the account session manager using the "pub" client we have.
|
|
as := testMQTTGetAccountSessionManager(t, s, "pub")
|
|
as.mu.RLock()
|
|
cache := as.rmsCache
|
|
as.mu.RUnlock()
|
|
|
|
// Wait to make sure at least the first update occurs
|
|
checkFor(t, 5*time.Second, 10*time.Millisecond, func() error {
|
|
v, ok := cache.Load("foo.bar")
|
|
if !ok {
|
|
return errors.New("not in the cache")
|
|
}
|
|
rm := v.(*mqttRetainedMsg)
|
|
if !bytes.Equal(rm.Msg, []byte("retained 3")) {
|
|
return fmt.Errorf("Retained message not updated, got %q", rm.Msg)
|
|
}
|
|
return nil
|
|
})
|
|
// Repeat starting a subscription to check the retained message and
|
|
// make sure it is not corrupted. With the bug, the payload will at
|
|
// the very least contain trailing "\r\n" and possibly be corrupted
|
|
// (and the race detector would report a race).
|
|
for range 50 {
|
|
checkRetained("retained 3")
|
|
}
|
|
close(ch)
|
|
wg.Wait()
|
|
}
|
|
for range 5 {
|
|
f()
|
|
}
|
|
}
|
|
|
|
func TestMQTTClusterReplicasCount(t *testing.T) {
|
|
for _, test := range []struct {
|
|
size int
|
|
replicas int
|
|
}{
|
|
{1, 1},
|
|
{2, 2},
|
|
{3, 3},
|
|
{5, 3},
|
|
} {
|
|
t.Run(fmt.Sprintf("size %v", test.size), func(t *testing.T) {
|
|
var s *Server
|
|
var o *Options
|
|
if test.size == 1 {
|
|
o = testMQTTDefaultOptions()
|
|
s = testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
} else {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", test.size)
|
|
defer cl.shutdown()
|
|
o = cl.opts[0]
|
|
s = cl.randomServer()
|
|
}
|
|
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
// Check the replicas of all MQTT streams
|
|
js, err := nc.JetStream()
|
|
if err != nil {
|
|
t.Fatalf("Error getting js: %v", err)
|
|
}
|
|
for _, sname := range []string{
|
|
mqttStreamName,
|
|
mqttRetainedMsgsStreamName,
|
|
mqttSessStreamName,
|
|
} {
|
|
t.Run(sname, func(t *testing.T) {
|
|
si, err := js.StreamInfo(sname)
|
|
if err != nil {
|
|
t.Fatalf("Error getting stream info: %v", err)
|
|
}
|
|
if si.Config.Replicas != test.replicas {
|
|
t.Fatalf("Expected %v replicas, got %v", test.replicas, si.Config.Replicas)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTClusterCanCreateSessionWithOnServerDown(t *testing.T) {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 3)
|
|
defer cl.shutdown()
|
|
o := cl.opts[0]
|
|
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
mc.Close()
|
|
|
|
// Shutdown one of the server.
|
|
sd := cl.servers[1].StoreDir()
|
|
defer os.RemoveAll(strings.TrimSuffix(sd, JetStreamStoreDir))
|
|
cl.servers[1].Shutdown()
|
|
|
|
// Make sure there is a meta leader
|
|
cl.waitOnPeerCount(2)
|
|
cl.waitOnLeader()
|
|
|
|
// Now try to create a new session. Since we use a single stream now for all sessions,
|
|
// this should succeed.
|
|
o = cl.opts[2]
|
|
// We may still get failures because of some JS APIs may timeout while things
|
|
// settle, so try again for a certain amount of times.
|
|
mc, rc = testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
}
|
|
|
|
func TestMQTTClusterPlacement(t *testing.T) {
|
|
sc := createJetStreamSuperCluster(t, 3, 2)
|
|
defer sc.shutdown()
|
|
|
|
c := sc.randomCluster()
|
|
lnc := c.createLeafNodesWithTemplateAndStartPort(jsClusterTemplWithLeafAndMQTT, "SPOKE", 3, 22111)
|
|
defer lnc.shutdown()
|
|
|
|
sc.waitOnPeerCount(9)
|
|
sc.waitOnLeader()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, lnc.opts[i%3].MQTT.Host, lnc.opts[i%3].MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
}
|
|
|
|
// Now check that MQTT assets have been created in the LEAF node's side, not the Hub.
|
|
nc := natsConnect(t, lnc.servers[0].ClientURL())
|
|
defer nc.Close()
|
|
js, err := nc.JetStream()
|
|
if err != nil {
|
|
t.Fatalf("Unable to get JetStream: %v", err)
|
|
}
|
|
count := 0
|
|
for si := range js.StreamsInfo() {
|
|
if si.Cluster == nil || si.Cluster.Name != "SPOKE" {
|
|
t.Fatalf("Expected asset %q to be placed on spoke cluster, was placed on %+v", si.Config.Name, si.Cluster)
|
|
}
|
|
for _, repl := range si.Cluster.Replicas {
|
|
if !strings.HasPrefix(repl.Name, "SPOKE-") {
|
|
t.Fatalf("Replica on the wrong cluster: %+v", repl)
|
|
}
|
|
}
|
|
if si.State.Consumers > 0 {
|
|
for ci := range js.ConsumersInfo(si.Config.Name) {
|
|
if ci.Cluster == nil || ci.Cluster.Name != "SPOKE" {
|
|
t.Fatalf("Expected asset %q to be placed on spoke cluster, was placed on %+v", ci.Name, si.Cluster)
|
|
}
|
|
for _, repl := range ci.Cluster.Replicas {
|
|
if !strings.HasPrefix(repl.Name, "SPOKE-") {
|
|
t.Fatalf("Replica on the wrong cluster: %+v", repl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
count++
|
|
}
|
|
if count == 0 {
|
|
t.Fatal("No stream found!")
|
|
}
|
|
}
|
|
|
|
func TestMQTTLeafnodeWithoutJSToClusterWithJSNoSharedSysAcc(t *testing.T) {
|
|
test := func(t *testing.T, resolution int) {
|
|
getClusterOpts := func(name string, i int) *Options {
|
|
o := testMQTTDefaultOptions()
|
|
o.ServerName = name
|
|
o.Cluster.Name = "hub"
|
|
// first two test cases rely on domain not being set in hub
|
|
if resolution > 1 {
|
|
o.JetStreamDomain = "DOMAIN"
|
|
}
|
|
o.Cluster.Host = "127.0.0.1"
|
|
o.Cluster.Port = 2790 + i
|
|
o.Routes = RoutesFromStr("nats://127.0.0.1:2791,nats://127.0.0.1:2792,nats://127.0.0.1:2793")
|
|
o.LeafNode.Host = "127.0.0.1"
|
|
o.LeafNode.Port = -1
|
|
return o
|
|
}
|
|
o1 := getClusterOpts("S1", 1)
|
|
s1 := testMQTTRunServer(t, o1)
|
|
defer testMQTTShutdownServer(s1)
|
|
|
|
o2 := getClusterOpts("S2", 2)
|
|
s2 := testMQTTRunServer(t, o2)
|
|
defer testMQTTShutdownServer(s2)
|
|
|
|
o3 := getClusterOpts("S3", 3)
|
|
s3 := testMQTTRunServer(t, o3)
|
|
defer testMQTTShutdownServer(s3)
|
|
|
|
cluster := []*Server{s1, s2, s3}
|
|
checkClusterFormed(t, cluster...)
|
|
checkFor(t, 10*time.Second, 50*time.Millisecond, func() error {
|
|
for _, s := range cluster {
|
|
if s.JetStreamIsLeader() {
|
|
// Need to wait for usage updates now to propagate to meta leader.
|
|
if len(s.JetStreamClusterPeers()) == len(cluster) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("no leader yet")
|
|
})
|
|
|
|
// Now define a leafnode that has mqtt enabled, but no JS. This should still work.
|
|
lno := testMQTTDefaultOptions()
|
|
// Make sure jetstream is not explicitly defined here.
|
|
lno.JetStream = false
|
|
switch resolution {
|
|
case 0:
|
|
// turn off jetstream in $G by adding another account and set mqtt domain option and set account default
|
|
lno.Accounts = append(lno.Accounts, NewAccount("unused-account"))
|
|
fallthrough
|
|
case 1:
|
|
lno.JsAccDefaultDomain = map[string]string{"$G": ""}
|
|
case 2:
|
|
lno.JsAccDefaultDomain = map[string]string{"$G": o1.JetStreamDomain}
|
|
case 3:
|
|
// turn off jetstream in $G by adding another account and set mqtt domain option
|
|
lno.Accounts = append(lno.Accounts, NewAccount("unused-account"))
|
|
fallthrough
|
|
case 4:
|
|
// actual solution
|
|
lno.MQTT.JsDomain = o1.JetStreamDomain
|
|
case 5:
|
|
// set per account overwrite and disable js in $G
|
|
lno.Accounts = append(lno.Accounts, NewAccount("unused-account"))
|
|
lno.JsAccDefaultDomain = map[string]string{
|
|
"$G": o1.JetStreamDomain,
|
|
}
|
|
}
|
|
// Whenever an account was added to disable JS in $G, enable it in the server
|
|
if len(lno.Accounts) > 0 {
|
|
lno.JetStream = true
|
|
lno.JetStreamDomain = "OTHER"
|
|
lno.StoreDir = t.TempDir()
|
|
}
|
|
|
|
// Use RoutesFromStr() to make an array of urls
|
|
urls := RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d,nats://127.0.0.1:%d,nats://127.0.0.1:%d",
|
|
o1.LeafNode.Port, o2.LeafNode.Port, o3.LeafNode.Port))
|
|
lno.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: urls}}
|
|
|
|
ln := RunServer(lno)
|
|
defer testMQTTShutdownServer(ln)
|
|
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Now connect to leafnode and subscribe
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: true}, lno.MQTT.Host, lno.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
|
|
connectAndPublish := func(o *Options) {
|
|
mp, rp := testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mp, rp, 1, false, false, "foo", 1, []byte("msg"))
|
|
}
|
|
// Connect a publisher from leafnode and publish, verify message is received.
|
|
connectAndPublish(lno)
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo", mqttPubQos1, []byte("msg"))
|
|
|
|
// Connect from one server in the cluster check it works from there too.
|
|
connectAndPublish(o3)
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo", mqttPubQos1, []byte("msg"))
|
|
|
|
// Connect from a server in the hub and subscribe
|
|
mc2, rc2 := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub2", cleanSess: true}, o2.MQTT.Host, o2.MQTT.Port, 5)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rc2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc2, rc2, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, rc2)
|
|
|
|
// Connect a publisher from leafnode and publish, verify message is received.
|
|
connectAndPublish(lno)
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "foo", mqttPubQos1, []byte("msg"))
|
|
|
|
// Connect from one server in the cluster check it works from there too.
|
|
connectAndPublish(o1)
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "foo", mqttPubQos1, []byte("msg"))
|
|
}
|
|
t.Run("backwards-compatibility-default-js-enabled-in-leaf", func(t *testing.T) {
|
|
test(t, 0) // test with JsAccDefaultDomain set to default (pointing at hub) but jetstream enabled in leaf node too
|
|
})
|
|
t.Run("backwards-compatibility-default-js-disabled-in-leaf", func(t *testing.T) {
|
|
// test with JsAccDefaultDomain set. Checks if it works with backwards compatibility code for empty domain
|
|
test(t, 1)
|
|
})
|
|
t.Run("backwards-compatibility-domain-js-disabled-in-leaf", func(t *testing.T) {
|
|
// test with JsAccDefaultDomain set. Checks if it works with backwards compatibility code for domain set
|
|
test(t, 2) // test with domain set in mqtt client
|
|
})
|
|
t.Run("mqtt-explicit-js-enabled-in-leaf", func(t *testing.T) {
|
|
test(t, 3) // test with domain set in mqtt client (pointing at hub) but jetstream enabled in leaf node too
|
|
})
|
|
t.Run("mqtt-explicit-js-disabled-in-leaf", func(t *testing.T) {
|
|
test(t, 4) // test with domain set in mqtt client
|
|
})
|
|
t.Run("backwards-compatibility-domain-js-enabled-in-leaf", func(t *testing.T) {
|
|
test(t, 5) // test with JsAccDefaultDomain set to domain (pointing at hub) but jetstream enabled in leaf node too
|
|
})
|
|
}
|
|
|
|
func TestMQTTImportExport(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
accounts {
|
|
org {
|
|
jetstream: enabled
|
|
users: [{user: org, password: pwd}]
|
|
imports = [{stream: {account: "device", subject: "foo"}, prefix: "org"}]
|
|
}
|
|
device {
|
|
users: [{user: device, password: pwd}]
|
|
exports = [{stream: "foo"}]
|
|
}
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
no_auth_user: device
|
|
`, t.TempDir())))
|
|
defer os.Remove(conf)
|
|
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
mc1, rc1 := testMQTTConnect(t, &mqttConnInfo{clientID: "sub1", user: "org", pass: "pwd", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, rc1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc1, rc1, []*mqttFilter{{filter: "org/foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc1, nil, rc1)
|
|
|
|
mc2, rc2 := testMQTTConnect(t, &mqttConnInfo{clientID: "sub2", user: "org", pass: "pwd", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rc2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc2, rc2, []*mqttFilter{{filter: "org/foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, rc2)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
natsPub(t, nc, "foo", []byte("msg"))
|
|
|
|
// Verify message is received on receiver side.
|
|
testMQTTCheckPubMsg(t, mc1, rc1, "org/foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc2, rc2, "org/foo", 0, []byte("msg"))
|
|
}
|
|
|
|
func TestMQTTSessionMovingDomains(t *testing.T) {
|
|
tmpl := strings.Replace(jsClusterTemplWithLeafAndMQTT, "{{leaf}}", `leafnodes { listen: 127.0.0.1:-1 }`, 1)
|
|
tmpl = strings.Replace(tmpl, "store_dir:", "domain: HUB, store_dir:", 1)
|
|
c := createJetStreamCluster(t, tmpl, "HUB", _EMPTY_, 3, 22020, true)
|
|
defer c.shutdown()
|
|
c.waitOnLeader()
|
|
|
|
tmpl = strings.Replace(jsClusterTemplWithLeafAndMQTT, "store_dir:", "domain: SPOKE, store_dir:", 1)
|
|
lnc := c.createLeafNodesWithTemplateAndStartPort(tmpl, "SPOKE", 3, 22111)
|
|
defer lnc.shutdown()
|
|
lnc.waitOnPeerCount(3)
|
|
|
|
connectSubAndDisconnect := func(host string, port int, present bool) {
|
|
t.Helper()
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, host, port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, present)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
testMQTTDisconnect(t, mc, nil)
|
|
}
|
|
|
|
// Create a session on the HUB. Make sure we don't use "clean" session so that
|
|
// it is not removed when the client connection closes.
|
|
for i := 0; i < 7; i++ {
|
|
var present bool
|
|
if i > 0 {
|
|
present = true
|
|
}
|
|
connectSubAndDisconnect(c.opts[0].MQTT.Host, c.opts[0].MQTT.Port, present)
|
|
}
|
|
|
|
// Now move to the SPOKE cluster, this is a brand new session there, so should not be present.
|
|
connectSubAndDisconnect(lnc.opts[1].MQTT.Host, lnc.opts[1].MQTT.Port, false)
|
|
|
|
// Move back to HUB cluster. Make it interesting by connecting to a different
|
|
// server in that cluster. This should work, and present flag should be true.
|
|
connectSubAndDisconnect(c.opts[2].MQTT.Host, c.opts[2].MQTT.Port, true)
|
|
}
|
|
|
|
type remoteConnSameClientIDLogger struct {
|
|
DummyLogger
|
|
warn chan string
|
|
}
|
|
|
|
func (l *remoteConnSameClientIDLogger) Warnf(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "remote connection has started with the same client ID") {
|
|
l.warn <- msg
|
|
}
|
|
}
|
|
|
|
func TestMQTTSessionsDifferentDomains(t *testing.T) {
|
|
tmpl := strings.Replace(jsClusterTemplWithLeafAndMQTT, "{{leaf}}", `leafnodes { listen: 127.0.0.1:-1 }`, 1)
|
|
c := createJetStreamCluster(t, tmpl, "HUB", _EMPTY_, 3, 22020, true)
|
|
defer c.shutdown()
|
|
c.waitOnLeader()
|
|
|
|
tmpl = strings.Replace(jsClusterTemplWithLeafAndMQTT, "store_dir:", "domain: LEAF1, store_dir:", 1)
|
|
lnc1 := c.createLeafNodesWithTemplateAndStartPort(tmpl, "LEAF1", 2, 22111)
|
|
defer lnc1.shutdown()
|
|
lnc1.waitOnPeerCount(2)
|
|
|
|
l := &remoteConnSameClientIDLogger{warn: make(chan string, 10)}
|
|
lnc1.servers[0].SetLogger(l, false, false)
|
|
|
|
tmpl = strings.Replace(jsClusterTemplWithLeafAndMQTT, "store_dir:", "domain: LEAF2, store_dir:", 1)
|
|
lnc2 := c.createLeafNodesWithTemplateAndStartPort(tmpl, "LEAF2", 2, 23111)
|
|
defer lnc2.shutdown()
|
|
lnc2.waitOnPeerCount(2)
|
|
|
|
o := &(lnc1.opts[0].MQTT)
|
|
mc1, rc1 := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.Host, o.Port, 5)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, rc1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc1, rc1, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc1, nil, rc1)
|
|
|
|
o = &(lnc2.opts[0].MQTT)
|
|
connectSubAndDisconnect := func(host string, port int, present bool) {
|
|
t.Helper()
|
|
mc, rc := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, host, port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, present)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
testMQTTDisconnect(t, mc, nil)
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
connectSubAndDisconnect(o.Host, o.Port, i > 0)
|
|
}
|
|
|
|
select {
|
|
case w := <-l.warn:
|
|
t.Fatalf("Got a warning: %v", w)
|
|
case <-time.After(500 * time.Millisecond):
|
|
// OK
|
|
}
|
|
}
|
|
|
|
func TestMQTTParseUnsub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
proto []byte
|
|
b byte
|
|
pl int
|
|
err string
|
|
wantErr bool
|
|
}{
|
|
{"reserved flag", nil, 3, 0, "wrong unsubscribe reserved flags", false},
|
|
{"ensure packet loaded", []byte{1, 2}, mqttUnsubscribeFlags, 10, io.ErrUnexpectedEOF.Error(), false},
|
|
{"error reading packet id", []byte{1}, mqttUnsubscribeFlags, 1, "reading packet identifier", false},
|
|
{"packet id cannot be zero", []byte{0, 0}, mqttUnsubscribeFlags, 2, errMQTTPacketIdentifierIsZero.Error(), false},
|
|
{"missing filters", []byte{0, 1}, mqttUnsubscribeFlags, 2, "subscribe protocol must contain at least 1 topic filter", false},
|
|
{"error reading topic", []byte{0, 1, 0, 2, 'a'}, mqttUnsubscribeFlags, 5, "topic filter", false},
|
|
{"empty topic", []byte{0, 1, 0, 0}, mqttUnsubscribeFlags, 4, errMQTTTopicFilterCannotBeEmpty.Error(), false},
|
|
{"invalid utf8 topic", []byte{0, 1, 0, 1, 241}, mqttUnsubscribeFlags, 5, "invalid utf8 for topic filter", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
_, _, err := c.mqttParseSubsOrUnsubs(r, test.b, test.pl, false)
|
|
if test.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected an error, got none")
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMQTTUnsub(t *testing.T, pi uint16, c net.Conn, r *mqttReader, filters []*mqttFilter) {
|
|
t.Helper()
|
|
w := newMQTTWriter(0)
|
|
pkLen := 2 // for pi
|
|
for i := 0; i < len(filters); i++ {
|
|
f := filters[i]
|
|
pkLen += 2 + len(f.filter)
|
|
}
|
|
w.WriteByte(mqttPacketUnsub | mqttUnsubscribeFlags)
|
|
w.WriteVarInt(pkLen)
|
|
w.WriteUint16(pi)
|
|
for i := 0; i < len(filters); i++ {
|
|
f := filters[i]
|
|
w.WriteBytes([]byte(f.filter))
|
|
}
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing UNSUBSCRIBE protocol: %v", err)
|
|
}
|
|
b, _ := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketUnsubAck {
|
|
t.Fatalf("Expected UNSUBACK packet %x, got %x", mqttPacketUnsubAck, pt)
|
|
}
|
|
rpi, err := r.readUint16("packet identifier")
|
|
if err != nil || rpi != pi {
|
|
t.Fatalf("Error with packet identifier expected=%v got: %v err=%v", pi, rpi, err)
|
|
}
|
|
}
|
|
|
|
func TestMQTTUnsub(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mcp, mpr := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, mpr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{user: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish and test msg received
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo", 0, []byte("msg"))
|
|
|
|
// Unsubscribe
|
|
testMQTTUnsub(t, 1, mc, r, []*mqttFilter{{filter: "foo"}})
|
|
|
|
// Publish and test msg not received
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Use of wildcards subs
|
|
filters := []*mqttFilter{
|
|
{filter: "foo/bar", qos: 0},
|
|
{filter: "foo/#", qos: 0},
|
|
}
|
|
testMQTTSub(t, 1, mc, r, filters, []byte{0, 0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Publish and check that message received twice
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo/bar", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", 0, []byte("msg"))
|
|
|
|
// Unsub the wildcard one
|
|
testMQTTUnsub(t, 1, mc, r, []*mqttFilter{{filter: "foo/#"}})
|
|
// Publish and check that message received once
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo/bar", 0, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/bar", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Unsub last
|
|
testMQTTUnsub(t, 1, mc, r, []*mqttFilter{{filter: "foo/bar"}})
|
|
// Publish and test msg not received
|
|
testMQTTPublish(t, mcp, r, 0, false, false, "foo/bar", 0, []byte("msg"))
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func testMQTTExpectDisconnect(t testing.TB, c net.Conn) {
|
|
t.Helper()
|
|
buf, err := testMQTTRead(c)
|
|
if err == nil {
|
|
t.Fatalf("Expected connection to be disconnected, got %s", buf)
|
|
}
|
|
// Distinguish real disconnection (EOF, connection reset) from timeout
|
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
|
t.Fatal("Expected a disconnect but got a timeout error")
|
|
}
|
|
}
|
|
|
|
func TestMQTTPublishTopicErrors(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
topic string
|
|
}{
|
|
{"empty", ""},
|
|
{"with single level wildcard", "foo/+"},
|
|
{"with multiple level wildcard", "foo/#"},
|
|
{"with tab", "foo\tbar"},
|
|
{"with line feed", "foo\nbar"},
|
|
{"with carriage return", "foo\rbar"},
|
|
{"with form feed", "foo\fbar"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, mc, r, 0, false, false, test.topic, 0, []byte("msg"))
|
|
testMQTTExpectDisconnect(t, mc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMQTTDisconnect(t testing.TB, c net.Conn, bw *bufio.Writer) {
|
|
t.Helper()
|
|
testMQTTDisconnectEx(t, c, bw, true)
|
|
}
|
|
|
|
func testMQTTDisconnectEx(t testing.TB, c net.Conn, bw *bufio.Writer, wait bool) {
|
|
t.Helper()
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(mqttPacketDisconnect)
|
|
w.WriteByte(0)
|
|
if bw != nil {
|
|
bw.Write(w.Bytes())
|
|
bw.Flush()
|
|
} else {
|
|
c.Write(w.Bytes())
|
|
}
|
|
if wait {
|
|
testMQTTExpectDisconnect(t, c)
|
|
}
|
|
}
|
|
|
|
func TestMQTTWill(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
sub := natsSubSync(t, nc, "will.topic")
|
|
natsFlush(t, nc)
|
|
|
|
willMsg := []byte("bye")
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
willExpected bool
|
|
willQoS byte
|
|
}{
|
|
{"will qos 0", true, 0},
|
|
{"will qos 1", true, 1},
|
|
{"will qos 2", true, 2},
|
|
{"proper disconnect no will", false, 0},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mcs, rs := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "will/#", qos: 2}}, []byte{2})
|
|
testMQTTFlush(t, mcs, nil, rs)
|
|
|
|
mc, r := testMQTTConnect(t,
|
|
&mqttConnInfo{
|
|
cleanSess: true,
|
|
will: &mqttWill{
|
|
topic: []byte("will/topic"),
|
|
message: willMsg,
|
|
qos: test.willQoS,
|
|
},
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
if test.willExpected {
|
|
mc.Close()
|
|
testMQTTCheckPubMsg(t, mcs, rs, "will/topic", test.willQoS<<1, willMsg)
|
|
wm := natsNexMsg(t, sub, time.Second)
|
|
if !bytes.Equal(wm.Data, willMsg) {
|
|
t.Fatalf("Expected will message to be %q, got %q", willMsg, wm.Data)
|
|
}
|
|
} else {
|
|
testMQTTDisconnect(t, mc, nil)
|
|
testMQTTExpectNothing(t, rs)
|
|
if wm, err := sub.NextMsg(100 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have receive a message, got subj=%q data=%q",
|
|
wm.Subject, wm.Data)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTQoS2WillReject(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.rejectQoS2Pub = true
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mcs, rs := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "will/#", qos: 2}}, []byte{2})
|
|
testMQTTFlush(t, mcs, nil, rs)
|
|
|
|
mc, r := testMQTTConnect(t,
|
|
&mqttConnInfo{
|
|
cleanSess: true,
|
|
will: &mqttWill{
|
|
topic: []byte("will/topic"),
|
|
message: []byte("bye"),
|
|
qos: 2,
|
|
},
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCQoS2WillRejected, false)
|
|
}
|
|
|
|
func TestMQTTWillRetain(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
pubQoS byte
|
|
subQoS byte
|
|
}{
|
|
{"pub QoS0 sub QoS0", 0, 0},
|
|
{"pub QoS0 sub QoS1", 0, 1},
|
|
{"pub QoS1 sub QoS0", 1, 0},
|
|
{"pub QoS1 sub QoS1", 1, 1},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mces, res := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mces.Close()
|
|
testMQTTCheckConnAck(t, res, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mces, res, []*mqttFilter{{filter: "will/#", qos: test.subQoS}}, []byte{test.subQoS})
|
|
|
|
willTopic := []byte("will/topic")
|
|
willMsg := []byte("bye")
|
|
|
|
mc, r := testMQTTConnect(t,
|
|
&mqttConnInfo{
|
|
cleanSess: true,
|
|
will: &mqttWill{
|
|
topic: willTopic,
|
|
message: willMsg,
|
|
qos: test.pubQoS,
|
|
retain: true,
|
|
},
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Disconnect the client
|
|
mc.Close()
|
|
|
|
// Wait for the server to process the connection close, which will
|
|
// cause the "will" message to be published (and retained).
|
|
checkClientsCount(t, s, 1)
|
|
|
|
// Create subscription on will topic and expect will message.
|
|
mcs, rs := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "will/#", qos: test.subQoS}}, []byte{test.subQoS})
|
|
pflags, _ := testMQTTGetPubMsg(t, mcs, rs, "will/topic", willMsg)
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("expected retain flag to be set, it was not: %v", pflags)
|
|
}
|
|
// Expected QoS will be the lesser of the pub/sub QoS.
|
|
expectedQoS := test.pubQoS
|
|
if test.subQoS == 0 {
|
|
expectedQoS = 0
|
|
}
|
|
if qos := mqttGetQoS(pflags); qos != expectedQoS {
|
|
t.Fatalf("expected qos to be %v, got %v", expectedQoS, qos)
|
|
}
|
|
|
|
// The existing subscription (prior to sending the will) should receive
|
|
// the will but the retain flag should not be set.
|
|
pflags, _ = testMQTTGetPubMsg(t, mces, res, "will/topic", willMsg)
|
|
if mqttIsRetained(pflags) {
|
|
t.Fatalf("expected retain flag to not be set, it was: %v", pflags)
|
|
}
|
|
// Expected QoS will be the lesser of the pub/sub QoS.
|
|
if qos := mqttGetQoS(pflags); qos != expectedQoS {
|
|
t.Fatalf("expected qos to be %v, got %v", expectedQoS, qos)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTWillRetainPermViolation(t *testing.T) {
|
|
template := `
|
|
port: -1
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
server_name: mqtt
|
|
authorization {
|
|
mqtt_perms = {
|
|
publish = ["%s"]
|
|
subscribe = ["foo", "bar"]
|
|
}
|
|
users = [
|
|
{user: mqtt, password: pass, permissions: $mqtt_perms}
|
|
]
|
|
}
|
|
mqtt {
|
|
port: -1
|
|
}
|
|
`
|
|
tdir := t.TempDir()
|
|
conf := createConfFile(t, fmt.Appendf(nil, template, tdir, "foo"))
|
|
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "mqtt",
|
|
pass: "pass",
|
|
}
|
|
|
|
// We create first a connection with the Will topic that the publisher
|
|
// is allowed to publish to.
|
|
ci.will = &mqttWill{
|
|
topic: []byte("foo"),
|
|
message: []byte("bye"),
|
|
qos: 1,
|
|
retain: true,
|
|
}
|
|
mc, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Disconnect, which will cause the Will to be sent with retain flag.
|
|
mc.Close()
|
|
|
|
// Wait for the server to process the connection close, which will
|
|
// cause the "will" message to be published (and retained).
|
|
checkClientsCount(t, s, 0)
|
|
|
|
// Create a subscription on the Will subject and we should receive it.
|
|
ci.will = nil
|
|
mcs, rs := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
pflags, _ := testMQTTGetPubMsg(t, mcs, rs, "foo", []byte("bye"))
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("expected retain flag to be set, it was not: %v", pflags)
|
|
}
|
|
if qos := mqttGetQoS(pflags); qos != 1 {
|
|
t.Fatalf("expected qos to be 1, got %v", qos)
|
|
}
|
|
testMQTTDisconnect(t, mcs, nil)
|
|
mcs.Close()
|
|
|
|
// Now create another connection with a Will that client is not allowed to publish to.
|
|
ci.will = &mqttWill{
|
|
topic: []byte("bar"),
|
|
message: []byte("bye"),
|
|
qos: 1,
|
|
retain: true,
|
|
}
|
|
mc, r = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Disconnect, to cause Will to be produced, but in that case should not be stored
|
|
// since user not allowed to publish on "bar".
|
|
mc.Close()
|
|
|
|
// Wait for the server to process the connection close, which will
|
|
// cause the "will" message to be published (and retained).
|
|
checkClientsCount(t, s, 0)
|
|
|
|
// Create sub on "bar" which user is allowed to subscribe to.
|
|
ci.will = nil
|
|
mcs, rs = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
// No Will should be published since it should not have been stored in the first place.
|
|
testMQTTExpectNothing(t, rs)
|
|
testMQTTDisconnect(t, mcs, nil)
|
|
mcs.Close()
|
|
|
|
// Now remove permission to publish on "foo" and check that a new subscription
|
|
// on "foo" is now not getting the will message because the original user no
|
|
// longer has permission to do so.
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(template, tdir, "baz"))
|
|
|
|
mcs, rs = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcs.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mcs, rs, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, rs)
|
|
testMQTTDisconnect(t, mcs, nil)
|
|
}
|
|
|
|
func TestMQTTPublishRetain(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
buf := strings.Builder{}
|
|
for i := 0; i < 128; i++ { // 128Kb
|
|
for j := 0; j < 64; j++ { // 16 * 64 = 1Kb
|
|
buf.Write([]byte("0123456789abcdef"))
|
|
}
|
|
}
|
|
large := buf.String()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
retained bool
|
|
sentValue string
|
|
expectedValue string
|
|
subGetsIt bool
|
|
}{
|
|
{"publish large retained", true, large, large, true},
|
|
{"publish retained", true, "retained", "retained", true},
|
|
{"publish not retained", false, "not retained", "retained", true},
|
|
{"remove retained", true, "", "", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mc1, rs1 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, rs1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc1, rs1, 0, false, test.retained, "foo", 0, []byte(test.sentValue))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
|
|
mc2, rs2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rs2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "foo/#", qos: 1}}, []byte{1})
|
|
|
|
if test.subGetsIt {
|
|
pflags, _ := testMQTTGetPubMsg(t, mc2, rs2, "foo", []byte(test.expectedValue))
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should have been set, it was not: flags=%v", pflags)
|
|
}
|
|
} else {
|
|
testMQTTExpectNothing(t, rs2)
|
|
}
|
|
|
|
testMQTTDisconnect(t, mc1, nil)
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTQoS2RetainedReject(t *testing.T) {
|
|
// Start the server with QOS2 enabled, and submit retained messages with QoS
|
|
// 1 and 2.
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
mc1, rs1 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, rs1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc1, rs1, 2, false, true, "foo", 1, []byte("qos2 failed"))
|
|
testMQTTPublish(t, mc1, rs1, 1, false, true, "bar", 2, []byte("qos1 retained"))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
testMQTTDisconnect(t, mc1, nil)
|
|
mc1.Close()
|
|
s.Shutdown()
|
|
|
|
// Restart the server with QOS2 disabled; we should be using the same
|
|
// JetStream store, so the retained message should still be there.
|
|
o.MQTT.rejectQoS2Pub = true
|
|
s = testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc2, rs2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rs2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "bar/#", qos: 2}}, []byte{2})
|
|
pflags, _ := testMQTTGetPubMsg(t, mc2, rs2, "bar", []byte("qos1 retained"))
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should have been set, it was not: flags=%v", pflags)
|
|
}
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "foo/#", qos: 2}}, []byte{2})
|
|
testMQTTExpectNothing(t, rs2)
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
}
|
|
|
|
func TestMQTTRetainFlag(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc1, rs1 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, rs1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc1, rs1, 0, false, true, "foo/0", 0, []byte("flag set"))
|
|
testMQTTPublish(t, mc1, rs1, 0, false, true, "foo/1", 0, []byte("flag set"))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
|
|
mc2, rs2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rs2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "foo/0", qos: 0}}, []byte{0})
|
|
pflags, _ := testMQTTGetPubMsg(t, mc2, rs2, "foo/0", []byte("flag set"))
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should have been set, it was not: flags=%v", pflags)
|
|
}
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "foo/1", qos: 1}}, []byte{1})
|
|
pflags, _ = testMQTTGetPubMsg(t, mc2, rs2, "foo/1", []byte("flag set"))
|
|
if !mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should have been set, it was not: flags=%v", pflags)
|
|
}
|
|
|
|
// For existing subscriptions, RETAIN flag should not be set: [MQTT-3.3.1-9].
|
|
testMQTTPublish(t, mc1, rs1, 0, false, true, "foo/0", 0, []byte("flag not set"))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
|
|
pflags, _ = testMQTTGetPubMsg(t, mc2, rs2, "foo/0", []byte("flag not set"))
|
|
if mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should not have been set, it was: flags=%v", pflags)
|
|
}
|
|
|
|
testMQTTPublish(t, mc1, rs1, 0, false, true, "foo/1", 0, []byte("flag not set"))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
|
|
pflags, _ = testMQTTGetPubMsg(t, mc2, rs2, "foo/1", []byte("flag not set"))
|
|
if mqttIsRetained(pflags) {
|
|
t.Fatalf("retain flag should not have been set, it was: flags=%v", pflags)
|
|
}
|
|
}
|
|
|
|
func TestMQTTPublishRetainPermViolation(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{
|
|
{
|
|
Username: "mqtt1",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{"bar"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "mqtt2",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo", "bar"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{">"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "mqtt3",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo.bar", "baz", "barbaz"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{">"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "mqtt4",
|
|
Password: "pass",
|
|
},
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
var asm *mqttAccountSessionManager
|
|
|
|
pubRetained := func(user, subject string) {
|
|
t.Helper()
|
|
mc, rs := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
clientID: "pub",
|
|
user: user,
|
|
pass: "pass",
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc, rs, 0, false, true, subject, 0, []byte("retained"))
|
|
testMQTTFlush(t, mc, nil, rs)
|
|
if asm == nil {
|
|
asm = testMQTTGetAccountSessionManager(t, s, "pub")
|
|
}
|
|
testMQTTDisconnect(t, mc, nil)
|
|
}
|
|
consumeRetained := func(user, subject string, expected bool) {
|
|
t.Helper()
|
|
mc, rs := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: user,
|
|
pass: "pass",
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rs, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, rs, []*mqttFilter{{filter: subject, qos: 0}}, []byte{0})
|
|
if expected {
|
|
testMQTTCheckPubMsg(t, mc, rs, subject, mqttPubFlagRetain, []byte("retained"))
|
|
} else {
|
|
testMQTTExpectNothing(t, rs)
|
|
}
|
|
testMQTTDisconnect(t, mc, nil)
|
|
}
|
|
|
|
// With user "mqtt", publish a retained message on "bar".
|
|
// Since this user has no permission, the server should not have stored it.
|
|
pubRetained("mqtt1", "bar")
|
|
|
|
// Verify that we can't get it with a new subscription.
|
|
consumeRetained("mqtt1", "bar", false)
|
|
|
|
// Use the user "mqtt2" that has permissions to publish on foo and bar.
|
|
// Publish on "foo" and check retained message can be received.
|
|
pubRetained("mqtt2", "foo")
|
|
consumeRetained("mqtt2", "foo", true)
|
|
|
|
// For user "mqtt3", we will publish on "foo/bar" and check retained
|
|
// message is properly received.
|
|
pubRetained("mqtt3", "foo/bar")
|
|
consumeRetained("mqtt3", "foo/bar", true)
|
|
|
|
// Simulate a message that would have been produced in a different server
|
|
// on subject "barbaz". We will use user "mqtt4" that has no pub permissions
|
|
// since we need to send to low-level "$MQTT.rmsgs.barbaz" subject...
|
|
nc := natsConnect(t, s.ClientURL(), nats.UserInfo("mqtt4", "pass"))
|
|
defer nc.Close()
|
|
msg := nats.NewMsg("$MQTT.rmsgs.barbaz")
|
|
msg.Header.Set(mqttNatsRetainedMessageOrigin, "SomeOtherServer")
|
|
msg.Header.Set(mqttNatsRetainedMessageTopic, "barbaz")
|
|
msg.Header.Set(mqttNatsRetainedMessageFlags, "1")
|
|
msg.Data = []byte("retained")
|
|
nc.PublishMsg(msg)
|
|
natsFlush(t, nc)
|
|
|
|
// Wait a bit to make sure it is processed.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Then check that it can be received
|
|
consumeRetained("mqtt3", "barbaz", true)
|
|
|
|
// Same with user "mqtt4" that does not have permissions defined, which
|
|
// means allowed to pub/sub on everything.
|
|
pubRetained("mqtt4", "bat")
|
|
consumeRetained("mqtt4", "bat", true)
|
|
|
|
// Do a config reload and make sure that the server does not panic
|
|
// and we can still get the retained messages.
|
|
no := *o
|
|
// Remove the "bar" publish permission from "mqtt2"
|
|
no.Users[1].Permissions.Publish = &SubjectPermission{Allow: []string{"foo"}}
|
|
// And the "foo.bar" and "barbaz" publish permissions from "mqtt3"
|
|
no.Users[2].Permissions.Publish = &SubjectPermission{Allow: []string{"baz"}}
|
|
err := s.ReloadOptions(&no)
|
|
require_NoError(t, err)
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
asm.mu.RLock()
|
|
defer asm.mu.RUnlock()
|
|
if _, ok := asm.retmsgs.Find([]byte("foo.bar")); ok {
|
|
return errors.New("foo.bar subject still in map")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Still message on "bar" should not exist
|
|
consumeRetained("mqtt1", "bar", false)
|
|
// This one should still be able to be received
|
|
consumeRetained("mqtt2", "foo", true)
|
|
// Retained message on "foo.bar" should have been removed.
|
|
consumeRetained("mqtt3", "foo/bar", false)
|
|
// However, message on "barbaz" should have been left alone since it
|
|
// was produced on a different server.
|
|
consumeRetained("mqtt3", "barbaz", true)
|
|
// And finally, this user that had no permission should still be able
|
|
// to get the retained message on "bat".
|
|
consumeRetained("mqtt4", "bat", true)
|
|
}
|
|
|
|
func TestMQTTPermissionsViolation(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{
|
|
{
|
|
Username: "mqtt",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo.bar"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{"foo.*"}, Deny: []string{"foo.baz"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "mqtt2",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo"}},
|
|
// We use to require explicit allow permission on "$MQTT.sub.>" when any
|
|
// subscribe permission were provided. We now implicitly allow it, but
|
|
// we want to make sure that, if needs arise, one can block the server
|
|
// to subscribe on "$MQTT.sub".
|
|
Subscribe: &SubjectPermission{Allow: []string{"foo"}, Deny: []string{"$MQTT.sub.>"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "observer",
|
|
Password: "pass",
|
|
},
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
user: "mqtt",
|
|
pass: "pass",
|
|
}
|
|
ci.clientID = "sub"
|
|
mc, rc := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/+", qos: 1}}, []byte{1})
|
|
// Should not be allowed to subscribe on specifically on "foo/baz"
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/baz", qos: 1}}, []byte{mqttSubAckFailure})
|
|
// MQTT literals "*" and ">" are converted and interpreted as NATS wildcards
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/*", qos: 1}}, []byte{1})
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/*/*", qos: 1}}, []byte{mqttSubAckFailure})
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/>", qos: 1}}, []byte{mqttSubAckFailure})
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/*/>", qos: 1}}, []byte{mqttSubAckFailure})
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/>/*", qos: 1}}, []byte{mqttSubAckFailure})
|
|
testMQTTFlush(t, mc, nil, rc)
|
|
|
|
ci.clientID = "pub"
|
|
ci.cleanSess = true
|
|
mp, rp := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// These should be received since publisher has the right to publish on foo.bar
|
|
testMQTTPublish(t, mp, rp, 0, false, false, "foo/bar", 0, []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo/bar", 0, []byte("msg1"))
|
|
testMQTTPublish(t, mp, rp, 1, false, false, "foo/bar", 1, []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, mc, rc, "foo/bar", mqttPubQos1, []byte("msg2"))
|
|
|
|
// MQTT clients should not be able to access internal "$MQTT.msgs.*" subjects
|
|
// unless explicitly granted by allow permissions.
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "$MQTT/msgs/#", qos: 1}}, []byte{mqttSubAckFailure})
|
|
|
|
onc := natsConnect(t, s.ClientURL(), nats.UserInfo("observer", "pass"))
|
|
defer onc.Close()
|
|
osub := natsSubSync(t, onc, "$MQTT.msgs.>")
|
|
natsFlush(t, onc)
|
|
testMQTTPublish(t, mp, rp, 0, false, false, "$MQTT/msgs/injected", 0, []byte("internal"))
|
|
if m, err := osub.NextMsg(250 * time.Millisecond); err == nil {
|
|
t.Fatalf("Expected no internal MQTT message to be published, got subject=%q", m.Subject)
|
|
}
|
|
|
|
// But these should not be cause pub has no permission to publish on foo.baz
|
|
testMQTTPublish(t, mp, rp, 0, false, false, "foo/baz", 0, []byte("msg3"))
|
|
testMQTTExpectNothing(t, rc)
|
|
testMQTTPublish(t, mp, rp, 1, false, false, "foo/baz", 1, []byte("msg4"))
|
|
testMQTTExpectNothing(t, rc)
|
|
|
|
// Disconnect publisher
|
|
testMQTTDisconnect(t, mp, nil)
|
|
mp.Close()
|
|
// Disconnect subscriber and restart it to make sure that it does not receive msg3/msg4
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
ci.cleanSess = false
|
|
ci.clientID = "sub"
|
|
mc, rc = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/+", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, rc)
|
|
mc.Close()
|
|
|
|
ci.cleanSess = true
|
|
ci.user = "mqtt2"
|
|
ci.clientID = "sub"
|
|
mc, rc = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
// This should fail because although this user is allowed to subscribe on "foo"
|
|
// we deny permission to subscribe on "$MQTT.sub.>", so the server won't be
|
|
// able to create the internal NATS subscription for the JS consumer.
|
|
testMQTTSub(t, 1, mc, rc, []*mqttFilter{{filter: "foo/baz", qos: 1}}, []byte{mqttSubAckFailure})
|
|
}
|
|
|
|
func TestMQTTCleanSession(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
clientID: "me",
|
|
cleanSess: false,
|
|
}
|
|
c, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
c, r = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
ci.cleanSess = true
|
|
c, r = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c, nil)
|
|
}
|
|
|
|
func TestMQTTDuplicateClientID(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
clientID: "me",
|
|
cleanSess: false,
|
|
}
|
|
c1, r1 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
c2, r2 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// The old client should be disconnected.
|
|
testMQTTExpectDisconnect(t, c1)
|
|
}
|
|
|
|
func TestMQTTPersistedSession(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, c, r,
|
|
[]*mqttFilter{
|
|
{filter: "foo/#", qos: 1},
|
|
{filter: "bar", qos: 1},
|
|
{filter: "baz", qos: 0},
|
|
},
|
|
[]byte{1, 1, 0})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Shutdown server, close connection and restart server. It should
|
|
// have restored the session and consumers.
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
s.Shutdown()
|
|
c.Close()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
// Create a publisher that will send qos1 so we verify that messages
|
|
// are stored for the persisted sessions.
|
|
c, r = testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, c, r, 1, false, false, "foo/bar", 1, []byte("msg0"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Recreate session
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Since consumers have been recovered, messages should be received
|
|
// (MQTT does not need client to recreate consumers for a recovered
|
|
// session)
|
|
|
|
// Check that qos1 publish message is received.
|
|
testMQTTCheckPubMsg(t, c, r, "foo/bar", mqttPubQos1, []byte("msg0"))
|
|
|
|
// Flush to prevent publishes to be done too soon since we are
|
|
// receiving the CONNACK before the subscriptions are restored.
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Now publish some messages to all subscriptions.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo.bar", []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo/bar", 0, []byte("msg1"))
|
|
|
|
natsPub(t, nc, "foo", []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo", 0, []byte("msg2"))
|
|
|
|
natsPub(t, nc, "bar", []byte("msg3"))
|
|
testMQTTCheckPubMsg(t, c, r, "bar", 0, []byte("msg3"))
|
|
|
|
natsPub(t, nc, "baz", []byte("msg4"))
|
|
testMQTTCheckPubMsg(t, c, r, "baz", 0, []byte("msg4"))
|
|
|
|
// Now unsub "bar" and verify that message published on this topic
|
|
// is not received.
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "bar"}})
|
|
natsPub(t, nc, "bar", []byte("msg5"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
nc.Close()
|
|
s.Shutdown()
|
|
c.Close()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
// Recreate a client
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
nc = natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo.bar", []byte("msg6"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo/bar", 0, []byte("msg6"))
|
|
|
|
natsPub(t, nc, "foo", []byte("msg7"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo", 0, []byte("msg7"))
|
|
|
|
// Make sure that we did not recover bar.
|
|
natsPub(t, nc, "bar", []byte("msg8"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
natsPub(t, nc, "baz", []byte("msg9"))
|
|
testMQTTCheckPubMsg(t, c, r, "baz", 0, []byte("msg9"))
|
|
|
|
// Have the sub client send a subscription downgrading the qos1 subscription.
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo/#", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
nc.Close()
|
|
s.Shutdown()
|
|
c.Close()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
// Recreate the sub client
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Publish as a qos1
|
|
c2, r2 := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, c2, r2, 1, false, false, "foo/bar", 1, []byte("msg10"))
|
|
|
|
// Verify that it is received as qos0 which is the qos of the subscription.
|
|
testMQTTCheckPubMsg(t, c, r, "foo/bar", 0, []byte("msg10"))
|
|
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
testMQTTDisconnect(t, c2, nil)
|
|
c2.Close()
|
|
|
|
// Finally, recreate the sub with clean session and ensure that all is gone
|
|
cisub.cleanSess = true
|
|
for i := 0; i < 2; i++ {
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
nc = natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo.bar", []byte("msg11"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
natsPub(t, nc, "foo", []byte("msg12"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Make sure that we did not recover bar.
|
|
natsPub(t, nc, "bar", []byte("msg13"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
natsPub(t, nc, "baz", []byte("msg14"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
nc.Close()
|
|
|
|
s.Shutdown()
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
}
|
|
}
|
|
|
|
func TestMQTTRejectsHashCollidingClientID(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// These IDs were found to collide under getHash()
|
|
victimID := "cid-03579431"
|
|
attackerID := "cid-09191235"
|
|
require_True(t, getHash(victimID) == getHash(attackerID))
|
|
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: victimID, cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
attacker, r := testMQTTConnect(t, &mqttConnInfo{clientID: attackerID, cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer attacker.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCIdentifierRejected, false)
|
|
testMQTTExpectDisconnect(t, attacker)
|
|
attacker.Close()
|
|
|
|
c, r = testMQTTConnect(t, &mqttConnInfo{clientID: victimID, cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo", []byte("msg"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo", 0, []byte("msg"))
|
|
}
|
|
|
|
func TestMQTTRecoverSessionAndAddNewSub(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub1", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Shutdown server, close connection and restart server. It should
|
|
// have restored the session and consumers.
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
s.Shutdown()
|
|
c.Close()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// No need for defer since it is done top of function
|
|
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
// Now add sub and make sure it does not crash
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Now repeat with a new client but without server restart.
|
|
cisub2 := &mqttConnInfo{clientID: "sub2", cleanSess: false}
|
|
c2, r2 := testMQTTConnect(t, cisub2, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c2, nil)
|
|
c2.Close()
|
|
|
|
c2, r2 = testMQTTConnect(t, cisub2, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, c2, r2, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c2, nil, r2)
|
|
}
|
|
|
|
func TestMQTTRecoverSessionWithSubAndClientResendSub(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
cisub1 := &mqttConnInfo{clientID: "sub1", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub1, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Have a client send a SUBSCRIBE protocol for foo, QoS1
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Restart the server now.
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
s.Shutdown()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// No need for defer since it is done top of function
|
|
|
|
// Now restart the client. Since the client was created with cleanSess==false,
|
|
// the server will have recorded the subscriptions for this client.
|
|
c, r = testMQTTConnect(t, cisub1, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
// At this point, the server has recreated the subscription on foo, QoS1.
|
|
|
|
// For applications that restart, it is possible (likely) that they
|
|
// will resend their SUBSCRIBE protocols, so do so now:
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
checkNumSub := func(clientID string) {
|
|
t.Helper()
|
|
|
|
// Find the MQTT client...
|
|
mc := testMQTTGetClient(t, s, clientID)
|
|
|
|
// Check how many NATS subscriptions are registered.
|
|
var fooSub int
|
|
var otherSub int
|
|
mc.mu.Lock()
|
|
for _, sub := range mc.subs {
|
|
switch string(sub.subject) {
|
|
case "foo":
|
|
fooSub++
|
|
default:
|
|
otherSub++
|
|
}
|
|
}
|
|
mc.mu.Unlock()
|
|
|
|
// We should have 2 subscriptions, one on "foo", and one for the JS durable
|
|
// consumer's delivery subject.
|
|
if fooSub != 1 {
|
|
t.Fatalf("Expected 1 sub on 'foo', got %v", fooSub)
|
|
}
|
|
if otherSub != 1 {
|
|
t.Fatalf("Expected 1 subscription for JS durable, got %v", otherSub)
|
|
}
|
|
}
|
|
checkNumSub("sub1")
|
|
|
|
c.Close()
|
|
|
|
// Now same but without the server restart in-between.
|
|
cisub2 := &mqttConnInfo{clientID: "sub2", cleanSess: false}
|
|
c, r = testMQTTConnect(t, cisub2, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
// Restart client
|
|
c, r = testMQTTConnect(t, cisub2, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
// Check client subs
|
|
checkNumSub("sub2")
|
|
}
|
|
|
|
func TestMQTTFlappingSession(t *testing.T) {
|
|
mqttSessJailDur = 250 * time.Millisecond
|
|
mqttFlapCleanItvl = 350 * time.Millisecond
|
|
defer func() {
|
|
mqttSessJailDur = mqttSessFlappingJailDur
|
|
mqttFlapCleanItvl = mqttSessFlappingCleanupInterval
|
|
}()
|
|
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{clientID: "flapper", cleanSess: false}
|
|
c, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Let's get a handle on the asm to check things later.
|
|
asm := testMQTTGetAccountSessionManager(t, s, "flapper")
|
|
|
|
// Start a new connection with the same clientID, which should replace
|
|
// the old one and put it in the flappers map.
|
|
c2, r2 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Should be disconnected...
|
|
testMQTTExpectDisconnect(t, c)
|
|
|
|
// Now try to reconnect "c" and we should fail. We have to do this manually,
|
|
// since we expect it to fail.
|
|
addr := net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port))
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error creating mqtt connection: %v", err)
|
|
}
|
|
defer c.Close()
|
|
proto := mqttCreateConnectProto(ci)
|
|
if _, err := testMQTTWrite(c, proto); err != nil {
|
|
t.Fatalf("Error writing protocols: %v", err)
|
|
}
|
|
// Misbehave and send a SUB protocol without waiting for the CONNACK
|
|
w := newMQTTWriter(0)
|
|
pkLen := 2 // for pi
|
|
// Topic "foo"
|
|
pkLen += 2 + 3 + 1
|
|
w.WriteByte(mqttPacketSub | mqttSubscribeFlags)
|
|
w.WriteVarInt(pkLen)
|
|
w.WriteUint16(1)
|
|
w.WriteBytes([]byte("foo"))
|
|
w.WriteByte(1)
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing protocols: %v", err)
|
|
}
|
|
// Now read the CONNACK and we should have been disconnected.
|
|
if _, err := testMQTTRead(c); err == nil {
|
|
t.Fatal("Expected connection to fail")
|
|
}
|
|
|
|
// This should be in the flappers map, but after 250ms should be cleared.
|
|
for i := 0; i < 2; i++ {
|
|
asm.mu.RLock()
|
|
_, present := asm.flappers["flapper"]
|
|
asm.mu.RUnlock()
|
|
if i == 0 {
|
|
if !present {
|
|
t.Fatal("Did not find the client ID in the flappers map")
|
|
}
|
|
// Wait for more than the cleanup interval
|
|
time.Sleep(mqttFlapCleanItvl + 100*time.Millisecond)
|
|
} else if present {
|
|
t.Fatal("The client ID should have been cleared from the map")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMQTTLockedSession(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
subClientID := "sub"
|
|
ci := &mqttConnInfo{clientID: subClientID, cleanSess: false}
|
|
c, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
sm := &s.mqtt.sessmgr
|
|
sm.mu.Lock()
|
|
asm := sm.sessions[globalAccountName]
|
|
sm.mu.Unlock()
|
|
if asm == nil {
|
|
t.Fatalf("account session manager not found")
|
|
}
|
|
|
|
// It is possible, however unlikely, to have received CONNACK while
|
|
// mqttProcessConnect is still running, and the session remains locked. Wait
|
|
// for it to finish.
|
|
checkFor(t, 250*time.Millisecond, 10*time.Millisecond, func() error {
|
|
asm.mu.RLock()
|
|
defer asm.mu.RUnlock()
|
|
if _, stillLocked := asm.sessLocked[subClientID]; stillLocked {
|
|
return fmt.Errorf("session still locked")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Get the session for "sub"
|
|
cli := testMQTTGetClient(t, s, subClientID)
|
|
sess := cli.mqtt.sess
|
|
|
|
// Pretend that the session above is locked.
|
|
if err := asm.lockSession(sess, cli); err != nil {
|
|
t.Fatalf("Unable to lock session: %v", err)
|
|
}
|
|
defer asm.unlockSession(sess)
|
|
|
|
// Now try to connect another client that wants to use "sub".
|
|
// We can't use testMQTTConnect() because it is going to fail.
|
|
addr := net.JoinHostPort(o.MQTT.Host, fmt.Sprintf("%d", o.MQTT.Port))
|
|
c2, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error creating mqtt connection: %v", err)
|
|
}
|
|
defer c2.Close()
|
|
proto := mqttCreateConnectProto(ci)
|
|
if _, err := testMQTTWrite(c2, proto); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
if _, err := testMQTTRead(c2); err == nil {
|
|
t.Fatal("Expected connection to fail")
|
|
}
|
|
|
|
// Now try again, but this time release the session while waiting
|
|
// to connect and it should succeed.
|
|
time.AfterFunc(250*time.Millisecond, func() { asm.unlockSession(sess) })
|
|
c3, r3 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c3.Close()
|
|
testMQTTCheckConnAck(t, r3, mqttConnAckRCConnectionAccepted, true)
|
|
}
|
|
|
|
func TestMQTTPersistRetainedMsg(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, c, r, 1, false, true, "moo/foo", 1, []byte("foo1"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "moo/foo", 1, []byte("foo2"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "moo/bar", 1, []byte("bar1"))
|
|
testMQTTPublish(t, c, r, 0, false, true, "moo/baz", 1, []byte("baz1"))
|
|
// Remove bar
|
|
testMQTTPublish(t, c, r, 1, false, true, "moo/bar", 1, nil)
|
|
testMQTTFlush(t, c, nil, r)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
s.Shutdown()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
t.Run("many subs one topic", func(t *testing.T) {
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "moo/foo", qos: 1}}, []byte{1})
|
|
testMQTTCheckPubMsg(t, c, r, "moo/foo", mqttPubFlagRetain|mqttPubQos1, []byte("foo2"))
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "moo/baz", qos: 1}}, []byte{1})
|
|
testMQTTCheckPubMsg(t, c, r, "moo/baz", mqttPubFlagRetain, []byte("baz1"))
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "moo/bar", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, r)
|
|
})
|
|
|
|
runSingleSubscribe := func(filters []*mqttFilter) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
testMQTTSub(t, 1, c, r, filters, nil)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
flags, pi, topic, payload := testMQTTReadPubPacket(t, r)
|
|
if (flags & mqttPubFlagRetain) == 0 {
|
|
t.Fatalf("Expected flags to have retain set, got %v", flags)
|
|
}
|
|
if (flags & mqttPubFlagDup) != 0 {
|
|
t.Fatalf("Expected flags to not have Dup set, got %v", flags)
|
|
}
|
|
var expQOS byte
|
|
var expPayload []byte
|
|
switch topic {
|
|
case "moo/foo":
|
|
expQOS = mqttPubQos1
|
|
expPayload = []byte("foo2")
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
case "moo/baz":
|
|
expQOS = 0
|
|
expPayload = []byte("baz1")
|
|
default:
|
|
t.Fatalf("Unexpected topic: %v", topic)
|
|
}
|
|
if (flags & mqttPubFlagQoS) != expQOS {
|
|
t.Fatalf("Expected flags to have QOS %x set, got %x", expQOS, flags)
|
|
}
|
|
if string(payload) != string(expPayload) {
|
|
t.Fatalf("Expected payload to be %q, got %v", expPayload, string(payload))
|
|
}
|
|
}
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
}
|
|
|
|
t.Run("one sub many topics", runSingleSubscribe([]*mqttFilter{
|
|
{filter: "moo/foo", qos: 1},
|
|
{filter: "moo/baz", qos: 1},
|
|
{filter: "moo/bar", qos: 1},
|
|
}))
|
|
|
|
t.Run("one sub wildcard plus", runSingleSubscribe([]*mqttFilter{
|
|
{filter: "moo/+", qos: 1},
|
|
}))
|
|
|
|
t.Run("one sub wildcard hash", runSingleSubscribe([]*mqttFilter{
|
|
{filter: "moo/#", qos: 1},
|
|
}))
|
|
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
}
|
|
|
|
func TestMQTTRetainedMsgCleanup(t *testing.T) {
|
|
mqttRetainedCacheTTL = 250 * time.Millisecond
|
|
defer func() { mqttRetainedCacheTTL = mqttDefaultRetainedCacheTTL }()
|
|
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{clientID: "cache", cleanSess: true}
|
|
c, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Send a retained message.
|
|
testMQTTPublish(t, c, r, 1, false, true, "foo", 1, []byte("msg"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Start a subscription.
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1|mqttPubFlagRetain, []byte("msg"))
|
|
|
|
time.Sleep(2 * mqttRetainedCacheTTL)
|
|
|
|
// Make sure not in cache anymore
|
|
asm := testMQTTGetAccountSessionManager(t, s, "cache")
|
|
if v, ok := asm.rmsCache.Load("foo"); ok {
|
|
t.Fatalf("Should not be in cache, got %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestMQTTRestoreRetainedMsgs(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{clientID: "retain", cleanSess: true}
|
|
c, r := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Send several retained messages on different topic
|
|
testMQTTPublish(t, c, r, 1, false, true, "foo", 1, []byte("msg1"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "bar", 1, []byte("msg2"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "baz", 1, []byte("msg3"))
|
|
|
|
// Remove the two last ones (by sending empty body)
|
|
testMQTTPublish(t, c, r, 1, false, true, "bar", 1, []byte(""))
|
|
testMQTTPublish(t, c, r, 1, false, true, "baz", 1, []byte(""))
|
|
testMQTTFlush(t, c, nil, r)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Now restart the server. We had a bug where we would wait to restore retained
|
|
// messages based on stream last sequence, which was wrong.
|
|
s.Shutdown()
|
|
s, err := NewServer(o)
|
|
require_NoError(t, err)
|
|
l := &captureWarnLogger{warn: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
s.Start()
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
start := time.Now()
|
|
c, r = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
dur := time.Since(start)
|
|
if dur > 2*time.Second {
|
|
var warnMsg string
|
|
select {
|
|
case warnMsg = <-l.warn:
|
|
default:
|
|
}
|
|
t.Fatalf("Likely timing out restoring retained messages (%s)", warnMsg)
|
|
}
|
|
}
|
|
|
|
func TestMQTTConnAckFirstPacket(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.NoLog, o.Debug, o.Trace = true, false, false
|
|
s := RunServer(o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
ch := make(chan struct{}, 1)
|
|
ready := make(chan struct{})
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
close(ready)
|
|
for {
|
|
nc.Publish("foo", []byte("msg"))
|
|
select {
|
|
case <-ch:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
<-ready
|
|
for i := 0; i < 100; i++ {
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
w := newMQTTWriter(0)
|
|
w.WriteByte(mqttPacketDisconnect)
|
|
w.WriteByte(0)
|
|
c.Write(w.Bytes())
|
|
// Wait to be disconnected, we can't use testMQTTDisconnect() because
|
|
// it would fail because we may still receive some NATS messages.
|
|
var b [10]byte
|
|
for {
|
|
if _, err := c.Read(b[:]); err != nil {
|
|
break
|
|
}
|
|
}
|
|
c.Close()
|
|
}
|
|
close(ch)
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestMQTTRedeliveryAckWait(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 250 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("foo1"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 2, []byte("foo2"))
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
flags := mqttPubQos1
|
|
if i > 0 {
|
|
flags |= mqttPubFlagDup
|
|
}
|
|
pi1 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", flags, []byte("foo1"))
|
|
pi2 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", flags, []byte("foo2"))
|
|
|
|
if pi1 != 1 || pi2 != 2 {
|
|
t.Fatalf("Unexpected pi values: %v, %v", pi1, pi2)
|
|
}
|
|
}
|
|
// Ack first message
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, 1)
|
|
// Redelivery should only be for second message now
|
|
for i := 0; i < 2; i++ {
|
|
flags := mqttPubQos1 | mqttPubFlagDup
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, "foo", flags, []byte("foo2"))
|
|
if pi != 2 {
|
|
t.Fatalf("Unexpected pi to be 2, got %v", pi)
|
|
}
|
|
}
|
|
|
|
// Restart client, should receive second message with pi==2
|
|
c.Close()
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
// Check that message is received with proper pi
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1|mqttPubFlagDup, []byte("foo2"))
|
|
if pi != 2 {
|
|
t.Fatalf("Unexpected pi to be 2, got %v", pi)
|
|
}
|
|
// Now ack second message
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, 2)
|
|
// Flush to make sure it is processed before checking client's maps
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
// Look for the sub client
|
|
mc := testMQTTGetClient(t, s, "sub")
|
|
mc.mu.Lock()
|
|
sess := mc.mqtt.sess
|
|
sess.mu.Lock()
|
|
lpi := len(sess.pendingPublish)
|
|
var lsseq int
|
|
for _, sseqToPi := range sess.cpending {
|
|
lsseq += len(sseqToPi)
|
|
}
|
|
sess.mu.Unlock()
|
|
mc.mu.Unlock()
|
|
if lpi != 0 || lsseq != 0 {
|
|
t.Fatalf("Maps should be empty, got %v, %v", lpi, lsseq)
|
|
}
|
|
}
|
|
|
|
// - [MQTT-3.10.4-3] If a Server deletes a Subscription It MUST complete the
|
|
// delivery of any QoS 1 or QoS 2 messages which it has started to send to the
|
|
// Client.
|
|
//
|
|
// Test flow:
|
|
// - Subscribe to foo, publish 3 QoS2 messages.
|
|
// - After one is PUBCOMP-ed, and one is PUBREC-ed, Unsubscribe.
|
|
// - See that the remaining 2 are fully delivered.
|
|
func TestMQTTQoS2InflightMsgsDeliveredAfterUnsubscribe(t *testing.T) {
|
|
// This test has proven flaky on Travis, so skip for now. Line 4926, the 3rd
|
|
// testMQTTCheckPubMsgNoAck sometimes returns `data1`, instead of `data3`
|
|
// that we are expecting. It must be a retry since we did not acknowledge
|
|
// `data1` until later.
|
|
t.Skip()
|
|
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 10 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
var qos2 byte = 2
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: qos2}}, []byte{qos2})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// send 3 messages
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", 441, []byte("data1"))
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", 442, []byte("data2"))
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", 443, []byte("data3"))
|
|
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
subPI1 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data1"))
|
|
subPI2 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data2"))
|
|
// subPI3 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data3"))
|
|
_ = testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data3"))
|
|
|
|
// fully receive first message
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, subPI1)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI1)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, subPI1)
|
|
|
|
// Do not PUBCOMP the 2nd message yet.
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, subPI2)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI2)
|
|
|
|
// Unsubscribe
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: qos2}})
|
|
|
|
// We expect that PI2 and PI3 will continue to be delivered, from their
|
|
// respective states.
|
|
gotPI2PubRel := false
|
|
|
|
// TODO: Currently, we do not get the unacknowledged PUBLISH re-delivered
|
|
// after an UNSUBSCRIBE. Ongoing discussion if we should/must.
|
|
// gotPI3Publish := false
|
|
// gotPI3PubRel := false
|
|
for !gotPI2PubRel /* || !gotPI3Publish || !gotPI3PubRel */ {
|
|
b, _ /* len */ := testMQTTReadPacket(t, r)
|
|
switch b & mqttPacketMask {
|
|
case mqttPacketPubRel:
|
|
pi, err := r.readUint16("packet identifier")
|
|
if err != nil {
|
|
t.Fatalf("got unexpected error: %v", err)
|
|
}
|
|
switch pi {
|
|
case subPI2:
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, pi)
|
|
gotPI2PubRel = true
|
|
// case subPI3:
|
|
// testMQTTSendPIPacket(mqttPacketPubComp, t, c, pi)
|
|
// gotPI3PubRel = true
|
|
default:
|
|
t.Fatalf("Expected PI %v got: %v", subPI2, pi)
|
|
}
|
|
|
|
// case mqttPacketPub:
|
|
// _, pi, _ := testMQTTGetPubMsgExEx(t, c, r, b, len, "foo", []byte("data3"))
|
|
// if pi != subPI3 {
|
|
// t.Fatalf("Expected PI %v got: %v", subPI3, pi)
|
|
// }
|
|
// gotPI3Publish = true
|
|
// testMQTTSendPIPacket(mqttPacketPubRec, t, c, subPI3)
|
|
|
|
default:
|
|
t.Fatalf("Unexpected packet type: %v", b&mqttPacketMask)
|
|
}
|
|
}
|
|
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTQoS2RejectPublishDuplicates(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
var qos2 byte = 2
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: qos2}}, []byte{qos2})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Publish 3 different with same PI before we get any PUBREC back, then
|
|
// complete the PUBREL/PUBCOMP flow as needed. Only one message (first
|
|
// payload) should be delivered. PUBRECs,
|
|
var pubPI uint16 = 444
|
|
testMQTTSendPublishPacket(t, cp, qos2, false, false, "foo", pubPI, []byte("data1"))
|
|
testMQTTSendPublishPacket(t, cp, qos2, true, false, "foo", pubPI, []byte("data2"))
|
|
testMQTTSendPublishPacket(t, cp, qos2, false, false, "foo", pubPI, []byte("data3"))
|
|
|
|
for i := 0; i < 3; i++ {
|
|
// [MQTT-4.3.3-1] The receiver
|
|
//
|
|
// - MUST respond with a PUBREC containing the Packet Identifier from
|
|
// the incoming PUBLISH Packet, having accepted ownership of the
|
|
// Application Message.
|
|
//
|
|
// - Until it has received the corresponding PUBREL packet, the Receiver
|
|
// MUST acknowledge any subsequent PUBLISH packet with the same Packet
|
|
// Identifier by sending a PUBREC. It MUST NOT cause duplicate messages
|
|
// to be delivered to any onward recipients in this case.
|
|
testMQTTReadPIPacket(mqttPacketPubRec, t, rp, pubPI)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
testMQTTSendPIPacket(mqttPacketPubRel|0x2, t, cp, pubPI)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
// [MQTT-4.3.3-1] MUST respond to a PUBREL packet by sending a PUBCOMP
|
|
// packet containing the same Packet Identifier as the PUBREL.
|
|
testMQTTReadPIPacket(mqttPacketPubComp, t, rp, pubPI)
|
|
}
|
|
|
|
// [MQTT-4.3.3-1] After it has sent a PUBCOMP, the receiver MUST treat any
|
|
// subsequent PUBLISH packet that contains that Packet Identifier as being a
|
|
// new publication.
|
|
//
|
|
// Publish another message, identical to the first one. Since the server
|
|
// already sent us a PUBCOMP, it will deliver this message, for a total of 2
|
|
// delivered.
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", pubPI, []byte("data5"))
|
|
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
// Verify we got a total of 2 messages.
|
|
subPI1 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data1"))
|
|
subPI2 := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data5"))
|
|
for _, pi := range []uint16{subPI1, subPI2} {
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, pi)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, pi)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, pi)
|
|
}
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTQoS2RetriesPublish(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 100 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
var qos2 byte = 2
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: qos2}}, []byte{qos2})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Publish a message and close the pub connection.
|
|
var pubPI uint16 = 444
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", pubPI, []byte("data1"))
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
// See that we got the message delivered to the sub, but don't PUBREC it
|
|
// yet.
|
|
subPI := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data1"))
|
|
|
|
// See that the message is redelivered again 2 times, with the DUP on, before we PUBREC it.
|
|
for i := 0; i < 2; i++ {
|
|
expectedFlags := mqttPubQoS2 | mqttPubFlagDup
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, "foo", expectedFlags, []byte("data1"))
|
|
if pi != subPI {
|
|
t.Fatalf("Expected pi to be %v, got %v", subPI, pi)
|
|
}
|
|
}
|
|
|
|
// Finish the exchange and make sure there are no more attempts.
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, subPI)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, subPI)
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTQoS2RetriesPubRel(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 50 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
var qos2 byte = 2
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: qos2}}, []byte{qos2})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Publish a message and close the pub connection.
|
|
var pubPI uint16 = 444
|
|
testMQTTPublish(t, cp, rp, qos2, false, false, "foo", pubPI, []byte("data1"))
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
// See that we got the message delivered to the sub, PUBREC it and expect a
|
|
// PUBREL from the server.
|
|
subPI := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQoS2, []byte("data1"))
|
|
testMQTTSendPIPacket(mqttPacketPubRec, t, c, subPI)
|
|
|
|
// See that we get PUBREL redelivered several times, there's no DUP flag to
|
|
// check.
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI)
|
|
testMQTTReadPIPacket(mqttPacketPubRel, t, r, subPI)
|
|
|
|
// Finish the exchange and make sure there are no more attempts.
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, c, subPI)
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTAckWaitConfigChange(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 250 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
sendMsg := func(topic, payload string) {
|
|
t.Helper()
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, topic, 1, []byte(payload))
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
}
|
|
sendMsg("foo", "msg1")
|
|
|
|
for i := 0; i < 2; i++ {
|
|
flags := mqttPubQos1
|
|
if i > 0 {
|
|
flags |= mqttPubFlagDup
|
|
}
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", flags, []byte("msg1"))
|
|
}
|
|
|
|
// Restart the server with a different AckWait option value.
|
|
// Verify that MQTT sub restart succeeds. It will keep the
|
|
// original value.
|
|
c.Close()
|
|
s.Shutdown()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.MQTT.AckWait = 10 * time.Millisecond
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1|mqttPubFlagDup, []byte("msg1"))
|
|
start := time.Now()
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1|mqttPubFlagDup, []byte("msg1"))
|
|
if dur := time.Since(start); dur < 200*time.Millisecond {
|
|
t.Fatalf("AckWait seem to have changed for existing subscription: %v", dur)
|
|
}
|
|
|
|
// Create new subscription
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
sendMsg("bar", "msg2")
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "bar", mqttPubQos1, []byte("msg2"))
|
|
start = time.Now()
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "bar", mqttPubQos1|mqttPubFlagDup, []byte("msg2"))
|
|
if dur := time.Since(start); dur > 50*time.Millisecond {
|
|
t.Fatalf("AckWait new value not used by new sub: %v", dur)
|
|
}
|
|
c.Close()
|
|
}
|
|
|
|
func TestMQTTUnsubscribeWithPendingAcks(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = 250 * time.Millisecond
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg"))
|
|
testMQTTDisconnect(t, cp, nil)
|
|
cp.Close()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
flags := mqttPubQos1
|
|
if i > 0 {
|
|
flags |= mqttPubFlagDup
|
|
}
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", flags, []byte("msg"))
|
|
}
|
|
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "foo"}})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
mc := testMQTTGetClient(t, s, "sub")
|
|
mc.mu.Lock()
|
|
sess := mc.mqtt.sess
|
|
sess.mu.Lock()
|
|
pal := len(sess.pendingPublish)
|
|
sess.mu.Unlock()
|
|
mc.mu.Unlock()
|
|
if pal != 0 {
|
|
t.Fatalf("Expected pending ack map to be empty, got %v", pal)
|
|
}
|
|
}
|
|
|
|
func TestMQTTMaxAckPending(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.MaxAckPending = 1
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownRestartedServer(&s)
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
dir := strings.TrimSuffix(s.JetStreamConfig().StoreDir, JetStreamStoreDir)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg2"))
|
|
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
// Check that we don't receive the second one due to max ack pending
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Now ack first message
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
// Now we should receive message 2
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg2"))
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Give a chance to the server to "close" the consumer
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
for ci := range js.ConsumersInfo("$MQTT_msgs") {
|
|
if ci.PushBound {
|
|
return fmt.Errorf("Consumer still connected")
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Send 2 messages while sub is offline
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg3"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg4"))
|
|
|
|
// Restart consumer
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Should receive only message 3
|
|
pi = testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg3"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Ack and get the next
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg4"))
|
|
|
|
// Make sure this message gets ack'ed
|
|
mcli := testMQTTGetClient(t, s, cisub.clientID)
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
mcli.mu.Lock()
|
|
sess := mcli.mqtt.sess
|
|
sess.mu.Lock()
|
|
np := len(sess.pendingPublish)
|
|
sess.mu.Unlock()
|
|
mcli.mu.Unlock()
|
|
if np != 0 {
|
|
return fmt.Errorf("Still %v pending messages", np)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Check that change to config does not prevent restart of sub.
|
|
cp.Close()
|
|
c.Close()
|
|
s.Shutdown()
|
|
|
|
o.Port = -1
|
|
o.MQTT.Port = -1
|
|
o.MQTT.MaxAckPending = 2
|
|
o.StoreDir = dir
|
|
s = testMQTTRunServer(t, o)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
cp, rp = testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg5"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg6"))
|
|
|
|
// Restart consumer
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Should receive only message 5
|
|
pi = testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg5"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Ack and get the next
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg6"))
|
|
}
|
|
|
|
func TestMQTTMaxAckPendingForMultipleSubs(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.AckWait = time.Second
|
|
o.MQTT.MaxAckPending = 1
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: true}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
pi := testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
|
|
// Now send a second message but on topic bar
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "bar", 1, []byte("msg2"))
|
|
|
|
// JS allows us to limit per consumer, but we apply the limit to the
|
|
// session, so although JS will attempt to delivery this message,
|
|
// the MQTT code will suppress it.
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Ack the first message.
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, c, pi)
|
|
|
|
// Now we should get the second message
|
|
testMQTTCheckPubMsg(t, c, r, "bar", mqttPubQos1|mqttPubFlagDup, []byte("msg2"))
|
|
}
|
|
|
|
func TestMQTTMaxAckPendingOverLimit(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.MaxAckPending = 20000
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
checkTMax := func(sess *mqttSession, expected int) {
|
|
t.Helper()
|
|
sess.mu.Lock()
|
|
tmax := sess.tmaxack
|
|
sess.mu.Unlock()
|
|
if tmax != expected {
|
|
t.Fatalf("Expected current tmax to be %v, got %v", expected, tmax)
|
|
}
|
|
}
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc := testMQTTGetClient(t, s, "sub")
|
|
mc.mu.Lock()
|
|
sess := mc.mqtt.sess
|
|
mc.mu.Unlock()
|
|
|
|
// After this one, total would be 20000
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
checkTMax(sess, 20000)
|
|
// This one will count for 2, so total will be 60000
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar/#", qos: 1}}, []byte{1})
|
|
checkTMax(sess, 60000)
|
|
|
|
// This should fail
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{mqttSubAckFailure})
|
|
checkTMax(sess, 60000)
|
|
|
|
// Remove the one with wildcard
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "bar/#"}})
|
|
checkTMax(sess, 20000)
|
|
|
|
// Now we could add 2 more without wildcards
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
checkTMax(sess, 40000)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "baz", qos: 1}}, []byte{1})
|
|
checkTMax(sess, 60000)
|
|
|
|
// Again, this one should fail
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bat", qos: 1}}, []byte{mqttSubAckFailure})
|
|
checkTMax(sess, 60000)
|
|
|
|
// Now remove all and check that we are at 0
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "foo"}})
|
|
checkTMax(sess, 40000)
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "bar"}})
|
|
checkTMax(sess, 20000)
|
|
testMQTTUnsub(t, 1, c, r, []*mqttFilter{{filter: "baz"}})
|
|
checkTMax(sess, 0)
|
|
}
|
|
|
|
func TestMQTTConfigReload(t *testing.T) {
|
|
template := `
|
|
jetstream: true
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
ack_wait: %s
|
|
max_ack_pending: %s
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(template, `"5s"`, `10000`)))
|
|
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
if val := o.MQTT.AckWait; val != 5*time.Second {
|
|
t.Fatalf("Invalid ackwait: %v", val)
|
|
}
|
|
if val := o.MQTT.MaxAckPending; val != 10000 {
|
|
t.Fatalf("Invalid ackwait: %v", val)
|
|
}
|
|
|
|
changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(template, `"250ms"`, `1`)))
|
|
if err := s.Reload(); err != nil {
|
|
t.Fatalf("Error on reload: %v", err)
|
|
}
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg2"))
|
|
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
start := time.Now()
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1|mqttPubFlagDup, []byte("msg1"))
|
|
if dur := time.Since(start); dur > 500*time.Millisecond {
|
|
t.Fatalf("AckWait not applied? dur=%v", dur)
|
|
}
|
|
c.Close()
|
|
cp.Close()
|
|
testMQTTShutdownServer(s)
|
|
|
|
changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(template, `"30s"`, `1`)))
|
|
s, o = RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub.cleanSess = true
|
|
c, r = testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
cipub = &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
cp, rp = testMQTTConnect(t, cipub, o.MQTT.Host, o.MQTT.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg2"))
|
|
|
|
testMQTTCheckPubMsgNoAck(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
// Increase the max ack pending
|
|
changeCurrentConfigContentWithNewContent(t, conf, []byte(fmt.Sprintf(template, `"30s"`, `10`)))
|
|
// Reload now
|
|
if err := s.Reload(); err != nil {
|
|
t.Fatalf("Error on reload: %v", err)
|
|
}
|
|
|
|
// Reload will have effect only on new subscriptions.
|
|
// Create a new subscription, and we should not be able to get the 2 messages.
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "bar", 1, []byte("msg3"))
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "bar", 1, []byte("msg4"))
|
|
|
|
testMQTTCheckPubMsg(t, c, r, "bar", mqttPubQos1, []byte("msg3"))
|
|
testMQTTCheckPubMsg(t, c, r, "bar", mqttPubQos1, []byte("msg4"))
|
|
}
|
|
|
|
func TestMQTTStreamInfoReturnsNonEmptySubject(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
// Check that we can query all MQTT streams. MQTT streams are
|
|
// created without subject filter, however, if we return them like this,
|
|
// the 'nats' utility will fail to display them due to some xml validation.
|
|
for _, sname := range []string{
|
|
mqttStreamName,
|
|
mqttRetainedMsgsStreamName,
|
|
} {
|
|
t.Run(sname, func(t *testing.T) {
|
|
resp, err := nc.Request(fmt.Sprintf(JSApiStreamInfoT, sname), nil, time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
var bResp JSApiStreamInfoResponse
|
|
if err = json.Unmarshal(resp.Data, &bResp); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if len(bResp.Config.Subjects) == 0 {
|
|
t.Fatalf("No subject returned, which will cause nats tooling to fail: %+v", bResp.Config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTWebsocketToMQTTPort(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
ci := &mqttConnInfo{cleanSess: true, ws: true}
|
|
if _, _, err := testMQTTConnectRetryWithError(t, ci, o.MQTT.Host, o.MQTT.Port, 0); err == nil {
|
|
t.Fatal("Expected error during connect")
|
|
}
|
|
select {
|
|
case e := <-l.errCh:
|
|
if !strings.Contains(e, errMQTTNotWebsocketPort.Error()) {
|
|
t.Fatalf("Unexpected error: %v", e)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("No error regarding wrong port")
|
|
}
|
|
}
|
|
|
|
func TestMQTTWebsocket(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
template := `
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
accounts {
|
|
MQTT {
|
|
jetstream: enabled
|
|
users [
|
|
{user: "mqtt", pass: "pwd", connection_types: ["%s"%s]}
|
|
]
|
|
}
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
}
|
|
`
|
|
s, o, conf := runReloadServerWithContent(t, []byte(fmt.Sprintf(template, tdir, jwt.ConnectionTypeMqtt, "")))
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
cisub := &mqttConnInfo{clientID: "sub", user: "mqtt", pass: "pwd", ws: true}
|
|
c, r := testMQTTConnect(t, cisub, o.Websocket.Host, o.Websocket.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCNotAuthorized, false)
|
|
c.Close()
|
|
|
|
ws := fmt.Sprintf(`, "%s"`, jwt.ConnectionTypeMqttWS)
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(template, tdir, jwt.ConnectionTypeMqtt, ws))
|
|
|
|
cisub = &mqttConnInfo{clientID: "sub", user: "mqtt", pass: "pwd", ws: true}
|
|
c, r = testMQTTConnect(t, cisub, o.Websocket.Host, o.Websocket.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
cipub := &mqttConnInfo{clientID: "pub", user: "mqtt", pass: "pwd", ws: true}
|
|
cp, rp := testMQTTConnect(t, cipub, o.Websocket.Host, o.Websocket.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
}
|
|
|
|
type chunkWriteConn struct {
|
|
net.Conn
|
|
}
|
|
|
|
func (cwc *chunkWriteConn) Write(p []byte) (int, error) {
|
|
max := len(p)
|
|
cs := rand.Intn(max) + 1
|
|
if cs < max {
|
|
if pn, perr := cwc.Conn.Write(p[:cs]); perr != nil {
|
|
return pn, perr
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
if pn, perr := cwc.Conn.Write(p[cs:]); perr != nil {
|
|
return pn, perr
|
|
}
|
|
return len(p), nil
|
|
}
|
|
return cwc.Conn.Write(p)
|
|
}
|
|
|
|
func TestMQTTPartial(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
ws bool
|
|
}{
|
|
{"standard", false},
|
|
{"websocket", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ci := &mqttConnInfo{cleanSess: true, ws: test.ws}
|
|
host, port := o.MQTT.Host, o.MQTT.Port
|
|
if test.ws {
|
|
host, port = o.Websocket.Host, o.Websocket.Port
|
|
}
|
|
c, r := testMQTTConnect(t, ci, host, port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
c = &chunkWriteConn{Conn: c}
|
|
|
|
cp, rp := testMQTTConnect(t, ci, host, port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
cp = &chunkWriteConn{Conn: cp}
|
|
|
|
subj := nuid.Next()
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: subj, qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
for i := 0; i < 10; i++ {
|
|
testMQTTPublish(t, cp, rp, 1, false, false, subj, 1, []byte("msg"))
|
|
testMQTTCheckPubMsg(t, c, r, subj, mqttPubQos1, []byte("msg"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTWebsocketTLS(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
cert_file: '../test/configs/certs/server-cert.pem'
|
|
key_file: '../test/configs/certs/server-key.pem'
|
|
ca_file: '../test/configs/certs/ca.pem'
|
|
}
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", ws: true, tls: true}, o.Websocket.Host, o.Websocket.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
cp, rp := testMQTTConnect(t, &mqttConnInfo{clientID: "pub", ws: true, tls: true}, o.Websocket.Host, o.Websocket.Port)
|
|
defer cp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, cp, rp, 1, false, false, "foo", 1, []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg1"))
|
|
}
|
|
|
|
func TestMQTTTransferSessionStreamsToMuxed(t *testing.T) {
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 3)
|
|
defer cl.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, cl.randomServer())
|
|
defer nc.Close()
|
|
|
|
// Create 2 streams that start with "$MQTT_sess_" to check for transfer to new
|
|
// mux'ed unique "$MQTT_sess" stream. One of this stream will not contain a
|
|
// proper session record, and we will check that the stream does not get deleted.
|
|
sessStreamName1 := mqttSessionsStreamNamePrefix + getHash("sub")
|
|
if _, err := js.AddStream(&nats.StreamConfig{
|
|
Name: sessStreamName1,
|
|
Subjects: []string{sessStreamName1},
|
|
Replicas: 3,
|
|
MaxMsgs: 1,
|
|
}); err != nil {
|
|
t.Fatalf("Unable to add stream: %v", err)
|
|
}
|
|
// Then add the session record
|
|
ps := mqttPersistedSession{
|
|
ID: "sub",
|
|
Subs: map[string]byte{"foo": 1},
|
|
Cons: map[string]*ConsumerConfig{"foo": {
|
|
Durable: "d6INCtp3_cK39H5WHEtOSU7sLy2oQv3",
|
|
DeliverSubject: "$MQTT.sub.cK39H5WHEtOSU7sLy2oQrR",
|
|
DeliverPolicy: DeliverNew,
|
|
AckPolicy: AckExplicit,
|
|
FilterSubject: "$MQTT.msgs.foo",
|
|
MaxAckPending: 1024,
|
|
}},
|
|
}
|
|
b, _ := json.Marshal(&ps)
|
|
if _, err := js.Publish(sessStreamName1, b); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
|
|
// Create the stream that has "$MQTT_sess_" prefix, but that is not really a MQTT session stream
|
|
sessStreamName2 := mqttSessionsStreamNamePrefix + "ivan"
|
|
if _, err := js.AddStream(&nats.StreamConfig{
|
|
Name: sessStreamName2,
|
|
Subjects: []string{sessStreamName2},
|
|
Replicas: 3,
|
|
MaxMsgs: 1,
|
|
}); err != nil {
|
|
t.Fatalf("Unable to add stream: %v", err)
|
|
}
|
|
if _, err := js.Publish(sessStreamName2, []byte("some content")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
|
|
cl.waitOnStreamLeader(globalAccountName, sessStreamName1)
|
|
cl.waitOnStreamLeader(globalAccountName, sessStreamName2)
|
|
|
|
// Now create a real MQTT connection
|
|
o := cl.opts[0]
|
|
sc, sr := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub"}, o.MQTT.Host, o.MQTT.Port, 10)
|
|
defer sc.Close()
|
|
testMQTTCheckConnAck(t, sr, mqttConnAckRCConnectionAccepted, true)
|
|
|
|
// Check that old session stream is gone, but the non session stream is still present.
|
|
var gotIt = false
|
|
for info := range js.StreamsInfo() {
|
|
if strings.HasPrefix(info.Config.Name, mqttSessionsStreamNamePrefix) {
|
|
if strings.HasSuffix(info.Config.Name, "_ivan") {
|
|
gotIt = true
|
|
} else {
|
|
t.Fatalf("The stream %q should have been deleted", info.Config.Name)
|
|
}
|
|
}
|
|
}
|
|
if !gotIt {
|
|
t.Fatalf("The stream %q should not have been deleted", mqttSessionsStreamNamePrefix+"ivan")
|
|
}
|
|
|
|
// We want to check that the record was properly transferred.
|
|
rmsg, err := js.GetMsg(mqttSessStreamName, 2)
|
|
if err != nil {
|
|
t.Fatalf("Unable to get session message: %v", err)
|
|
}
|
|
ps2 := &mqttPersistedSession{}
|
|
if err := json.Unmarshal(rmsg.Data, ps2); err != nil {
|
|
t.Fatalf("Error unpacking session record: %v", err)
|
|
}
|
|
if ps2.ID != "sub" {
|
|
t.Fatalf("Unexpected session record, %+v vs %+v", ps2, ps)
|
|
}
|
|
if qos, ok := ps2.Subs["foo"]; !ok || qos != 1 {
|
|
t.Fatalf("Unexpected session record, %+v vs %+v", ps2, ps)
|
|
}
|
|
if cons, ok := ps2.Cons["foo"]; !ok || !reflect.DeepEqual(cons, ps.Cons["foo"]) {
|
|
t.Fatalf("Unexpected session record, %+v vs %+v", ps2, ps)
|
|
}
|
|
|
|
// Make sure we don't attempt to transfer again by creating a subscription
|
|
// on the "stream names" API, which is used to get the list of streams to transfer
|
|
sub := natsSubSync(t, nc, JSApiStreams)
|
|
|
|
// Make sure to connect an MQTT client from a different node so that this node
|
|
// gets a connection for the account for the first time and tries to create
|
|
// all MQTT streams, etc..
|
|
o = cl.opts[1]
|
|
sc, sr = testMQTTConnectRetry(t, &mqttConnInfo{clientID: "sub2"}, o.MQTT.Host, o.MQTT.Port, 10)
|
|
defer sc.Close()
|
|
testMQTTCheckConnAck(t, sr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
if _, err := sub.NextMsg(200 * time.Millisecond); err == nil {
|
|
t.Fatal("Looks like attempt to transfer was done again")
|
|
}
|
|
}
|
|
|
|
func TestMQTTConnectAndDisconnectEvent(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
accounts {
|
|
MQTT {
|
|
jetstream: enabled
|
|
users: [{user: "mqtt", password: "pwd"}]
|
|
}
|
|
SYS {
|
|
users: [{user: "sys", password: "pwd"}]
|
|
}
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
system_account: "SYS"
|
|
`, t.TempDir())))
|
|
defer os.Remove(conf)
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL(), nats.UserInfo("sys", "pwd"))
|
|
defer nc.Close()
|
|
|
|
accConn := natsSubSync(t, nc, fmt.Sprintf(connectEventSubj, "MQTT"))
|
|
accDisc := natsSubSync(t, nc, fmt.Sprintf(disconnectEventSubj, "MQTT"))
|
|
accAuth := natsSubSync(t, nc, fmt.Sprintf(authErrorEventSubj, s.ID()))
|
|
natsFlush(t, nc)
|
|
|
|
checkConnEvent := func(data []byte, expected string) {
|
|
t.Helper()
|
|
var ce ConnectEventMsg
|
|
json.Unmarshal(data, &ce)
|
|
if ce.Client.MQTTClient != expected {
|
|
t.Fatalf("Expected client ID %q, got this connect event: %+v", expected, ce)
|
|
}
|
|
}
|
|
checkDiscEvent := func(data []byte, expected string) {
|
|
t.Helper()
|
|
var de DisconnectEventMsg
|
|
json.Unmarshal(data, &de)
|
|
if de.Client.MQTTClient != expected {
|
|
t.Fatalf("Expected client ID %q, got this disconnect event: %+v", expected, de)
|
|
}
|
|
}
|
|
|
|
c1, r1 := testMQTTConnect(t, &mqttConnInfo{user: "mqtt", pass: "pwd", clientID: "conn1", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
cm := natsNexMsg(t, accConn, time.Second)
|
|
checkConnEvent(cm.Data, "conn1")
|
|
|
|
c2, r2 := testMQTTConnect(t, &mqttConnInfo{user: "mqtt", pass: "pwd", clientID: "conn2", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
cm = natsNexMsg(t, accConn, time.Second)
|
|
checkConnEvent(cm.Data, "conn2")
|
|
|
|
c3, r3 := testMQTTConnect(t, &mqttConnInfo{user: "mqtt", pass: "pwd", clientID: "conn3", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c3.Close()
|
|
testMQTTCheckConnAck(t, r3, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
cm = natsNexMsg(t, accConn, time.Second)
|
|
checkConnEvent(cm.Data, "conn3")
|
|
|
|
testMQTTDisconnect(t, c3, nil)
|
|
c3.Close()
|
|
cm = natsNexMsg(t, accDisc, time.Second)
|
|
checkDiscEvent(cm.Data, "conn3")
|
|
|
|
// Now try a bad auth
|
|
c4, r4 := testMQTTConnect(t, &mqttConnInfo{clientID: "conn4", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c4.Close()
|
|
testMQTTCheckConnAck(t, r4, mqttConnAckRCNotAuthorized, false)
|
|
// This will generate an auth error, which is a disconnect event
|
|
cm = natsNexMsg(t, accAuth, time.Second)
|
|
checkDiscEvent(cm.Data, "conn4")
|
|
|
|
url := fmt.Sprintf("http://127.0.0.1:%d/", s.MonitorAddr().Port)
|
|
for mode := 0; mode < 2; mode++ {
|
|
c := pollConnz(t, s, mode, url+"connz", nil)
|
|
if c.Conns == nil || len(c.Conns) != 3 {
|
|
t.Fatalf("Expected 3 connections in array, got %v", len(c.Conns))
|
|
}
|
|
|
|
// Check that client ID is present
|
|
for _, conn := range c.Conns {
|
|
if conn.Type == clientTypeStringMap[MQTT] && conn.MQTTClient == _EMPTY_ {
|
|
t.Fatalf("Expected a client ID to be set, got %+v", conn)
|
|
}
|
|
}
|
|
|
|
// Check that we can select based on client ID:
|
|
c = pollConnz(t, s, mode, url+"connz?mqtt_client=conn2", &ConnzOptions{MQTTClient: "conn2"})
|
|
if c.Conns == nil || len(c.Conns) != 1 {
|
|
t.Fatalf("Expected 1 connection in array, got %v", len(c.Conns))
|
|
}
|
|
if c.Conns[0].MQTTClient != "conn2" {
|
|
t.Fatalf("Unexpected client ID: %+v", c.Conns[0])
|
|
}
|
|
|
|
// Check that we have the closed ones
|
|
c = pollConnz(t, s, mode, url+"connz?state=closed", &ConnzOptions{State: ConnClosed})
|
|
if c.Conns == nil || len(c.Conns) != 2 {
|
|
t.Fatalf("Expected 2 connections in array, got %v", len(c.Conns))
|
|
}
|
|
for _, conn := range c.Conns {
|
|
if conn.MQTTClient == _EMPTY_ {
|
|
t.Fatalf("Expected a client ID, got %+v", conn)
|
|
}
|
|
}
|
|
|
|
// Check that we can select with client ID for closed state
|
|
c = pollConnz(t, s, mode, url+"connz?state=closed&mqtt_client=conn3", &ConnzOptions{State: ConnClosed, MQTTClient: "conn3"})
|
|
if c.Conns == nil || len(c.Conns) != 1 {
|
|
t.Fatalf("Expected 1 connection in array, got %v", len(c.Conns))
|
|
}
|
|
if c.Conns[0].MQTTClient != "conn3" {
|
|
t.Fatalf("Unexpected client ID: %+v", c.Conns[0])
|
|
}
|
|
// Check that we can select with client ID for closed state (but in this case not found)
|
|
c = pollConnz(t, s, mode, url+"connz?state=closed&mqtt_client=conn5", &ConnzOptions{State: ConnClosed, MQTTClient: "conn5"})
|
|
if len(c.Conns) != 0 {
|
|
t.Fatalf("Expected 0 connection in array, got %v", len(c.Conns))
|
|
}
|
|
}
|
|
|
|
reply := nc.NewRespInbox()
|
|
replySub := natsSubSync(t, nc, reply)
|
|
|
|
// Test system events now
|
|
for _, test := range []struct {
|
|
opt any
|
|
cid string
|
|
}{
|
|
{&ConnzOptions{MQTTClient: "conn1"}, "conn1"},
|
|
{&ConnzOptions{MQTTClient: "conn3", State: ConnClosed}, "conn3"},
|
|
{&ConnzOptions{MQTTClient: "conn4", State: ConnClosed}, "conn4"},
|
|
{&ConnzOptions{MQTTClient: "conn5"}, _EMPTY_},
|
|
{json.RawMessage(`{"mqtt_client":"conn1"}`), "conn1"},
|
|
{json.RawMessage(fmt.Sprintf(`{"mqtt_client":"conn3", "state":%v}`, ConnClosed)), "conn3"},
|
|
{json.RawMessage(fmt.Sprintf(`{"mqtt_client":"conn4", "state":%v}`, ConnClosed)), "conn4"},
|
|
{json.RawMessage(`{"mqtt_client":"conn5"}`), _EMPTY_},
|
|
} {
|
|
t.Run("sys connz", func(t *testing.T) {
|
|
b, _ := json.Marshal(test.opt)
|
|
|
|
// set a header to make sure request parsing knows to ignore them
|
|
nc.PublishMsg(&nats.Msg{
|
|
Subject: fmt.Sprintf("%s.CONNZ", serverStatsPingReqSubj),
|
|
Reply: reply,
|
|
Data: b,
|
|
})
|
|
|
|
msg := natsNexMsg(t, replySub, time.Second)
|
|
var response ServerAPIResponse
|
|
if err := json.Unmarshal(msg.Data, &response); err != nil {
|
|
t.Fatalf("Error unmarshalling response json: %v", err)
|
|
}
|
|
tmp, _ := json.Marshal(response.Data)
|
|
cz := &Connz{}
|
|
if err := json.Unmarshal(tmp, cz); err != nil {
|
|
t.Fatalf("Error unmarshalling connz: %v", err)
|
|
}
|
|
if test.cid == _EMPTY_ {
|
|
if len(cz.Conns) != 0 {
|
|
t.Fatalf("Expected no connections, got %v", len(cz.Conns))
|
|
}
|
|
return
|
|
}
|
|
if len(cz.Conns) != 1 {
|
|
t.Fatalf("Expected single connection, got %v", len(cz.Conns))
|
|
}
|
|
conn := cz.Conns[0]
|
|
if conn.MQTTClient != test.cid {
|
|
t.Fatalf("Expected client ID %q, got %q", test.cid, conn.MQTTClient)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTClientIDInLogStatements(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
l := &captureDebugLogger{dbgCh: make(chan string, 10)}
|
|
s.SetLogger(l, true, false)
|
|
|
|
cisub := &mqttConnInfo{clientID: "my_client_id", cleanSess: false}
|
|
c, r := testMQTTConnect(t, cisub, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
tm := time.NewTimer(2 * time.Second)
|
|
var connected bool
|
|
var disconnected bool
|
|
for {
|
|
select {
|
|
case dl := <-l.dbgCh:
|
|
if strings.Contains(dl, "my_client_id") {
|
|
if strings.Contains(dl, "Client connected") {
|
|
connected = true
|
|
} else if strings.Contains(dl, "Client connection closed") {
|
|
disconnected = true
|
|
}
|
|
if connected && disconnected {
|
|
// OK!
|
|
return
|
|
}
|
|
}
|
|
case <-tm.C:
|
|
t.Fatal("Did not get the debug statements or client_id in them")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMQTTStreamReplicasOverride(t *testing.T) {
|
|
conf := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
stream_replicas: 3
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
cl := createJetStreamClusterWithTemplate(t, conf, "MQTT", 3)
|
|
defer cl.shutdown()
|
|
|
|
connectAndCheck := func(restarted bool) {
|
|
t.Helper()
|
|
|
|
o := cl.opts[0]
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "test", cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, restarted)
|
|
|
|
nc, js := jsClientConnect(t, cl.servers[2])
|
|
defer nc.Close()
|
|
|
|
streams := []string{mqttStreamName, mqttRetainedMsgsStreamName, mqttSessStreamName}
|
|
for _, sn := range streams {
|
|
si, err := js.StreamInfo(sn)
|
|
require_NoError(t, err)
|
|
if n := len(si.Cluster.Replicas); n != 2 {
|
|
t.Fatalf("Expected stream %q to have 2 replicas, got %v", sn, n)
|
|
}
|
|
}
|
|
}
|
|
connectAndCheck(false)
|
|
|
|
cl.stopAll()
|
|
for _, o := range cl.opts {
|
|
o.MQTT.StreamReplicas = 2
|
|
}
|
|
cl.restartAllSamePorts()
|
|
cl.waitOnStreamLeader(globalAccountName, mqttStreamName)
|
|
cl.waitOnStreamLeader(globalAccountName, mqttRetainedMsgsStreamName)
|
|
cl.waitOnStreamLeader(globalAccountName, mqttSessStreamName)
|
|
|
|
l := &captureWarnLogger{warn: make(chan string, 10)}
|
|
cl.servers[0].SetLogger(l, false, false)
|
|
|
|
connectAndCheck(true)
|
|
|
|
select {
|
|
case w := <-l.warn:
|
|
if !strings.Contains(w, "current is 3 but configuration is 2") {
|
|
t.Fatalf("Unexpected warning: %q", w)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Should have warned against replicas mismatch")
|
|
}
|
|
}
|
|
|
|
func TestMQTTStreamReplicasConfigReload(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
tmpl := `
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
stream_replicas: %v
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, tdir, 3)))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
_, _, err := testMQTTConnectRetryWithError(t, &mqttConnInfo{clientID: "mqtt", cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if err == nil {
|
|
t.Fatal("Expected to fail, did not")
|
|
}
|
|
|
|
select {
|
|
case e := <-l.errCh:
|
|
if !strings.Contains(e, NewJSStreamReplicasNotSupportedError().Description) {
|
|
t.Fatalf("Expected error regarding replicas, got %v", e)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get the error regarding replicas count")
|
|
}
|
|
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(tmpl, tdir, 1))
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "mqtt", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
}
|
|
|
|
func TestMQTTStreamReplicasInsufficientResources(t *testing.T) {
|
|
conf := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
stream_replicas: 5
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
cl := createJetStreamClusterWithTemplate(t, conf, "MQTT", 3)
|
|
defer cl.shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
for _, s := range cl.servers {
|
|
s.SetLogger(l, false, false)
|
|
}
|
|
|
|
o := cl.opts[1]
|
|
_, _, err := testMQTTConnectRetryWithError(t, &mqttConnInfo{clientID: "mqtt", cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 0)
|
|
if err == nil {
|
|
t.Fatal("Expected to fail, did not")
|
|
}
|
|
|
|
select {
|
|
case e := <-l.errCh:
|
|
if !strings.Contains(e, fmt.Sprintf("%d", NewJSClusterNoPeersError(errors.New("")).ErrCode)) {
|
|
t.Fatalf("Expected error regarding no peers error, got %v", e)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get the error regarding replicas count")
|
|
}
|
|
}
|
|
|
|
func TestMQTTConsumerReplicasValidate(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
for _, test := range []struct {
|
|
name string
|
|
sr int
|
|
cr int
|
|
err bool
|
|
}{
|
|
{"stream replicas neg", -1, 3, false},
|
|
{"stream replicas 0", 0, 3, false},
|
|
{"consumer replicas neg", 0, -1, false},
|
|
{"consumer replicas 0", -1, 0, false},
|
|
{"consumer replicas too high", 1, 2, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o.MQTT.StreamReplicas = test.sr
|
|
o.MQTT.ConsumerReplicas = test.cr
|
|
err := validateMQTTOptions(o)
|
|
if test.err {
|
|
if err == nil {
|
|
t.Fatal("Expected error, did not get one")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot be higher") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// OK
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// This was ill-advised since the messages stream is currently interest policy.
|
|
// Interest policy streams require consumers match the replica count.
|
|
// Will leave her for now to make sure we do not override.
|
|
func TestMQTTConsumerReplicasOverride(t *testing.T) {
|
|
conf := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
stream_replicas: 5
|
|
consumer_replicas: 1
|
|
consumer_memory_storage: true
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
cl := createJetStreamClusterWithTemplate(t, conf, "MQTT", 5)
|
|
defer cl.shutdown()
|
|
|
|
connectAndCheck := func(subject string, restarted bool) {
|
|
t.Helper()
|
|
|
|
o := cl.opts[0]
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "test", cleanSess: false}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, restarted)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: subject, qos: 1}}, []byte{1})
|
|
|
|
nc, js := jsClientConnect(t, cl.servers[2])
|
|
defer nc.Close()
|
|
|
|
for ci := range js.ConsumersInfo(mqttStreamName) {
|
|
if ci.Config.FilterSubject == mqttStreamSubjectPrefix+subject {
|
|
if rf := len(ci.Cluster.Replicas) + 1; rf != 5 {
|
|
t.Fatalf("Expected consumer to be R5, got: %d", rf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
connectAndCheck("foo", false)
|
|
|
|
cl.stopAll()
|
|
for _, o := range cl.opts {
|
|
o.MQTT.ConsumerReplicas = 2
|
|
o.MQTT.ConsumerMemoryStorage = false
|
|
}
|
|
cl.restartAllSamePorts()
|
|
cl.waitOnStreamLeader(globalAccountName, mqttStreamName)
|
|
cl.waitOnStreamLeader(globalAccountName, mqttRetainedMsgsStreamName)
|
|
cl.waitOnStreamLeader(globalAccountName, mqttSessStreamName)
|
|
|
|
connectAndCheck("bar", true)
|
|
}
|
|
|
|
func TestMQTTConsumerMemStorageReload(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
tmpl := `
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
consumer_memory_storage: %s
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, tdir, "false")))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(tmpl, tdir, "true"))
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
mset, err := s.GlobalAccount().lookupStream(mqttStreamName)
|
|
if err != nil {
|
|
t.Fatalf("Error looking up stream: %v", err)
|
|
}
|
|
var cons *consumer
|
|
mset.mu.RLock()
|
|
for _, c := range mset.consumers {
|
|
cons = c
|
|
break
|
|
}
|
|
mset.mu.RUnlock()
|
|
cons.mu.RLock()
|
|
st := cons.store.Type()
|
|
cons.mu.RUnlock()
|
|
if st != MemoryStorage {
|
|
t.Fatalf("Expected storage %v, got %v", MemoryStorage, st)
|
|
}
|
|
}
|
|
|
|
// Test for auto-cleanup of consumers.
|
|
func TestMQTTConsumerInactiveThreshold(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
consumer_inactive_threshold: %q
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, tdir, "0.2s")))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{clientID: "test", cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
ci := <-js.ConsumersInfo("$MQTT_msgs")
|
|
|
|
// Make sure we clean up this consumer.
|
|
mc.Close()
|
|
checkFor(t, 2*time.Second, 50*time.Millisecond, func() error {
|
|
_, err := js.ConsumerInfo(ci.Stream, ci.Name)
|
|
if err == nil {
|
|
return fmt.Errorf("Consumer still present")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Check reload.
|
|
// We will not redo existing consumers however.
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(tmpl, tdir, "22s"))
|
|
if opts := s.getOpts(); opts.MQTT.ConsumerInactiveThreshold != 22*time.Second {
|
|
t.Fatalf("Expected reloaded value of %v but got %v", 22*time.Second, opts.MQTT.ConsumerInactiveThreshold)
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubjectMapping(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
|
|
mappings = {
|
|
foo0: bar0
|
|
foo1: bar1
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
for _, qos := range []byte{0, 1} {
|
|
t.Run(fmt.Sprintf("qos%d", qos), func(t *testing.T) {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
|
|
bar := fmt.Sprintf("bar%v", qos)
|
|
foo := fmt.Sprintf("foo%v", qos)
|
|
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: bar, qos: qos}}, []byte{qos})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
mcp, rp := testMQTTConnect(t, &mqttConnInfo{clientID: "pub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
natsPub(t, nc, foo, []byte("msg0"))
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, 0, []byte("msg0"))
|
|
|
|
testMQTTPublish(t, mcp, rp, 0, false, false, foo, 0, []byte("msg1"))
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, 0, []byte("msg1"))
|
|
|
|
testMQTTPublish(t, mcp, rp, 0, false, true, foo, 0, []byte("msg1_retained"))
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, 0, []byte("msg1_retained"))
|
|
|
|
testMQTTPublish(t, mcp, rp, 1, false, false, foo, 1, []byte("msg2"))
|
|
// For the receiving side, the expected QoS is based on the subscription's QoS,
|
|
// not the publisher.
|
|
expected := byte(0)
|
|
if qos == 1 {
|
|
expected = mqttPubQos1
|
|
}
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, expected, []byte("msg2"))
|
|
|
|
testMQTTPublish(t, mcp, rp, 1, false, true, foo, 1, []byte("msg2_retained"))
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, expected, []byte("msg2_retained"))
|
|
|
|
testMQTTDisconnect(t, mcp, nil)
|
|
mcp.Close()
|
|
|
|
// Try the with the "will" with QoS0 first
|
|
mcp, rp = testMQTTConnect(t, &mqttConnInfo{
|
|
clientID: "pub",
|
|
cleanSess: true,
|
|
will: &mqttWill{topic: []byte(foo), qos: 0, message: []byte("willmsg1")}},
|
|
o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTFlush(t, mcp, nil, rp)
|
|
|
|
// Close the connection without proper disconnect for will to be sent
|
|
mcp.Close()
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, 0, []byte("willmsg1"))
|
|
|
|
// Try the with the "will" with QoS1 now
|
|
mcp, rp = testMQTTConnect(t, &mqttConnInfo{
|
|
clientID: "pub",
|
|
cleanSess: true,
|
|
will: &mqttWill{topic: []byte(foo), qos: 1, message: []byte("willmsg2")}},
|
|
o.MQTT.Host, o.MQTT.Port)
|
|
defer mcp.Close()
|
|
testMQTTCheckConnAck(t, rp, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTFlush(t, mcp, nil, rp)
|
|
|
|
// Close the connection without proper disconnect for will to be sent
|
|
mcp.Close()
|
|
testMQTTCheckPubMsgNoAck(t, mc, r, bar, expected, []byte("willmsg2"))
|
|
|
|
si, err := js.StreamInfo("$MQTT_msgs")
|
|
require_NoError(t, err)
|
|
if qos == 0 {
|
|
require_True(t, si.State.Msgs == 0)
|
|
require_True(t, si.State.NumSubjects == 0)
|
|
} else {
|
|
// Number of QoS1 messages: 1 regular, 1 retained, 1 from will.
|
|
require_True(t, si.State.Msgs == 3)
|
|
require_True(t, si.State.NumSubjects == 1)
|
|
}
|
|
testMQTTDisconnect(t, mc, nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTSubjectMappingWithImportExport(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
|
|
accounts {
|
|
A {
|
|
users = [{user: "a", password: "pwd"}]
|
|
mappings = {
|
|
bar: foo
|
|
}
|
|
exports = [{service: "bar"}]
|
|
jetstream: enabled
|
|
}
|
|
B {
|
|
users = [{user: "b", password: "pwd"}]
|
|
mappings = {
|
|
foo: bar
|
|
}
|
|
imports = [{service: {account: "A", subject: "bar"}}]
|
|
jetstream: enabled
|
|
}
|
|
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
c1, r1 := testMQTTConnect(t, &mqttConnInfo{user: "a", pass: "pwd", clientID: "a1", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c1, r1, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, c1, nil, r1)
|
|
|
|
c2, r2 := testMQTTConnect(t, &mqttConnInfo{user: "a", pass: "pwd", clientID: "a2", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c2, r2, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c2, nil, r2)
|
|
|
|
c3, r3 := testMQTTConnect(t, &mqttConnInfo{user: "a", pass: "pwd", clientID: "a3", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c3.Close()
|
|
testMQTTCheckConnAck(t, r3, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c3, r3, []*mqttFilter{{filter: "bar", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, c3, nil, r3)
|
|
|
|
c4, r4 := testMQTTConnect(t, &mqttConnInfo{user: "a", pass: "pwd", clientID: "a4", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c4.Close()
|
|
testMQTTCheckConnAck(t, r4, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c4, r4, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, c4, nil, r4)
|
|
|
|
bc, br := testMQTTConnect(t, &mqttConnInfo{user: "b", pass: "pwd", clientID: "b0", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer bc.Close()
|
|
testMQTTCheckConnAck(t, br, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
bc1, br1 := testMQTTConnect(t, &mqttConnInfo{user: "b", pass: "pwd", clientID: "b1", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer bc1.Close()
|
|
testMQTTCheckConnAck(t, br1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, bc1, br1, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, bc1, nil, br1)
|
|
|
|
bc2, br2 := testMQTTConnect(t, &mqttConnInfo{user: "b", pass: "pwd", clientID: "b2", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer bc2.Close()
|
|
testMQTTCheckConnAck(t, br2, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, bc2, br2, []*mqttFilter{{filter: "b", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, bc2, nil, br2)
|
|
|
|
bc3, br3 := testMQTTConnect(t, &mqttConnInfo{user: "b", pass: "pwd", clientID: "b3", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer bc3.Close()
|
|
testMQTTCheckConnAck(t, br3, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, bc3, br3, []*mqttFilter{{filter: "bar", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, bc3, nil, br3)
|
|
|
|
bc4, br4 := testMQTTConnect(t, &mqttConnInfo{user: "b", pass: "pwd", clientID: "b4", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer bc4.Close()
|
|
testMQTTCheckConnAck(t, br4, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, bc4, br4, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, bc4, nil, br4)
|
|
|
|
nc := natsConnect(t, fmt.Sprintf("nats://b:pwd@%s:%d", o.Host, o.Port))
|
|
defer nc.Close()
|
|
|
|
natsPub(t, nc, "foo", []byte("msg0"))
|
|
testMQTTCheckPubMsgNoAck(t, c1, r1, "foo", 0, []byte("msg0"))
|
|
testMQTTCheckPubMsgNoAck(t, c2, r2, "foo", 0, []byte("msg0"))
|
|
testMQTTExpectNothing(t, r3)
|
|
testMQTTExpectNothing(t, r4)
|
|
testMQTTExpectNothing(t, br1)
|
|
testMQTTExpectNothing(t, br2)
|
|
testMQTTCheckPubMsgNoAck(t, bc3, br3, "bar", 0, []byte("msg0"))
|
|
testMQTTCheckPubMsgNoAck(t, bc4, br4, "bar", 0, []byte("msg0"))
|
|
|
|
testMQTTPublish(t, bc, br, 0, false, false, "foo", 0, []byte("msg1"))
|
|
testMQTTCheckPubMsgNoAck(t, c1, r1, "foo", 0, []byte("msg1"))
|
|
testMQTTCheckPubMsgNoAck(t, c2, r2, "foo", 0, []byte("msg1"))
|
|
testMQTTExpectNothing(t, r3)
|
|
testMQTTExpectNothing(t, r4)
|
|
testMQTTExpectNothing(t, br1)
|
|
testMQTTExpectNothing(t, br2)
|
|
testMQTTCheckPubMsgNoAck(t, bc3, br3, "bar", 0, []byte("msg1"))
|
|
testMQTTCheckPubMsgNoAck(t, bc4, br4, "bar", 0, []byte("msg1"))
|
|
|
|
testMQTTPublish(t, bc, br, 1, false, false, "foo", 1, []byte("msg2"))
|
|
testMQTTCheckPubMsgNoAck(t, c1, r1, "foo", 0, []byte("msg2"))
|
|
testMQTTCheckPubMsgNoAck(t, c2, r2, "foo", mqttPubQos1, []byte("msg2"))
|
|
testMQTTExpectNothing(t, r3)
|
|
testMQTTExpectNothing(t, r4)
|
|
testMQTTExpectNothing(t, br1)
|
|
testMQTTExpectNothing(t, br2)
|
|
testMQTTCheckPubMsgNoAck(t, bc3, br3, "bar", 0, []byte("msg2"))
|
|
testMQTTCheckPubMsgNoAck(t, bc4, br4, "bar", mqttPubQos1, []byte("msg2"))
|
|
|
|
// Connection nc is for account B
|
|
check := func(nc *nats.Conn, subj string) {
|
|
t.Helper()
|
|
req, err := json.Marshal(&JSApiStreamInfoRequest{SubjectsFilter: ">"})
|
|
require_NoError(t, err)
|
|
resp, err := nc.Request(fmt.Sprintf(JSApiStreamInfoT, "$MQTT_msgs"), req, time.Second)
|
|
require_NoError(t, err)
|
|
var si StreamInfo
|
|
err = json.Unmarshal(resp.Data, &si)
|
|
require_NoError(t, err)
|
|
require_True(t, si.State.Msgs == 1)
|
|
require_True(t, si.State.NumSubjects == 1)
|
|
require_True(t, si.State.Subjects[subj] == 1)
|
|
}
|
|
// Currently, nc is for account B
|
|
check(nc, "$MQTT.msgs.bar")
|
|
|
|
nc.Close()
|
|
nc = natsConnect(t, fmt.Sprintf("nats://a:pwd@%s:%d", o.Host, o.Port))
|
|
defer nc.Close()
|
|
check(nc, "$MQTT.msgs.foo")
|
|
}
|
|
|
|
func TestMQTTSubRetainedRace(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
|
|
useCases := []struct {
|
|
name string
|
|
f func(t *testing.T, s *Server, o *Options, subTopic, pubTopic string, QOS byte)
|
|
}{
|
|
{"new top level", testMQTTNewSubRetainedRace},
|
|
{"existing top level", testMQTTNewSubWithExistingTopLevelRetainedRace},
|
|
}
|
|
pubTopic := "/bar"
|
|
subTopics := []string{"#", "/bar", "/+", "/#"}
|
|
QOS := []byte{0, 1, 2}
|
|
for _, tc := range useCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for _, subTopic := range subTopics {
|
|
t.Run(subTopic, func(t *testing.T) {
|
|
for _, qos := range QOS {
|
|
t.Run(fmt.Sprintf("QOS%d", qos), func(t *testing.T) {
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
tc.f(t, s, o, subTopic, pubTopic, qos)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testMQTTNewSubRetainedRace(t *testing.T, s *Server, o *Options, subTopic, pubTopic string, QOS byte) {
|
|
expectedFlags := (QOS << 1) | mqttPubFlagRetain
|
|
payload := []byte("testmsg")
|
|
|
|
pubID := nuid.Next()
|
|
pubc, pubr := testMQTTConnectRetry(t, &mqttConnInfo{clientID: pubID, cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 3)
|
|
testMQTTCheckConnAck(t, pubr, mqttConnAckRCConnectionAccepted, false)
|
|
defer testMQTTDisconnectEx(t, pubc, nil, true)
|
|
defer pubc.Close()
|
|
testMQTTPublish(t, pubc, pubr, QOS, false, true, pubTopic, 1, payload)
|
|
|
|
// Wait for retained messages stream to be populated.
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
acc, err := s.lookupAccount(globalAccountName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mset, err := acc.lookupStream(mqttRetainedMsgsStreamName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mset.state().Msgs != 1 {
|
|
return errors.New("retained message not populated yet")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
subID := nuid.Next()
|
|
subc, subr := testMQTTConnect(t, &mqttConnInfo{clientID: subID, cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, subr, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, subc, subr, []*mqttFilter{{filter: subTopic, qos: QOS}}, []byte{QOS})
|
|
|
|
testMQTTCheckPubMsg(t, subc, subr, pubTopic, expectedFlags, payload)
|
|
if QOS == 2 {
|
|
testMQTTCheckPubAck(t, subr, mqttPacketPubRel)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, subc, 1)
|
|
}
|
|
|
|
testMQTTDisconnectEx(t, subc, nil, true)
|
|
subc.Close()
|
|
}
|
|
|
|
func testMQTTNewSubWithExistingTopLevelRetainedRace(t *testing.T, s *Server, o *Options, subTopic, pubTopic string, QOS byte) {
|
|
expectedFlags := (QOS << 1) | mqttPubFlagRetain
|
|
payload := []byte("testmsg")
|
|
|
|
pubID := nuid.Next()
|
|
pubc, pubr := testMQTTConnectRetry(t, &mqttConnInfo{clientID: pubID, cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 3)
|
|
testMQTTCheckConnAck(t, pubr, mqttConnAckRCConnectionAccepted, false)
|
|
defer testMQTTDisconnectEx(t, pubc, nil, true)
|
|
defer pubc.Close()
|
|
|
|
subID := nuid.Next()
|
|
subc, subr := testMQTTConnect(t, &mqttConnInfo{clientID: subID, cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, subr, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Subscribe to `#` first.
|
|
testMQTTSub(t, 1, subc, subr, []*mqttFilter{{filter: `#`, qos: 0}}, []byte{0})
|
|
testMQTTExpectNothing(t, subr)
|
|
|
|
// Publish the retained message with QOS2, see that the `#` subscriber gets it.
|
|
testMQTTPublish(t, pubc, pubr, 2, false, true, pubTopic, 1, payload)
|
|
testMQTTCheckPubMsg(t, subc, subr, pubTopic, 0, payload)
|
|
testMQTTExpectNothing(t, subr)
|
|
|
|
// Wait for retained messages stream to be populated.
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
acc, err := s.lookupAccount(globalAccountName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mset, err := acc.lookupStream(mqttRetainedMsgsStreamName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mset.state().Msgs != 1 {
|
|
return errors.New("retained message not populated yet")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now subscribe to the topic we want to test. We should get the retained
|
|
// message there.
|
|
testMQTTSub(t, 1, subc, subr, []*mqttFilter{{filter: subTopic, qos: QOS}}, []byte{QOS})
|
|
testMQTTCheckPubMsg(t, subc, subr, pubTopic, expectedFlags, payload)
|
|
if QOS == 2 {
|
|
testMQTTCheckPubAck(t, subr, mqttPacketPubRel)
|
|
testMQTTSendPIPacket(mqttPacketPubComp, t, subc, 1)
|
|
}
|
|
|
|
testMQTTDisconnectEx(t, subc, nil, true)
|
|
subc.Close()
|
|
}
|
|
|
|
// Issue https://github.com/nats-io/nats-server/issues/3924
|
|
// The MQTT Server MUST NOT match Topic Filters starting with a wildcard character (# or +),
|
|
// with Topic Names beginning with a $ character [MQTT-4.7.2-1]
|
|
func TestMQTTSubjectWildcardStart(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc1, r1 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc2, r2 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mc3, r3 := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc3.Close()
|
|
testMQTTCheckConnAck(t, r3, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// These will fail already with the bug due to messages already being queue up before the subAck.
|
|
testMQTTSub(t, 1, mc1, r1, []*mqttFilter{{filter: "*", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc1, nil, r1)
|
|
|
|
testMQTTSub(t, 1, mc2, r2, []*mqttFilter{{filter: "#", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, r2)
|
|
|
|
testMQTTSub(t, 1, mc3, r3, []*mqttFilter{{filter: "*/foo", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc2, nil, r2)
|
|
|
|
// Just as a barrier
|
|
natsFlush(t, nc)
|
|
|
|
// Now publish
|
|
|
|
// NATS Publish
|
|
msg := []byte("HELLO WORLD")
|
|
natsPub(t, nc, "foo", msg)
|
|
|
|
// Check messages received
|
|
testMQTTCheckPubMsg(t, mc1, r1, "foo", 0, msg)
|
|
testMQTTExpectNothing(t, r1)
|
|
|
|
testMQTTCheckPubMsg(t, mc2, r2, "foo", 0, msg)
|
|
testMQTTExpectNothing(t, r2)
|
|
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
// Anything that starts with $ is reserved against wildcard subjects like above.
|
|
natsPub(t, nc, "$JS.foo", msg)
|
|
testMQTTExpectNothing(t, r1)
|
|
testMQTTExpectNothing(t, r2)
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
// Now do MQTT QoS-0
|
|
testMQTTPublish(t, mc, r, 0, false, false, "foo", 0, msg)
|
|
|
|
testMQTTCheckPubMsg(t, mc1, r1, "foo", 0, msg)
|
|
testMQTTExpectNothing(t, r1)
|
|
|
|
testMQTTCheckPubMsg(t, mc2, r2, "foo", 0, msg)
|
|
testMQTTExpectNothing(t, r2)
|
|
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
testMQTTPublish(t, mc, r, 0, false, false, "$JS/foo", 1, msg)
|
|
|
|
testMQTTExpectNothing(t, r1)
|
|
testMQTTExpectNothing(t, r2)
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
// Now do MQTT QoS-1
|
|
msg = []byte("HELLO WORLD - RETAINED")
|
|
testMQTTPublish(t, mc, r, 1, false, false, "$JS/foo", 4, msg)
|
|
|
|
testMQTTExpectNothing(t, r1)
|
|
testMQTTExpectNothing(t, r2)
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
testMQTTPublish(t, mc, r, 1, false, false, "foo", 2, msg)
|
|
|
|
testMQTTCheckPubMsg(t, mc1, r1, "foo", 0, msg)
|
|
testMQTTExpectNothing(t, r1)
|
|
|
|
testMQTTCheckPubMsg(t, mc2, r2, "foo", 2, msg)
|
|
testMQTTExpectNothing(t, r2)
|
|
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
testMQTTPublish(t, mc, r, 1, false, false, "foo/foo", 3, msg)
|
|
|
|
testMQTTExpectNothing(t, r1)
|
|
|
|
testMQTTCheckPubMsg(t, mc2, r2, "foo/foo", 2, msg)
|
|
testMQTTExpectNothing(t, r2)
|
|
|
|
testMQTTCheckPubMsg(t, mc3, r3, "foo/foo", 2, msg)
|
|
testMQTTExpectNothing(t, r3)
|
|
|
|
// Make sure we did not retain the messages prefixed with $.
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
si, err := js.StreamInfo(mqttStreamName)
|
|
require_NoError(t, err)
|
|
|
|
require_True(t, si.State.Msgs == 0)
|
|
}
|
|
|
|
func TestMQTTTopicWithDot(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc := natsConnect(t, s.ClientURL(), nats.UserInfo("mqtt", "pwd"))
|
|
defer nc.Close()
|
|
|
|
sub := natsSubSync(t, nc, "*.*")
|
|
|
|
c1, r1 := testMQTTConnect(t, &mqttConnInfo{user: "mqtt", pass: "pwd", clientID: "conn1", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c1.Close()
|
|
testMQTTCheckConnAck(t, r1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c1, r1, []*mqttFilter{{filter: "spBv1.0/plant1", qos: 0}}, []byte{0})
|
|
testMQTTSub(t, 1, c1, r1, []*mqttFilter{{filter: "spBv1.0/plant2", qos: 1}}, []byte{1})
|
|
|
|
c2, r2 := testMQTTConnect(t, &mqttConnInfo{user: "mqtt", pass: "pwd", clientID: "conn2", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c2.Close()
|
|
testMQTTCheckConnAck(t, r2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTPublish(t, c2, r2, 0, false, false, "spBv1.0/plant1", 0, []byte("msg1"))
|
|
testMQTTCheckPubMsg(t, c1, r1, "spBv1.0/plant1", 0, []byte("msg1"))
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, msg.Subject, "spBv1//0.plant1")
|
|
|
|
testMQTTPublish(t, c2, r2, 1, false, false, "spBv1.0/plant2", 1, []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, c1, r1, "spBv1.0/plant2", mqttPubQos1, []byte("msg2"))
|
|
msg = natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, msg.Subject, "spBv1//0.plant2")
|
|
|
|
natsPub(t, nc, "spBv1//0.plant1", []byte("msg3"))
|
|
testMQTTCheckPubMsg(t, c1, r1, "spBv1.0/plant1", 0, []byte("msg3"))
|
|
msg = natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, msg.Subject, "spBv1//0.plant1")
|
|
|
|
natsPub(t, nc, "spBv1//0.plant2", []byte("msg4"))
|
|
testMQTTCheckPubMsg(t, c1, r1, "spBv1.0/plant2", 0, []byte("msg4"))
|
|
msg = natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, msg.Subject, "spBv1//0.plant2")
|
|
}
|
|
|
|
// Issue https://github.com/nats-io/nats-server/issues/4291
|
|
func TestMQTTJetStreamRepublishAndQoS0Subscribers(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
`, t.TempDir())))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
// Setup stream with republish on it.
|
|
_, err := js.AddStream(&nats.StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
RePublish: &nats.RePublish{
|
|
Source: "foo",
|
|
Destination: "mqtt.foo",
|
|
},
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
// Create QoS0 subscriber to catch re-publishes.
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "mqtt/foo", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
msg := []byte("HELLO WORLD")
|
|
_, err = js.Publish("foo", msg)
|
|
require_NoError(t, err)
|
|
|
|
testMQTTCheckPubMsg(t, mc, r, "mqtt/foo", 0, msg)
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTDecodeRetainedMessage(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
consumer_inactive_threshold: %q
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, tdir, "0.2s")))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Connect and publish a retained message, this will be in the "newer" form,
|
|
// with the metadata in the header.
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc, r, 0, false, true, "foo/1", 0, []byte("msg1"))
|
|
mc.Close()
|
|
|
|
// Store a "legacy", JSON-encoded payload directly into the stream.
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
rm := mqttRetainedMsg{
|
|
Origin: "test",
|
|
Subject: "foo.2",
|
|
Topic: "foo/2",
|
|
Flags: mqttPubFlagRetain,
|
|
Msg: []byte("msg2"),
|
|
}
|
|
jsonData, _ := json.Marshal(rm)
|
|
_, err := js.PublishMsg(&nats.Msg{
|
|
Subject: mqttRetainedMsgsStreamSubject + rm.Subject,
|
|
Data: jsonData,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Error publishing retained message to JS directly: %v", err)
|
|
}
|
|
|
|
// Restart the server to see that it picks up both retained messages on restart.
|
|
s.Shutdown()
|
|
s = RunServer(o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Connect again, subscribe, and check that we get both messages.
|
|
mc, r = testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/+", qos: 0}}, []byte{0})
|
|
for i := 0; i < 2; i++ {
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Expected PUBLISH packet %x, got %x", mqttPacketPub, pt)
|
|
}
|
|
_, _, topic, _ := testMQTTGetPubMsgExEx(t, mc, r, mqttPubFlagRetain, pl, "", nil)
|
|
if string(topic) != "foo/1" && string(topic) != "foo/2" {
|
|
t.Fatalf("Expected foo/1 or foo/2, got %q", topic)
|
|
}
|
|
}
|
|
testMQTTExpectNothing(t, r)
|
|
mc.Close()
|
|
|
|
// Clear both retained messages.
|
|
mc, r = testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc, r, 0, false, true, "foo/1", 0, []byte{})
|
|
testMQTTPublish(t, mc, r, 0, false, true, "foo/2", 0, []byte{})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
testMQTTDisconnect(t, mc, nil)
|
|
mc.Close()
|
|
|
|
// Connect again, subscribe, and check that we get nothing.
|
|
mc, r = testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/+", qos: 0}}, []byte{0})
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTSparkbDeathHandling(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// protoMetrics is protobuf-encoded test data, with no timestamp.
|
|
//
|
|
// p := &sproto.Payload{
|
|
// Metrics: []*sproto.Payload_Metric{
|
|
// newSparkbMetric_Uint64(sparkbBDSeqMetric, uint64(sparkbBDSeq)),
|
|
// newSparkbMetric(sparkbDeviceMetric1, "value-at-death-1"),
|
|
// newSparkbMetric(sparkbDeviceMetric2, "value-at-death-2"),
|
|
// },
|
|
// Timestamp: nil,
|
|
// }
|
|
protoMetrics := []byte{0x12, 0x0b, 0x0a, 0x05, 0x62, 0x64, 0x53, 0x65, 0x71, 0x20, 0x04, 0x58, 0x02, 0x12, 0x24, 0x0a, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x31, 0x20, 0x0c, 0x7a, 0x10, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2d, 0x61, 0x74, 0x2d, 0x64, 0x65, 0x61, 0x74, 0x68, 0x2d, 0x31, 0x12, 0x24, 0x0a, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x32, 0x20, 0x0c, 0x7a, 0x10, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2d, 0x61, 0x74, 0x2d, 0x64, 0x65, 0x61, 0x74, 0x68, 0x2d, 0x32}
|
|
|
|
proto0Timestamp := []byte{0x08, 0x00}
|
|
|
|
protoDeadbeefTimestamp := []byte{0x08, 0xef, 0xfd, 0xb6, 0xf5, 0xfd, 0xde, 0xef, 0xd6, 0xde, 0x01}
|
|
|
|
mcPub, rPub := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcPub.Close()
|
|
testMQTTCheckConnAck(t, rPub, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
mcSub, rSub := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mcSub.Close()
|
|
testMQTTCheckConnAck(t, rSub, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mcSub, rSub, []*mqttFilter{{filter: "spBv1.0/ggg/#", qos: 0}}, []byte{0})
|
|
|
|
for _, test := range []*struct {
|
|
name string
|
|
payload []byte
|
|
expectTimestampOffset int
|
|
expectUnchanged bool
|
|
}{
|
|
{"append", protoMetrics, len(protoMetrics), false},
|
|
{"replace at the end", append(protoMetrics, proto0Timestamp...), len(protoMetrics), false},
|
|
{"replace at the start", append(protoDeadbeefTimestamp, protoMetrics...), 0, false},
|
|
{"invalid", []byte{0xde, 0xad, 0xbe, 0xef}, 0, true}, // invalid payload
|
|
{"invalid fixed32", []byte{0x0d, 0x01}, 0, true},
|
|
{"invalid fixed64", []byte{0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, 0, true},
|
|
} {
|
|
var sendPI uint16
|
|
for _, topic := range []string{"NDEATH/nnn", "DDEATH/nnn/ddd"} {
|
|
sendPI++
|
|
t.Run(test.name+" "+topic, func(t *testing.T) {
|
|
testMQTTPublish(t, mcPub, rPub, 1, false, false, "spBv1.0/ggg/"+topic, sendPI, test.payload)
|
|
|
|
flags, pi, _, received := testMQTTGetPubMsgEx(t, mcSub, rSub, "", nil)
|
|
if mqttGetQoS(flags) == 1 {
|
|
testMQTTSendPIPacket(mqttPacketPubAck, t, mcSub, pi)
|
|
}
|
|
|
|
if test.expectUnchanged {
|
|
if !bytes.Equal(test.payload, received) {
|
|
t.Fatalf("Expected payload to be unchanged, got %q", received)
|
|
}
|
|
return
|
|
}
|
|
|
|
if test.expectTimestampOffset > len(received) {
|
|
t.Fatalf("Expected timestamp offset %v to be less than %v", test.expectTimestampOffset, len(received))
|
|
}
|
|
if test.expectTimestampOffset > 0 {
|
|
if !bytes.Equal(protoMetrics, received[:test.expectTimestampOffset]) {
|
|
t.Fatalf("Expected payload to be unchanged up to %v, got %q", test.expectTimestampOffset, received)
|
|
}
|
|
}
|
|
|
|
tsBytes := received[test.expectTimestampOffset:]
|
|
if tsBytes[0] != 0x08 {
|
|
t.Fatalf("Expected timestamp to start with 0x08, got %x", tsBytes)
|
|
}
|
|
v, _, err := protoScanVarint(tsBytes[1:])
|
|
if err != nil {
|
|
t.Fatalf("Error scanning varint: %v", err)
|
|
}
|
|
|
|
ts := time.UnixMilli(int64(v))
|
|
if time.Now().Before(ts) {
|
|
t.Fatalf("Expected timestamp to be in the past, got %v", ts)
|
|
}
|
|
if time.Since(ts) > time.Second {
|
|
t.Fatalf("Expected timestamp to be within a second, got %v", time.Since(ts))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMQTTSparkbBirthHandling(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Publish an NBIRTH message. Make sure it is received as both the original
|
|
// subject, and as a $sparkplug/certificates one.
|
|
|
|
const NBORN = "NODE BORN FAKE MESSAGE"
|
|
const DBORN = "DEVICE BORN FAKE MESSAGE"
|
|
|
|
tests := []*struct {
|
|
topic string
|
|
payload string
|
|
flags byte
|
|
mc net.Conn
|
|
r *mqttReader
|
|
}{
|
|
{
|
|
topic: "spBv1.0/ggg/NBIRTH/nnn",
|
|
payload: NBORN,
|
|
flags: 0,
|
|
},
|
|
{
|
|
topic: "spBv1.0/ggg/DBIRTH/nnn/ddd",
|
|
payload: DBORN,
|
|
flags: 0,
|
|
},
|
|
{
|
|
topic: "$sparkplug/certificates/spBv1.0/ggg/NBIRTH/nnn",
|
|
payload: NBORN,
|
|
flags: mqttPubFlagRetain,
|
|
},
|
|
{
|
|
topic: "$sparkplug/certificates/spBv1.0/ggg/DBIRTH/nnn/ddd",
|
|
payload: DBORN,
|
|
flags: mqttPubFlagRetain,
|
|
},
|
|
}
|
|
|
|
t.Run("$sparkplug messages delivered", func(t *testing.T) {
|
|
// Connect the subscribers, unique for each topic we monitor.
|
|
for i, test := range tests {
|
|
test.mc, test.r = testMQTTConnect(t, &mqttConnInfo{cleanSess: true, clientID: fmt.Sprintf("sub-%v", i)}, o.MQTT.Host, o.MQTT.Port)
|
|
defer test.mc.Close()
|
|
testMQTTCheckConnAck(t, test.r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Subscribne at QoS2 to make sure the messages are posted at QoS0 and
|
|
// not truncated to sub QoS.
|
|
testMQTTSub(t, 1, test.mc, test.r, []*mqttFilter{{filter: test.topic, qos: 2}}, []byte{2})
|
|
}
|
|
|
|
// connect the publisher
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true, clientID: "pub"}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// {Publish} [ND]BIRTH messages. Note that IRL these are protobuf payloads.
|
|
testMQTTPublish(t, mc, r, 0, false, false, "spBv1.0/ggg/NBIRTH/nnn", 0, []byte("NODE BORN FAKE MESSAGE"))
|
|
testMQTTPublish(t, mc, r, 0, false, false, "spBv1.0/ggg/DBIRTH/nnn/ddd", 0, []byte("DEVICE BORN FAKE MESSAGE"))
|
|
|
|
// Check that the subscribers got the messages.
|
|
for _, test := range tests {
|
|
testMQTTCheckPubMsg(t, test.mc, test.r, test.topic, test.flags, []byte(test.payload))
|
|
}
|
|
})
|
|
|
|
t.Run("$sparkplug messages retained", func(t *testing.T) {
|
|
// Connect/subscribe again, and make sure that only retained messages are there.
|
|
for i, test := range tests {
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true, clientID: fmt.Sprintf("sub-%v", i+100)}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: test.topic, qos: 2}}, []byte{2})
|
|
if test.flags&mqttPubFlagRetain != 0 {
|
|
testMQTTCheckPubMsg(t, mc, r, test.topic, test.flags, []byte(test.payload))
|
|
} else {
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMQTTMaxPayloadEnforced(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
server_name: test_mqtt_max_payload
|
|
port: -1
|
|
max_payload: 1024
|
|
mqtt { listen: "127.0.0.1:-1" }
|
|
jetstream: { domain: "TEST", max_mem_store: 8MB, max_file_store: 8MB, store_dir: "`+t.TempDir()+`" }
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
mp := o.MQTT.Port
|
|
host := o.MQTT.Host
|
|
mc, _ := testMQTTConnect(t, &mqttConnInfo{clientID: "cid", cleanSess: true}, host, mp)
|
|
defer mc.Close()
|
|
|
|
oversized := bytes.Repeat([]byte{'A'}, 1500)
|
|
testMQTTSendPublishPacket(t, mc, 0, false, false, "foo", 0, oversized)
|
|
|
|
testMQTTExpectDisconnect(t, mc)
|
|
}
|
|
|
|
func TestMQTTJSApiMapping(t *testing.T) {
|
|
td := t.TempDir()
|
|
hubDir := filepath.Join(td, "hub")
|
|
hubConf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: hub
|
|
listen: "127.0.0.1:-1"
|
|
jetstream {
|
|
domain: "HUB"
|
|
store_dir: "%s"
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
isolate: true
|
|
}
|
|
accounts: {
|
|
SYS: { users: [ { user:s, password:x } ] }
|
|
HUB: {
|
|
jetstream: true
|
|
users: [
|
|
{
|
|
user:h
|
|
password:x
|
|
permissions: {
|
|
subscribe: {allow: ["foo", "bar", "_INBOX.>"], deny: ["baz"] }
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
system_account: SYS
|
|
`, hubDir))
|
|
hub, ohub := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafDir := filepath.Join(td, "leaf")
|
|
leafTmpl := `
|
|
server_name: leaf
|
|
listen: "127.0.0.1:-1"
|
|
jetstream {
|
|
domain: "LEAF"
|
|
store_dir: "%s"
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
js_api_timeout: "500ms"
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{ urls: [ nats-leaf://s:[email protected]:%d ], account: SYS }
|
|
{ urls: [ nats-leaf://h:[email protected]:%d ], account: HUB }
|
|
]
|
|
}
|
|
accounts: {
|
|
SYS: { users: [ { user:s, password:x } ] }
|
|
HUB: {
|
|
jetstream: false
|
|
users: [ { user:h, password:x } ]
|
|
mappings: {
|
|
"$JS.API.>" : "$JS.HUB.API.>"
|
|
%s
|
|
}
|
|
}
|
|
LEAF: {
|
|
jetstream: true
|
|
users: [ { user:l, password:x } ]
|
|
}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
leafConf := createConfFile(t, fmt.Appendf(nil, leafTmpl, leafDir, ohub.LeafNode.Port, ohub.LeafNode.Port, ""))
|
|
leaf, oleaf := RunServerWithConfig(leafConf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, leaf, 2)
|
|
|
|
testRetained := func(user string, port int, msg string) {
|
|
t.Helper()
|
|
// Create a producer
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: user,
|
|
pass: "x",
|
|
}, "127.0.0.1", port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, c, r, 0, false, true, "foo", 0, []byte(msg))
|
|
testMQTTFlush(t, c, nil, r)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
// Create a consumer
|
|
c, r = testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: user,
|
|
pass: "x",
|
|
}, "127.0.0.1", port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubFlagRetain, []byte(msg))
|
|
}
|
|
|
|
// Connect to leaf
|
|
testRetained("l", oleaf.MQTT.Port, "hi leaf")
|
|
|
|
// Now same test in the hub
|
|
testRetained("h", ohub.MQTT.Port, "hi hub")
|
|
|
|
// Now test the hub account in the leaf node. We expect it to fail
|
|
// because we don't have the mapping for the sessions to be persisted
|
|
// in the hub.
|
|
_, _, err := testMQTTConnectRetryWithError(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "h",
|
|
pass: "x",
|
|
}, oleaf.MQTT.Host, oleaf.MQTT.Port, 0)
|
|
if err == nil {
|
|
t.Fatal("Expected failure to connect, but did connect")
|
|
}
|
|
|
|
// Config reload the leaf server to add the missing mapping.
|
|
reloadUpdateConfig(t, leaf, leafConf, fmt.Sprintf(leafTmpl,
|
|
leafDir, ohub.LeafNode.Port, ohub.LeafNode.Port, `"$MQTT.sess.LEAF.>" : "$MQTT.sess.HUB.>"`))
|
|
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "h",
|
|
pass: "x",
|
|
}, "127.0.0.1", oleaf.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubFlagRetain, []byte("hi hub"))
|
|
}
|
|
|
|
func TestMQTTMappingsQoS0(t *testing.T) {
|
|
td := t.TempDir()
|
|
dir := filepath.Join(td, "js")
|
|
conf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: server
|
|
jetstream {
|
|
domain: "A"
|
|
store_dir: "%s"
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts: {
|
|
A: {
|
|
jetstream: true
|
|
users: [ { user:a, password:x }]
|
|
exports: [ { stream: "foo.>" }]
|
|
mappings: { "baz.>" : "bazz.>" }
|
|
}
|
|
B: {
|
|
jetstream: true
|
|
users: [ { user:b, password:x }]
|
|
imports: [ { stream: { account: A, subject: "foo.>" }, to: "bar.>" } ]
|
|
}
|
|
C: {
|
|
jetstream: true
|
|
users: [ { user:c, password:x }]
|
|
imports: [ { stream: { account: A, subject: "foo.>" }, to: "baz.>" } ]
|
|
}
|
|
D: {
|
|
jetstream: true
|
|
users: [ { user:d, password:x }]
|
|
# Same imports than for account C
|
|
imports: [ { stream: { account: A, subject: "foo.>" }, to: "baz.>" } ]
|
|
}
|
|
}
|
|
`, dir))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// First, we check for mapping inside the same account A.
|
|
aSubConn, aSubReader := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
clientID: "sub",
|
|
user: "a",
|
|
pass: "x",
|
|
}, "127.0.0.1", o.MQTT.Port)
|
|
defer aSubConn.Close()
|
|
testMQTTCheckConnAck(t, aSubReader, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, aSubConn, aSubReader, []*mqttFilter{{filter: "#", qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, aSubConn, nil, aSubReader)
|
|
|
|
aPubConn, aPubReader := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
clientID: "pub",
|
|
user: "a",
|
|
pass: "x",
|
|
}, "127.0.0.1", o.MQTT.Port)
|
|
defer aPubConn.Close()
|
|
testMQTTCheckConnAck(t, aPubReader, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, aPubConn, aPubReader, 0, false, true, "baz/x", 0, []byte("msg1"))
|
|
// Because of mapping, the subscription should receive the message on "bazz/x"
|
|
testMQTTCheckPubMsg(t, aSubConn, aSubReader, "bazz/x", 0, []byte("msg1"))
|
|
|
|
// Helper to create a connection and sub for a given user on given topic
|
|
createSub := func(user, topic string) (net.Conn, *mqttReader) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
clientID: nuid.Next(),
|
|
user: user,
|
|
pass: "x",
|
|
}, "127.0.0.1", o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: topic, qos: 0}}, []byte{0})
|
|
testMQTTFlush(t, c, nil, r)
|
|
return c, r
|
|
}
|
|
bConn1, bReader1 := createSub("b", "bar/#")
|
|
defer bConn1.Close()
|
|
bConn2, bReader2 := createSub("b", "bar/+")
|
|
defer bConn2.Close()
|
|
cConn1, cReader1 := createSub("c", "baz/#")
|
|
defer cConn1.Close()
|
|
cConn2, cReader2 := createSub("c", "baz/+")
|
|
defer cConn2.Close()
|
|
dConn1, dReader1 := createSub("d", "baz/#")
|
|
defer dConn1.Close()
|
|
dConn2, dReader2 := createSub("d", "baz/+")
|
|
defer dConn2.Close()
|
|
|
|
// Now from "A" publisher, publishes on "foo/x", and with mapping across
|
|
// accounts, we should get the expected results. We send 2 messages in
|
|
// a row and verify that we get those, and no more than that.
|
|
testMQTTPublish(t, aPubConn, aPubReader, 0, false, true, "foo/x", 0, []byte("msg2"))
|
|
testMQTTPublish(t, aPubConn, aPubReader, 0, false, true, "foo/x", 0, []byte("msg3"))
|
|
|
|
// "B" consumers should receive on "bar/x"
|
|
conns := []net.Conn{bConn1, bConn2}
|
|
readers := []*mqttReader{bReader1, bReader2}
|
|
for i := range len(conns) {
|
|
testMQTTCheckPubMsg(t, conns[i], readers[i], "bar/x", 0, []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, conns[i], readers[i], "bar/x", 0, []byte("msg3"))
|
|
testMQTTExpectNothing(t, readers[i])
|
|
}
|
|
// For "C" and "D" consumers, it should be "baz/x"
|
|
conns = []net.Conn{cConn1, cConn2, dConn1, dConn2}
|
|
readers = []*mqttReader{cReader1, cReader2, dReader1, dReader2}
|
|
for i := range len(conns) {
|
|
testMQTTCheckPubMsg(t, conns[i], readers[i], "baz/x", 0, []byte("msg2"))
|
|
testMQTTCheckPubMsg(t, conns[i], readers[i], "baz/x", 0, []byte("msg3"))
|
|
testMQTTExpectNothing(t, readers[i])
|
|
}
|
|
}
|
|
|
|
func TestMQTTSliceHeadersAndDecodeRetainedMessage(t *testing.T) {
|
|
// First check low level mqttSliceHeaders
|
|
for _, test := range []struct {
|
|
name string
|
|
hdr string
|
|
expected []string
|
|
}{
|
|
// Valid cases
|
|
{"one key and some random", hdrLine + "key1:val1\r\nsomeotherkey:someval\r\n", []string{"val1", _EMPTY_, _EMPTY_}},
|
|
{"two keys", hdrLine + "key2:val2\r\nthisisnotkey1:someval\r\nkey1:val1\r\n", []string{"val1", "val2", _EMPTY_}},
|
|
{"three keys", hdrLine + "key2:val2\r\nkey3:val3\r\nkey1:val1\r\n", []string{"val1", "val2", "val3"}},
|
|
{"space before value", hdrLine + "somekey:someval\r\nkey2: val2withspacebefore\r\n", []string{_EMPTY_, "val2withspacebefore", _EMPTY_}},
|
|
{"space between key and colon sign", hdrLine + "key1:val1\r\nkey2 :val2\r\nkey3 : val3\r\n", []string{"val1", "val2", "val3"}},
|
|
// Error cases
|
|
{"no hdr line", "key1:val1\r\n", []string{_EMPTY_, _EMPTY_, _EMPTY_}},
|
|
{"key length 0", hdrLine + "key1:val1\r\n:val2\r\nkey3:val3\r\n", []string{"val1", _EMPTY_, _EMPTY_}},
|
|
{"key is only spaces", hdrLine + "key1:val1\r\nkey2:val2\r\n :val3\r\n", []string{"val1", "val2", _EMPTY_}},
|
|
{"value no crlf", hdrLine + "key1:val1\r\nkey2:val2", []string{"val1", _EMPTY_, _EMPTY_}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
headers := map[string][]byte{
|
|
"key1": nil,
|
|
"key2": nil,
|
|
"key3": nil,
|
|
}
|
|
mqttSliceHeaders(headers, []byte(test.hdr))
|
|
for i := range len(headers) {
|
|
key := fmt.Sprintf("key%d", i+1)
|
|
val := string(headers[key])
|
|
if ev := test.expected[i]; ev != val {
|
|
t.Fatalf("For key %q, expected value to be %q, got %q", key, ev, val)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Now test mqttDecodeRetainedMessage() itself.
|
|
t.Run("flag with delete marker", func(t *testing.T) {
|
|
hdr := fmt.Appendf(nil, "%sNmqtt-RFlags:%c1\r\n\r\n", hdrLine, mqttRetainedFlagDelMarker)
|
|
rm, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rm.Flags, 1)
|
|
})
|
|
t.Run("flag not a number", func(t *testing.T) {
|
|
hdr := fmt.Appendf(nil, "%sNmqtt-RFlags:bad\r\n\r\n", hdrLine)
|
|
_, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, []byte("msg"))
|
|
require_Error(t, err, errMQTTInvalidRetainFlags)
|
|
})
|
|
t.Run("flag not a number with delete marker", func(t *testing.T) {
|
|
hdr := fmt.Appendf(nil, "%sNmqtt-RFlags:%cad\r\n\r\n", hdrLine, mqttRetainedFlagDelMarker)
|
|
_, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, []byte("msg"))
|
|
require_Error(t, err, errMQTTInvalidRetainFlags)
|
|
})
|
|
t.Run("flag too big", func(t *testing.T) {
|
|
hdr := fmt.Appendf(nil, "%sNmqtt-RFlags:%c15\r\n\r\n", hdrLine, mqttRetainedFlagDelMarker)
|
|
_, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, []byte("msg"))
|
|
require_Error(t, err, errMQTTInvalidRetainFlags)
|
|
})
|
|
t.Run("flag invalid qos", func(t *testing.T) {
|
|
hdr := fmt.Appendf(nil, "%sNmqtt-RFlags:%c7\r\n\r\n", hdrLine, mqttRetainedFlagDelMarker)
|
|
_, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, []byte("msg"))
|
|
require_Error(t, err, errMQTTInvalidRetainFlags)
|
|
})
|
|
t.Run("decode retained msg with space before header value", func(t *testing.T) {
|
|
msg, hdrLen := mqttEncodeRetainedMessage(&mqttRetainedMsg{
|
|
Topic: "foo/x",
|
|
Origin: " Origin", // Add spaces in front on purpose
|
|
Source: "Source",
|
|
Flags: 1,
|
|
Msg: []byte("msg1"),
|
|
})
|
|
hdr := msg[:hdrLen]
|
|
msg = msg[hdrLen:]
|
|
rm, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.foo.x", hdr, msg)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rm.Topic, "foo/x")
|
|
require_Equal(t, rm.Subject, "foo.x")
|
|
require_Equal(t, rm.Origin, "Origin")
|
|
require_Equal(t, rm.Source, "Source")
|
|
require_Equal(t, rm.Flags, 1)
|
|
require_Equal(t, string(rm.Msg), "msg1")
|
|
})
|
|
t.Run("decode retained msg with subject transformed", func(t *testing.T) {
|
|
msg, hdrLen := mqttEncodeRetainedMessage(&mqttRetainedMsg{
|
|
Topic: "foo/x",
|
|
Origin: "Origin",
|
|
Source: "Source",
|
|
Flags: 1,
|
|
Msg: []byte("msg2"),
|
|
})
|
|
hdr := msg[:hdrLen]
|
|
msg = msg[hdrLen:]
|
|
// Use different subject when calling the function. Make sure the
|
|
// topic is properly reflecting the subject.
|
|
rm, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, msg)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rm.Topic, "bar/x")
|
|
require_Equal(t, rm.Subject, "bar.x")
|
|
require_Equal(t, rm.Origin, "Origin")
|
|
require_Equal(t, rm.Source, "Source")
|
|
require_Equal(t, rm.Flags, 1)
|
|
require_Equal(t, string(rm.Msg), "msg2")
|
|
})
|
|
t.Run("decode deleted retained message", func(t *testing.T) {
|
|
msg, hdrLen := mqttEncodeRetainedMessage(&mqttRetainedMsg{
|
|
Topic: "foo/x",
|
|
Origin: "Origin",
|
|
Source: "Source",
|
|
Flags: 1,
|
|
Msg: nil,
|
|
})
|
|
hdr := msg[:hdrLen]
|
|
msg = msg[hdrLen:]
|
|
// Use different subject too
|
|
rm, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", hdr, msg)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rm.Topic, "bar/x")
|
|
require_Equal(t, rm.Subject, "bar.x")
|
|
require_Equal(t, rm.Origin, "Origin")
|
|
require_Equal(t, rm.Source, "Source")
|
|
require_Equal(t, rm.Flags, 1)
|
|
require_Len(t, len(rm.Msg), 0)
|
|
})
|
|
t.Run("decode retained message as JSON with bad flags", func(t *testing.T) {
|
|
rmo := &mqttRetainedMsg{Flags: 15}
|
|
msg, err := json.Marshal(rmo)
|
|
require_NoError(t, err)
|
|
_, err = mqttDecodeRetainedMessage("$MQTT.rmsgs.foo.x", nil, msg)
|
|
require_Error(t, err, errMQTTInvalidRetainFlags)
|
|
})
|
|
t.Run("decode retained message as JSON subject transform", func(t *testing.T) {
|
|
rmo := &mqttRetainedMsg{
|
|
Topic: "foo/x",
|
|
Origin: "Origin",
|
|
Source: "Source",
|
|
Flags: 1,
|
|
Msg: []byte("hello"),
|
|
}
|
|
msg, err := json.Marshal(rmo)
|
|
require_NoError(t, err)
|
|
rm, err := mqttDecodeRetainedMessage("$MQTT.rmsgs.bar.x", nil, msg)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rm.Topic, "bar/x")
|
|
require_Equal(t, rm.Subject, "bar.x")
|
|
require_Equal(t, rm.Origin, "Origin")
|
|
require_Equal(t, rm.Source, "Source")
|
|
require_Equal(t, rm.Flags, 1)
|
|
require_Equal(t, string(rm.Msg), "hello")
|
|
})
|
|
}
|
|
|
|
func TestMQTTRetainedMessageWithDelSubjectIsNotRestored(t *testing.T) {
|
|
tdir := t.TempDir()
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream {
|
|
store_dir = %q
|
|
}
|
|
|
|
mqtt {
|
|
listen: 127.0.0.1:-1
|
|
consumer_inactive_threshold: %q
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
conf := createConfFile(t, fmt.Appendf(nil, tmpl, tdir, "0.2s"))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Publish one normal retained message through MQTT.
|
|
mc, r := testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc, r, 0, false, true, "foo/ok", 0, []byte("ok"))
|
|
mc.Close()
|
|
|
|
// Store a legacy retained message directly into JetStream using a subject
|
|
// that contains DEL. That subject can no longer be indexed after restore.
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
rm := mqttRetainedMsg{
|
|
Origin: "test",
|
|
Subject: "foo.\x7fbad",
|
|
Topic: "foo/\x7fbad",
|
|
Flags: mqttPubFlagRetain,
|
|
Msg: []byte("bad"),
|
|
}
|
|
jsonData, _ := json.Marshal(rm)
|
|
_, err := js.PublishMsg(&nats.Msg{
|
|
Subject: mqttRetainedMsgsStreamSubject + rm.Subject,
|
|
Data: jsonData,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
// Restart the server so retained-message recovery runs through
|
|
// processRetainedMsg().
|
|
s.Shutdown()
|
|
s = RunServer(o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, r = testMQTTConnectRetry(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port, 5)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo/+", qos: 0}}, []byte{0})
|
|
testMQTTCheckPubMsg(t, mc, r, "foo/ok", mqttPubFlagRetain, []byte("ok"))
|
|
testMQTTExpectNothing(t, r)
|
|
}
|
|
|
|
func TestMQTTRetainedMsgRemovedFromMapIfNotInStream(t *testing.T) {
|
|
mqttRetainedCacheTTL = 250 * time.Millisecond
|
|
defer func() { mqttRetainedCacheTTL = mqttDefaultRetainedCacheTTL }()
|
|
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{clientID: "pub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, c, r, 0, false, true, "foo", 0, []byte("msg1"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
checkRetained := func(expected string) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 0}}, []byte{0})
|
|
if expected == _EMPTY_ {
|
|
testMQTTExpectNothing(t, r)
|
|
} else {
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubFlagRetain, []byte(expected))
|
|
}
|
|
}
|
|
checkRetained("msg1")
|
|
|
|
testMQTTPublish(t, c, r, 0, false, true, "foo", 0, []byte("msg2"))
|
|
testMQTTFlush(t, c, nil, r)
|
|
|
|
checkRetained("msg2")
|
|
|
|
// Now we will get the current sequence for the retained message and
|
|
// remove it from the stream. We expect to get a warning that indicates
|
|
// that the load failed. Restarting the subscription should not longer
|
|
// cause this warning and the retained message should have been removed
|
|
// from the map/cache.
|
|
l := &captureWarnLogger{warn: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
asm := testMQTTGetAccountSessionManager(t, s, "pub")
|
|
// Make sure it is in the cache
|
|
rm := asm.getCachedRetainedMsg("foo")
|
|
require_NotNil(t, rm)
|
|
// Get the retained message sequence from the tree.
|
|
asm.mu.RLock()
|
|
rf, ok := asm.retmsgs.Find([]byte("foo"))
|
|
asm.mu.RUnlock()
|
|
require_True(t, ok)
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
err := js.DeleteMsg(mqttRetainedMsgsStreamName, rf.sseq)
|
|
require_NoError(t, err)
|
|
|
|
// Wait for more than the cache TTL
|
|
time.Sleep(2 * mqttRetainedCacheTTL)
|
|
|
|
checkRetained(_EMPTY_)
|
|
|
|
// We should have got a warning.
|
|
select {
|
|
case w := <-l.warn:
|
|
if !strings.Contains(w, ApiErrors[JSNoMessageFoundErr].Description) {
|
|
t.Fatalf("Unexpected warning: %q", w)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("Test timed out")
|
|
}
|
|
|
|
// But restarting it should not cause the server to try to load the retained
|
|
// message again. So we should not have a warning.
|
|
checkRetained(_EMPTY_)
|
|
|
|
select {
|
|
case w := <-l.warn:
|
|
if strings.Contains(w, ApiErrors[JSNoMessageFoundErr].Description) {
|
|
t.Fatalf("Got the warning: %q", w)
|
|
}
|
|
case <-time.After(250 * time.Millisecond):
|
|
// OK
|
|
}
|
|
|
|
// Finally, check that the retmsgs map is empty.
|
|
asm.mu.RLock()
|
|
ok = asm.retmsgs.Size() == 0
|
|
asm.mu.RUnlock()
|
|
require_True(t, ok)
|
|
}
|
|
|
|
func TestMQTTCrossAccountRetain(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
importTransform string
|
|
sourceDest string
|
|
bDest string
|
|
getLastMsgSubj string
|
|
}{
|
|
{"without transform", "", "", "foo/x", "$MQTT.rmsgs.foo.x"},
|
|
{"with transform", `, to: "foobar.>"`, "$MQTT.rmsgs.foobar.>", "foobar/x", "$MQTT.rmsgs.foobar.x"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
td := t.TempDir()
|
|
dir := filepath.Join(td, "js")
|
|
conf := createConfFile(t, fmt.Appendf(nil, `
|
|
server_name: server
|
|
listen: "127.0.0.1:-1"
|
|
jetstream {
|
|
domain: "MYDOMAIN"
|
|
store_dir: "%s"
|
|
}
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts: {
|
|
A: {
|
|
jetstream: true
|
|
users: [ { user:a, password:x }]
|
|
exports: [
|
|
{ stream: "foo.>" }
|
|
{ service: "$JS.API.>", response_type: stream }
|
|
{ stream: "a2b.>" }
|
|
]
|
|
}
|
|
B: {
|
|
jetstream: true
|
|
users: [ { user:b, password:x }]
|
|
imports: [
|
|
{ stream: { account: A, subject: "foo.>" }%s }
|
|
{ service: { account: A, subject: "$JS.API.>"}, to: "A.$JS.API.>" }
|
|
{ stream: { account: A, subject: "a2b.>" } }
|
|
]
|
|
}
|
|
}
|
|
`, dir, test.importTransform))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// Connect a user on "B" to create the MQTT assets.
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{cleanSess: true, user: "b", pass: "x"}, "127.0.0.1", o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
|
|
pubRetained := func(user, dest, msg string) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: user,
|
|
pass: "x",
|
|
}, "127.0.0.1", o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, c, r, 0, false, true, dest, 0, []byte(msg))
|
|
testMQTTFlush(t, c, nil, r)
|
|
}
|
|
|
|
// Publish a retained message from account "A".
|
|
retainInAMsg := "Retain in A"
|
|
pubRetained("a", "foo/x", retainInAMsg)
|
|
|
|
// Now we are going to do something unusual, which is to update
|
|
// the MQTT retain stream in "B" to source from "A".
|
|
nc, js := jsClientConnect(t, s, nats.UserInfo("b", "x"))
|
|
defer nc.Close()
|
|
|
|
si, err := js.StreamInfo(mqttRetainedMsgsStreamName)
|
|
require_NoError(t, err)
|
|
src := &nats.StreamSource{
|
|
Name: mqttRetainedMsgsStreamName,
|
|
External: &nats.ExternalStream{
|
|
APIPrefix: "A.$JS.API",
|
|
DeliverPrefix: "a2b",
|
|
},
|
|
SubjectTransforms: []nats.SubjectTransformConfig{
|
|
{
|
|
Source: mqttRetainedMsgsStreamSubject + "foo.>",
|
|
Destination: test.sourceDest,
|
|
},
|
|
},
|
|
}
|
|
si.Config.Sources = []*nats.StreamSource{src}
|
|
_, err = js.UpdateStream(&si.Config)
|
|
require_NoError(t, err)
|
|
|
|
// Now wait to make sure that the "B" retained messages stream
|
|
// contains the message with body "Retain in A"
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
msg, err := js.GetLastMsg(mqttRetainedMsgsStreamName, test.getLastMsgSubj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !bytes.Contains(msg.Data, []byte(retainInAMsg)) {
|
|
return fmt.Errorf("Message is not from A: %q", msg.Data)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
getRetained := func(user, dest, msg string) {
|
|
t.Helper()
|
|
c, r := testMQTTConnect(t, &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: user,
|
|
pass: "x",
|
|
}, "127.0.0.1", o.MQTT.Port)
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: dest, qos: 0}}, []byte{0})
|
|
if msg == _EMPTY_ {
|
|
testMQTTExpectNothing(t, r)
|
|
} else {
|
|
testMQTTCheckPubMsg(t, c, r, dest, mqttPubFlagRetain, []byte(msg))
|
|
}
|
|
}
|
|
|
|
// The retained message in account A should of course be "Retain in A"
|
|
getRetained("a", "foo/x", retainInAMsg)
|
|
// But because of the sourcing, the retained message in "B" should be
|
|
// the retained message from the "A" account.
|
|
getRetained("b", test.bDest, retainInAMsg)
|
|
|
|
// Now publish a retained message from "B" account and make sure that
|
|
// it is correctly replacing "Retain in A".
|
|
retainInBMsg := "Retain in B"
|
|
pubRetained("b", test.bDest, retainInBMsg)
|
|
// Check that we can receive it.
|
|
getRetained("b", test.bDest, retainInBMsg)
|
|
// And "A" still has the "Retain in A" message.
|
|
getRetained("a", "foo/x", retainInAMsg)
|
|
|
|
// Publish from "A" a new message:
|
|
retainInAMsg = "Retain in A2"
|
|
pubRetained("a", "foo/x", retainInAMsg)
|
|
// Make sure that this retained appears on "A" and "B".
|
|
getRetained("a", "foo/x", retainInAMsg)
|
|
getRetained("b", test.bDest, retainInAMsg)
|
|
|
|
// Now publish an empty body retained message from "A". This
|
|
// should remove the retained message from both "A" and "B".
|
|
pubRetained("a", "foo/x", _EMPTY_)
|
|
|
|
// We will check that the message gets removed from the stream.
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
_, err := js.GetLastMsg(mqttRetainedMsgsStreamName, test.getLastMsgSubj)
|
|
if err == nats.ErrMsgNotFound {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Message still present or unexpected error %v", err)
|
|
})
|
|
// The helper will use "expect nothing" if the given string is empty.
|
|
getRetained("a", "foo/x", _EMPTY_)
|
|
getRetained("b", test.bDest, _EMPTY_)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMQTTSessionPersistSpoofPrevented verifies that an MQTT client cannot
|
|
// publish to internal $MQTT.JSA.* subjects to spoof session-persist messages
|
|
// and evict other sessions.
|
|
func TestMQTTSessionPersistSpoofPrevented(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{
|
|
{
|
|
Username: "victim",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo.>"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{"foo.>"}},
|
|
},
|
|
},
|
|
{
|
|
Username: "attacker",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo.>"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{"foo.>"}},
|
|
},
|
|
},
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
// Connect the victim MQTT client with a persistent session.
|
|
vc, vr := testMQTTConnect(t, &mqttConnInfo{
|
|
clientID: "victim",
|
|
user: "victim",
|
|
pass: "pass",
|
|
cleanSess: true,
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer vc.Close()
|
|
testMQTTCheckConnAck(t, vr, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, vc, vr, []*mqttFilter{{filter: "foo/+", qos: 1}}, []byte{1})
|
|
testMQTTFlush(t, vc, nil, vr)
|
|
|
|
// Connect the attacker MQTT client.
|
|
ac, ar := testMQTTConnect(t, &mqttConnInfo{
|
|
clientID: "attacker",
|
|
user: "attacker",
|
|
pass: "pass",
|
|
cleanSess: true,
|
|
}, o.MQTT.Host, o.MQTT.Port)
|
|
defer ac.Close()
|
|
testMQTTCheckConnAck(t, ar, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
// Craft a forged session-persist message targeting the victim's session.
|
|
// MQTT topic "/" maps to NATS ".", so this becomes:
|
|
// $MQTT.JSA.fakenode.SP.<victimHash>.fakeuuid
|
|
victimHash := getHash("victim")
|
|
spoofTopic := fmt.Sprintf("$MQTT/JSA/fakenode/%s/%s/fakeuuid", mqttJSASessPersist, victimHash)
|
|
forgedPayload := []byte(`{"stream":"` + mqttSessStreamName + `","seq":999}`)
|
|
|
|
// Attempt the spoofed publish. With the fix, the implicit allow bypass
|
|
// for MQTT clients on $MQTT.* subjects is removed, so this publish is
|
|
// denied by the permission check (attacker only has "foo.>" allow).
|
|
testMQTTPublish(t, ac, ar, 0, false, false, spoofTopic, 0, forgedPayload)
|
|
testMQTTFlush(t, ac, nil, ar)
|
|
|
|
// Give the server time to process if the message leaked through.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Verify the victim session was NOT evicted by round-tripping a message.
|
|
testMQTTPublish(t, ac, ar, 0, false, false, "foo/bar", 0, []byte("still-alive"))
|
|
testMQTTCheckPubMsg(t, vc, vr, "foo/bar", 0, []byte("still-alive"))
|
|
}
|
|
|
|
func TestMQTTPublishSubjectLeafNodeParserFailures(t *testing.T) {
|
|
const (
|
|
hdrLen = 25
|
|
leafControlLimit = MAX_CONTROL_LINE_SIZE * 16
|
|
)
|
|
|
|
hdr := []byte("NATS/1.0\r\nNmqtt-Pub:0\r\n\r\n")
|
|
require_Equal(t, len(hdr), hdrLen)
|
|
|
|
convertMQTTTopic := func(t *testing.T, topic []byte) []byte {
|
|
t.Helper()
|
|
require_NoError(t, mqttValidateTopic(topic, "topic"))
|
|
subject, err := mqttTopicToNATSPubSubject(topic)
|
|
require_NoError(t, err)
|
|
return subject
|
|
}
|
|
|
|
parseLeafHMSG := func(subject []byte) error {
|
|
c := dummyClient()
|
|
c.kind = LEAF
|
|
c.leaf = &leaf{}
|
|
c.flags.set(connectReceived)
|
|
proto := fmt.Appendf(nil, "HMSG %s %d %d\r\n", subject, hdrLen, hdrLen)
|
|
proto = append(proto, hdr...)
|
|
proto = append(proto, _CRLF_...)
|
|
return c.parse(proto)
|
|
}
|
|
|
|
leafHMSGArgLen := func(subject []byte) int {
|
|
return len(subject) + len(" 25 25")
|
|
}
|
|
|
|
makeRepeatedLevelTopic := func(levels int) []byte {
|
|
topic := bytes.Repeat([]byte("a."), levels)
|
|
return topic[:len(topic)-1]
|
|
}
|
|
|
|
makeSlashExpansionTopic := func(t *testing.T) []byte {
|
|
t.Helper()
|
|
var topic []byte
|
|
for {
|
|
if len(topic) > 0 {
|
|
topic = append(topic, '/', '/')
|
|
}
|
|
topic = append(topic, 'a')
|
|
subject := convertMQTTTopic(t, topic)
|
|
if len(subject) > leafControlLimit-len(" 25 25") {
|
|
return topic
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
topic []byte
|
|
wantErr bool
|
|
wantExpansion bool
|
|
wantArgLen int
|
|
}{
|
|
{
|
|
name: "converted subject at leaf control line limit is accepted",
|
|
topic: bytes.Repeat([]byte{'a'}, leafControlLimit-len(" 25 25")),
|
|
wantErr: false,
|
|
wantArgLen: leafControlLimit,
|
|
},
|
|
{
|
|
name: "literal topic exceeds leaf control line limit",
|
|
topic: bytes.Repeat([]byte{'a'}, leafControlLimit-len(" 25 25")+1),
|
|
wantErr: true,
|
|
wantArgLen: leafControlLimit + 1,
|
|
},
|
|
{
|
|
name: "dot expansion can make a shorter topic exceed leaf control line limit",
|
|
topic: makeRepeatedLevelTopic(21845),
|
|
wantErr: true,
|
|
wantExpansion: true,
|
|
},
|
|
{
|
|
name: "slash escaping can make a shorter topic exceed leaf control line limit",
|
|
topic: makeSlashExpansionTopic(t),
|
|
wantErr: true,
|
|
wantExpansion: true,
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
subject := convertMQTTTopic(t, test.topic)
|
|
if test.wantArgLen > 0 {
|
|
require_Equal(t, leafHMSGArgLen(subject), test.wantArgLen)
|
|
}
|
|
if test.wantExpansion {
|
|
require_True(t, len(test.topic) <= len(subject))
|
|
}
|
|
if err := parseLeafHMSG(subject); test.wantErr {
|
|
require_Error(t, err, ErrMaxControlLine)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
topic []byte
|
|
}{
|
|
{name: "space", topic: []byte("foo bar")},
|
|
{name: "tab", topic: []byte("foo\tbar")},
|
|
{name: "carriage return", topic: []byte("foo\rbar")},
|
|
{name: "line feed", topic: []byte("foo\nbar")},
|
|
{name: "carriage return line feed", topic: []byte("foo\r\nbar")},
|
|
{name: "null", topic: []byte("foo\x00bar")},
|
|
{name: "invalid utf8", topic: []byte{0xff}},
|
|
{name: "single level wildcard", topic: []byte("foo/+")},
|
|
{name: "multi level wildcard", topic: []byte("foo/#")},
|
|
} {
|
|
t.Run("rejected before leaf forwarding/"+test.name, func(t *testing.T) {
|
|
if err := mqttValidateTopic(test.topic, "topic"); err == nil {
|
|
_, err = mqttTopicToNATSPubSubject(test.topic)
|
|
require_Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Benchmarks
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
const (
|
|
mqttPubSubj = "a"
|
|
mqttBenchBufLen = 32768
|
|
)
|
|
|
|
func mqttBenchPubQoS0(b *testing.B, subject, payload string, numSubs int) {
|
|
b.StopTimer()
|
|
b.ReportAllocs()
|
|
o := testMQTTDefaultOptions()
|
|
s := RunServer(o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{clientID: "pub", cleanSess: true}
|
|
c, br := testMQTTConnect(b, ci, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(b, br, mqttConnAckRCConnectionAccepted, false)
|
|
w := newMQTTWriter(0)
|
|
testMQTTWritePublishPacket(b, w, 0, false, false, subject, 0, []byte(payload))
|
|
sendOp := w.Bytes()
|
|
|
|
dch := make(chan error, 1)
|
|
totalSize := int64(len(sendOp))
|
|
cdch := 0
|
|
|
|
createSub := func(i int) {
|
|
ci := &mqttConnInfo{clientID: fmt.Sprintf("sub%d", i), cleanSess: true}
|
|
cs, brs := testMQTTConnect(b, ci, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(b, brs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(b, 1, cs, brs, []*mqttFilter{{filter: subject, qos: 0}}, []byte{0})
|
|
testMQTTFlush(b, cs, nil, brs)
|
|
|
|
w := newMQTTWriter(0)
|
|
varHeaderAndPayload := 2 + len(subject) + len(payload)
|
|
w.WriteVarInt(varHeaderAndPayload)
|
|
size := 1 + w.Len() + varHeaderAndPayload
|
|
totalSize += int64(size)
|
|
|
|
go func() {
|
|
mqttBenchConsumeMsgQoS0(cs, int64(b.N)*int64(size), dch)
|
|
cs.Close()
|
|
}()
|
|
}
|
|
for i := 0; i < numSubs; i++ {
|
|
createSub(i + 1)
|
|
cdch++
|
|
}
|
|
|
|
bw := bufio.NewWriterSize(c, mqttBenchBufLen)
|
|
b.SetBytes(totalSize)
|
|
b.StartTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
bw.Write(sendOp)
|
|
}
|
|
testMQTTFlush(b, c, bw, br)
|
|
for i := 0; i < cdch; i++ {
|
|
if e := <-dch; e != nil {
|
|
b.Fatal(e.Error())
|
|
}
|
|
}
|
|
b.StopTimer()
|
|
c.Close()
|
|
s.Shutdown()
|
|
}
|
|
|
|
func mqttBenchConsumeMsgQoS0(c net.Conn, total int64, dch chan<- error) {
|
|
var buf [mqttBenchBufLen]byte
|
|
var err error
|
|
var n int
|
|
for size := int64(0); size < total; {
|
|
n, err = c.Read(buf[:])
|
|
if err != nil {
|
|
break
|
|
}
|
|
size += int64(n)
|
|
}
|
|
dch <- err
|
|
}
|
|
|
|
func mqttBenchPubQoS1(b *testing.B, subject, payload string, numSubs int) {
|
|
b.StopTimer()
|
|
o := testMQTTDefaultOptions()
|
|
o.MQTT.MaxAckPending = 0xFFFF
|
|
s := RunServer(o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{cleanSess: true}
|
|
c, br := testMQTTConnect(b, ci, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(b, br, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
w := newMQTTWriter(0)
|
|
testMQTTWritePublishPacket(b, w, 1, false, false, subject, 1, []byte(payload))
|
|
// For reported bytes we will count the PUBLISH + PUBACK (4 bytes)
|
|
totalSize := int64(w.Len() + 4)
|
|
w.Reset()
|
|
|
|
pi := uint16(1)
|
|
maxpi := uint16(60000)
|
|
ppich := make(chan error, 10)
|
|
dch := make(chan error, 1+numSubs)
|
|
cdch := 1
|
|
// Start go routine to consume PUBACK for published QoS 1 messages.
|
|
go mqttBenchConsumePubAck(c, b.N, dch, ppich)
|
|
|
|
createSub := func(i int) {
|
|
ci := &mqttConnInfo{clientID: fmt.Sprintf("sub%d", i), cleanSess: true}
|
|
cs, brs := testMQTTConnect(b, ci, o.MQTT.Host, o.MQTT.Port)
|
|
testMQTTCheckConnAck(b, brs, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(b, 1, cs, brs, []*mqttFilter{{filter: subject, qos: 1}}, []byte{1})
|
|
testMQTTFlush(b, cs, nil, brs)
|
|
|
|
w := newMQTTWriter(0)
|
|
varHeaderAndPayload := 2 + len(subject) + 2 + len(payload)
|
|
w.WriteVarInt(varHeaderAndPayload)
|
|
size := 1 + w.Len() + varHeaderAndPayload
|
|
// Add to the bytes reported the size of message sent to subscriber + PUBACK (4 bytes)
|
|
totalSize += int64(size + 4)
|
|
|
|
go func() {
|
|
mqttBenchConsumeMsgQos1(cs, b.N, size, dch)
|
|
cs.Close()
|
|
}()
|
|
}
|
|
for i := 0; i < numSubs; i++ {
|
|
createSub(i + 1)
|
|
cdch++
|
|
}
|
|
|
|
flush := func() {
|
|
b.Helper()
|
|
if _, err := c.Write(w.Bytes()); err != nil {
|
|
b.Fatalf("Error on write: %v", err)
|
|
}
|
|
w.Reset()
|
|
}
|
|
|
|
b.SetBytes(totalSize)
|
|
b.StartTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if pi <= maxpi {
|
|
testMQTTWritePublishPacket(b, w, 1, false, false, subject, pi, []byte(payload))
|
|
pi++
|
|
if w.Len() >= mqttBenchBufLen {
|
|
flush()
|
|
}
|
|
} else {
|
|
if w.Len() > 0 {
|
|
flush()
|
|
}
|
|
if pi > 60000 {
|
|
pi = 1
|
|
maxpi = 0
|
|
}
|
|
if e := <-ppich; e != nil {
|
|
b.Fatal(e.Error())
|
|
}
|
|
maxpi += 10000
|
|
i--
|
|
}
|
|
}
|
|
if w.Len() > 0 {
|
|
flush()
|
|
}
|
|
for i := 0; i < cdch; i++ {
|
|
if e := <-dch; e != nil {
|
|
b.Fatal(e.Error())
|
|
}
|
|
}
|
|
b.StopTimer()
|
|
c.Close()
|
|
s.Shutdown()
|
|
}
|
|
|
|
func mqttBenchConsumeMsgQos1(c net.Conn, total, size int, dch chan<- error) {
|
|
var buf [mqttBenchBufLen]byte
|
|
pubAck := [4]byte{mqttPacketPubAck, 0x2, 0, 0}
|
|
var err error
|
|
var n int
|
|
var pi uint16
|
|
var prev int
|
|
for i := 0; i < total; {
|
|
n, err = c.Read(buf[:])
|
|
if err != nil {
|
|
break
|
|
}
|
|
n += prev
|
|
for ; n >= size; n -= size {
|
|
i++
|
|
pi++
|
|
pubAck[2] = byte(pi >> 8)
|
|
pubAck[3] = byte(pi)
|
|
if _, err = c.Write(pubAck[:4]); err != nil {
|
|
dch <- err
|
|
return
|
|
}
|
|
if pi == 60000 {
|
|
pi = 0
|
|
}
|
|
}
|
|
prev = n
|
|
}
|
|
dch <- err
|
|
}
|
|
|
|
func mqttBenchConsumePubAck(c net.Conn, total int, dch, ppich chan<- error) {
|
|
var buf [mqttBenchBufLen]byte
|
|
var err error
|
|
var n int
|
|
var pi uint16
|
|
var prev int
|
|
for i := 0; i < total; {
|
|
n, err = c.Read(buf[:])
|
|
if err != nil {
|
|
break
|
|
}
|
|
n += prev
|
|
for ; n >= 4; n -= 4 {
|
|
i++
|
|
pi++
|
|
if pi%10000 == 0 {
|
|
ppich <- nil
|
|
}
|
|
if pi == 60001 {
|
|
pi = 0
|
|
}
|
|
}
|
|
prev = n
|
|
}
|
|
ppich <- err
|
|
dch <- err
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub_______0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, "", 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub_______8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(8), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub______32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(32), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub_____128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(128), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub_____256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(256), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_Pub_______1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(1024), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1___0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, "", 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1___8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(8), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1__32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(32), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1_128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(128), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1_256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(256), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub1___1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(1024), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2___0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, "", 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2___8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(8), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2__32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(32), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2_128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(128), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2_256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(256), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS0_PubSub2___1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS0(b, mqttPubSubj, sizedString(1024), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub_______0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, "", 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub_______8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(8), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub______32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(32), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub_____128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(128), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub_____256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(256), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_Pub_______1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(1024), 0)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1___0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, "", 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1___8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(8), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1__32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(32), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1_128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(128), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1_256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(256), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub1___1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(1024), 1)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2___0b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, "", 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2___8b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(8), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2__32b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(32), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2_128b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(128), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2_256b_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(256), 2)
|
|
}
|
|
|
|
func BenchmarkMQTT_QoS1_PubSub2___1K_Payload(b *testing.B) {
|
|
mqttBenchPubQoS1(b, mqttPubSubj, sizedString(1024), 2)
|
|
}
|