mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
6432 lines
192 KiB
Go
6432 lines
192 KiB
Go
// Copyright 2020 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.
|
|
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"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 = 4 * 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 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 := &mqttWriter{}
|
|
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 = createDir(t, "mqtt_js")
|
|
}
|
|
s, err := NewServer(o)
|
|
if err != nil {
|
|
t.Fatalf("Error creating server: %v", err)
|
|
}
|
|
l := &DummyLogger{}
|
|
s.SetLogger(l, true, true)
|
|
go 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(`
|
|
mqtt {
|
|
port: -1
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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(`
|
|
jetstream: enabled
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
tls {
|
|
cert_file: "./configs/certs/server.pem"
|
|
key_file: "./configs/certs/key.pem"
|
|
}
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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},
|
|
} {
|
|
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"},
|
|
// 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
|
|
}, ""},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(test.content))
|
|
defer removeFile(t, conf)
|
|
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", fmt.Sprintf("%s:%d", o.MQTT.Host, 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)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
s2.Start()
|
|
wg.Done()
|
|
}()
|
|
|
|
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", fmt.Sprintf("%s:%d", o.MQTT.Host, 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 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 := fmt.Sprintf("%s:%d", host, 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 := &mqttWriter{}
|
|
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 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 := fmt.Sprintf("%s:%d", o.MQTT.Host, 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); 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") {
|
|
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", fmt.Sprintf("%s:%d", o.MQTT.Host, 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", fmt.Sprintf("%s:%d", o.MQTT.Host, o.MQTT.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error on dial: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
w := &mqttWriter{}
|
|
mqttWritePublish(w, 0, false, false, "foo", 0, []byte("hello"))
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error publishing: %v", err)
|
|
}
|
|
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
|
|
pl int
|
|
err string
|
|
}{
|
|
{"packet in buffer error", []byte{0}, 10, io.ErrUnexpectedEOF.Error()},
|
|
{"bad proto name", []byte{0, 4, 'B', 'A', 'D'}, 5, "protocol name"},
|
|
{"invalid proto name", []byte{0, 3, 'B', 'A', 'D'}, 5, "expected connect packet with protocol name"},
|
|
{"old proto not supported", []byte{0, 6, 'M', 'Q', 'I', 's', 'd', 'p'}, 8, "older protocol"},
|
|
{"error on protocol level", []byte{0, 4, 'M', 'Q', 'T', 'T'}, 6, "protocol level"},
|
|
{"unacceptable protocol version", []byte{0, 4, 'M', 'Q', 'T', 'T', 10}, 7, "unacceptable protocol version"},
|
|
{"error on flags", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel}, 7, "flags"},
|
|
{"reserved flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1}, 8, errMQTTConnFlagReserved.Error()},
|
|
{"will qos without will flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1 << 3}, 8, "if Will flag is set to 0, Will QoS must be 0 too"},
|
|
{"will retain without will flag", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 1 << 5}, 8, errMQTTWillAndRetainFlag.Error()},
|
|
{"will qos", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 3<<3 | 1<<2}, 8, "if Will flag is set to 1, Will QoS can be 0, 1 or 2"},
|
|
{"no user but password", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagPasswordFlag}, 8, errMQTTPasswordFlagAndNoUser.Error()},
|
|
{"missing keep alive", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0}, 8, "keep alive"},
|
|
{"missing client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1}, 10, "client ID"},
|
|
{"empty client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 0}, 12, errMQTTCIDEmptyNeedsCleanFlag.Error()},
|
|
{"invalid utf8 client ID", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, 0, 0, 1, 0, 1, 241}, 13, "invalid utf8 for client ID"},
|
|
{"missing will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0}, 12, "Will topic"},
|
|
{"empty will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 0}, 14, errMQTTEmptyWillTopic.Error()},
|
|
{"invalid utf8 will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 241}, 15, "invalid utf8 for Will topic"},
|
|
{"invalid wildcard will topic", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, '#'}, 15, "wildcards not allowed"},
|
|
{"error on will message", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagWillFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 'a', 0, 3}, 17, "Will message"},
|
|
{"error on username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0}, 12, "user name"},
|
|
{"empty username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 0}, 14, errMQTTEmptyUsername.Error()},
|
|
{"invalid utf8 username", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 241}, 15, "invalid utf8 for user name"},
|
|
{"error on password", []byte{0, 4, 'M', 'Q', 'T', 'T', mqttProtoLevel, mqttConnFlagUsernameFlag | mqttConnFlagPasswordFlag | mqttConnFlagCleanSession, 0, 0, 0, 0, 0, 1, 'a'}, 15, "password"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
if _, _, err := c.mqttParseConnect(r, test.pl); 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 := fmt.Sprintf("%s:%d", o.MQTT.Host, 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 := &mqttWriter{}
|
|
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 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 TestMQTTUnsupportedPackets(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
packetType byte
|
|
}{
|
|
{"pubrec", mqttPacketPubRec},
|
|
{"pubrel", mqttPacketPubRel},
|
|
{"pubcomp", mqttPacketPubComp},
|
|
} {
|
|
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)
|
|
|
|
w := &mqttWriter{}
|
|
pt := test.packetType
|
|
if test.packetType == mqttPacketPubRel {
|
|
pt |= byte(0x2)
|
|
}
|
|
w.WriteByte(pt)
|
|
w.WriteVarInt(2)
|
|
w.WriteUint16(1)
|
|
mc.Write(w.Bytes())
|
|
|
|
testMQTTExpectDisconnect(t, mc)
|
|
})
|
|
}
|
|
}
|
|
|
|
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/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", ""},
|
|
// 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.bar", "foo.bar", "", "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 %q", test.err, err.Error())
|
|
}
|
|
return
|
|
}
|
|
toNATS := string(res)
|
|
if toNATS != test.natsSubject {
|
|
t.Fatalf("Expected subject %q got %q", test.natsSubject, toNATS)
|
|
}
|
|
|
|
res = natsSubjectToMQTTTopic(string(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"},
|
|
|
|
{"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.>"},
|
|
} {
|
|
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
|
|
}{
|
|
{"reserved flag", nil, 3, 0, "wrong subscribe reserved flags"},
|
|
{"ensure packet loaded", []byte{1, 2}, mqttSubscribeFlags, 10, io.ErrUnexpectedEOF.Error()},
|
|
{"error reading packet id", []byte{1}, mqttSubscribeFlags, 1, "reading packet identifier"},
|
|
{"missing filters", []byte{0, 1}, mqttSubscribeFlags, 2, "subscribe protocol must contain at least 1 topic filter"},
|
|
{"error reading topic", []byte{0, 1, 0, 2, 'a'}, mqttSubscribeFlags, 5, "topic filter"},
|
|
{"empty topic", []byte{0, 1, 0, 0}, mqttSubscribeFlags, 4, errMQTTTopicFilterCannotBeEmpty.Error()},
|
|
{"invalid utf8 topic", []byte{0, 1, 0, 1, 241}, mqttSubscribeFlags, 5, "invalid utf8 for topic filter"},
|
|
{"missing qos", []byte{0, 1, 0, 1, 'a'}, mqttSubscribeFlags, 5, "QoS"},
|
|
{"invalid qos", []byte{0, 1, 0, 1, 'a', 3}, mqttSubscribeFlags, 6, "subscribe QoS value must be 0, 1 or 2"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
if _, _, err := c.mqttParseSubsOrUnsubs(r, test.b, test.pl, true); 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 := &mqttWriter{}
|
|
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 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()
|
|
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: "foo", qos: 0},
|
|
{filter: "bar", qos: 1},
|
|
{filter: "baz", qos: 2}, // Since we don't support, we should receive a result of 1
|
|
{filter: "foo/#/bar", qos: 0}, // Invalid sub, so we should receive a result of mqttSubAckFailure
|
|
}
|
|
expected := []byte{
|
|
0,
|
|
1,
|
|
1,
|
|
mqttSubAckFailure,
|
|
}
|
|
testMQTTSub(t, 1, mc, r, subs, expected)
|
|
}
|
|
|
|
func testMQTTFlush(t testing.TB, c net.Conn, bw *bufio.Writer, r *mqttReader) {
|
|
t.Helper()
|
|
w := &mqttWriter{}
|
|
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()
|
|
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, flags byte, payload []byte) {
|
|
t.Helper()
|
|
pflags, pi := testMQTTGetPubMsg(t, c, r, topic, payload)
|
|
if pflags != flags {
|
|
t.Fatalf("Expected flags to be %x, got %x", flags, pflags)
|
|
}
|
|
if pi > 0 {
|
|
testMQTTSendPubAck(t, c, pi)
|
|
}
|
|
}
|
|
|
|
func testMQTTCheckPubMsgNoAck(t testing.TB, c net.Conn, r *mqttReader, topic string, flags byte, payload []byte) uint16 {
|
|
t.Helper()
|
|
pflags, pi := testMQTTGetPubMsg(t, c, r, topic, payload)
|
|
if pflags != flags {
|
|
t.Fatalf("Expected flags to be %x, got %x", flags, pflags)
|
|
}
|
|
return pi
|
|
}
|
|
|
|
func testMQTTGetPubMsg(t testing.TB, c net.Conn, r *mqttReader, topic string, payload []byte) (byte, uint16) {
|
|
t.Helper()
|
|
b, pl := testMQTTReadPacket(t, r)
|
|
if pt := b & mqttPacketMask; pt != mqttPacketPub {
|
|
t.Fatalf("Expected PUBLISH packet %x, got %x", mqttPacketPub, pt)
|
|
}
|
|
pflags := b & mqttPacketFlagMask
|
|
qos := (pflags & mqttPubFlagQoS) >> 1
|
|
start := r.pos
|
|
ptopic, err := r.readString("topic name")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if 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 !bytes.Equal(payload, ppayload) {
|
|
t.Fatalf("Expected payload %q, got %q", payload, ppayload)
|
|
}
|
|
r.pos += msgLen
|
|
return pflags, pi
|
|
}
|
|
|
|
func testMQTTSendPubAck(t testing.TB, c net.Conn, pi uint16) {
|
|
t.Helper()
|
|
w := &mqttWriter{}
|
|
w.WriteByte(mqttPacketPubAck)
|
|
w.WriteVarInt(2)
|
|
w.WriteUint16(pi)
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing PUBACK: %v", err)
|
|
}
|
|
}
|
|
|
|
func testMQTTPublish(t testing.TB, c net.Conn, r *mqttReader, qos byte, dup, retain bool, topic string, pi uint16, payload []byte) {
|
|
t.Helper()
|
|
w := &mqttWriter{}
|
|
mqttWritePublish(w, qos, dup, retain, topic, pi, payload)
|
|
if _, err := testMQTTWrite(c, w.Bytes()); err != nil {
|
|
t.Fatalf("Error writing PUBLISH proto: %v", err)
|
|
}
|
|
if qos > 0 {
|
|
// Since we don't support QoS 2, we should get disconnected
|
|
if qos == 2 {
|
|
testMQTTExpectDisconnect(t, c)
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMQTTParsePub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
flags byte
|
|
proto []byte
|
|
pl int
|
|
err string
|
|
}{
|
|
{"qos not supported", 0x4, nil, 0, "not supported"},
|
|
{"packet in buffer error", 0, nil, 10, io.ErrUnexpectedEOF.Error()},
|
|
{"error on topic", 0, []byte{0, 3, 'f', 'o'}, 4, "topic"},
|
|
{"empty topic", 0, []byte{0, 0}, 2, errMQTTTopicIsEmpty.Error()},
|
|
{"wildcards topic", 0, []byte{0, 1, '#'}, 3, "wildcards not allowed"},
|
|
{"error on packet identifier", mqttPubQos1, []byte{0, 3, 'f', 'o', 'o'}, 5, "packet identifier"},
|
|
{"invalid packet identifier", mqttPubQos1, []byte{0, 3, 'f', 'o', 'o', 0, 0}, 7, errMQTTPacketIdentifierIsZero.Error()},
|
|
} {
|
|
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}
|
|
if err := c.mqttParsePub(r, test.pl, pp); err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Expected error %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTParsePubAck(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
proto []byte
|
|
pl int
|
|
err string
|
|
}{
|
|
{"packet in buffer error", nil, 10, io.ErrUnexpectedEOF.Error()},
|
|
{"error reading packet identifier", []byte{0}, 1, "packet identifier"},
|
|
{"invalid packet identifier", []byte{0, 0}, 2, errMQTTPacketIdentifierIsZero.Error()},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
if _, err := mqttParsePubAck(r, test.pl); 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 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 TestMQTTSubQoS(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)
|
|
}
|
|
testMQTTSendPubAck(t, mc, pi1)
|
|
testMQTTSendPubAck(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 2 but server will downgrade to 1
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "bar", qos: 2}}, []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})
|
|
}
|
|
|
|
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 {
|
|
natsPubReq(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{pending: make(map[uint16]*mqttPending)}
|
|
sub := &subscription{mqtt: &mqttSub{qos: 1}}
|
|
|
|
sess.ppi = 0xFFFF
|
|
pi, _ := sess.trackPending(1, _EMPTY_, sub)
|
|
if pi != 1 {
|
|
t.Fatalf("Expected 1, got %v", pi)
|
|
}
|
|
|
|
p := &mqttPending{}
|
|
for i := 1; i <= 0xFFFF; i++ {
|
|
sess.pending[uint16(i)] = p
|
|
}
|
|
pi, _ = sess.trackPending(1, _EMPTY_, sub)
|
|
if pi != 0 {
|
|
t.Fatalf("Expected 0, got %v", pi)
|
|
}
|
|
|
|
delete(sess.pending, 1234)
|
|
pi, _ = sess.trackPending(1, _EMPTY_, sub)
|
|
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, 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, 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 topTest.restart {
|
|
cl.stopAll()
|
|
cl.restartAll()
|
|
|
|
streams := []string{mqttStreamName, mqttRetainedMsgsStreamName, mqttSessStreamName}
|
|
for _, sn := range streams {
|
|
cl.waitOnStreamLeader(globalAccountName, sn)
|
|
}
|
|
cl.waitOnConsumerLeader(globalAccountName, mqttRetainedMsgsStreamName, "$MQTT_rmsgs_esFhDys3")
|
|
cl.waitOnConsumerLeader(globalAccountName, mqttRetainedMsgsStreamName, "$MQTT_rmsgs_z3WIzPtj")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// 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 TestMQTTRetainedMsgNetworkUpdates(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
mc, rc := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: true}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, rc, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
c := testMQTTGetClient(t, s, "sub")
|
|
asm := c.mqtt.asm
|
|
|
|
// For this test, we are going to simulate updates arriving in a
|
|
// mixed order and verify that we have the expected outcome.
|
|
check := func(t *testing.T, subject string, present bool, current, floor uint64) {
|
|
t.Helper()
|
|
asm.mu.RLock()
|
|
defer asm.mu.RUnlock()
|
|
erm, ok := asm.retmsgs[subject]
|
|
if present && !ok {
|
|
t.Fatalf("Subject %q not present", subject)
|
|
} else if !present && ok {
|
|
t.Fatalf("Subject %q should not be present", subject)
|
|
} else if !present {
|
|
return
|
|
}
|
|
if floor != erm.floor {
|
|
t.Fatalf("Expected floor to be %v, got %v", floor, erm.floor)
|
|
}
|
|
if erm.sseq != current {
|
|
t.Fatalf("Expected current sequence to be %v, got %v", current, erm.sseq)
|
|
}
|
|
}
|
|
|
|
type action struct {
|
|
add bool
|
|
seq uint64
|
|
}
|
|
for _, test := range []struct {
|
|
subject string
|
|
order []action
|
|
seq uint64
|
|
floor uint64
|
|
}{
|
|
{"foo.1", []action{{true, 1}, {true, 2}, {true, 3}}, 3, 0},
|
|
{"foo.2", []action{{true, 3}, {true, 1}, {true, 2}}, 3, 0},
|
|
{"foo.3", []action{{true, 1}, {false, 1}, {true, 2}}, 2, 0},
|
|
{"foo.4", []action{{false, 2}, {true, 1}, {true, 3}, {true, 2}}, 3, 0},
|
|
{"foo.5", []action{{false, 2}, {true, 1}, {true, 2}}, 0, 2},
|
|
{"foo.6", []action{{true, 1}, {true, 2}, {false, 2}}, 0, 2},
|
|
} {
|
|
t.Run(test.subject, func(t *testing.T) {
|
|
for _, a := range test.order {
|
|
if a.add {
|
|
rm := &mqttRetainedMsg{sseq: a.seq}
|
|
asm.handleRetainedMsg(test.subject, rm)
|
|
} else {
|
|
asm.handleRetainedMsgDel(test.subject, a.seq)
|
|
}
|
|
}
|
|
check(t, test.subject, true, test.seq, test.floor)
|
|
})
|
|
}
|
|
|
|
for _, subject := range []string{"foo.5", "foo.6"} {
|
|
t.Run("clear_"+subject, func(t *testing.T) {
|
|
// Now add a new message, which should clear the floor.
|
|
rm := &mqttRetainedMsg{sseq: 3}
|
|
asm.handleRetainedMsg(subject, rm)
|
|
check(t, subject, true, 3, 0)
|
|
// Now do a non network delete and make sure it is gone.
|
|
asm.handleRetainedMsgDel(subject, 0)
|
|
check(t, subject, false, 0, 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
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 := testMQTTConnect(t, &mqttConnInfo{clientID: "sub", cleanSess: false}, 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})
|
|
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 geting 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.
|
|
time.Sleep(250 * 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 = createDir(t, "mqtt_js_ln")
|
|
}
|
|
|
|
// 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(`
|
|
listen: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream {
|
|
store_dir=org_dir
|
|
}
|
|
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
|
|
`))
|
|
defer os.Remove(conf)
|
|
defer os.RemoveAll("org_dir")
|
|
|
|
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 ...interface{}) {
|
|
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
|
|
}{
|
|
{"reserved flag", nil, 3, 0, "wrong unsubscribe reserved flags"},
|
|
{"ensure packet loaded", []byte{1, 2}, mqttUnsubscribeFlags, 10, io.ErrUnexpectedEOF.Error()},
|
|
{"error reading packet id", []byte{1}, mqttUnsubscribeFlags, 1, "reading packet identifier"},
|
|
{"missing filters", []byte{0, 1}, mqttUnsubscribeFlags, 2, "subscribe protocol must contain at least 1 topic filter"},
|
|
{"error reading topic", []byte{0, 1, 0, 2, 'a'}, mqttUnsubscribeFlags, 5, "topic filter"},
|
|
{"empty topic", []byte{0, 1, 0, 0}, mqttUnsubscribeFlags, 4, errMQTTTopicFilterCannotBeEmpty.Error()},
|
|
{"invalid utf8 topic", []byte{0, 1, 0, 1, 241}, mqttUnsubscribeFlags, 5, "invalid utf8 for topic filter"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := &mqttReader{}
|
|
r.reset(test.proto)
|
|
mqtt := &mqtt{r: r}
|
|
c := &client{mqtt: mqtt}
|
|
if _, _, err := c.mqttParseSubsOrUnsubs(r, test.b, test.pl, false); 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 := &mqttWriter{}
|
|
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()
|
|
if buf, err := testMQTTRead(c); err == nil {
|
|
t.Fatalf("Expected connection to be disconnected, got %s", buf)
|
|
}
|
|
}
|
|
|
|
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/#"},
|
|
} {
|
|
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()
|
|
w := &mqttWriter{}
|
|
w.WriteByte(mqttPacketDisconnect)
|
|
w.WriteByte(0)
|
|
if bw != nil {
|
|
bw.Write(w.Bytes())
|
|
bw.Flush()
|
|
} else {
|
|
c.Write(w.Bytes())
|
|
}
|
|
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},
|
|
{"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: 1}}, []byte{1})
|
|
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 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)
|
|
|
|
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, 0)
|
|
|
|
// 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 pflags&mqttPubFlagRetain == 0 {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMQTTWillRetainPermViolation(t *testing.T) {
|
|
template := `
|
|
port: -1
|
|
jetstream: enabled
|
|
server_name: mqtt
|
|
authorization {
|
|
mqtt_perms = {
|
|
publish = ["%s"]
|
|
subscribe = ["foo", "bar", "$MQTT.sub.>"]
|
|
}
|
|
users = [
|
|
{user: mqtt, password: pass, permissions: $mqtt_perms}
|
|
]
|
|
}
|
|
mqtt {
|
|
port: -1
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(template, "foo")))
|
|
defer removeFile(t, conf)
|
|
|
|
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 pflags&mqttPubFlagRetain == 0 {
|
|
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)
|
|
|
|
// 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)
|
|
|
|
// 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, "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)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
retained bool
|
|
sentValue string
|
|
expectedValue string
|
|
subGetsIt bool
|
|
}{
|
|
{"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 pflags&mqttPubFlagRetain == 0 {
|
|
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 TestMQTTPublishRetainPermViolation(t *testing.T) {
|
|
o := testMQTTDefaultOptions()
|
|
o.Users = []*User{
|
|
{
|
|
Username: "mqtt",
|
|
Password: "pass",
|
|
Permissions: &Permissions{
|
|
Publish: &SubjectPermission{Allow: []string{"foo"}},
|
|
Subscribe: &SubjectPermission{Allow: []string{"bar", "$MQTT.sub.>"}},
|
|
},
|
|
},
|
|
}
|
|
s := testMQTTRunServer(t, o)
|
|
defer testMQTTShutdownServer(s)
|
|
|
|
ci := &mqttConnInfo{
|
|
cleanSess: true,
|
|
user: "mqtt",
|
|
pass: "pass",
|
|
}
|
|
|
|
mc1, rs1 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc1.Close()
|
|
testMQTTCheckConnAck(t, rs1, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTPublish(t, mc1, rs1, 0, false, true, "bar", 0, []byte("retained"))
|
|
testMQTTFlush(t, mc1, nil, rs1)
|
|
|
|
mc2, rs2 := testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc2.Close()
|
|
testMQTTCheckConnAck(t, rs2, mqttConnAckRCConnectionAccepted, false)
|
|
|
|
testMQTTSub(t, 1, mc2, rs2, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, rs2)
|
|
|
|
testMQTTDisconnect(t, mc1, nil)
|
|
testMQTTDisconnect(t, mc2, nil)
|
|
}
|
|
|
|
func TestMQTTPublishViolation(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.*", "$MQTT.sub.>"}},
|
|
},
|
|
},
|
|
}
|
|
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})
|
|
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"))
|
|
|
|
// 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)
|
|
}
|
|
|
|
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, r = testMQTTConnect(t, ci, o.MQTT.Host, o.MQTT.Port)
|
|
defer c.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, true)
|
|
testMQTTDisconnect(t, c, nil)
|
|
|
|
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 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.
|
|
cli := testMQTTGetClient(t, s, "flapper")
|
|
asm := cli.mqtt.asm
|
|
|
|
// 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 := fmt.Sprintf("%s:%d", o.MQTT.Host, 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 connect: %v", err)
|
|
}
|
|
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)
|
|
|
|
ci := &mqttConnInfo{clientID: "sub", 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")
|
|
}
|
|
|
|
// Get the session for "sub"
|
|
cli := testMQTTGetClient(t, s, "sub")
|
|
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 := fmt.Sprintf("%s:%d", o.MQTT.Host, 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, "foo", 1, []byte("foo1"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "foo", 1, []byte("foo2"))
|
|
testMQTTPublish(t, c, r, 1, false, true, "bar", 1, []byte("bar1"))
|
|
testMQTTPublish(t, c, r, 0, false, true, "baz", 1, []byte("baz1"))
|
|
// Remove bar
|
|
testMQTTPublish(t, c, r, 1, false, true, "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)
|
|
// There is already the defer for shutdown at top of function
|
|
|
|
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})
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubFlagRetain|mqttPubQos1, []byte("foo2"))
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "baz", qos: 1}}, []byte{1})
|
|
testMQTTCheckPubMsg(t, c, r, "baz", mqttPubFlagRetain, []byte("baz1"))
|
|
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "bar", qos: 1}}, []byte{1})
|
|
testMQTTExpectNothing(t, r)
|
|
|
|
testMQTTDisconnect(t, c, nil)
|
|
c.Close()
|
|
}
|
|
|
|
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 := &mqttWriter{}
|
|
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
|
|
testMQTTSendPubAck(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
|
|
testMQTTSendPubAck(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.pending)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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.pending)
|
|
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
|
|
testMQTTSendPubAck(t, c, pi)
|
|
// Now we should receive message 2
|
|
testMQTTCheckPubMsg(t, c, r, "foo", mqttPubQos1, []byte("msg2"))
|
|
testMQTTDisconnect(t, c, nil)
|
|
|
|
// 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
|
|
testMQTTSendPubAck(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.pending)
|
|
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
|
|
testMQTTSendPubAck(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.
|
|
testMQTTSendPubAck(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`)))
|
|
defer removeFile(t, conf)
|
|
|
|
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(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream: enabled
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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) {
|
|
template := `
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream: enabled
|
|
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, jwt.ConnectionTypeMqtt, "")))
|
|
defer removeFile(t, conf)
|
|
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, 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(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream: enabled
|
|
mqtt {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
websocket {
|
|
listen: "127.0.0.1:-1"
|
|
no_tls: true
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream: enabled
|
|
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'
|
|
}
|
|
}
|
|
`))
|
|
defer removeFile(t, conf)
|
|
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 + string(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(`
|
|
listen: "127.0.0.1:-1"
|
|
http: "127.0.0.1:-1"
|
|
server_name: "mqtt"
|
|
jetstream: enabled
|
|
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"
|
|
`))
|
|
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)
|
|
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 := pollConz(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 = pollConz(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 = pollConz(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 = pollConz(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 = pollConz(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 interface{}
|
|
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) {
|
|
tmpl := `
|
|
jetstream: enabled
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
stream_replicas: %v
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, 3)))
|
|
defer removeFile(t, conf)
|
|
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, 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, NewJSInsufficientResourcesError().Description) {
|
|
t.Fatalf("Expected error regarding insufficient resources, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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: "foo", 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+"foo" {
|
|
if len(ci.Cluster.Replicas) != 0 {
|
|
t.Fatalf("Expected consumer to be R1, got: %+v", ci.Cluster)
|
|
}
|
|
} else {
|
|
if len(ci.Cluster.Replicas) != 1 {
|
|
t.Fatalf("Expected consumer to be R2, got: %+v", ci.Cluster)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 TestMQTTConsumerReplicasReload(t *testing.T) {
|
|
tmpl := `
|
|
jetstream: enabled
|
|
server_name: mqtt
|
|
mqtt {
|
|
port: -1
|
|
consumer_replicas: %v
|
|
consumer_memory_storage: %s
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, 3, "false")))
|
|
defer removeFile(t, conf)
|
|
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)
|
|
testMQTTSub(t, 1, c, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{mqttSubAckFailure})
|
|
|
|
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, 1, "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)
|
|
}
|
|
}
|
|
|
|
func TestMQTTConsumerReplicasExceedsParentStream(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
|
|
consumer_replicas: 4
|
|
}
|
|
|
|
# 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[0]
|
|
mc, r := testMQTTConnect(t, &mqttConnInfo{clientID: "test", cleanSess: false}, o.MQTT.Host, o.MQTT.Port)
|
|
defer mc.Close()
|
|
testMQTTCheckConnAck(t, r, mqttConnAckRCConnectionAccepted, false)
|
|
testMQTTSub(t, 1, mc, r, []*mqttFilter{{filter: "foo", qos: 1}}, []byte{mqttSubAckFailure})
|
|
|
|
select {
|
|
case e := <-l.errCh:
|
|
if !strings.Contains(e, NewJSConsumerReplicasExceedsStreamError().Description) {
|
|
t.Fatalf("Expected error regarding replicas exceeded parent, got %v", e)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get the error regarding replicas count")
|
|
}
|
|
}
|
|
|
|
type unableToDeleteConsLogger struct {
|
|
DummyLogger
|
|
errCh chan string
|
|
}
|
|
|
|
func (l *unableToDeleteConsLogger) Errorf(format string, args ...interface{}) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "unable to delete consumer") {
|
|
l.errCh <- msg
|
|
}
|
|
}
|
|
|
|
func TestMQTTSessionNotDeletedOnDeleteConsumerError(t *testing.T) {
|
|
org := mqttJSAPITimeout
|
|
mqttJSAPITimeout = 1000 * time.Millisecond
|
|
defer func() { mqttJSAPITimeout = org }()
|
|
|
|
cl := createJetStreamClusterWithTemplate(t, testMQTTGetClusterTemplaceNoLeaf(), "MQTT", 2)
|
|
defer cl.shutdown()
|
|
|
|
o := cl.opts[0]
|
|
s1 := cl.servers[0]
|
|
// Plug error logger to s1
|
|
l := &unableToDeleteConsLogger{errCh: make(chan string, 10)}
|
|
s1.SetLogger(l, false, false)
|
|
|
|
nc, js := jsClientConnect(t, s1)
|
|
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: 1}}, []byte{1})
|
|
testMQTTFlush(t, mc, nil, r)
|
|
|
|
// Now shutdown server 2, we should lose quorum
|
|
cl.servers[1].Shutdown()
|
|
|
|
// Close the MQTT client:
|
|
testMQTTDisconnect(t, mc, nil)
|
|
|
|
// We should have reported that there was an error deleting the consumer
|
|
select {
|
|
case <-l.errCh:
|
|
// OK
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Server did not report any error")
|
|
}
|
|
|
|
// Now restart the server 2 so that we can check that the session is still persisted.
|
|
cl.restartAllSamePorts()
|
|
cl.waitOnStreamLeader(globalAccountName, mqttSessStreamName)
|
|
|
|
si, err := js.StreamInfo(mqttSessStreamName)
|
|
require_NoError(t, err)
|
|
require_True(t, si.State.Msgs == 1)
|
|
}
|
|
|
|
// Test for auto-cleanup of consumers.
|
|
func TestMQTTConsumerInactiveThreshold(t *testing.T) {
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: mqtt
|
|
jetstream: enabled
|
|
|
|
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, "0.2s")))
|
|
defer removeFile(t, conf)
|
|
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, "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)
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Benchmarks
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
const (
|
|
mqttPubSubj = "a"
|
|
mqttBenchBufLen = 32768
|
|
)
|
|
|
|
func mqttBenchPubQoS0(b *testing.B, subject, payload string, numSubs int) {
|
|
b.StopTimer()
|
|
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 := &mqttWriter{}
|
|
mqttWritePublish(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 := &mqttWriter{}
|
|
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 := &mqttWriter{}
|
|
mqttWritePublish(w, 1, false, false, subject, 1, []byte(payload))
|
|
// For reported bytes we will count the PUBLISH + PUBACK (4 bytes)
|
|
totalSize := int64(len(w.Bytes()) + 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 := &mqttWriter{}
|
|
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 {
|
|
mqttWritePublish(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)
|
|
}
|