mirror of
https://github.com/mochi-mqtt/server.git
synced 2025-09-26 20:21:12 +08:00

* Another code implementation for Inline Client Subscriptions. * Added a few test cases. * Changed the return value types of Server.Unsubscribe() and Subscribe() to boolean. * Implementing the delivery of retained messages and supporting multiple callbacks per topic using different inline client IDs. * Added validation checks for the legality of the inline client id during Subscribe and Unsubscribe. * Added validation checks for the legality of the client during Subscribe and Unsubscribe. * Fixed the TestServerSubscribe/invalid_client_id test case failure. * Add Server.inlineClient and Temporarily removing test cases for better code review readability. * Using server.inlineClient in server.InjectPacket(). * After unsubscribing, if there are other subscriptions in particle.inlineSubscriptions, particle cannot be deleted. * Add comments to particle.inlineSubscriptions and modify to return ErrTopicFilterInvalid when the topic is invalid during subscription. * Fixed some test case failures caused by adding inlineClient to the server. * More test cases have been added. * Optimization of test case code. * Modify server.go: When used as a publisher, treat the qos of inline client-published messages as 0. * Resolve conflict.
1598 lines
53 KiB
Go
1598 lines
53 KiB
Go
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2022 mochi-mqtt, mochi-co
|
|
// SPDX-FileContributor: mochi-co
|
|
|
|
// package mqtt provides a high performance, fully compliant MQTT v5 broker server with v3.1.1 backward compatibility.
|
|
package mqtt
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/mochi-mqtt/server/v2/hooks/storage"
|
|
"github.com/mochi-mqtt/server/v2/listeners"
|
|
"github.com/mochi-mqtt/server/v2/packets"
|
|
"github.com/mochi-mqtt/server/v2/system"
|
|
|
|
"log/slog"
|
|
)
|
|
|
|
const (
|
|
Version = "2.3.0" // the current server version.
|
|
defaultSysTopicInterval int64 = 1 // the interval between $SYS topic publishes
|
|
LocalListener = "local"
|
|
InlineClientId = "inline"
|
|
)
|
|
|
|
var (
|
|
// DefaultServerCapabilities defines the default features and capabilities provided by the server.
|
|
DefaultServerCapabilities = &Capabilities{
|
|
MaximumSessionExpiryInterval: math.MaxUint32, // maximum number of seconds to keep disconnected sessions
|
|
MaximumMessageExpiryInterval: 60 * 60 * 24, // maximum message expiry if message expiry is 0 or over
|
|
ReceiveMaximum: 1024, // maximum number of concurrent qos messages per client
|
|
MaximumQos: 2, // maxmimum qos value available to clients
|
|
RetainAvailable: 1, // retain messages is available
|
|
MaximumPacketSize: 0, // no maximum packet size
|
|
TopicAliasMaximum: math.MaxUint16, // maximum topic alias value
|
|
WildcardSubAvailable: 1, // wildcard subscriptions are available
|
|
SubIDAvailable: 1, // subscription identifiers are available
|
|
SharedSubAvailable: 1, // shared subscriptions are available
|
|
MinimumProtocolVersion: 3, // minimum supported mqtt version (3.0.0)
|
|
MaximumClientWritesPending: 1024 * 8, // maximum number of pending message writes for a client
|
|
}
|
|
|
|
ErrListenerIDExists = errors.New("listener id already exists") // a listener with the same id already exists.
|
|
ErrConnectionClosed = errors.New("connection not open") // connection is closed
|
|
)
|
|
|
|
// Capabilities indicates the capabilities and features provided by the server.
|
|
type Capabilities struct {
|
|
MaximumMessageExpiryInterval int64
|
|
MaximumClientWritesPending int32
|
|
MaximumSessionExpiryInterval uint32
|
|
MaximumPacketSize uint32
|
|
maximumPacketID uint32 // unexported, used for testing only
|
|
ReceiveMaximum uint16
|
|
TopicAliasMaximum uint16
|
|
SharedSubAvailable byte
|
|
MinimumProtocolVersion byte
|
|
Compatibilities Compatibilities
|
|
MaximumQos byte
|
|
RetainAvailable byte
|
|
WildcardSubAvailable byte
|
|
SubIDAvailable byte
|
|
}
|
|
|
|
// Compatibilities provides flags for using compatibility modes.
|
|
type Compatibilities struct {
|
|
ObscureNotAuthorized bool // return unspecified errors instead of not authorized
|
|
PassiveClientDisconnect bool // don't disconnect the client forcefully after sending disconnect packet (paho - spec violation)
|
|
AlwaysReturnResponseInfo bool // always return response info (useful for testing)
|
|
RestoreSysInfoOnRestart bool // restore system info from store as if server never stopped
|
|
NoInheritedPropertiesOnAck bool // don't allow inherited user properties on ack (paho - spec violation)
|
|
}
|
|
|
|
// Options contains configurable options for the server.
|
|
type Options struct {
|
|
// Capabilities defines the server features and behaviour. If you only wish to modify
|
|
// several of these values, set them explicitly - e.g.
|
|
// server.Options.Capabilities.MaximumClientWritesPending = 16 * 1024
|
|
Capabilities *Capabilities
|
|
|
|
// ClientNetWriteBufferSize specifies the size of the client *bufio.Writer write buffer.
|
|
ClientNetWriteBufferSize int
|
|
|
|
// ClientNetReadBufferSize specifies the size of the client *bufio.Reader read buffer.
|
|
ClientNetReadBufferSize int
|
|
|
|
// Logger specifies a custom configured implementation of zerolog to override
|
|
// the servers default logger configuration. If you wish to change the log level,
|
|
// of the default logger, you can do so by setting
|
|
// server := mqtt.New(nil)
|
|
// level := new(slog.LevelVar)
|
|
// server.Slog = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
// Level: level,
|
|
// }))
|
|
// level.Set(slog.LevelDebug)
|
|
Logger *slog.Logger
|
|
|
|
// SysTopicResendInterval specifies the interval between $SYS topic updates in seconds.
|
|
SysTopicResendInterval int64
|
|
}
|
|
|
|
// Server is an MQTT broker server. It should be created with server.New()
|
|
// in order to ensure all the internal fields are correctly populated.
|
|
type Server struct {
|
|
Options *Options // configurable server options
|
|
Listeners *listeners.Listeners // listeners are network interfaces which listen for new connections
|
|
Clients *Clients // clients known to the broker
|
|
Topics *TopicsIndex // an index of topic filter subscriptions and retained messages
|
|
Info *system.Info // values about the server commonly known as $SYS topics
|
|
loop *loop // loop contains tickers for the system event loop
|
|
done chan bool // indicate that the server is ending
|
|
Log *slog.Logger // minimal no-alloc logger
|
|
hooks *Hooks // hooks contains hooks for extra functionality such as auth and persistent storage.
|
|
inlineClient *Client // inlineClient is a special client used for inline subscriptions and inline Publish.
|
|
}
|
|
|
|
// loop contains interval tickers for the system events loop.
|
|
type loop struct {
|
|
sysTopics *time.Ticker // interval ticker for sending updating $SYS topics
|
|
clientExpiry *time.Ticker // interval ticker for cleaning expired clients
|
|
inflightExpiry *time.Ticker // interval ticker for cleaning up expired inflight messages
|
|
retainedExpiry *time.Ticker // interval ticker for cleaning retained messages
|
|
willDelaySend *time.Ticker // interval ticker for sending will messages with a delay
|
|
willDelayed *packets.Packets // activate LWT packets which will be sent after a delay
|
|
}
|
|
|
|
// ops contains server values which can be propagated to other structs.
|
|
type ops struct {
|
|
options *Options // a pointer to the server options and capabilities, for referencing in clients
|
|
info *system.Info // pointers to server system info
|
|
hooks *Hooks // pointer to the server hooks
|
|
log *slog.Logger // a structured logger for the client
|
|
}
|
|
|
|
// New returns a new instance of mochi mqtt broker. Optional parameters
|
|
// can be specified to override some default settings (see Options).
|
|
func New(opts *Options) *Server {
|
|
if opts == nil {
|
|
opts = new(Options)
|
|
}
|
|
|
|
opts.ensureDefaults()
|
|
|
|
s := &Server{
|
|
done: make(chan bool),
|
|
Clients: NewClients(),
|
|
Topics: NewTopicsIndex(),
|
|
Listeners: listeners.New(),
|
|
loop: &loop{
|
|
sysTopics: time.NewTicker(time.Second * time.Duration(opts.SysTopicResendInterval)),
|
|
clientExpiry: time.NewTicker(time.Second),
|
|
inflightExpiry: time.NewTicker(time.Second),
|
|
retainedExpiry: time.NewTicker(time.Second),
|
|
willDelaySend: time.NewTicker(time.Second),
|
|
willDelayed: packets.NewPackets(),
|
|
},
|
|
Options: opts,
|
|
Info: &system.Info{
|
|
Version: Version,
|
|
Started: time.Now().Unix(),
|
|
},
|
|
Log: opts.Logger,
|
|
hooks: &Hooks{
|
|
Log: opts.Logger,
|
|
},
|
|
}
|
|
s.inlineClient = s.NewClient(nil, LocalListener, InlineClientId, true)
|
|
s.Clients.Add(s.inlineClient)
|
|
|
|
return s
|
|
}
|
|
|
|
// ensureDefaults ensures that the server starts with sane default values, if none are provided.
|
|
func (o *Options) ensureDefaults() {
|
|
if o.Capabilities == nil {
|
|
o.Capabilities = DefaultServerCapabilities
|
|
}
|
|
|
|
o.Capabilities.maximumPacketID = math.MaxUint16 // spec maximum is 65535
|
|
|
|
if o.SysTopicResendInterval == 0 {
|
|
o.SysTopicResendInterval = defaultSysTopicInterval
|
|
}
|
|
|
|
if o.ClientNetWriteBufferSize == 0 {
|
|
o.ClientNetWriteBufferSize = 1024 * 2
|
|
}
|
|
|
|
if o.ClientNetReadBufferSize == 0 {
|
|
o.ClientNetReadBufferSize = 1024 * 2
|
|
}
|
|
|
|
if o.Logger == nil {
|
|
log := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
o.Logger = log
|
|
}
|
|
}
|
|
|
|
// NewClient returns a new Client instance, populated with all the required values and
|
|
// references to be used with the server. If you are using this client to directly publish
|
|
// messages from the embedding application, set the inline flag to true to bypass ACL and
|
|
// topic validation checks.
|
|
func (s *Server) NewClient(c net.Conn, listener string, id string, inline bool) *Client {
|
|
cl := newClient(c, &ops{ // [MQTT-3.1.2-6] implicit
|
|
options: s.Options,
|
|
info: s.Info,
|
|
hooks: s.hooks,
|
|
log: s.Log,
|
|
})
|
|
|
|
cl.ID = id
|
|
cl.Net.Listener = listener
|
|
|
|
if inline { // inline clients bypass acl and some validity checks.
|
|
cl.Net.Inline = true
|
|
// By default, we don't want to restrict developer publishes,
|
|
// but if you do, reset this after creating inline client.
|
|
cl.State.Inflight.ResetReceiveQuota(math.MaxInt32)
|
|
} else {
|
|
go cl.WriteLoop() // can only write to real clients
|
|
}
|
|
|
|
return cl
|
|
}
|
|
|
|
// AddHook attaches a new Hook to the server. Ideally, this should be called
|
|
// before the server is started with s.Serve().
|
|
func (s *Server) AddHook(hook Hook, config any) error {
|
|
nl := s.Log.With("hook", hook.ID())
|
|
hook.SetOpts(nl, &HookOptions{
|
|
Capabilities: s.Options.Capabilities,
|
|
})
|
|
|
|
s.Log.Info("added hook", "hook", hook.ID())
|
|
return s.hooks.Add(hook, config)
|
|
}
|
|
|
|
// AddListener adds a new network listener to the server, for receiving incoming client connections.
|
|
func (s *Server) AddListener(l listeners.Listener) error {
|
|
if _, ok := s.Listeners.Get(l.ID()); ok {
|
|
return ErrListenerIDExists
|
|
}
|
|
|
|
nl := s.Log.With(slog.String("listener", l.ID()))
|
|
err := l.Init(nl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Listeners.Add(l)
|
|
|
|
s.Log.Info("attached listener", "id", l.ID(), "protocol", l.Protocol(), "address", l.Address())
|
|
return nil
|
|
}
|
|
|
|
// Serve starts the event loops responsible for establishing client connections
|
|
// on all attached listeners, publishing the system topics, and starting all hooks.
|
|
func (s *Server) Serve() error {
|
|
s.Log.Info("mochi mqtt starting", "version", Version)
|
|
defer s.Log.Info("mochi mqtt server started")
|
|
|
|
if s.hooks.Provides(
|
|
StoredClients,
|
|
StoredInflightMessages,
|
|
StoredRetainedMessages,
|
|
StoredSubscriptions,
|
|
StoredSysInfo,
|
|
) {
|
|
err := s.readStore()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
go s.eventLoop() // spin up event loop for issuing $SYS values and closing server.
|
|
s.Listeners.ServeAll(s.EstablishConnection) // start listening on all listeners.
|
|
s.publishSysTopics() // begin publishing $SYS system values.
|
|
s.hooks.OnStarted()
|
|
|
|
return nil
|
|
}
|
|
|
|
// eventLoop loops forever, running various server housekeeping methods at different intervals.
|
|
func (s *Server) eventLoop() {
|
|
s.Log.Debug("system event loop started")
|
|
defer s.Log.Debug("system event loop halted")
|
|
|
|
for {
|
|
select {
|
|
case <-s.done:
|
|
s.loop.sysTopics.Stop()
|
|
return
|
|
case <-s.loop.sysTopics.C:
|
|
s.publishSysTopics()
|
|
case <-s.loop.clientExpiry.C:
|
|
s.clearExpiredClients(time.Now().Unix())
|
|
case <-s.loop.retainedExpiry.C:
|
|
s.clearExpiredRetainedMessages(time.Now().Unix())
|
|
case <-s.loop.willDelaySend.C:
|
|
s.sendDelayedLWT(time.Now().Unix())
|
|
case <-s.loop.inflightExpiry.C:
|
|
s.clearExpiredInflights(time.Now().Unix())
|
|
}
|
|
}
|
|
}
|
|
|
|
// EstablishConnection establishes a new client when a listener accepts a new connection.
|
|
func (s *Server) EstablishConnection(listener string, c net.Conn) error {
|
|
cl := s.NewClient(c, listener, "", false)
|
|
return s.attachClient(cl, listener)
|
|
}
|
|
|
|
// attachClient validates an incoming client connection and if viable, attaches the client
|
|
// to the server, performs session housekeeping, and reads incoming packets.
|
|
func (s *Server) attachClient(cl *Client, listener string) error {
|
|
defer cl.Stop(nil)
|
|
pk, err := s.readConnectionPacket(cl)
|
|
if err != nil {
|
|
return fmt.Errorf("read connection: %w", err)
|
|
}
|
|
|
|
cl.ParseConnect(listener, pk)
|
|
code := s.validateConnect(cl, pk) // [MQTT-3.1.4-1] [MQTT-3.1.4-2]
|
|
if code != packets.CodeSuccess {
|
|
if err := s.SendConnack(cl, code, false, nil); err != nil {
|
|
return fmt.Errorf("invalid connection send ack: %w", err)
|
|
}
|
|
return code // [MQTT-3.2.2-7] [MQTT-3.1.4-6]
|
|
}
|
|
|
|
err = s.hooks.OnConnect(cl, pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cl.refreshDeadline(cl.State.Keepalive)
|
|
if !s.hooks.OnConnectAuthenticate(cl, pk) { // [MQTT-3.1.4-2]
|
|
err := s.SendConnack(cl, packets.ErrBadUsernameOrPassword, false, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid connection send ack: %w", err)
|
|
}
|
|
|
|
return packets.ErrBadUsernameOrPassword
|
|
}
|
|
|
|
atomic.AddInt64(&s.Info.ClientsConnected, 1)
|
|
defer atomic.AddInt64(&s.Info.ClientsConnected, -1)
|
|
|
|
s.hooks.OnSessionEstablish(cl, pk)
|
|
|
|
sessionPresent := s.inheritClientSession(pk, cl)
|
|
s.Clients.Add(cl) // [MQTT-4.1.0-1]
|
|
|
|
err = s.SendConnack(cl, code, sessionPresent, nil) // [MQTT-3.1.4-5] [MQTT-3.2.0-1] [MQTT-3.2.0-2] &[MQTT-3.14.0-1]
|
|
if err != nil {
|
|
return fmt.Errorf("ack connection packet: %w", err)
|
|
}
|
|
|
|
s.loop.willDelayed.Delete(cl.ID) // [MQTT-3.1.3-9]
|
|
|
|
if sessionPresent {
|
|
err = cl.ResendInflightMessages(true)
|
|
if err != nil {
|
|
return fmt.Errorf("resend inflight: %w", err)
|
|
}
|
|
}
|
|
|
|
s.hooks.OnSessionEstablished(cl, pk)
|
|
|
|
err = cl.Read(s.receivePacket)
|
|
if err != nil {
|
|
s.sendLWT(cl)
|
|
cl.Stop(err)
|
|
} else {
|
|
cl.Properties.Will = Will{} // [MQTT-3.14.4-3] [MQTT-3.1.2-10]
|
|
}
|
|
s.Log.Debug("client disconnected", "error", err, "client", cl.ID, "remote", cl.Net.Remote, "listener", listener)
|
|
|
|
expire := (cl.Properties.ProtocolVersion == 5 && cl.Properties.Props.SessionExpiryInterval == 0) || (cl.Properties.ProtocolVersion < 5 && cl.Properties.Clean)
|
|
s.hooks.OnDisconnect(cl, err, expire)
|
|
|
|
if expire && atomic.LoadUint32(&cl.State.isTakenOver) == 0 {
|
|
cl.ClearInflights(math.MaxInt64, 0)
|
|
s.UnsubscribeClient(cl)
|
|
s.Clients.Delete(cl.ID) // [MQTT-4.1.0-2] ![MQTT-3.1.2-23]
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// readConnectionPacket reads the first incoming header for a connection, and if
|
|
// acceptable, returns the valid connection packet.
|
|
func (s *Server) readConnectionPacket(cl *Client) (pk packets.Packet, err error) {
|
|
fh := new(packets.FixedHeader)
|
|
err = cl.ReadFixedHeader(fh)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if fh.Type != packets.Connect {
|
|
return pk, packets.ErrProtocolViolationRequireFirstConnect // [MQTT-3.1.0-1]
|
|
}
|
|
|
|
pk, err = cl.ReadPacket(fh)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// receivePacket processes an incoming packet for a client, and issues a disconnect to the client
|
|
// if an error has occurred (if mqtt v5).
|
|
func (s *Server) receivePacket(cl *Client, pk packets.Packet) error {
|
|
err := s.processPacket(cl, pk)
|
|
if err != nil {
|
|
if code, ok := err.(packets.Code); ok &&
|
|
cl.Properties.ProtocolVersion == 5 &&
|
|
code.Code >= packets.ErrUnspecifiedError.Code {
|
|
s.DisconnectClient(cl, code)
|
|
}
|
|
|
|
s.Log.Warn("error processing packet", "error", err, "client", cl.ID, "listener", cl.Net.Listener, "pk", pk)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateConnect validates that a connect packet is compliant.
|
|
func (s *Server) validateConnect(cl *Client, pk packets.Packet) packets.Code {
|
|
code := pk.ConnectValidate() // [MQTT-3.1.4-1] [MQTT-3.1.4-2]
|
|
if code != packets.CodeSuccess {
|
|
return code
|
|
}
|
|
|
|
if cl.Properties.ProtocolVersion < 5 && !pk.Connect.Clean && pk.Connect.ClientIdentifier == "" {
|
|
return packets.ErrUnspecifiedError
|
|
}
|
|
|
|
if cl.Properties.ProtocolVersion < s.Options.Capabilities.MinimumProtocolVersion {
|
|
return packets.ErrUnsupportedProtocolVersion // [MQTT-3.1.2-2]
|
|
} else if cl.Properties.Will.Qos > s.Options.Capabilities.MaximumQos {
|
|
return packets.ErrQosNotSupported // [MQTT-3.2.2-12]
|
|
} else if cl.Properties.Will.Retain && s.Options.Capabilities.RetainAvailable == 0x00 {
|
|
return packets.ErrRetainNotSupported // [MQTT-3.2.2-13]
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
// inheritClientSession inherits the state of an existing client sharing the same
|
|
// connection ID. If clean is true, the state of any previously existing client
|
|
// session is abandoned.
|
|
func (s *Server) inheritClientSession(pk packets.Packet, cl *Client) bool {
|
|
if existing, ok := s.Clients.Get(pk.Connect.ClientIdentifier); ok {
|
|
s.DisconnectClient(existing, packets.ErrSessionTakenOver) // [MQTT-3.1.4-3]
|
|
if pk.Connect.Clean || (existing.Properties.Clean && existing.Properties.ProtocolVersion < 5) { // [MQTT-3.1.2-4] [MQTT-3.1.4-4]
|
|
s.UnsubscribeClient(existing)
|
|
existing.ClearInflights(math.MaxInt64, 0)
|
|
atomic.StoreUint32(&existing.State.isTakenOver, 1) // only set isTakenOver after unsubscribe has occurred
|
|
return false // [MQTT-3.2.2-3]
|
|
}
|
|
|
|
atomic.StoreUint32(&existing.State.isTakenOver, 1)
|
|
if existing.State.Inflight.Len() > 0 {
|
|
cl.State.Inflight = existing.State.Inflight.Clone() // [MQTT-3.1.2-5]
|
|
if cl.State.Inflight.maximumReceiveQuota == 0 && cl.ops.options.Capabilities.ReceiveMaximum != 0 {
|
|
cl.State.Inflight.ResetReceiveQuota(int32(cl.ops.options.Capabilities.ReceiveMaximum)) // server receive max per client
|
|
cl.State.Inflight.ResetSendQuota(int32(cl.Properties.Props.ReceiveMaximum)) // client receive max
|
|
}
|
|
}
|
|
|
|
for _, sub := range existing.State.Subscriptions.GetAll() {
|
|
existed := !s.Topics.Subscribe(cl.ID, sub) // [MQTT-3.8.4-3]
|
|
if !existed {
|
|
atomic.AddInt64(&s.Info.Subscriptions, 1)
|
|
}
|
|
cl.State.Subscriptions.Add(sub.Filter, sub)
|
|
}
|
|
|
|
// Clean the state of the existing client to prevent sequential take-overs
|
|
// from increasing memory usage by inflights + subs * client-id.
|
|
s.UnsubscribeClient(existing)
|
|
existing.ClearInflights(math.MaxInt64, 0)
|
|
|
|
s.Log.Debug("session taken over", "client", cl.ID, "old_remote", existing.Net.Remote, "new_remote", cl.Net.Remote)
|
|
|
|
return true // [MQTT-3.2.2-3]
|
|
}
|
|
|
|
if atomic.LoadInt64(&s.Info.ClientsConnected) > atomic.LoadInt64(&s.Info.ClientsMaximum) {
|
|
atomic.AddInt64(&s.Info.ClientsMaximum, 1)
|
|
}
|
|
|
|
return false // [MQTT-3.2.2-2]
|
|
}
|
|
|
|
// SendConnack returns a Connack packet to a client.
|
|
func (s *Server) SendConnack(cl *Client, reason packets.Code, present bool, properties *packets.Properties) error {
|
|
if properties == nil {
|
|
properties = &packets.Properties{
|
|
ReceiveMaximum: s.Options.Capabilities.ReceiveMaximum,
|
|
}
|
|
}
|
|
|
|
properties.ReceiveMaximum = s.Options.Capabilities.ReceiveMaximum // 3.2.2.3.3 Receive Maximum
|
|
if cl.State.ServerKeepalive { // You can set this dynamically using the OnConnect hook.
|
|
properties.ServerKeepAlive = cl.State.Keepalive // [MQTT-3.1.2-21]
|
|
properties.ServerKeepAliveFlag = true
|
|
}
|
|
|
|
if reason.Code >= packets.ErrUnspecifiedError.Code {
|
|
if cl.Properties.ProtocolVersion < 5 {
|
|
if v3reason, ok := packets.V5CodesToV3[reason]; ok { // NB v3 3.2.2.3 Connack return codes
|
|
reason = v3reason
|
|
}
|
|
}
|
|
|
|
properties.ReasonString = reason.Reason
|
|
ack := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Connack,
|
|
},
|
|
SessionPresent: false, // [MQTT-3.2.2-6]
|
|
ReasonCode: reason.Code, // [MQTT-3.2.2-8]
|
|
Properties: *properties,
|
|
}
|
|
return cl.WritePacket(ack)
|
|
}
|
|
|
|
if s.Options.Capabilities.MaximumQos < 2 {
|
|
properties.MaximumQos = s.Options.Capabilities.MaximumQos // [MQTT-3.2.2-9]
|
|
properties.MaximumQosFlag = true
|
|
}
|
|
|
|
if cl.Properties.Props.AssignedClientID != "" {
|
|
properties.AssignedClientID = cl.Properties.Props.AssignedClientID // [MQTT-3.1.3-7] [MQTT-3.2.2-16]
|
|
}
|
|
|
|
if cl.Properties.Props.SessionExpiryInterval > s.Options.Capabilities.MaximumSessionExpiryInterval {
|
|
properties.SessionExpiryInterval = s.Options.Capabilities.MaximumSessionExpiryInterval
|
|
properties.SessionExpiryIntervalFlag = true
|
|
cl.Properties.Props.SessionExpiryInterval = properties.SessionExpiryInterval
|
|
cl.Properties.Props.SessionExpiryIntervalFlag = true
|
|
}
|
|
|
|
ack := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Connack,
|
|
},
|
|
SessionPresent: present,
|
|
ReasonCode: reason.Code, // [MQTT-3.2.2-8]
|
|
Properties: *properties,
|
|
}
|
|
return cl.WritePacket(ack)
|
|
}
|
|
|
|
// processPacket processes an inbound packet for a client. Since the method is
|
|
// typically called as a goroutine, errors are primarily for test checking purposes.
|
|
func (s *Server) processPacket(cl *Client, pk packets.Packet) error {
|
|
var err error
|
|
|
|
switch pk.FixedHeader.Type {
|
|
case packets.Connect:
|
|
err = s.processConnect(cl, pk)
|
|
case packets.Disconnect:
|
|
err = s.processDisconnect(cl, pk)
|
|
case packets.Pingreq:
|
|
err = s.processPingreq(cl, pk)
|
|
case packets.Publish:
|
|
code := pk.PublishValidate(s.Options.Capabilities.TopicAliasMaximum)
|
|
if code != packets.CodeSuccess {
|
|
return code
|
|
}
|
|
err = s.processPublish(cl, pk)
|
|
case packets.Puback:
|
|
err = s.processPuback(cl, pk)
|
|
case packets.Pubrec:
|
|
err = s.processPubrec(cl, pk)
|
|
case packets.Pubrel:
|
|
err = s.processPubrel(cl, pk)
|
|
case packets.Pubcomp:
|
|
err = s.processPubcomp(cl, pk)
|
|
case packets.Subscribe:
|
|
code := pk.SubscribeValidate()
|
|
if code != packets.CodeSuccess {
|
|
return code
|
|
}
|
|
err = s.processSubscribe(cl, pk)
|
|
case packets.Unsubscribe:
|
|
code := pk.UnsubscribeValidate()
|
|
if code != packets.CodeSuccess {
|
|
return code
|
|
}
|
|
err = s.processUnsubscribe(cl, pk)
|
|
case packets.Auth:
|
|
code := pk.AuthValidate()
|
|
if code != packets.CodeSuccess {
|
|
return code
|
|
}
|
|
err = s.processAuth(cl, pk)
|
|
default:
|
|
return fmt.Errorf("no valid packet available; %v", pk.FixedHeader.Type)
|
|
}
|
|
|
|
s.hooks.OnPacketProcessed(cl, pk, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cl.State.Inflight.Len() > 0 && atomic.LoadInt32(&cl.State.Inflight.sendQuota) > 0 {
|
|
next, ok := cl.State.Inflight.NextImmediate()
|
|
if ok {
|
|
_ = cl.WritePacket(next)
|
|
if ok := cl.State.Inflight.Delete(next.PacketID); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
}
|
|
cl.State.Inflight.DecreaseSendQuota()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processConnect processes a Connect packet. The packet cannot be used to establish
|
|
// a new connection on an existing connection. See EstablishConnection instead.
|
|
func (s *Server) processConnect(cl *Client, _ packets.Packet) error {
|
|
s.sendLWT(cl)
|
|
return packets.ErrProtocolViolationSecondConnect // [MQTT-3.1.0-2]
|
|
}
|
|
|
|
// processPingreq processes a Pingreq packet.
|
|
func (s *Server) processPingreq(cl *Client, _ packets.Packet) error {
|
|
return cl.WritePacket(packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Pingresp, // [MQTT-3.12.4-1]
|
|
},
|
|
})
|
|
}
|
|
|
|
// Publish publishes a publish packet into the broker as if it were sent from the speicfied client.
|
|
// This is a convenience function which wraps InjectPacket. As such, this method can publish packets
|
|
// to any topic (including $SYS) and bypass ACL checks. The qos byte is used for limiting the
|
|
// outbound qos (mqtt v5) rather than issuing to the broker (we assume qos 2 complete).
|
|
func (s *Server) Publish(topic string, payload []byte, retain bool, qos byte) error {
|
|
return s.InjectPacket(s.inlineClient, packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Publish,
|
|
Qos: qos,
|
|
Retain: retain,
|
|
},
|
|
TopicName: topic,
|
|
Payload: payload,
|
|
PacketID: uint16(qos), // we never process the inbound qos, but we need a packet id for validity checks.
|
|
})
|
|
}
|
|
|
|
// Subscribe adds an inline subscription for the specified topic filter and subscription identifier
|
|
// with the provided handler function.
|
|
func (s *Server) Subscribe(filter string, subscriptionId int, handler InlineSubFn) error {
|
|
if handler == nil {
|
|
return packets.ErrInlineSubscriptionHandlerInvalid
|
|
} else if !IsValidFilter(filter, false) {
|
|
return packets.ErrTopicFilterInvalid
|
|
}
|
|
subscription := packets.Subscription{
|
|
Identifier: subscriptionId,
|
|
Filter: filter,
|
|
}
|
|
|
|
pk := s.hooks.OnSubscribe(s.inlineClient, packets.Packet{ // subscribe like a normal client.
|
|
Origin: s.inlineClient.ID,
|
|
FixedHeader: packets.FixedHeader{Type: packets.Subscribe},
|
|
Filters: packets.Subscriptions{subscription},
|
|
})
|
|
|
|
inlineSubscription := InlineSubscription{
|
|
Subscription: subscription,
|
|
Handler: handler,
|
|
}
|
|
|
|
s.Topics.InlineSubscribe(inlineSubscription)
|
|
s.hooks.OnSubscribed(s.inlineClient, pk, []byte{packets.CodeSuccess.Code})
|
|
|
|
// Handling retained messages.
|
|
for _, pkv := range s.Topics.Messages(filter) { // [MQTT-3.8.4-4]
|
|
handler(s.inlineClient, inlineSubscription.Subscription, pkv)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe removes an inline subscription for the specified subscription and topic filter.
|
|
// It allows you to unsubscribe a specific subscription from the internal subscription
|
|
// associated with the given topic filter.
|
|
func (s *Server) Unsubscribe(filter string, subscriptionId int) error {
|
|
if !IsValidFilter(filter, false) {
|
|
return packets.ErrTopicFilterInvalid
|
|
}
|
|
|
|
pk := s.hooks.OnUnsubscribe(s.inlineClient, packets.Packet{
|
|
Origin: s.inlineClient.ID,
|
|
FixedHeader: packets.FixedHeader{Type: packets.Unsubscribe},
|
|
Filters: packets.Subscriptions{
|
|
{
|
|
Identifier: subscriptionId,
|
|
Filter: filter,
|
|
},
|
|
},
|
|
})
|
|
|
|
s.Topics.InlineUnsubscribe(subscriptionId, filter)
|
|
s.hooks.OnUnsubscribed(s.inlineClient, pk)
|
|
return nil
|
|
}
|
|
|
|
// InjectPacket injects a packet into the broker as if it were sent from the specified client.
|
|
// InlineClients using this method can publish packets to any topic (including $SYS) and bypass ACL checks.
|
|
func (s *Server) InjectPacket(cl *Client, pk packets.Packet) error {
|
|
pk.ProtocolVersion = cl.Properties.ProtocolVersion
|
|
|
|
err := s.processPacket(cl, pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
atomic.AddInt64(&cl.ops.info.PacketsReceived, 1)
|
|
if pk.FixedHeader.Type == packets.Publish {
|
|
atomic.AddInt64(&cl.ops.info.MessagesReceived, 1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processPublish processes a Publish packet.
|
|
func (s *Server) processPublish(cl *Client, pk packets.Packet) error {
|
|
if !cl.Net.Inline && !IsValidFilter(pk.TopicName, true) {
|
|
return nil
|
|
}
|
|
|
|
if atomic.LoadInt32(&cl.State.Inflight.receiveQuota) == 0 {
|
|
return s.DisconnectClient(cl, packets.ErrReceiveMaximum) // ~[MQTT-3.3.4-7] ~[MQTT-3.3.4-8]
|
|
}
|
|
|
|
if !cl.Net.Inline && !s.hooks.OnACLCheck(cl, pk.TopicName, true) {
|
|
return nil
|
|
}
|
|
|
|
pk.Origin = cl.ID
|
|
pk.Created = time.Now().Unix()
|
|
|
|
if !cl.Net.Inline {
|
|
if pki, ok := cl.State.Inflight.Get(pk.PacketID); ok {
|
|
if pki.FixedHeader.Type == packets.Pubrec { // [MQTT-4.3.3-10]
|
|
ack := s.buildAck(pk.PacketID, packets.Pubrec, 0, pk.Properties, packets.ErrPacketIdentifierInUse)
|
|
return cl.WritePacket(ack)
|
|
}
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok { // [MQTT-4.3.2-5]
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if pk.Properties.TopicAliasFlag && pk.Properties.TopicAlias > 0 { // [MQTT-3.3.2-11]
|
|
pk.TopicName = cl.State.TopicAliases.Inbound.Set(pk.Properties.TopicAlias, pk.TopicName)
|
|
}
|
|
|
|
if pk.FixedHeader.Qos > s.Options.Capabilities.MaximumQos {
|
|
pk.FixedHeader.Qos = s.Options.Capabilities.MaximumQos // [MQTT-3.2.2-9] Reduce Qos based on server max qos capability
|
|
}
|
|
|
|
pkx, err := s.hooks.OnPublish(cl, pk)
|
|
if err == nil {
|
|
pk = pkx
|
|
} else if errors.Is(err, packets.ErrRejectPacket) {
|
|
return nil
|
|
} else if errors.Is(err, packets.CodeSuccessIgnore) {
|
|
pk.Ignore = true
|
|
} else if cl.Properties.ProtocolVersion == 5 && pk.FixedHeader.Qos > 0 && errors.As(err, new(packets.Code)) {
|
|
err = cl.WritePacket(s.buildAck(pk.PacketID, packets.Puback, 0, pk.Properties, err.(packets.Code)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if pk.FixedHeader.Retain { // [MQTT-3.3.1-5] ![MQTT-3.3.1-8]
|
|
s.retainMessage(cl, pk)
|
|
}
|
|
|
|
// If it's inlineClient, it can't handle PUBREC and PUBREL.
|
|
// When it publishes a package with a qos > 0, the server treats
|
|
// the package as qos=0, and the client receives it as qos=1 or 2.
|
|
if pk.FixedHeader.Qos == 0 || cl.Net.Inline {
|
|
s.publishToSubscribers(pk)
|
|
s.hooks.OnPublished(cl, pk)
|
|
return nil
|
|
}
|
|
|
|
cl.State.Inflight.DecreaseReceiveQuota()
|
|
ack := s.buildAck(pk.PacketID, packets.Puback, 0, pk.Properties, packets.QosCodes[pk.FixedHeader.Qos]) // [MQTT-4.3.2-4]
|
|
if pk.FixedHeader.Qos == 2 {
|
|
ack = s.buildAck(pk.PacketID, packets.Pubrec, 0, pk.Properties, packets.CodeSuccess) // [MQTT-3.3.4-1] [MQTT-4.3.3-8]
|
|
}
|
|
|
|
if ok := cl.State.Inflight.Set(ack); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, 1)
|
|
s.hooks.OnQosPublish(cl, ack, ack.Created, 0)
|
|
}
|
|
|
|
err = cl.WritePacket(ack)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pk.FixedHeader.Qos == 1 {
|
|
if ok := cl.State.Inflight.Delete(ack.PacketID); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
}
|
|
cl.State.Inflight.IncreaseReceiveQuota()
|
|
s.hooks.OnQosComplete(cl, ack)
|
|
}
|
|
|
|
s.publishToSubscribers(pk)
|
|
s.hooks.OnPublished(cl, pk)
|
|
|
|
return nil
|
|
}
|
|
|
|
// retainMessage adds a message to a topic, and if a persistent store is provided,
|
|
// adds the message to the store to be reloaded if necessary.
|
|
func (s *Server) retainMessage(cl *Client, pk packets.Packet) {
|
|
if s.Options.Capabilities.RetainAvailable == 0 || pk.Ignore {
|
|
return
|
|
}
|
|
|
|
out := pk.Copy(false)
|
|
r := s.Topics.RetainMessage(out)
|
|
s.hooks.OnRetainMessage(cl, pk, r)
|
|
atomic.StoreInt64(&s.Info.Retained, int64(s.Topics.Retained.Len()))
|
|
}
|
|
|
|
// publishToSubscribers publishes a publish packet to all subscribers with matching topic filters.
|
|
func (s *Server) publishToSubscribers(pk packets.Packet) {
|
|
if pk.Ignore {
|
|
return
|
|
}
|
|
|
|
if pk.Created == 0 {
|
|
pk.Created = time.Now().Unix()
|
|
}
|
|
|
|
pk.Expiry = pk.Created + s.Options.Capabilities.MaximumMessageExpiryInterval
|
|
if pk.Properties.MessageExpiryInterval > 0 {
|
|
pk.Expiry = pk.Created + int64(pk.Properties.MessageExpiryInterval)
|
|
}
|
|
|
|
subscribers := s.Topics.Subscribers(pk.TopicName)
|
|
if len(subscribers.Shared) > 0 {
|
|
subscribers = s.hooks.OnSelectSubscribers(subscribers, pk)
|
|
if len(subscribers.SharedSelected) == 0 {
|
|
subscribers.SelectShared()
|
|
}
|
|
subscribers.MergeSharedSelected()
|
|
}
|
|
|
|
for _, inlineSubscription := range subscribers.InlineSubscriptions {
|
|
inlineSubscription.Handler(s.inlineClient, inlineSubscription.Subscription, pk)
|
|
}
|
|
|
|
for id, subs := range subscribers.Subscriptions {
|
|
if cl, ok := s.Clients.Get(id); ok {
|
|
_, err := s.publishToClient(cl, subs, pk)
|
|
if err != nil {
|
|
s.Log.Debug("failed publishing packet", "error", err, "client", cl.ID, "packet", pk)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) publishToClient(cl *Client, sub packets.Subscription, pk packets.Packet) (packets.Packet, error) {
|
|
if sub.NoLocal && pk.Origin == cl.ID {
|
|
return pk, nil // [MQTT-3.8.3-3]
|
|
}
|
|
|
|
out := pk.Copy(false)
|
|
if !s.hooks.OnACLCheck(cl, pk.TopicName, false) {
|
|
return out, packets.ErrNotAuthorized
|
|
}
|
|
if !sub.FwdRetainedFlag && ((cl.Properties.ProtocolVersion == 5 && !sub.RetainAsPublished) || cl.Properties.ProtocolVersion < 5) { // ![MQTT-3.3.1-13] [v3 MQTT-3.3.1-9]
|
|
out.FixedHeader.Retain = false // [MQTT-3.3.1-12]
|
|
}
|
|
|
|
if len(sub.Identifiers) > 0 { // [MQTT-3.3.4-3]
|
|
out.Properties.SubscriptionIdentifier = []int{}
|
|
for _, id := range sub.Identifiers {
|
|
out.Properties.SubscriptionIdentifier = append(out.Properties.SubscriptionIdentifier, id) // [MQTT-3.3.4-4] ![MQTT-3.3.4-5]
|
|
}
|
|
sort.Ints(out.Properties.SubscriptionIdentifier)
|
|
}
|
|
|
|
if out.FixedHeader.Qos > sub.Qos {
|
|
out.FixedHeader.Qos = sub.Qos
|
|
}
|
|
|
|
if out.FixedHeader.Qos > s.Options.Capabilities.MaximumQos {
|
|
out.FixedHeader.Qos = s.Options.Capabilities.MaximumQos // [MQTT-3.2.2-9]
|
|
}
|
|
|
|
if cl.Properties.Props.TopicAliasMaximum > 0 {
|
|
var aliasExists bool
|
|
out.Properties.TopicAlias, aliasExists = cl.State.TopicAliases.Outbound.Set(pk.TopicName)
|
|
if out.Properties.TopicAlias > 0 {
|
|
out.Properties.TopicAliasFlag = true
|
|
if aliasExists {
|
|
out.TopicName = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
if out.FixedHeader.Qos > 0 {
|
|
i, err := cl.NextPacketID() // [MQTT-4.3.2-1] [MQTT-4.3.3-1]
|
|
if err != nil {
|
|
s.hooks.OnPacketIDExhausted(cl, pk)
|
|
s.Log.Warn("packet ids exhausted", "error", err, "client", cl.ID, "listener", cl.Net.Listener)
|
|
return out, packets.ErrQuotaExceeded
|
|
}
|
|
|
|
out.PacketID = uint16(i) // [MQTT-2.2.1-4]
|
|
sentQuota := atomic.LoadInt32(&cl.State.Inflight.sendQuota)
|
|
|
|
if ok := cl.State.Inflight.Set(out); ok { // [MQTT-4.3.2-3] [MQTT-4.3.3-3]
|
|
atomic.AddInt64(&s.Info.Inflight, 1)
|
|
s.hooks.OnQosPublish(cl, out, out.Created, 0)
|
|
cl.State.Inflight.DecreaseSendQuota()
|
|
}
|
|
|
|
if sentQuota == 0 && atomic.LoadInt32(&cl.State.Inflight.maximumSendQuota) > 0 {
|
|
out.Expiry = -1
|
|
cl.State.Inflight.Set(out)
|
|
return out, nil
|
|
}
|
|
}
|
|
|
|
if cl.Net.Conn == nil || cl.Closed() {
|
|
return out, packets.CodeDisconnect
|
|
}
|
|
|
|
select {
|
|
case cl.State.outbound <- &out:
|
|
atomic.AddInt32(&cl.State.outboundQty, 1)
|
|
default:
|
|
atomic.AddInt64(&s.Info.MessagesDropped, 1)
|
|
cl.ops.hooks.OnPublishDropped(cl, pk)
|
|
cl.State.Inflight.Delete(out.PacketID) // packet was dropped due to irregular circumstances, so rollback inflight.
|
|
cl.State.Inflight.IncreaseSendQuota()
|
|
return out, packets.ErrPendingClientWritesExceeded
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Server) publishRetainedToClient(cl *Client, sub packets.Subscription, existed bool) {
|
|
if IsSharedFilter(sub.Filter) {
|
|
return // 4.8.2 Non-normative - Shared Subscriptions - No Retained Messages are sent to the Session when it first subscribes.
|
|
}
|
|
|
|
if sub.RetainHandling == 1 && existed || sub.RetainHandling == 2 { // [MQTT-3.3.1-10] [MQTT-3.3.1-11]
|
|
return
|
|
}
|
|
|
|
sub.FwdRetainedFlag = true
|
|
for _, pkv := range s.Topics.Messages(sub.Filter) { // [MQTT-3.8.4-4]
|
|
_, err := s.publishToClient(cl, sub, pkv)
|
|
if err != nil {
|
|
s.Log.Debug("failed to publish retained message", "error", err, "client", cl.ID, "listener", cl.Net.Listener, "packet", pkv)
|
|
continue
|
|
}
|
|
s.hooks.OnRetainPublished(cl, pkv)
|
|
}
|
|
}
|
|
|
|
// buildAck builds a standardised ack message for Puback, Pubrec, Pubrel, Pubcomp packets.
|
|
func (s *Server) buildAck(packetID uint16, pkt, qos byte, properties packets.Properties, reason packets.Code) packets.Packet {
|
|
if s.Options.Capabilities.Compatibilities.NoInheritedPropertiesOnAck {
|
|
properties = packets.Properties{}
|
|
}
|
|
if reason.Code >= packets.ErrUnspecifiedError.Code {
|
|
properties.ReasonString = reason.Reason
|
|
}
|
|
|
|
pk := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: pkt,
|
|
Qos: qos,
|
|
},
|
|
PacketID: packetID, // [MQTT-2.2.1-5]
|
|
ReasonCode: reason.Code, // [MQTT-3.4.2-1]
|
|
Properties: properties,
|
|
Created: time.Now().Unix(),
|
|
Expiry: time.Now().Unix() + s.Options.Capabilities.MaximumMessageExpiryInterval,
|
|
}
|
|
|
|
return pk
|
|
}
|
|
|
|
// processPuback processes a Puback packet, denoting completion of a QOS 1 packet sent from the server.
|
|
func (s *Server) processPuback(cl *Client, pk packets.Packet) error {
|
|
if _, ok := cl.State.Inflight.Get(pk.PacketID); !ok {
|
|
return nil // omit, but would be packets.ErrPacketIdentifierNotFound
|
|
}
|
|
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok { // [MQTT-4.3.2-5]
|
|
cl.State.Inflight.IncreaseSendQuota()
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
s.hooks.OnQosComplete(cl, pk)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processPubrec processes a Pubrec packet, denoting receipt of a QOS 2 packet sent from the server.
|
|
func (s *Server) processPubrec(cl *Client, pk packets.Packet) error {
|
|
if _, ok := cl.State.Inflight.Get(pk.PacketID); !ok { // [MQTT-4.3.3-7] [MQTT-4.3.3-13]
|
|
return cl.WritePacket(s.buildAck(pk.PacketID, packets.Pubrel, 1, pk.Properties, packets.ErrPacketIdentifierNotFound))
|
|
}
|
|
|
|
if pk.ReasonCode >= packets.ErrUnspecifiedError.Code || !pk.ReasonCodeValid() { // [MQTT-4.3.3-4]
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
}
|
|
cl.ops.hooks.OnQosDropped(cl, pk)
|
|
return nil // as per MQTT5 Section 4.13.2 paragraph 2
|
|
}
|
|
|
|
ack := s.buildAck(pk.PacketID, packets.Pubrel, 1, pk.Properties, packets.CodeSuccess) // [MQTT-4.3.3-4] ![MQTT-4.3.3-6]
|
|
cl.State.Inflight.DecreaseReceiveQuota() // -1 RECV QUOTA
|
|
cl.State.Inflight.Set(ack) // [MQTT-4.3.3-5]
|
|
return cl.WritePacket(ack)
|
|
}
|
|
|
|
// processPubrel processes a Pubrel packet, denoting completion of a QOS 2 packet sent from the client.
|
|
func (s *Server) processPubrel(cl *Client, pk packets.Packet) error {
|
|
if _, ok := cl.State.Inflight.Get(pk.PacketID); !ok { // [MQTT-4.3.3-7] [MQTT-4.3.3-13]
|
|
return cl.WritePacket(s.buildAck(pk.PacketID, packets.Pubcomp, 0, pk.Properties, packets.ErrPacketIdentifierNotFound))
|
|
}
|
|
|
|
if pk.ReasonCode >= packets.ErrUnspecifiedError.Code || !pk.ReasonCodeValid() { // [MQTT-4.3.3-9]
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
}
|
|
cl.ops.hooks.OnQosDropped(cl, pk)
|
|
return nil
|
|
}
|
|
|
|
ack := s.buildAck(pk.PacketID, packets.Pubcomp, 0, pk.Properties, packets.CodeSuccess) // [MQTT-4.3.3-11]
|
|
cl.State.Inflight.Set(ack)
|
|
|
|
err := cl.WritePacket(ack)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cl.State.Inflight.IncreaseReceiveQuota() // +1 RECV QUOTA
|
|
cl.State.Inflight.IncreaseSendQuota() // +1 SENT QUOTA
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok { // [MQTT-4.3.3-12]
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
s.hooks.OnQosComplete(cl, pk)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processPubcomp processes a Pubcomp packet, denoting completion of a QOS 2 packet sent from the server.
|
|
func (s *Server) processPubcomp(cl *Client, pk packets.Packet) error {
|
|
// regardless of whether the pubcomp is a success or failure, we end the qos flow, delete inflight, and restore the quotas.
|
|
cl.State.Inflight.IncreaseReceiveQuota() // +1 RECV QUOTA
|
|
cl.State.Inflight.IncreaseSendQuota() // +1 SENT QUOTA
|
|
if ok := cl.State.Inflight.Delete(pk.PacketID); ok {
|
|
atomic.AddInt64(&s.Info.Inflight, -1)
|
|
s.hooks.OnQosComplete(cl, pk)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processSubscribe processes a Subscribe packet.
|
|
func (s *Server) processSubscribe(cl *Client, pk packets.Packet) error {
|
|
pk = s.hooks.OnSubscribe(cl, pk)
|
|
code := packets.CodeSuccess
|
|
if _, ok := cl.State.Inflight.Get(pk.PacketID); ok {
|
|
code = packets.ErrPacketIdentifierInUse
|
|
}
|
|
|
|
filterExisted := make([]bool, len(pk.Filters))
|
|
reasonCodes := make([]byte, len(pk.Filters))
|
|
for i, sub := range pk.Filters {
|
|
if code != packets.CodeSuccess {
|
|
reasonCodes[i] = code.Code // NB 3.9.3 Non-normative 0x91
|
|
continue
|
|
} else if !IsValidFilter(sub.Filter, false) {
|
|
reasonCodes[i] = packets.ErrTopicFilterInvalid.Code
|
|
} else if sub.NoLocal && IsSharedFilter(sub.Filter) {
|
|
reasonCodes[i] = packets.ErrProtocolViolationInvalidSharedNoLocal.Code // [MQTT-3.8.3-4]
|
|
} else if !s.hooks.OnACLCheck(cl, sub.Filter, false) {
|
|
reasonCodes[i] = packets.ErrNotAuthorized.Code
|
|
if s.Options.Capabilities.Compatibilities.ObscureNotAuthorized {
|
|
reasonCodes[i] = packets.ErrUnspecifiedError.Code
|
|
}
|
|
} else {
|
|
isNew := s.Topics.Subscribe(cl.ID, sub) // [MQTT-3.8.4-3]
|
|
if isNew {
|
|
atomic.AddInt64(&s.Info.Subscriptions, 1)
|
|
}
|
|
cl.State.Subscriptions.Add(sub.Filter, sub) // [MQTT-3.2.2-10]
|
|
|
|
if sub.Qos > s.Options.Capabilities.MaximumQos {
|
|
sub.Qos = s.Options.Capabilities.MaximumQos // [MQTT-3.2.2-9]
|
|
}
|
|
|
|
filterExisted[i] = !isNew
|
|
reasonCodes[i] = sub.Qos // [MQTT-3.9.3-1] [MQTT-3.8.4-7]
|
|
}
|
|
|
|
if reasonCodes[i] > packets.CodeGrantedQos2.Code && cl.Properties.ProtocolVersion < 5 { // MQTT3
|
|
reasonCodes[i] = packets.ErrUnspecifiedError.Code
|
|
}
|
|
}
|
|
|
|
ack := packets.Packet{ // [MQTT-3.8.4-1] [MQTT-3.8.4-5]
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Suback,
|
|
},
|
|
PacketID: pk.PacketID, // [MQTT-2.2.1-6] [MQTT-3.8.4-2]
|
|
ReasonCodes: reasonCodes, // [MQTT-3.8.4-6]
|
|
Properties: packets.Properties{
|
|
User: pk.Properties.User,
|
|
},
|
|
}
|
|
|
|
if code.Code >= packets.ErrUnspecifiedError.Code {
|
|
ack.Properties.ReasonString = code.Reason
|
|
}
|
|
|
|
s.hooks.OnSubscribed(cl, pk, reasonCodes)
|
|
err := cl.WritePacket(ack)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, sub := range pk.Filters { // [MQTT-3.3.1-9]
|
|
if reasonCodes[i] >= packets.ErrUnspecifiedError.Code {
|
|
continue
|
|
}
|
|
|
|
s.publishRetainedToClient(cl, sub, filterExisted[i])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processUnsubscribe processes an unsubscribe packet.
|
|
func (s *Server) processUnsubscribe(cl *Client, pk packets.Packet) error {
|
|
code := packets.CodeSuccess
|
|
if _, ok := cl.State.Inflight.Get(pk.PacketID); ok {
|
|
code = packets.ErrPacketIdentifierInUse
|
|
}
|
|
|
|
pk = s.hooks.OnUnsubscribe(cl, pk)
|
|
reasonCodes := make([]byte, len(pk.Filters))
|
|
for i, sub := range pk.Filters { // [MQTT-3.10.4-6] [MQTT-3.11.3-1]
|
|
if code != packets.CodeSuccess {
|
|
reasonCodes[i] = code.Code // NB 3.11.3 Non-normative 0x91
|
|
continue
|
|
}
|
|
|
|
if q := s.Topics.Unsubscribe(sub.Filter, cl.ID); q {
|
|
atomic.AddInt64(&s.Info.Subscriptions, -1)
|
|
reasonCodes[i] = packets.CodeSuccess.Code
|
|
} else {
|
|
reasonCodes[i] = packets.CodeNoSubscriptionExisted.Code
|
|
}
|
|
|
|
cl.State.Subscriptions.Delete(sub.Filter) // [MQTT-3.10.4-2] [MQTT-3.10.4-2] ~[MQTT-3.10.4-3]
|
|
}
|
|
|
|
ack := packets.Packet{ // [MQTT-3.10.4-4]
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Unsuback,
|
|
},
|
|
PacketID: pk.PacketID, // [MQTT-2.2.1-6] [MQTT-3.10.4-5]
|
|
ReasonCodes: reasonCodes, // [MQTT-3.11.3-2]
|
|
Properties: packets.Properties{
|
|
User: pk.Properties.User,
|
|
},
|
|
}
|
|
|
|
if code.Code >= packets.ErrUnspecifiedError.Code {
|
|
ack.Properties.ReasonString = code.Reason
|
|
}
|
|
|
|
s.hooks.OnUnsubscribed(cl, pk)
|
|
return cl.WritePacket(ack)
|
|
}
|
|
|
|
// UnsubscribeClient unsubscribes a client from all of their subscriptions.
|
|
func (s *Server) UnsubscribeClient(cl *Client) {
|
|
i := 0
|
|
filterMap := cl.State.Subscriptions.GetAll()
|
|
filters := make([]packets.Subscription, len(filterMap))
|
|
for k := range filterMap {
|
|
cl.State.Subscriptions.Delete(k)
|
|
}
|
|
|
|
if atomic.LoadUint32(&cl.State.isTakenOver) == 1 {
|
|
return
|
|
}
|
|
|
|
for k, v := range filterMap {
|
|
if s.Topics.Unsubscribe(k, cl.ID) {
|
|
atomic.AddInt64(&s.Info.Subscriptions, -1)
|
|
}
|
|
filters[i] = v
|
|
i++
|
|
}
|
|
s.hooks.OnUnsubscribed(cl, packets.Packet{FixedHeader: packets.FixedHeader{Type: packets.Unsubscribe}, Filters: filters})
|
|
}
|
|
|
|
// processAuth processes an Auth packet.
|
|
func (s *Server) processAuth(cl *Client, pk packets.Packet) error {
|
|
_, err := s.hooks.OnAuthPacket(cl, pk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processDisconnect processes a Disconnect packet.
|
|
func (s *Server) processDisconnect(cl *Client, pk packets.Packet) error {
|
|
if pk.Properties.SessionExpiryIntervalFlag {
|
|
if pk.Properties.SessionExpiryInterval > 0 && cl.Properties.Props.SessionExpiryInterval == 0 {
|
|
return packets.ErrProtocolViolationZeroNonZeroExpiry
|
|
}
|
|
|
|
cl.Properties.Props.SessionExpiryInterval = pk.Properties.SessionExpiryInterval
|
|
cl.Properties.Props.SessionExpiryIntervalFlag = true
|
|
}
|
|
|
|
s.loop.willDelayed.Delete(cl.ID) // [MQTT-3.1.3-9] [MQTT-3.1.2-8]
|
|
cl.Stop(packets.CodeDisconnect) // [MQTT-3.14.4-2]
|
|
|
|
return nil
|
|
}
|
|
|
|
// DisconnectClient sends a Disconnect packet to a client and then closes the client connection.
|
|
func (s *Server) DisconnectClient(cl *Client, code packets.Code) error {
|
|
out := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Disconnect,
|
|
},
|
|
ReasonCode: code.Code,
|
|
Properties: packets.Properties{},
|
|
}
|
|
|
|
if code.Code >= packets.ErrUnspecifiedError.Code {
|
|
out.Properties.ReasonString = code.Reason // // [MQTT-3.14.2-1]
|
|
}
|
|
|
|
// We already have a code we are using to disconnect the client, so we are not
|
|
// interested if the write packet fails due to a closed connection (as we are closing it).
|
|
err := cl.WritePacket(out)
|
|
if !s.Options.Capabilities.Compatibilities.PassiveClientDisconnect {
|
|
cl.Stop(code)
|
|
if code.Code >= packets.ErrUnspecifiedError.Code {
|
|
return code
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// publishSysTopics publishes the current values to the server $SYS topics.
|
|
// Due to the int to string conversions this method is not as cheap as
|
|
// some of the others so the publishing interval should be set appropriately.
|
|
func (s *Server) publishSysTopics() {
|
|
pk := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Publish,
|
|
Retain: true,
|
|
},
|
|
Created: time.Now().Unix(),
|
|
}
|
|
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
atomic.StoreInt64(&s.Info.MemoryAlloc, int64(m.HeapInuse))
|
|
atomic.StoreInt64(&s.Info.Threads, int64(runtime.NumGoroutine()))
|
|
atomic.StoreInt64(&s.Info.Time, time.Now().Unix())
|
|
atomic.StoreInt64(&s.Info.Uptime, time.Now().Unix()-atomic.LoadInt64(&s.Info.Started))
|
|
atomic.StoreInt64(&s.Info.ClientsTotal, int64(s.Clients.Len()))
|
|
atomic.StoreInt64(&s.Info.ClientsDisconnected, atomic.LoadInt64(&s.Info.ClientsTotal)-atomic.LoadInt64(&s.Info.ClientsConnected))
|
|
|
|
topics := map[string]string{
|
|
SysPrefix + "/broker/version": s.Info.Version,
|
|
SysPrefix + "/broker/time": AtomicItoa(&s.Info.Time),
|
|
SysPrefix + "/broker/uptime": AtomicItoa(&s.Info.Uptime),
|
|
SysPrefix + "/broker/started": AtomicItoa(&s.Info.Started),
|
|
SysPrefix + "/broker/load/bytes/received": AtomicItoa(&s.Info.BytesReceived),
|
|
SysPrefix + "/broker/load/bytes/sent": AtomicItoa(&s.Info.BytesSent),
|
|
SysPrefix + "/broker/clients/connected": AtomicItoa(&s.Info.ClientsConnected),
|
|
SysPrefix + "/broker/clients/disconnected": AtomicItoa(&s.Info.ClientsDisconnected),
|
|
SysPrefix + "/broker/clients/maximum": AtomicItoa(&s.Info.ClientsMaximum),
|
|
SysPrefix + "/broker/clients/total": AtomicItoa(&s.Info.ClientsTotal),
|
|
SysPrefix + "/broker/packets/received": AtomicItoa(&s.Info.PacketsReceived),
|
|
SysPrefix + "/broker/packets/sent": AtomicItoa(&s.Info.PacketsSent),
|
|
SysPrefix + "/broker/messages/received": AtomicItoa(&s.Info.MessagesReceived),
|
|
SysPrefix + "/broker/messages/sent": AtomicItoa(&s.Info.MessagesSent),
|
|
SysPrefix + "/broker/messages/dropped": AtomicItoa(&s.Info.MessagesDropped),
|
|
SysPrefix + "/broker/messages/inflight": AtomicItoa(&s.Info.Inflight),
|
|
SysPrefix + "/broker/retained": AtomicItoa(&s.Info.Retained),
|
|
SysPrefix + "/broker/subscriptions": AtomicItoa(&s.Info.Subscriptions),
|
|
SysPrefix + "/broker/system/memory": AtomicItoa(&s.Info.MemoryAlloc),
|
|
SysPrefix + "/broker/system/threads": AtomicItoa(&s.Info.Threads),
|
|
}
|
|
|
|
for topic, payload := range topics {
|
|
pk.TopicName = topic
|
|
pk.Payload = []byte(payload)
|
|
s.Topics.RetainMessage(pk.Copy(false))
|
|
s.publishToSubscribers(pk)
|
|
}
|
|
|
|
s.hooks.OnSysInfoTick(s.Info)
|
|
}
|
|
|
|
// Close attempts to gracefully shut down the server, all listeners, clients, and stores.
|
|
func (s *Server) Close() error {
|
|
close(s.done)
|
|
s.Listeners.CloseAll(s.closeListenerClients)
|
|
s.hooks.OnStopped()
|
|
s.hooks.Stop()
|
|
|
|
s.Log.Info("mochi mqtt server stopped")
|
|
return nil
|
|
}
|
|
|
|
// closeListenerClients closes all clients on the specified listener.
|
|
func (s *Server) closeListenerClients(listener string) {
|
|
clients := s.Clients.GetByListener(listener)
|
|
for _, cl := range clients {
|
|
s.DisconnectClient(cl, packets.ErrServerShuttingDown)
|
|
}
|
|
}
|
|
|
|
// sendLWT issues an LWT message to a topic when a client disconnects.
|
|
func (s *Server) sendLWT(cl *Client) {
|
|
if atomic.LoadUint32(&cl.Properties.Will.Flag) == 0 {
|
|
return
|
|
}
|
|
|
|
modifiedLWT := s.hooks.OnWill(cl, cl.Properties.Will)
|
|
|
|
pk := packets.Packet{
|
|
FixedHeader: packets.FixedHeader{
|
|
Type: packets.Publish,
|
|
Retain: modifiedLWT.Retain, // [MQTT-3.1.2-14] [MQTT-3.1.2-15]
|
|
Qos: modifiedLWT.Qos,
|
|
},
|
|
TopicName: modifiedLWT.TopicName,
|
|
Payload: modifiedLWT.Payload,
|
|
Properties: packets.Properties{
|
|
User: modifiedLWT.User,
|
|
},
|
|
Origin: cl.ID,
|
|
Created: time.Now().Unix(),
|
|
}
|
|
|
|
if cl.Properties.Will.WillDelayInterval > 0 {
|
|
pk.Connect.WillProperties.WillDelayInterval = cl.Properties.Will.WillDelayInterval
|
|
pk.Expiry = time.Now().Unix() + int64(pk.Connect.WillProperties.WillDelayInterval)
|
|
s.loop.willDelayed.Add(cl.ID, pk)
|
|
return
|
|
}
|
|
|
|
if pk.FixedHeader.Retain {
|
|
s.retainMessage(cl, pk)
|
|
}
|
|
|
|
s.publishToSubscribers(pk) // [MQTT-3.1.2-8]
|
|
atomic.StoreUint32(&cl.Properties.Will.Flag, 0) // [MQTT-3.1.2-10]
|
|
s.hooks.OnWillSent(cl, pk)
|
|
}
|
|
|
|
// readStore reads in any data from the persistent datastore (if applicable).
|
|
func (s *Server) readStore() error {
|
|
if s.hooks.Provides(StoredClients) {
|
|
clients, err := s.hooks.StoredClients()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load clients; %w", err)
|
|
}
|
|
s.loadClients(clients)
|
|
s.Log.Debug("loaded clients from store", "len", len(clients))
|
|
}
|
|
|
|
if s.hooks.Provides(StoredSubscriptions) {
|
|
subs, err := s.hooks.StoredSubscriptions()
|
|
if err != nil {
|
|
return fmt.Errorf("load subscriptions; %w", err)
|
|
}
|
|
s.loadSubscriptions(subs)
|
|
s.Log.Debug("loaded subscriptions from store", "len", len(subs))
|
|
}
|
|
|
|
if s.hooks.Provides(StoredInflightMessages) {
|
|
inflight, err := s.hooks.StoredInflightMessages()
|
|
if err != nil {
|
|
return fmt.Errorf("load inflight; %w", err)
|
|
}
|
|
s.loadInflight(inflight)
|
|
s.Log.Debug("loaded inflights from store", "len", len(inflight))
|
|
}
|
|
|
|
if s.hooks.Provides(StoredRetainedMessages) {
|
|
retained, err := s.hooks.StoredRetainedMessages()
|
|
if err != nil {
|
|
return fmt.Errorf("load retained; %w", err)
|
|
}
|
|
s.loadRetained(retained)
|
|
s.Log.Debug("loaded retained messages from store", "len", len(retained))
|
|
}
|
|
|
|
if s.hooks.Provides(StoredSysInfo) {
|
|
sysInfo, err := s.hooks.StoredSysInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("load server info; %w", err)
|
|
}
|
|
s.loadServerInfo(sysInfo.Info)
|
|
s.Log.Debug("loaded $SYS info from store")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadServerInfo restores server info from the datastore.
|
|
func (s *Server) loadServerInfo(v system.Info) {
|
|
if s.Options.Capabilities.Compatibilities.RestoreSysInfoOnRestart {
|
|
atomic.StoreInt64(&s.Info.BytesReceived, v.BytesReceived)
|
|
atomic.StoreInt64(&s.Info.BytesSent, v.BytesSent)
|
|
atomic.StoreInt64(&s.Info.ClientsMaximum, v.ClientsMaximum)
|
|
atomic.StoreInt64(&s.Info.ClientsTotal, v.ClientsTotal)
|
|
atomic.StoreInt64(&s.Info.ClientsDisconnected, v.ClientsDisconnected)
|
|
atomic.StoreInt64(&s.Info.MessagesReceived, v.MessagesReceived)
|
|
atomic.StoreInt64(&s.Info.MessagesSent, v.MessagesSent)
|
|
atomic.StoreInt64(&s.Info.MessagesDropped, v.MessagesDropped)
|
|
atomic.StoreInt64(&s.Info.PacketsReceived, v.PacketsReceived)
|
|
atomic.StoreInt64(&s.Info.PacketsSent, v.PacketsSent)
|
|
atomic.StoreInt64(&s.Info.InflightDropped, v.InflightDropped)
|
|
}
|
|
atomic.StoreInt64(&s.Info.Retained, v.Retained)
|
|
atomic.StoreInt64(&s.Info.Inflight, v.Inflight)
|
|
atomic.StoreInt64(&s.Info.Subscriptions, v.Subscriptions)
|
|
}
|
|
|
|
// loadSubscriptions restores subscriptions from the datastore.
|
|
func (s *Server) loadSubscriptions(v []storage.Subscription) {
|
|
for _, sub := range v {
|
|
sb := packets.Subscription{
|
|
Filter: sub.Filter,
|
|
RetainHandling: sub.RetainHandling,
|
|
Qos: sub.Qos,
|
|
RetainAsPublished: sub.RetainAsPublished,
|
|
NoLocal: sub.NoLocal,
|
|
Identifier: sub.Identifier,
|
|
}
|
|
if s.Topics.Subscribe(sub.Client, sb) {
|
|
if cl, ok := s.Clients.Get(sub.Client); ok {
|
|
cl.State.Subscriptions.Add(sub.Filter, sb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadClients restores clients from the datastore.
|
|
func (s *Server) loadClients(v []storage.Client) {
|
|
for _, c := range v {
|
|
cl := s.NewClient(nil, c.Listener, c.ID, false)
|
|
cl.Properties.Username = c.Username
|
|
cl.Properties.Clean = c.Clean
|
|
cl.Properties.ProtocolVersion = c.ProtocolVersion
|
|
cl.Properties.Props = packets.Properties{
|
|
SessionExpiryInterval: c.Properties.SessionExpiryInterval,
|
|
SessionExpiryIntervalFlag: c.Properties.SessionExpiryIntervalFlag,
|
|
AuthenticationMethod: c.Properties.AuthenticationMethod,
|
|
AuthenticationData: c.Properties.AuthenticationData,
|
|
RequestProblemInfoFlag: c.Properties.RequestProblemInfoFlag,
|
|
RequestProblemInfo: c.Properties.RequestProblemInfo,
|
|
RequestResponseInfo: c.Properties.RequestResponseInfo,
|
|
ReceiveMaximum: c.Properties.ReceiveMaximum,
|
|
TopicAliasMaximum: c.Properties.TopicAliasMaximum,
|
|
User: c.Properties.User,
|
|
MaximumPacketSize: c.Properties.MaximumPacketSize,
|
|
}
|
|
cl.Properties.Will = Will(c.Will)
|
|
s.Clients.Add(cl)
|
|
}
|
|
}
|
|
|
|
// loadInflight restores inflight messages from the datastore.
|
|
func (s *Server) loadInflight(v []storage.Message) {
|
|
for _, msg := range v {
|
|
if client, ok := s.Clients.Get(msg.Origin); ok {
|
|
client.State.Inflight.Set(msg.ToPacket())
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadRetained restores retained messages from the datastore.
|
|
func (s *Server) loadRetained(v []storage.Message) {
|
|
for _, msg := range v {
|
|
s.Topics.RetainMessage(msg.ToPacket())
|
|
}
|
|
}
|
|
|
|
// clearExpiredClients deletes all clients which have been disconnected for longer
|
|
// than their given expiry intervals.
|
|
func (s *Server) clearExpiredClients(dt int64) {
|
|
for id, client := range s.Clients.GetAll() {
|
|
disconnected := atomic.LoadInt64(&client.State.disconnected)
|
|
if disconnected == 0 {
|
|
continue
|
|
}
|
|
|
|
expire := s.Options.Capabilities.MaximumSessionExpiryInterval
|
|
if client.Properties.ProtocolVersion == 5 && client.Properties.Props.SessionExpiryIntervalFlag {
|
|
expire = client.Properties.Props.SessionExpiryInterval
|
|
}
|
|
|
|
if disconnected+int64(expire) < dt {
|
|
s.hooks.OnClientExpired(client)
|
|
s.Clients.Delete(id) // [MQTT-4.1.0-2]
|
|
}
|
|
}
|
|
}
|
|
|
|
// clearExpiredRetainedMessage deletes retained messages from topics if they have expired.
|
|
func (s *Server) clearExpiredRetainedMessages(now int64) {
|
|
for filter, pk := range s.Topics.Retained.GetAll() {
|
|
if (pk.Expiry > 0 && pk.Expiry < now) || pk.Created+s.Options.Capabilities.MaximumMessageExpiryInterval < now {
|
|
s.Topics.Retained.Delete(filter)
|
|
s.hooks.OnRetainedExpired(filter)
|
|
}
|
|
}
|
|
}
|
|
|
|
// clearExpiredInflights deletes any inflight messages which have expired.
|
|
func (s *Server) clearExpiredInflights(now int64) {
|
|
for _, client := range s.Clients.GetAll() {
|
|
if deleted := client.ClearInflights(now, s.Options.Capabilities.MaximumMessageExpiryInterval); len(deleted) > 0 {
|
|
for _, id := range deleted {
|
|
s.hooks.OnQosDropped(client, packets.Packet{PacketID: id})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendDelayedLWT sends any LWT messages which have reached their issue time.
|
|
func (s *Server) sendDelayedLWT(dt int64) {
|
|
for id, pk := range s.loop.willDelayed.GetAll() {
|
|
if dt > pk.Expiry {
|
|
s.publishToSubscribers(pk) // [MQTT-3.1.2-8]
|
|
if cl, ok := s.Clients.Get(id); ok {
|
|
if pk.FixedHeader.Retain {
|
|
s.retainMessage(cl, pk)
|
|
}
|
|
cl.Properties.Will = Will{} // [MQTT-3.1.2-10]
|
|
s.hooks.OnWillSent(cl, pk)
|
|
}
|
|
s.loop.willDelayed.Delete(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AtomicItoa converts an int64 point to a string.
|
|
func AtomicItoa(ptr *int64) string {
|
|
return strconv.FormatInt(atomic.LoadInt64(ptr), 10)
|
|
}
|