6864 lines
196 KiB
Go
6864 lines
196 KiB
Go
// Copyright 2012-2026 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/klauspost/compress/s2"
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats-server/v2/internal/fastrand"
|
|
)
|
|
|
|
// Type of client connection.
|
|
const (
|
|
// CLIENT is an end user.
|
|
CLIENT = iota
|
|
// ROUTER represents another server in the cluster.
|
|
ROUTER
|
|
// GATEWAY is a link between 2 clusters.
|
|
GATEWAY
|
|
// SYSTEM is an internal system client.
|
|
SYSTEM
|
|
// LEAF is for leaf node connections.
|
|
LEAF
|
|
// JETSTREAM is an internal jetstream client.
|
|
JETSTREAM
|
|
// ACCOUNT is for the internal client for accounts.
|
|
ACCOUNT
|
|
)
|
|
|
|
// Internal clients. kind should be SYSTEM, JETSTREAM or ACCOUNT
|
|
func isInternalClient(kind int) bool {
|
|
return kind == SYSTEM || kind == JETSTREAM || kind == ACCOUNT
|
|
}
|
|
|
|
// Extended type of a CLIENT connection. This is returned by c.clientType()
|
|
// and indicate what type of client connection we are dealing with.
|
|
// If invoked on a non CLIENT connection, NON_CLIENT type is returned.
|
|
const (
|
|
// If the connection is not a CLIENT connection.
|
|
NON_CLIENT = iota
|
|
// Regular NATS client.
|
|
NATS
|
|
// MQTT client.
|
|
MQTT
|
|
// Websocket client.
|
|
WS
|
|
)
|
|
|
|
const (
|
|
// ClientProtoZero is the original Client protocol from 2009.
|
|
// http://nats.io/documentation/internals/nats-protocol/
|
|
ClientProtoZero = iota
|
|
// ClientProtoInfo signals a client can receive more then the original INFO block.
|
|
// This can be used to update clients on other cluster members, etc.
|
|
ClientProtoInfo
|
|
)
|
|
|
|
const (
|
|
pingProto = "PING" + _CRLF_
|
|
pongProto = "PONG" + _CRLF_
|
|
errProto = "-ERR '%s'" + _CRLF_
|
|
okProto = "+OK" + _CRLF_
|
|
)
|
|
|
|
// TLS Hanshake client types
|
|
const (
|
|
tlsHandshakeLeaf = "leafnode"
|
|
tlsHandshakeMQTT = "mqtt"
|
|
)
|
|
|
|
const (
|
|
// Scratch buffer size for the processMsg() calls.
|
|
msgScratchSize = 1024
|
|
msgHeadProto = "RMSG "
|
|
msgHeadProtoLen = len(msgHeadProto)
|
|
|
|
// For controlling dynamic buffer sizes.
|
|
startBufSize = 512 // For INFO/CONNECT block
|
|
minBufSize = 64 // Smallest to shrink to for PING/PONG
|
|
maxBufSize = 65536 // 64k
|
|
shortsToShrink = 2 // Trigger to shrink dynamic buffers
|
|
maxFlushPending = 10 // Max fsps to have in order to wait for writeLoop
|
|
readLoopReport = 2 * time.Second
|
|
|
|
// Server should not send a PING (for RTT) before the first PONG has
|
|
// been sent to the client. However, in case some client libs don't
|
|
// send CONNECT+PING, cap the maximum time before server can send
|
|
// the RTT PING.
|
|
maxNoRTTPingBeforeFirstPong = 2 * time.Second
|
|
|
|
// For stalling fast producers
|
|
stallClientMinDuration = 2 * time.Millisecond
|
|
stallClientMaxDuration = 5 * time.Millisecond
|
|
stallTotalAllowed = 10 * time.Millisecond
|
|
)
|
|
|
|
var readLoopReportThreshold = readLoopReport
|
|
|
|
// Represent client booleans with a bitmask
|
|
type clientFlag uint16
|
|
|
|
const (
|
|
hdrLine = "NATS/1.0\r\n"
|
|
emptyHdrLine = "NATS/1.0\r\n\r\n"
|
|
)
|
|
|
|
// Some client state represented as flags
|
|
const (
|
|
connectReceived clientFlag = 1 << iota // The CONNECT proto has been received
|
|
infoReceived // The INFO protocol has been received
|
|
firstPongSent // The first PONG has been sent
|
|
handshakeComplete // For TLS clients, indicate that the handshake is complete
|
|
flushOutbound // Marks client as having a flushOutbound call in progress.
|
|
noReconnect // Indicate that on close, this connection should not attempt a reconnect
|
|
closeConnection // Marks that closeConnection has already been called.
|
|
connMarkedClosed // Marks that markConnAsClosed has already been called.
|
|
writeLoopStarted // Marks that the writeLoop has been started.
|
|
skipFlushOnClose // Marks that flushOutbound() should not be called on connection close.
|
|
expectConnect // Marks if this connection is expected to send a CONNECT
|
|
connectProcessFinished // Marks if this connection has finished the connect process.
|
|
compressionNegotiated // Marks if this connection has negotiated compression level with remote.
|
|
didTLSFirst // Marks if this connection requested and was accepted doing the TLS handshake first (prior to INFO).
|
|
isSlowConsumer // Marks connection as a slow consumer.
|
|
)
|
|
|
|
// set the flag (would be equivalent to set the boolean to true)
|
|
func (cf *clientFlag) set(c clientFlag) {
|
|
*cf |= c
|
|
}
|
|
|
|
// clear the flag (would be equivalent to set the boolean to false)
|
|
func (cf *clientFlag) clear(c clientFlag) {
|
|
*cf &= ^c
|
|
}
|
|
|
|
// isSet returns true if the flag is set, false otherwise
|
|
func (cf clientFlag) isSet(c clientFlag) bool {
|
|
return cf&c != 0
|
|
}
|
|
|
|
// setIfNotSet will set the flag `c` only if that flag was not already
|
|
// set and return true to indicate that the flag has been set. Returns
|
|
// false otherwise.
|
|
func (cf *clientFlag) setIfNotSet(c clientFlag) bool {
|
|
if *cf&c == 0 {
|
|
*cf |= c
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ClosedState is the reason client was closed. This will
|
|
// be passed into calls to clearConnection, but will only
|
|
// be stored in ConnInfo for monitoring.
|
|
type ClosedState int
|
|
|
|
const (
|
|
ClientClosed = ClosedState(iota + 1)
|
|
AuthenticationTimeout
|
|
AuthenticationViolation
|
|
TLSHandshakeError
|
|
SlowConsumerPendingBytes
|
|
SlowConsumerWriteDeadline
|
|
WriteError
|
|
ReadError
|
|
ParseError
|
|
StaleConnection
|
|
ProtocolViolation
|
|
BadClientProtocolVersion
|
|
WrongPort
|
|
MaxAccountConnectionsExceeded
|
|
MaxConnectionsExceeded
|
|
MaxPayloadExceeded
|
|
MaxControlLineExceeded
|
|
MaxSubscriptionsExceeded
|
|
DuplicateRoute
|
|
RouteRemoved
|
|
ServerShutdown
|
|
AuthenticationExpired
|
|
WrongGateway
|
|
MissingAccount
|
|
Revocation
|
|
InternalClient
|
|
MsgHeaderViolation
|
|
NoRespondersRequiresHeaders
|
|
ClusterNameConflict
|
|
DuplicateRemoteLeafnodeConnection
|
|
DuplicateClientID
|
|
DuplicateServerName
|
|
MinimumVersionRequired
|
|
ClusterNamesIdentical
|
|
Kicked
|
|
ProxyNotTrusted
|
|
ProxyRequired
|
|
)
|
|
|
|
// Some flags passed to processMsgResults
|
|
const pmrNoFlag int = 0
|
|
const (
|
|
pmrCollectQueueNames int = 1 << iota
|
|
pmrIgnoreEmptyQueueFilter
|
|
pmrAllowSendFromRouteToRoute
|
|
pmrMsgImportedFromService
|
|
)
|
|
|
|
type WriteTimeoutPolicy uint8
|
|
|
|
const (
|
|
WriteTimeoutPolicyDefault = iota
|
|
WriteTimeoutPolicyClose
|
|
WriteTimeoutPolicyRetry
|
|
)
|
|
|
|
// String returns a human-friendly value. Only used in varz.
|
|
func (p WriteTimeoutPolicy) String() string {
|
|
switch p {
|
|
case WriteTimeoutPolicyClose:
|
|
return "close"
|
|
case WriteTimeoutPolicyRetry:
|
|
return "retry"
|
|
default:
|
|
return _EMPTY_
|
|
}
|
|
}
|
|
|
|
type client struct {
|
|
// Here first because of use of atomics, and memory alignment.
|
|
stats
|
|
gwReplyMapping
|
|
kind int
|
|
srv *Server
|
|
acc *Account
|
|
perms *permissions
|
|
in readCache
|
|
parseState
|
|
opts ClientOpts
|
|
rrTracking *rrTracking
|
|
mpay int32
|
|
msubs int32
|
|
mcl int32
|
|
mu sync.Mutex
|
|
cid uint64
|
|
start time.Time
|
|
nonce []byte
|
|
pubKey string
|
|
nc net.Conn
|
|
ncs atomic.Value
|
|
ncsAcc atomic.Value
|
|
ncsUser atomic.Value
|
|
out outbound
|
|
user *NkeyUser
|
|
host string
|
|
port uint16
|
|
subs map[string]*subscription
|
|
replies map[string]*resp
|
|
mperms *msgDeny
|
|
darray []string
|
|
pcd map[*client]struct{}
|
|
atmr *time.Timer
|
|
expires time.Time
|
|
ping pinfo
|
|
msgb [msgScratchSize]byte
|
|
last time.Time
|
|
lastIn time.Time
|
|
proxyKey string
|
|
|
|
repliesSincePrune uint16
|
|
lastReplyPrune time.Time
|
|
|
|
headers bool
|
|
|
|
rtt time.Duration
|
|
rttStart time.Time
|
|
|
|
route *route
|
|
gw *gateway
|
|
leaf *leaf
|
|
ws *websocket
|
|
mqtt *mqtt
|
|
|
|
flags clientFlag // Compact booleans into a single field. Size will be increased when needed.
|
|
|
|
rref byte
|
|
|
|
trace bool
|
|
echo bool
|
|
noIcb bool
|
|
iproc bool // In-Process connection, set at creation and immutable.
|
|
|
|
tags jwt.TagList
|
|
nameTag string
|
|
|
|
tlsTo *time.Timer
|
|
|
|
// Authentication error override. This is used because the authentication
|
|
// stack is simply returning a boolean, and the only authentication error
|
|
// reported is the generic `ErrAuthentication`. In the authentication code,
|
|
// if we want to report a different error, we can now set this field
|
|
// and `authViolation()` will use that one.
|
|
authErr error
|
|
}
|
|
|
|
type rrTracking struct {
|
|
rmap map[string]*remoteLatency
|
|
ptmr *time.Timer
|
|
lrt time.Duration
|
|
}
|
|
|
|
// Struct for PING initiation from the server.
|
|
type pinfo struct {
|
|
tmr *time.Timer
|
|
out int
|
|
}
|
|
|
|
// outbound holds pending data for a socket.
|
|
type outbound struct {
|
|
nb net.Buffers // Pending buffers for send, each has fixed capacity as per nbPool below.
|
|
wnb net.Buffers // Working copy of "nb", reused on each flushOutbound call, partial writes may leave entries here for next iteration.
|
|
pb int64 // Total pending/queued bytes.
|
|
fsp int32 // Flush signals that are pending per producer from readLoop's pcd.
|
|
wtp WriteTimeoutPolicy // What do we do on a write timeout?
|
|
sg *sync.Cond // To signal writeLoop that there is data to flush.
|
|
wdl time.Duration // Snapshot of write deadline.
|
|
mp int64 // Snapshot of max pending for client.
|
|
lft time.Duration // Last flush time for Write.
|
|
stc chan struct{} // Stall chan we create to slow down producers on overrun, e.g. fan-in.
|
|
cw *s2.Writer
|
|
}
|
|
|
|
const nbMaxVectorSize = 1024 // == IOV_MAX on Linux/Darwin and most other Unices (except Solaris/AIX)
|
|
|
|
const nbPoolSizeSmall = 512 // Underlying array size of small buffer
|
|
const nbPoolSizeMedium = 4096 // Underlying array size of medium buffer
|
|
const nbPoolSizeLarge = 65536 // Underlying array size of large buffer
|
|
|
|
var nbPoolSmall = &sync.Pool{
|
|
New: func() any {
|
|
b := [nbPoolSizeSmall]byte{}
|
|
return &b
|
|
},
|
|
}
|
|
|
|
var nbPoolMedium = &sync.Pool{
|
|
New: func() any {
|
|
b := [nbPoolSizeMedium]byte{}
|
|
return &b
|
|
},
|
|
}
|
|
|
|
var nbPoolLarge = &sync.Pool{
|
|
New: func() any {
|
|
b := [nbPoolSizeLarge]byte{}
|
|
return &b
|
|
},
|
|
}
|
|
|
|
// nbPoolGet returns a frame that is a best-effort match for the given size.
|
|
// Once a pooled frame is no longer needed, it should be recycled by passing
|
|
// it to nbPoolPut.
|
|
func nbPoolGet(sz int) []byte {
|
|
switch {
|
|
case sz <= nbPoolSizeSmall:
|
|
return nbPoolSmall.Get().(*[nbPoolSizeSmall]byte)[:0]
|
|
case sz <= nbPoolSizeMedium:
|
|
return nbPoolMedium.Get().(*[nbPoolSizeMedium]byte)[:0]
|
|
default:
|
|
return nbPoolLarge.Get().(*[nbPoolSizeLarge]byte)[:0]
|
|
}
|
|
}
|
|
|
|
// nbPoolPut recycles a frame that was retrieved from nbPoolGet. It is not
|
|
// safe to return multiple slices referring to chunks of the same underlying
|
|
// array as this may create overlaps when the buffers are returned to their
|
|
// original size, resulting in race conditions.
|
|
func nbPoolPut(b []byte) {
|
|
switch cap(b) {
|
|
case nbPoolSizeSmall:
|
|
b := (*[nbPoolSizeSmall]byte)(b[0:nbPoolSizeSmall])
|
|
nbPoolSmall.Put(b)
|
|
case nbPoolSizeMedium:
|
|
b := (*[nbPoolSizeMedium]byte)(b[0:nbPoolSizeMedium])
|
|
nbPoolMedium.Put(b)
|
|
case nbPoolSizeLarge:
|
|
b := (*[nbPoolSizeLarge]byte)(b[0:nbPoolSizeLarge])
|
|
nbPoolLarge.Put(b)
|
|
default:
|
|
// Ignore frames that are the wrong size, this might happen
|
|
// with WebSocket/MQTT messages as they are framed
|
|
}
|
|
}
|
|
|
|
type perm struct {
|
|
allow *Sublist
|
|
deny *Sublist
|
|
}
|
|
|
|
type permissions struct {
|
|
// Have these 2 first for memory alignment due to the use of atomic.
|
|
pcsz int32
|
|
prun int32
|
|
sub perm
|
|
pub perm
|
|
resp *ResponsePermission
|
|
pcache sync.Map
|
|
}
|
|
|
|
// This is used to dynamically track responses and reply subjects
|
|
// for dynamic permissioning.
|
|
type resp struct {
|
|
t time.Time
|
|
n int
|
|
}
|
|
|
|
// msgDeny is used when a user permission for subscriptions has a deny
|
|
// clause but a subscription could be made that is of broader scope.
|
|
// e.g. deny = "foo", but user subscribes to "*". That subscription should
|
|
// succeed but no message sent on foo should be delivered.
|
|
type msgDeny struct {
|
|
deny *Sublist
|
|
dcache map[string]bool
|
|
}
|
|
|
|
// routeTarget collects information regarding routes and queue groups for
|
|
// sending information to a remote.
|
|
type routeTarget struct {
|
|
sub *subscription
|
|
qs []byte
|
|
_qs [32]byte
|
|
}
|
|
|
|
const (
|
|
maxResultCacheSize = 512
|
|
maxDenyPermCacheSize = 256
|
|
maxPermCacheSize = 128
|
|
pruneSize = 32
|
|
routeTargetInit = 8
|
|
replyPermLimit = 4096
|
|
replyPruneTime = time.Second
|
|
)
|
|
|
|
// Represent read cache booleans with a bitmask
|
|
type readCacheFlag uint16
|
|
|
|
const (
|
|
hasMappings readCacheFlag = 1 << iota // For account subject mappings.
|
|
switchToCompression readCacheFlag = 1 << 1
|
|
)
|
|
|
|
const sysGroup = "_sys_"
|
|
|
|
// Used in readloop to cache hot subject lookups and group statistics.
|
|
type readCache struct {
|
|
// These are for clients who are bound to a single account.
|
|
genid uint64
|
|
results map[string]*SublistResult
|
|
|
|
// This is for routes and gateways to have their own L1 as well that is account aware.
|
|
pacache map[string]*perAccountCache
|
|
|
|
// This is for when we deliver messages across a route. We use this structure
|
|
// to make sure to only send one message and properly scope to queues as needed.
|
|
rts []routeTarget
|
|
|
|
// These are all temporary totals for an invocation of a read in readloop.
|
|
msgs int32
|
|
bytes int32
|
|
subs int32
|
|
|
|
rsz int32 // Read buffer size
|
|
srs int32 // Short reads, used for dynamic buffer resizing.
|
|
|
|
// These are for readcache flags to avoid locks.
|
|
flags readCacheFlag
|
|
|
|
// Capture the time we started processing our readLoop.
|
|
start time.Time
|
|
|
|
// Total time stalled so far for readLoop processing.
|
|
tst time.Duration
|
|
}
|
|
|
|
// set the flag (would be equivalent to set the boolean to true)
|
|
func (rcf *readCacheFlag) set(c readCacheFlag) {
|
|
*rcf |= c
|
|
}
|
|
|
|
// clear the flag (would be equivalent to set the boolean to false)
|
|
func (rcf *readCacheFlag) clear(c readCacheFlag) {
|
|
*rcf &= ^c
|
|
}
|
|
|
|
// isSet returns true if the flag is set, false otherwise
|
|
func (rcf readCacheFlag) isSet(c readCacheFlag) bool {
|
|
return rcf&c != 0
|
|
}
|
|
|
|
const (
|
|
defaultMaxPerAccountCacheSize = 8192
|
|
defaultClosedSubsCheckInterval = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
maxPerAccountCacheSize = defaultMaxPerAccountCacheSize
|
|
closedSubsCheckInterval = defaultClosedSubsCheckInterval
|
|
)
|
|
|
|
// perAccountCache is for L1 semantics for inbound messages from a route or gateway to mimic the performance of clients.
|
|
type perAccountCache struct {
|
|
acc *Account
|
|
results *SublistResult
|
|
genid uint64
|
|
}
|
|
|
|
func (c *client) String() (id string) {
|
|
loaded := c.ncs.Load()
|
|
if loaded != nil {
|
|
return loaded.(string)
|
|
}
|
|
|
|
return _EMPTY_
|
|
}
|
|
|
|
// GetNonce returns the nonce that was presented to the user on connection
|
|
func (c *client) GetNonce() []byte {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
return c.nonce
|
|
}
|
|
|
|
// GetName returns the application supplied name for the connection.
|
|
func (c *client) GetName() string {
|
|
c.mu.Lock()
|
|
name := c.opts.Name
|
|
c.mu.Unlock()
|
|
return name
|
|
}
|
|
|
|
// GetOpts returns the client options provided by the application.
|
|
func (c *client) GetOpts() *ClientOpts {
|
|
return &c.opts
|
|
}
|
|
|
|
// GetTLSConnectionState returns the TLS ConnectionState if TLS is enabled, nil
|
|
// otherwise. Implements the ClientAuth interface.
|
|
func (c *client) GetTLSConnectionState() *tls.ConnectionState {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.nc == nil {
|
|
return nil
|
|
}
|
|
tc, ok := c.nc.(*tls.Conn)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
state := tc.ConnectionState()
|
|
return &state
|
|
}
|
|
|
|
// For CLIENT connections, this function returns the client type, that is,
|
|
// NATS (for regular clients), MQTT or WS for websocket.
|
|
// If this is invoked for a non CLIENT connection, NON_CLIENT is returned.
|
|
//
|
|
// This function does not lock the client and accesses fields that are supposed
|
|
// to be immutable and therefore it can be invoked outside of the client's lock.
|
|
func (c *client) clientType() int {
|
|
switch c.kind {
|
|
case CLIENT:
|
|
if c.isMqtt() {
|
|
return MQTT
|
|
} else if c.isWebsocket() {
|
|
return WS
|
|
}
|
|
return NATS
|
|
default:
|
|
return NON_CLIENT
|
|
}
|
|
}
|
|
|
|
var clientTypeStringMap = map[int]string{
|
|
NON_CLIENT: _EMPTY_,
|
|
NATS: "nats",
|
|
WS: "websocket",
|
|
MQTT: "mqtt",
|
|
}
|
|
|
|
func (c *client) clientTypeString() string {
|
|
if typeStringVal, ok := clientTypeStringMap[c.clientType()]; ok {
|
|
return typeStringVal
|
|
}
|
|
return _EMPTY_
|
|
}
|
|
|
|
// This is the main subscription struct that indicates
|
|
// interest in published messages.
|
|
// FIXME(dlc) - This is getting bloated for normal subs, need
|
|
// to optionally have an opts section for non-normal stuff.
|
|
type subscription struct {
|
|
client *client
|
|
im *streamImport // This is for import stream support.
|
|
rsi bool
|
|
si bool
|
|
shadow []*subscription // This is to track shadowed accounts.
|
|
icb msgHandler
|
|
subject []byte
|
|
queue []byte
|
|
sid []byte
|
|
origin []byte
|
|
nm int64
|
|
max int64
|
|
qw int32
|
|
closed int32
|
|
mqtt *mqttSub
|
|
}
|
|
|
|
// Indicate that this subscription is closed.
|
|
// This is used in pruning of route and gateway cache items.
|
|
func (s *subscription) close() {
|
|
atomic.StoreInt32(&s.closed, 1)
|
|
}
|
|
|
|
// Return true if this subscription was unsubscribed
|
|
// or its connection has been closed.
|
|
func (s *subscription) isClosed() bool {
|
|
return atomic.LoadInt32(&s.closed) == 1
|
|
}
|
|
|
|
type ClientOpts struct {
|
|
Echo bool `json:"echo"`
|
|
Verbose bool `json:"verbose"`
|
|
Pedantic bool `json:"pedantic"`
|
|
TLSRequired bool `json:"tls_required"`
|
|
Nkey string `json:"nkey,omitempty"`
|
|
JWT string `json:"jwt,omitempty"`
|
|
Sig string `json:"sig,omitempty"`
|
|
Token string `json:"auth_token,omitempty"`
|
|
Username string `json:"user,omitempty"`
|
|
Password string `json:"pass,omitempty"`
|
|
Name string `json:"name"`
|
|
Lang string `json:"lang"`
|
|
Version string `json:"version"`
|
|
Protocol int `json:"protocol"`
|
|
Account string `json:"account,omitempty"`
|
|
AccountNew bool `json:"new_account,omitempty"`
|
|
Headers bool `json:"headers,omitempty"`
|
|
NoResponders bool `json:"no_responders,omitempty"`
|
|
|
|
// Routes and Leafnodes only
|
|
Import *SubjectPermission `json:"import,omitempty"`
|
|
Export *SubjectPermission `json:"export,omitempty"`
|
|
|
|
// Leafnodes
|
|
RemoteAccount string `json:"remote_account,omitempty"`
|
|
|
|
// Proxy would include its own nonce signature.
|
|
ProxySig string `json:"proxy_sig,omitempty"`
|
|
}
|
|
|
|
var defaultOpts = ClientOpts{Verbose: true, Pedantic: true, Echo: true}
|
|
var internalOpts = ClientOpts{Verbose: false, Pedantic: false, Echo: false}
|
|
|
|
func (c *client) setTraceLevel() {
|
|
if c.kind == SYSTEM && !(atomic.LoadInt32(&c.srv.logging.traceSysAcc) != 0) {
|
|
c.trace = false
|
|
} else {
|
|
c.trace = (atomic.LoadInt32(&c.srv.logging.trace) != 0)
|
|
}
|
|
}
|
|
|
|
// Lock should be held
|
|
func (c *client) initClient() {
|
|
s := c.srv
|
|
c.cid = atomic.AddUint64(&s.gcid, 1)
|
|
|
|
// Outbound data structure setup
|
|
c.out.sg = sync.NewCond(&(c.mu))
|
|
opts := s.getOpts()
|
|
// Snapshots to avoid mutex access in fast paths.
|
|
c.out.wdl = opts.WriteDeadline
|
|
switch {
|
|
case c.kind == ROUTER && opts.Cluster.WriteDeadline > 0:
|
|
c.out.wdl = opts.Cluster.WriteDeadline
|
|
case c.kind == GATEWAY && opts.Gateway.WriteDeadline > 0:
|
|
c.out.wdl = opts.Gateway.WriteDeadline
|
|
case c.kind == LEAF && opts.LeafNode.WriteDeadline > 0:
|
|
c.out.wdl = opts.LeafNode.WriteDeadline
|
|
}
|
|
switch c.kind {
|
|
case ROUTER:
|
|
if c.out.wtp = opts.Cluster.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault {
|
|
c.out.wtp = WriteTimeoutPolicyRetry
|
|
}
|
|
case LEAF:
|
|
if c.out.wtp = opts.LeafNode.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault {
|
|
c.out.wtp = WriteTimeoutPolicyRetry
|
|
}
|
|
case GATEWAY:
|
|
if c.out.wtp = opts.Gateway.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault {
|
|
c.out.wtp = WriteTimeoutPolicyRetry
|
|
}
|
|
default:
|
|
if c.out.wtp = opts.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault {
|
|
c.out.wtp = WriteTimeoutPolicyClose
|
|
}
|
|
}
|
|
c.out.mp = opts.MaxPending
|
|
// Snapshot max control line since currently can not be changed on reload and we
|
|
// were checking it on each call to parse. If this changes and we allow MaxControlLine
|
|
// to be reloaded without restart, this code will need to change.
|
|
c.mcl = int32(opts.MaxControlLine)
|
|
if c.mcl == 0 {
|
|
c.mcl = MAX_CONTROL_LINE_SIZE
|
|
}
|
|
|
|
c.subs = make(map[string]*subscription)
|
|
c.echo = true
|
|
|
|
c.setTraceLevel()
|
|
|
|
// This is a scratch buffer used for processMsg()
|
|
// The msg header starts with "RMSG ", which can be used
|
|
// for both local and routes.
|
|
// in bytes that is [82 77 83 71 32].
|
|
c.msgb = [msgScratchSize]byte{82, 77, 83, 71, 32}
|
|
|
|
// This is to track pending clients that have data to be flushed
|
|
// after we process inbound msgs from our own connection.
|
|
c.pcd = make(map[*client]struct{})
|
|
|
|
// snapshot the string version of the connection
|
|
var conn string
|
|
if c.nc != nil {
|
|
if addr := c.nc.RemoteAddr(); addr != nil {
|
|
if conn = addr.String(); conn != _EMPTY_ {
|
|
host, port, _ := net.SplitHostPort(conn)
|
|
iPort, _ := strconv.ParseUint(port, 10, 16)
|
|
c.host, c.port = host, uint16(iPort)
|
|
if c.isWebsocket() && c.ws.clientIP != _EMPTY_ {
|
|
cip := c.ws.clientIP
|
|
// Surround IPv6 addresses with square brackets, as
|
|
// net.JoinHostPort would do...
|
|
if strings.Contains(cip, ":") {
|
|
cip = "[" + cip + "]"
|
|
}
|
|
conn = fmt.Sprintf("%s/%s", cip, conn)
|
|
}
|
|
// Now that we have extracted host and port, escape
|
|
// the string because it is going to be used in Sprintf
|
|
conn = strings.ReplaceAll(conn, "%", "%%")
|
|
}
|
|
}
|
|
}
|
|
|
|
switch c.kind {
|
|
case CLIENT:
|
|
switch c.clientType() {
|
|
case NATS:
|
|
c.ncs.Store(fmt.Sprintf("%s - cid:%d", conn, c.cid))
|
|
case WS:
|
|
c.ncs.Store(fmt.Sprintf("%s - wid:%d", conn, c.cid))
|
|
case MQTT:
|
|
var ws string
|
|
if c.isWebsocket() {
|
|
ws = "_ws"
|
|
}
|
|
c.ncs.Store(fmt.Sprintf("%s - mid%s:%d", conn, ws, c.cid))
|
|
}
|
|
case ROUTER:
|
|
c.ncs.Store(fmt.Sprintf("%s - rid:%d", conn, c.cid))
|
|
case GATEWAY:
|
|
c.ncs.Store(fmt.Sprintf("%s - gid:%d", conn, c.cid))
|
|
case LEAF:
|
|
var ws string
|
|
if c.isWebsocket() {
|
|
ws = "_ws"
|
|
}
|
|
c.ncs.Store(fmt.Sprintf("%s - lid%s:%d", conn, ws, c.cid))
|
|
case SYSTEM:
|
|
c.ncs.Store("SYSTEM")
|
|
case JETSTREAM:
|
|
c.ncs.Store("JETSTREAM")
|
|
case ACCOUNT:
|
|
c.ncs.Store("ACCOUNT")
|
|
}
|
|
}
|
|
|
|
// RemoteAddress expose the Address of the client connection,
|
|
// nil when not connected or unknown
|
|
func (c *client) RemoteAddress() net.Addr {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.nc == nil {
|
|
return nil
|
|
}
|
|
|
|
return c.nc.RemoteAddr()
|
|
}
|
|
|
|
// Helper function to report errors.
|
|
func (c *client) reportErrRegisterAccount(acc *Account, err error) {
|
|
if err == ErrTooManyAccountConnections {
|
|
c.maxAccountConnExceeded()
|
|
return
|
|
}
|
|
c.Errorf("Problem registering with account %q: %s", acc.Name, err)
|
|
c.sendErr("Failed Account Registration")
|
|
}
|
|
|
|
// Kind returns the client kind and will be one of the defined constants like CLIENT, ROUTER, GATEWAY, LEAF
|
|
func (c *client) Kind() int {
|
|
c.mu.Lock()
|
|
kind := c.kind
|
|
c.mu.Unlock()
|
|
|
|
return kind
|
|
}
|
|
|
|
// registerWithAccount will register the given user with a specific
|
|
// account. This will change the subject namespace.
|
|
func (c *client) registerWithAccount(acc *Account) error {
|
|
if acc == nil {
|
|
return ErrBadAccount
|
|
}
|
|
acc.mu.RLock()
|
|
bad := acc.sl == nil
|
|
acc.mu.RUnlock()
|
|
if bad {
|
|
return ErrBadAccount
|
|
}
|
|
// If we were previously registered, usually to $G, do accounting here to remove.
|
|
if c.acc != nil {
|
|
if prev := c.acc.removeClient(c); prev == 1 && c.srv != nil {
|
|
c.srv.decActiveAccounts()
|
|
}
|
|
}
|
|
|
|
c.mu.Lock()
|
|
// This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...)
|
|
if c.isClosed() && !isInternalClient(c.kind) {
|
|
c.mu.Unlock()
|
|
return ErrConnectionClosed
|
|
}
|
|
kind := c.kind
|
|
srv := c.srv
|
|
c.acc = acc
|
|
c.applyAccountLimits()
|
|
c.mu.Unlock()
|
|
|
|
// Check if we have a max connections violation
|
|
if kind == CLIENT && acc.MaxTotalConnectionsReached() {
|
|
return ErrTooManyAccountConnections
|
|
} else if kind == LEAF {
|
|
// Check if we are already connected to this cluster.
|
|
if rc := c.remoteCluster(); rc != _EMPTY_ && acc.hasLeafNodeCluster(rc) {
|
|
return ErrLeafNodeLoop
|
|
}
|
|
if acc.MaxTotalLeafNodesReached() {
|
|
return ErrTooManyAccountConnections
|
|
}
|
|
}
|
|
|
|
// Add in new one.
|
|
if prev := acc.addClient(c); prev == 0 && srv != nil {
|
|
srv.incActiveAccounts()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper to determine if we have met or exceeded max subs.
|
|
func (c *client) subsAtLimit() bool {
|
|
return c.msubs != jwt.NoLimit && len(c.subs) >= int(c.msubs)
|
|
}
|
|
|
|
func minLimit(value *int32, limit int32) bool {
|
|
v := atomic.LoadInt32(value)
|
|
if v != jwt.NoLimit {
|
|
if limit != jwt.NoLimit {
|
|
if limit < v {
|
|
atomic.StoreInt32(value, limit)
|
|
return true
|
|
}
|
|
}
|
|
} else if limit != jwt.NoLimit {
|
|
atomic.StoreInt32(value, limit)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Apply account limits
|
|
// Lock is held on entry.
|
|
// FIXME(dlc) - Should server be able to override here?
|
|
func (c *client) applyAccountLimits() {
|
|
if c.acc == nil || (c.kind != CLIENT && c.kind != LEAF) {
|
|
return
|
|
}
|
|
atomic.StoreInt32(&c.mpay, jwt.NoLimit)
|
|
c.msubs = jwt.NoLimit
|
|
if c.opts.JWT != _EMPTY_ { // user jwt implies account
|
|
if uc, _ := jwt.DecodeUserClaims(c.opts.JWT); uc != nil {
|
|
atomic.StoreInt32(&c.mpay, clampInt64ToInt32(uc.Limits.Payload))
|
|
c.msubs = clampInt64ToInt32(uc.Limits.Subs)
|
|
if uc.IssuerAccount != _EMPTY_ && uc.IssuerAccount != uc.Issuer {
|
|
if scope, ok := c.acc.signingKeys[uc.Issuer]; ok {
|
|
if userScope, ok := scope.(*jwt.UserScope); ok {
|
|
// if signing key disappeared or changed and we don't get here, the client will be disconnected
|
|
c.mpay = clampInt64ToInt32(userScope.Template.Limits.Payload)
|
|
c.msubs = clampInt64ToInt32(userScope.Template.Limits.Subs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c.acc.mu.RLock()
|
|
minLimit(&c.mpay, c.acc.mpay)
|
|
minLimit(&c.msubs, c.acc.msubs)
|
|
c.acc.mu.RUnlock()
|
|
|
|
s := c.srv
|
|
opts := s.getOpts()
|
|
mPay := opts.MaxPayload
|
|
// options encode unlimited differently
|
|
if mPay == 0 {
|
|
mPay = jwt.NoLimit
|
|
}
|
|
mSubs := int32(opts.MaxSubs)
|
|
if mSubs == 0 {
|
|
mSubs = jwt.NoLimit
|
|
}
|
|
wasUnlimited := c.mpay == jwt.NoLimit
|
|
if minLimit(&c.mpay, mPay) && !wasUnlimited {
|
|
c.Errorf("Max Payload set to %d from server overrides account or user config", opts.MaxPayload)
|
|
}
|
|
wasUnlimited = c.msubs == jwt.NoLimit
|
|
if minLimit(&c.msubs, mSubs) && !wasUnlimited {
|
|
c.Errorf("Max Subscriptions set to %d from server overrides account or user config", opts.MaxSubs)
|
|
}
|
|
if c.subsAtLimit() {
|
|
go func() {
|
|
c.maxSubsExceeded()
|
|
time.Sleep(20 * time.Millisecond)
|
|
c.closeConnection(MaxSubscriptionsExceeded)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// RegisterUser allows auth to call back into a new client
|
|
// with the authenticated user. This is used to map
|
|
// any permissions into the client and setup accounts.
|
|
func (c *client) RegisterUser(user *User) {
|
|
// Register with proper account and sublist.
|
|
if user.Account != nil {
|
|
if err := c.registerWithAccount(user.Account); err != nil {
|
|
c.reportErrRegisterAccount(user.Account, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
c.mu.Lock()
|
|
|
|
// Assign permissions.
|
|
if user.Permissions == nil {
|
|
// Reset perms to nil in case client previously had them.
|
|
c.perms = nil
|
|
c.mperms = nil
|
|
} else {
|
|
c.setPermissions(user.Permissions)
|
|
}
|
|
|
|
// allows custom authenticators to set a username to be reported in
|
|
// server events and more
|
|
if user.Username != _EMPTY_ {
|
|
c.opts.Username = user.Username
|
|
}
|
|
|
|
// if a deadline time stamp is set we start a timer to disconnect the user at that time
|
|
if !user.ConnectionDeadline.IsZero() {
|
|
c.setExpirationTimerUnlocked(time.Until(user.ConnectionDeadline))
|
|
}
|
|
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// RegisterNkeyUser allows auth to call back into a new nkey
|
|
// client with the authenticated user. This is used to map
|
|
// any permissions into the client and setup accounts.
|
|
func (c *client) RegisterNkeyUser(user *NkeyUser) error {
|
|
// Register with proper account and sublist.
|
|
if user.Account != nil {
|
|
if err := c.registerWithAccount(user.Account); err != nil {
|
|
c.reportErrRegisterAccount(user.Account, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.user = user
|
|
// Assign permissions.
|
|
if user.Permissions == nil {
|
|
// Reset perms to nil in case client previously had them.
|
|
c.perms = nil
|
|
c.mperms = nil
|
|
} else {
|
|
c.setPermissions(user.Permissions)
|
|
}
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func splitSubjectQueue(sq string) ([]byte, []byte, error) {
|
|
vals := strings.Fields(strings.TrimSpace(sq))
|
|
s := []byte(vals[0])
|
|
var q []byte
|
|
if len(vals) == 2 {
|
|
q = []byte(vals[1])
|
|
} else if len(vals) > 2 {
|
|
return nil, nil, fmt.Errorf("invalid subject-queue %q", sq)
|
|
}
|
|
return s, q, nil
|
|
}
|
|
|
|
// Initializes client.perms structure.
|
|
// Lock is held on entry.
|
|
func (c *client) setPermissions(perms *Permissions) {
|
|
if perms == nil {
|
|
return
|
|
}
|
|
c.perms = &permissions{}
|
|
slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
|
|
|
|
// Loop over publish permissions
|
|
if perms.Publish != nil {
|
|
if perms.Publish.Allow != nil {
|
|
c.perms.pub.allow = NewSublist(slcache)
|
|
}
|
|
for _, pubSubject := range perms.Publish.Allow {
|
|
sub := &subscription{subject: []byte(pubSubject)}
|
|
c.perms.pub.allow.Insert(sub)
|
|
}
|
|
if len(perms.Publish.Deny) > 0 {
|
|
c.perms.pub.deny = NewSublist(slcache)
|
|
}
|
|
for _, pubSubject := range perms.Publish.Deny {
|
|
sub := &subscription{subject: []byte(pubSubject)}
|
|
c.perms.pub.deny.Insert(sub)
|
|
}
|
|
}
|
|
|
|
// Check if we are allowed to send responses.
|
|
if perms.Response != nil {
|
|
rp := *perms.Response
|
|
c.perms.resp = &rp
|
|
c.replies = make(map[string]*resp)
|
|
}
|
|
|
|
// Loop over subscribe permissions
|
|
if perms.Subscribe != nil {
|
|
var err error
|
|
if len(perms.Subscribe.Allow) > 0 {
|
|
c.perms.sub.allow = NewSublist(slcache)
|
|
}
|
|
for _, subSubject := range perms.Subscribe.Allow {
|
|
sub := &subscription{}
|
|
sub.subject, sub.queue, err = splitSubjectQueue(subSubject)
|
|
if err != nil {
|
|
c.Errorf("%s", err.Error())
|
|
continue
|
|
}
|
|
c.perms.sub.allow.Insert(sub)
|
|
}
|
|
if len(perms.Subscribe.Deny) > 0 {
|
|
c.perms.sub.deny = NewSublist(slcache)
|
|
// Also hold onto this array for later.
|
|
c.darray = perms.Subscribe.Deny
|
|
}
|
|
for _, subSubject := range perms.Subscribe.Deny {
|
|
sub := &subscription{}
|
|
sub.subject, sub.queue, err = splitSubjectQueue(subSubject)
|
|
if err != nil {
|
|
c.Errorf("%s", err.Error())
|
|
continue
|
|
}
|
|
c.perms.sub.deny.Insert(sub)
|
|
}
|
|
}
|
|
|
|
// If we are a leafnode and we are the hub copy the extracted perms
|
|
// to resend back to soliciting server. These are reversed from the
|
|
// way routes interpret them since this is how the soliciting server
|
|
// will receive these back in an update INFO.
|
|
if c.isHubLeafNode() {
|
|
c.opts.Import = perms.Subscribe
|
|
c.opts.Export = perms.Publish
|
|
}
|
|
}
|
|
|
|
// Build public permissions from internal ones.
|
|
// Used for user info requests.
|
|
func (c *client) publicPermissions() *Permissions {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.perms == nil {
|
|
return nil
|
|
}
|
|
perms := &Permissions{
|
|
Publish: &SubjectPermission{},
|
|
Subscribe: &SubjectPermission{},
|
|
}
|
|
|
|
_subs := [32]*subscription{}
|
|
|
|
// Publish
|
|
if c.perms.pub.allow != nil {
|
|
subs := _subs[:0]
|
|
c.perms.pub.allow.All(&subs)
|
|
for _, sub := range subs {
|
|
perms.Publish.Allow = append(perms.Publish.Allow, string(sub.subject))
|
|
}
|
|
}
|
|
if c.perms.pub.deny != nil {
|
|
subs := _subs[:0]
|
|
c.perms.pub.deny.All(&subs)
|
|
for _, sub := range subs {
|
|
perms.Publish.Deny = append(perms.Publish.Deny, string(sub.subject))
|
|
}
|
|
}
|
|
// Subsribe
|
|
if c.perms.sub.allow != nil {
|
|
subs := _subs[:0]
|
|
c.perms.sub.allow.All(&subs)
|
|
for _, sub := range subs {
|
|
perms.Subscribe.Allow = append(perms.Subscribe.Allow, string(sub.subject))
|
|
}
|
|
}
|
|
if c.perms.sub.deny != nil {
|
|
subs := _subs[:0]
|
|
c.perms.sub.deny.All(&subs)
|
|
for _, sub := range subs {
|
|
perms.Subscribe.Deny = append(perms.Subscribe.Deny, string(sub.subject))
|
|
}
|
|
}
|
|
// Responses.
|
|
if c.perms.resp != nil {
|
|
rp := *c.perms.resp
|
|
perms.Response = &rp
|
|
}
|
|
|
|
return perms
|
|
}
|
|
|
|
type denyType int
|
|
|
|
const (
|
|
pub = denyType(iota + 1)
|
|
sub
|
|
both
|
|
)
|
|
|
|
// Merge client.perms structure with additional pub deny permissions
|
|
// Lock is held on entry.
|
|
func (c *client) mergeDenyPermissions(what denyType, denyPubs []string) {
|
|
if len(denyPubs) == 0 {
|
|
return
|
|
}
|
|
if c.perms == nil {
|
|
c.perms = &permissions{}
|
|
}
|
|
slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
|
|
var perms []*perm
|
|
switch what {
|
|
case pub:
|
|
perms = []*perm{&c.perms.pub}
|
|
case sub:
|
|
perms = []*perm{&c.perms.sub}
|
|
case both:
|
|
perms = []*perm{&c.perms.pub, &c.perms.sub}
|
|
}
|
|
for _, p := range perms {
|
|
if p.deny == nil {
|
|
p.deny = NewSublist(slcache)
|
|
}
|
|
FOR_DENY:
|
|
for _, subj := range denyPubs {
|
|
r := p.deny.Match(subj)
|
|
for _, v := range r.qsubs {
|
|
for _, s := range v {
|
|
if string(s.subject) == subj {
|
|
continue FOR_DENY
|
|
}
|
|
}
|
|
}
|
|
for _, s := range r.psubs {
|
|
if string(s.subject) == subj {
|
|
continue FOR_DENY
|
|
}
|
|
}
|
|
sub := &subscription{subject: []byte(subj)}
|
|
p.deny.Insert(sub)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge client.perms structure with additional pub deny permissions
|
|
// Client lock must not be held on entry
|
|
func (c *client) mergeDenyPermissionsLocked(what denyType, denyPubs []string) {
|
|
c.mu.Lock()
|
|
c.mergeDenyPermissions(what, denyPubs)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Check to see if we have an expiration for the user JWT via base claims.
|
|
// FIXME(dlc) - Clear on connect with new JWT.
|
|
func (c *client) setExpiration(claims *jwt.ClaimsData, validFor time.Duration) {
|
|
if claims.Expires == 0 {
|
|
if validFor != 0 {
|
|
c.setExpirationTimer(validFor)
|
|
}
|
|
return
|
|
}
|
|
expiresAt := time.Duration(0)
|
|
tn := time.Now().Unix()
|
|
if claims.Expires > tn {
|
|
expiresAt = time.Duration(claims.Expires-tn) * time.Second
|
|
}
|
|
if validFor != 0 && validFor < expiresAt {
|
|
c.setExpirationTimer(validFor)
|
|
} else {
|
|
c.setExpirationTimer(expiresAt)
|
|
}
|
|
}
|
|
|
|
// This will load up the deny structure used for filtering delivered
|
|
// messages based on a deny clause for subscriptions.
|
|
// Lock should be held.
|
|
func (c *client) loadMsgDenyFilter() {
|
|
c.mperms = &msgDeny{NewSublistWithCache(), make(map[string]bool)}
|
|
for _, sub := range c.darray {
|
|
c.mperms.deny.Insert(&subscription{subject: []byte(sub)})
|
|
}
|
|
}
|
|
|
|
// writeLoop is the main socket write functionality.
|
|
// Runs in its own Go routine.
|
|
func (c *client) writeLoop() {
|
|
defer c.srv.grWG.Done()
|
|
c.mu.Lock()
|
|
if c.isClosed() {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
c.flags.set(writeLoopStarted)
|
|
c.mu.Unlock()
|
|
|
|
// Used to check that we did flush from last wake up.
|
|
waitOk := true
|
|
var closed bool
|
|
|
|
// Main loop. Will wait to be signaled and then will use
|
|
// buffered outbound structure for efficient writev to the underlying socket.
|
|
for {
|
|
c.mu.Lock()
|
|
if closed = c.isClosed(); !closed {
|
|
owtf := c.out.fsp > 0 && c.out.pb < maxBufSize && c.out.fsp < maxFlushPending
|
|
if waitOk && (c.out.pb == 0 || owtf) {
|
|
c.out.sg.Wait()
|
|
// Check that connection has not been closed while lock was released
|
|
// in the conditional wait.
|
|
closed = c.isClosed()
|
|
}
|
|
}
|
|
if closed {
|
|
c.flushAndClose(false)
|
|
c.mu.Unlock()
|
|
|
|
// We should always call closeConnection() to ensure that state is
|
|
// properly cleaned-up. It will be a no-op if already done.
|
|
c.closeConnection(WriteError)
|
|
|
|
// Now explicitly call reconnect(). Thanks to ref counting, we know
|
|
// that the reconnect will execute only after connection has been
|
|
// removed from the server state.
|
|
c.reconnect()
|
|
return
|
|
}
|
|
// Flush data
|
|
waitOk = c.flushOutbound()
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// flushClients will make sure to flush any clients we may have
|
|
// sent to during processing. We pass in a budget as a time.Duration
|
|
// for how much time to spend in place flushing for this client.
|
|
func (c *client) flushClients(budget time.Duration) time.Time {
|
|
last := time.Now()
|
|
|
|
// Check pending clients for flush.
|
|
for cp := range c.pcd {
|
|
// TODO(dlc) - Wonder if it makes more sense to create a new map?
|
|
delete(c.pcd, cp)
|
|
|
|
// Queue up a flush for those in the set
|
|
cp.mu.Lock()
|
|
// Update last activity for message delivery
|
|
cp.last = last
|
|
// Remove ourselves from the pending list.
|
|
cp.out.fsp--
|
|
|
|
// Just ignore if this was closed.
|
|
if cp.isClosed() {
|
|
cp.mu.Unlock()
|
|
continue
|
|
}
|
|
|
|
if budget > 0 && cp.out.lft < 2*budget && cp.flushOutbound() {
|
|
budget -= cp.out.lft
|
|
} else {
|
|
cp.flushSignal()
|
|
}
|
|
|
|
cp.mu.Unlock()
|
|
}
|
|
return last
|
|
}
|
|
|
|
func (c *client) resetReadLoopStallTime() {
|
|
if c.in.tst >= stallClientMaxDuration {
|
|
c.rateLimitFormatWarnf("Producer was stalled for a total of %v", c.in.tst.Round(time.Millisecond))
|
|
}
|
|
c.in.tst = 0
|
|
}
|
|
|
|
// readLoop is the main socket read functionality.
|
|
// Runs in its own Go routine.
|
|
func (c *client) readLoop(pre []byte) {
|
|
// Grab the connection off the client, it will be cleared on a close.
|
|
// We check for that after the loop, but want to avoid a nil dereference
|
|
c.mu.Lock()
|
|
s := c.srv
|
|
defer s.grWG.Done()
|
|
if c.isClosed() {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
nc := c.nc
|
|
ws := c.isWebsocket()
|
|
if c.isMqtt() {
|
|
c.mqtt.r = &mqttReader{reader: nc}
|
|
}
|
|
c.in.rsz = startBufSize
|
|
|
|
// Check the per-account-cache for closed subscriptions
|
|
cpacc := c.kind == ROUTER || c.kind == GATEWAY
|
|
// Last per-account-cache check for closed subscriptions
|
|
lpacc := time.Now()
|
|
acc := c.acc
|
|
var masking bool
|
|
if ws {
|
|
masking = c.ws.maskread
|
|
}
|
|
checkCompress := c.kind == ROUTER || c.kind == LEAF
|
|
c.mu.Unlock()
|
|
|
|
defer func() {
|
|
if c.isMqtt() {
|
|
s.mqttHandleClosedClient(c)
|
|
}
|
|
// These are used only in the readloop, so we can set them to nil
|
|
// on exit of the readLoop.
|
|
c.in.results, c.in.pacache = nil, nil
|
|
}()
|
|
|
|
// Start read buffer.
|
|
b := make([]byte, c.in.rsz)
|
|
|
|
// Websocket clients will return several slices if there are multiple
|
|
// websocket frames in the blind read. For non WS clients though, we
|
|
// will always have 1 slice per loop iteration. So we define this here
|
|
// so non WS clients will use bufs[0] = b[:n].
|
|
var _bufs [1][]byte
|
|
bufs := _bufs[:1]
|
|
|
|
var wsr *wsReadInfo
|
|
if ws {
|
|
wsr = &wsReadInfo{mask: masking}
|
|
wsr.init()
|
|
}
|
|
|
|
var decompress bool
|
|
var reader io.Reader
|
|
reader = nc
|
|
|
|
for {
|
|
var n int
|
|
var err error
|
|
|
|
// If we have a pre buffer parse that first.
|
|
if len(pre) > 0 {
|
|
b = pre
|
|
n = len(pre)
|
|
pre = nil
|
|
} else {
|
|
n, err = reader.Read(b)
|
|
// If we have any data we will try to parse and exit at the end.
|
|
if n == 0 && err != nil {
|
|
c.closeConnection(closedStateForErr(err))
|
|
return
|
|
}
|
|
}
|
|
// Check if the account has mappings and if so set the local readcache flag.
|
|
// We check here to make sure any changes such as config reload are reflected here.
|
|
if c.kind == CLIENT || c.kind == LEAF {
|
|
if acc.hasMappings() {
|
|
c.in.flags.set(hasMappings)
|
|
} else {
|
|
c.in.flags.clear(hasMappings)
|
|
}
|
|
}
|
|
|
|
c.in.start = time.Now()
|
|
|
|
// Clear inbound stats cache
|
|
c.in.msgs = 0
|
|
c.in.bytes = 0
|
|
c.in.subs = 0
|
|
|
|
if ws {
|
|
err = c.wsReadAndParse(wsr, reader, b[:n])
|
|
if err != nil {
|
|
// Match the normal parse path: any already-buffered deliveries
|
|
// need their pending flush signals drained before we close.
|
|
c.flushClients(0)
|
|
if err != io.EOF {
|
|
c.Errorf("read error: %v", err)
|
|
}
|
|
c.closeConnection(closedStateForErr(err))
|
|
return
|
|
}
|
|
c.resetReadLoopStallTime()
|
|
goto postParse
|
|
} else {
|
|
bufs[0] = b[:n]
|
|
}
|
|
|
|
// Main call into parser for inbound data. This will generate callouts
|
|
// to process messages, etc.
|
|
for i := 0; i < len(bufs); i++ {
|
|
if err := c.parse(bufs[i]); err != nil {
|
|
if err == ErrMinimumVersionRequired {
|
|
// Special case here, currently only for leaf node connections.
|
|
// processLeafConnect() already sent the rejection and closed
|
|
// the connection, so there is nothing else to do here.
|
|
return
|
|
}
|
|
if dur := time.Since(c.in.start); dur >= readLoopReportThreshold {
|
|
c.Warnf("Readloop processing time: %v", dur)
|
|
}
|
|
// Need to call flushClients because some of the clients have been
|
|
// assigned messages and their "fsp" incremented, and need now to be
|
|
// decremented and their writeLoop signaled.
|
|
c.flushClients(0)
|
|
// handled inline
|
|
if err != ErrMaxPayload && err != ErrAuthentication {
|
|
c.Error(err)
|
|
c.closeConnection(ProtocolViolation)
|
|
}
|
|
return
|
|
}
|
|
c.resetReadLoopStallTime()
|
|
}
|
|
|
|
postParse:
|
|
// If we are a ROUTER/LEAF and have processed an INFO, it is possible that
|
|
// we are asked to switch to compression now.
|
|
if checkCompress && c.in.flags.isSet(switchToCompression) {
|
|
c.in.flags.clear(switchToCompression)
|
|
// For now we support only s2 compression...
|
|
reader = s2.NewReader(nc)
|
|
decompress = true
|
|
}
|
|
|
|
// Updates stats for client and server that were collected
|
|
// from parsing through the buffer.
|
|
if c.in.msgs > 0 {
|
|
inMsgs := int64(c.in.msgs)
|
|
inBytes := int64(c.in.bytes)
|
|
|
|
atomic.AddInt64(&c.inMsgs, inMsgs)
|
|
atomic.AddInt64(&c.inBytes, inBytes)
|
|
|
|
if acc != nil {
|
|
acc.stats.Lock()
|
|
acc.stats.inMsgs += inMsgs
|
|
acc.stats.inBytes += inBytes
|
|
if c.kind == LEAF {
|
|
acc.stats.ln.inMsgs += int64(inMsgs)
|
|
acc.stats.ln.inBytes += int64(inBytes)
|
|
}
|
|
acc.stats.Unlock()
|
|
}
|
|
|
|
atomic.AddInt64(&s.inMsgs, inMsgs)
|
|
atomic.AddInt64(&s.inBytes, inBytes)
|
|
}
|
|
|
|
// Signal to writeLoop to flush to socket.
|
|
last := c.flushClients(0)
|
|
|
|
// Update activity, check read buffer size.
|
|
c.mu.Lock()
|
|
|
|
// Activity based on interest changes or data/msgs.
|
|
// Also update last receive activity for ping sender
|
|
if c.in.msgs > 0 || c.in.subs > 0 {
|
|
c.last = last
|
|
c.lastIn = last
|
|
}
|
|
|
|
if n >= cap(b) {
|
|
c.in.srs = 0
|
|
} else if n < cap(b)/2 { // divide by 2 b/c we want less than what we would shrink to.
|
|
c.in.srs++
|
|
}
|
|
|
|
// Update read buffer size as/if needed.
|
|
if n >= cap(b) && cap(b) < maxBufSize {
|
|
// Grow
|
|
c.in.rsz = int32(cap(b) * 2)
|
|
b = make([]byte, c.in.rsz)
|
|
} else if n < cap(b) && cap(b) > minBufSize && c.in.srs > shortsToShrink {
|
|
// Shrink, for now don't accelerate, ping/pong will eventually sort it out.
|
|
c.in.rsz = int32(cap(b) / 2)
|
|
b = make([]byte, c.in.rsz)
|
|
}
|
|
// re-snapshot the account since it can change during reload, etc.
|
|
acc = c.acc
|
|
// Refresh nc because in some cases, we have upgraded c.nc to TLS.
|
|
if nc != c.nc {
|
|
nc = c.nc
|
|
if decompress && nc != nil {
|
|
// For now we support only s2 compression...
|
|
reader.(*s2.Reader).Reset(nc)
|
|
} else if !decompress {
|
|
reader = nc
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
// Connection was closed
|
|
if nc == nil {
|
|
return
|
|
}
|
|
|
|
if dur := time.Since(c.in.start); dur >= readLoopReportThreshold {
|
|
c.Warnf("Readloop processing time: %v", dur)
|
|
}
|
|
|
|
// We could have had a read error from above but still read some data.
|
|
// If so do the close here unconditionally.
|
|
if err != nil {
|
|
c.closeConnection(closedStateForErr(err))
|
|
return
|
|
}
|
|
|
|
if cpacc && (c.in.start.Sub(lpacc)) >= closedSubsCheckInterval {
|
|
c.pruneClosedSubFromPerAccountCache()
|
|
lpacc = time.Now()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Returns the appropriate closed state for a given read error.
|
|
func closedStateForErr(err error) ClosedState {
|
|
if err == io.EOF {
|
|
return ClientClosed
|
|
}
|
|
return ReadError
|
|
}
|
|
|
|
// collapsePtoNB will either returned framed WebSocket buffers or it will
|
|
// return a reference to c.out.nb.
|
|
func (c *client) collapsePtoNB() (net.Buffers, int64) {
|
|
if c.isWebsocket() {
|
|
return c.wsCollapsePtoNB()
|
|
}
|
|
return c.out.nb, c.out.pb
|
|
}
|
|
|
|
// flushOutbound will flush outbound buffer to a client.
|
|
// Will return true if data was attempted to be written.
|
|
// Lock must be held
|
|
func (c *client) flushOutbound() bool {
|
|
if c.flags.isSet(flushOutbound) {
|
|
// For CLIENT connections, it is possible that the readLoop calls
|
|
// flushOutbound(). If writeLoop and readLoop compete and we are
|
|
// here we should release the lock to reduce the risk of spinning.
|
|
c.mu.Unlock()
|
|
runtime.Gosched()
|
|
c.mu.Lock()
|
|
return false
|
|
}
|
|
c.flags.set(flushOutbound)
|
|
defer func() {
|
|
// Check flushAndClose() for explanation on why we do this.
|
|
if c.isClosed() {
|
|
for i := range c.out.wnb {
|
|
nbPoolPut(c.out.wnb[i])
|
|
}
|
|
c.out.wnb = nil
|
|
}
|
|
c.flags.clear(flushOutbound)
|
|
}()
|
|
|
|
// Check for nothing to do.
|
|
if c.nc == nil || c.srv == nil || c.out.pb == 0 {
|
|
return true // true because no need to queue a signal.
|
|
}
|
|
|
|
// In the case of a normal socket connection, "collapsed" is just a ref
|
|
// to "nb". In the case of WebSockets, additional framing is added to
|
|
// anything that is waiting in "nb". Also keep a note of how many bytes
|
|
// were queued before we release the mutex.
|
|
collapsed, attempted := c.collapsePtoNB()
|
|
|
|
// Frustratingly, (net.Buffers).WriteTo() modifies the receiver so we
|
|
// can't work on "nb" directly — while the mutex is unlocked during IO,
|
|
// something else might call queueOutbound and modify it. So instead we
|
|
// need a working copy — we'll operate on "wnb" instead. Note that in
|
|
// the case of a partial write, "wnb" may have remaining data from the
|
|
// previous write, and in the case of WebSockets, that data may already
|
|
// be framed, so we are careful not to re-frame "wnb" here. Instead we
|
|
// will just frame up "nb" and append it onto whatever is left on "wnb".
|
|
// "nb" will be set to nil so that we can manipulate "collapsed" outside
|
|
// of the client's lock, which is interesting in case of compression.
|
|
c.out.nb = nil
|
|
|
|
// In case it goes away after releasing the lock.
|
|
nc := c.nc
|
|
|
|
// Capture this (we change the value in some tests)
|
|
wdl := c.out.wdl
|
|
|
|
// Check for compression
|
|
cw := c.out.cw
|
|
if cw != nil {
|
|
// We will have to adjust once we have compressed, so remove for now.
|
|
c.out.pb -= attempted
|
|
if c.isWebsocket() {
|
|
c.ws.fs -= attempted
|
|
}
|
|
}
|
|
|
|
// Do NOT hold lock during actual IO.
|
|
c.mu.Unlock()
|
|
|
|
// Compress outside of the lock
|
|
if cw != nil {
|
|
var err error
|
|
bb := bytes.Buffer{}
|
|
|
|
cw.Reset(&bb)
|
|
for _, buf := range collapsed {
|
|
if err == nil {
|
|
_, err = cw.Write(buf)
|
|
}
|
|
// Return always after consumed or error.
|
|
nbPoolPut(buf)
|
|
}
|
|
if err == nil {
|
|
err = cw.Close()
|
|
}
|
|
if err != nil {
|
|
c.Errorf("Error compressing data: %v", err)
|
|
// We need to grab the lock now before marking as closed and exiting
|
|
c.mu.Lock()
|
|
c.markConnAsClosed(WriteError)
|
|
return false
|
|
}
|
|
collapsed = append(net.Buffers(nil), bb.Bytes())
|
|
attempted = int64(len(collapsed[0]))
|
|
}
|
|
|
|
// This is safe to do outside of the lock since "collapsed" is no longer
|
|
// referenced in c.out.nb (which can be modified in queueOutboud() while
|
|
// the lock is released).
|
|
c.out.wnb = append(c.out.wnb, collapsed...)
|
|
var _orig [nbMaxVectorSize][]byte
|
|
orig := append(_orig[:0], c.out.wnb...)
|
|
|
|
// Since WriteTo is lopping things off the beginning, we need to remember
|
|
// the start position of the underlying array so that we can get back to it.
|
|
// Otherwise we'll always "slide forward" and that will result in reallocs.
|
|
startOfWnb := c.out.wnb[0:]
|
|
|
|
// flush here
|
|
start := time.Now()
|
|
|
|
var n int64 // Total bytes written
|
|
var wn int64 // Bytes written per loop
|
|
var err error // Error from last write, if any
|
|
for len(c.out.wnb) > 0 {
|
|
// Limit the number of vectors to no more than nbMaxVectorSize,
|
|
// which if 1024, will mean a maximum of 64MB in one go.
|
|
wnb := c.out.wnb
|
|
if len(wnb) > nbMaxVectorSize {
|
|
wnb = wnb[:nbMaxVectorSize]
|
|
}
|
|
consumed := len(wnb)
|
|
|
|
// Actual write to the socket. The deadline applies to each batch
|
|
// rather than the total write, such that the configured deadline
|
|
// can be tuned to a known maximum quantity (64MB).
|
|
nc.SetWriteDeadline(time.Now().Add(wdl))
|
|
wn, err = wnb.WriteTo(nc)
|
|
nc.SetWriteDeadline(time.Time{})
|
|
|
|
// Update accounting, move wnb slice onwards if needed, or stop
|
|
// if a write error was reported that wasn't a short write.
|
|
n += wn
|
|
c.out.wnb = c.out.wnb[consumed-len(wnb):]
|
|
if err != nil && err != io.ErrShortWrite {
|
|
break
|
|
}
|
|
}
|
|
|
|
lft := time.Since(start)
|
|
|
|
// Re-acquire client lock.
|
|
c.mu.Lock()
|
|
|
|
// Adjust if we were compressing.
|
|
if cw != nil {
|
|
c.out.pb += attempted
|
|
if c.isWebsocket() {
|
|
c.ws.fs += attempted
|
|
}
|
|
}
|
|
|
|
// At this point, "wnb" has been mutated by WriteTo and any consumed
|
|
// buffers have been lopped off the beginning, so in order to return
|
|
// them to the pool, we need to look at the difference between "orig"
|
|
// and "wnb".
|
|
for i := 0; i < len(orig)-len(c.out.wnb); i++ {
|
|
nbPoolPut(orig[i])
|
|
}
|
|
|
|
// At this point it's possible that "nb" has been modified by another
|
|
// call to queueOutbound while the lock was released, so we'll leave
|
|
// those for the next iteration. Meanwhile it's possible that we only
|
|
// managed a partial write of "wnb", so we'll shift anything that
|
|
// remains up to the beginning of the array to prevent reallocating.
|
|
// Anything left in "wnb" has already been framed for WebSocket conns
|
|
// so leave them alone for the next call to flushOutbound.
|
|
c.out.wnb = append(startOfWnb[:0], c.out.wnb...)
|
|
|
|
// If we've written everything but the underlying array of our working
|
|
// buffer has grown excessively then free it — the GC will tidy it up
|
|
// and we can allocate a new one next time.
|
|
if len(c.out.wnb) == 0 && cap(c.out.wnb) > nbPoolSizeLarge*8 {
|
|
c.out.wnb = nil
|
|
}
|
|
|
|
// Ignore ErrShortWrite errors, they will be handled as partials.
|
|
var gotWriteTimeout bool
|
|
if err != nil && err != io.ErrShortWrite {
|
|
// Handle timeout error (slow consumer) differently
|
|
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
|
gotWriteTimeout = true
|
|
if closed := c.handleWriteTimeout(n, attempted, len(orig)); closed {
|
|
return true
|
|
}
|
|
} else {
|
|
// Other errors will cause connection to be closed.
|
|
// For clients, report as debug but for others report as error.
|
|
report := c.Debugf
|
|
if c.kind != CLIENT {
|
|
report = c.Errorf
|
|
}
|
|
report("Error flushing: %v", err)
|
|
c.markConnAsClosed(WriteError)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Update flush time statistics.
|
|
c.out.lft = lft
|
|
|
|
// Subtract from pending bytes and messages.
|
|
c.out.pb -= n
|
|
if c.isWebsocket() {
|
|
c.ws.fs -= n
|
|
}
|
|
|
|
// Check that if there is still data to send and writeLoop is in wait,
|
|
// then we need to signal.
|
|
if c.out.pb > 0 {
|
|
c.flushSignal()
|
|
}
|
|
|
|
// Check if we have a stalled gate and if so and we are recovering release
|
|
// any stalled producers. Only kind==CLIENT will stall.
|
|
if c.out.stc != nil && (n == attempted || c.out.pb < c.out.mp/4*3) {
|
|
close(c.out.stc)
|
|
c.out.stc = nil
|
|
}
|
|
// Check if the connection is recovering from being a slow consumer.
|
|
if !gotWriteTimeout && c.flags.isSet(isSlowConsumer) {
|
|
c.Noticef("Slow Consumer Recovered: Flush took %.3fs with %d chunks of %d total bytes.", time.Since(start).Seconds(), len(orig), attempted)
|
|
c.flags.clear(isSlowConsumer)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// This is invoked from flushOutbound() for io/timeout error (slow consumer).
|
|
// Returns a boolean to indicate if the connection has been closed or not.
|
|
// Lock is held on entry.
|
|
func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) bool {
|
|
if tlsConn, ok := c.nc.(*tls.Conn); ok {
|
|
if !tlsConn.ConnectionState().HandshakeComplete {
|
|
// Likely a TLSTimeout error instead...
|
|
c.markConnAsClosed(TLSHandshakeError)
|
|
// Would need to coordinate with tlstimeout()
|
|
// to avoid double logging, so skip logging
|
|
// here, and don't report a slow consumer error.
|
|
return true
|
|
}
|
|
} else if c.flags.isSet(expectConnect) && !c.flags.isSet(connectReceived) {
|
|
// Under some conditions, a connection may hit a slow consumer write deadline
|
|
// before the authorization timeout. If that is the case, then we handle
|
|
// as slow consumer though we do not increase the counter as that can be
|
|
// misleading.
|
|
c.markConnAsClosed(SlowConsumerWriteDeadline)
|
|
return true
|
|
}
|
|
alreadySC := c.flags.isSet(isSlowConsumer)
|
|
scState := "Detected"
|
|
if alreadySC {
|
|
scState = "State"
|
|
}
|
|
|
|
// Aggregate slow consumers.
|
|
atomic.AddInt64(&c.srv.slowConsumers, 1)
|
|
switch c.kind {
|
|
case CLIENT:
|
|
c.srv.scStats.clients.Add(1)
|
|
case ROUTER:
|
|
// Only count each Slow Consumer event once.
|
|
if !alreadySC {
|
|
c.srv.scStats.routes.Add(1)
|
|
}
|
|
case GATEWAY:
|
|
c.srv.scStats.gateways.Add(1)
|
|
case LEAF:
|
|
c.srv.scStats.leafs.Add(1)
|
|
}
|
|
if c.acc != nil {
|
|
c.acc.stats.Lock()
|
|
c.acc.stats.slowConsumers++
|
|
c.acc.stats.Unlock()
|
|
}
|
|
c.Noticef("Slow Consumer %s: WriteDeadline of %v exceeded with %d chunks of %d total bytes.",
|
|
scState, c.out.wdl, numChunks, attempted)
|
|
|
|
// We always close CLIENT connections, or when nothing was written at all...
|
|
if c.out.wtp == WriteTimeoutPolicyClose || written == 0 {
|
|
c.markConnAsClosed(SlowConsumerWriteDeadline)
|
|
return true
|
|
} else {
|
|
c.flags.setIfNotSet(isSlowConsumer)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Marks this connection has closed with the given reason.
|
|
// Sets the connMarkedClosed flag and skipFlushOnClose depending on the reason.
|
|
// Depending on the kind of connection, the connection will be saved.
|
|
// If a writeLoop has been started, the final flush will be done there, otherwise
|
|
// flush and close of TCP connection is done here in place.
|
|
// Returns true if closed in place, flase otherwise.
|
|
// Lock is held on entry.
|
|
func (c *client) markConnAsClosed(reason ClosedState) {
|
|
// Possibly set skipFlushOnClose flag even if connection has already been
|
|
// mark as closed. The rationale is that a connection may be closed with
|
|
// a reason that justifies a flush (say after sending an -ERR), but then
|
|
// the flushOutbound() gets a write error. If that happens, connection
|
|
// being lost, there is no reason to attempt to flush again during the
|
|
// teardown when the writeLoop exits.
|
|
var skipFlush bool
|
|
switch reason {
|
|
case ReadError, WriteError, SlowConsumerPendingBytes, SlowConsumerWriteDeadline, TLSHandshakeError:
|
|
c.flags.set(skipFlushOnClose)
|
|
skipFlush = true
|
|
case StaleConnection:
|
|
// Track stale connections statistics.
|
|
atomic.AddInt64(&c.srv.staleConnections, 1)
|
|
switch c.kind {
|
|
case CLIENT:
|
|
c.srv.staleStats.clients.Add(1)
|
|
case ROUTER:
|
|
c.srv.staleStats.routes.Add(1)
|
|
case GATEWAY:
|
|
c.srv.staleStats.gateways.Add(1)
|
|
case LEAF:
|
|
c.srv.staleStats.leafs.Add(1)
|
|
}
|
|
}
|
|
if c.flags.isSet(connMarkedClosed) {
|
|
return
|
|
}
|
|
c.flags.set(connMarkedClosed)
|
|
// For a websocket client, unless we are told not to flush, enqueue
|
|
// a websocket CloseMessage based on the reason.
|
|
if !skipFlush && c.isWebsocket() && !c.ws.closeSent {
|
|
c.wsEnqueueCloseMessage(reason)
|
|
}
|
|
// Be consistent with the creation: for routes, gateways and leaf,
|
|
// we use Noticef on create, so use that too for delete.
|
|
if c.srv != nil {
|
|
if c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY {
|
|
var tags []string
|
|
var remoteName string
|
|
switch {
|
|
case c.kind == LEAF && c.leaf != nil:
|
|
remoteName = c.leaf.remoteServer
|
|
case c.kind == ROUTER && c.route != nil:
|
|
remoteName = c.route.remoteName
|
|
case c.kind == GATEWAY && c.gw != nil:
|
|
remoteName = c.gw.remoteName
|
|
}
|
|
if remoteName != _EMPTY_ {
|
|
tags = append(tags, fmt.Sprintf("Remote: %s", remoteName))
|
|
}
|
|
if len(tags) > 0 {
|
|
c.Noticef("%s connection closed: %s - %s", c.kindString(), reason, strings.Join(tags, ", "))
|
|
} else {
|
|
c.Noticef("%s connection closed: %s", c.kindString(), reason)
|
|
}
|
|
} else { // Client, System, Jetstream, and Account connections.
|
|
c.Debugf("%s connection closed: %s", c.kindString(), reason)
|
|
}
|
|
}
|
|
|
|
// Save off the connection if its a client or leafnode.
|
|
if c.kind == CLIENT || c.kind == LEAF {
|
|
if nc := c.nc; nc != nil && c.srv != nil {
|
|
// TODO: May want to send events to single go routine instead
|
|
// of creating a new go routine for each save.
|
|
// Pass the c.subs as a reference. It may be set to nil in
|
|
// closeConnection.
|
|
go c.srv.saveClosedClient(c, nc, c.subs, reason)
|
|
}
|
|
}
|
|
// If writeLoop exists, let it do the final flush, close and teardown.
|
|
if c.flags.isSet(writeLoopStarted) {
|
|
// Since we want the writeLoop to do the final flush and tcp close,
|
|
// we want the reconnect to be done there too. However, it should'nt
|
|
// happen before the connection has been removed from the server
|
|
// state (end of closeConnection()). This ref count allows us to
|
|
// guarantee that.
|
|
c.rref++
|
|
c.flushSignal()
|
|
return
|
|
}
|
|
// Flush (if skipFlushOnClose is not set) and close in place. If flushing,
|
|
// use a small WriteDeadline.
|
|
c.flushAndClose(true)
|
|
}
|
|
|
|
// flushSignal will use server to queue the flush IO operation to a pool of flushers.
|
|
// Lock must be held.
|
|
func (c *client) flushSignal() {
|
|
// Check that sg is not nil, which will happen if the connection is closed.
|
|
if c.out.sg != nil {
|
|
c.out.sg.Signal()
|
|
}
|
|
}
|
|
|
|
// Traces a message.
|
|
// Will NOT check if tracing is enabled, does NOT need the client lock.
|
|
func (c *client) traceMsgInternal(msg []byte, delivered bool, hdrSize int) {
|
|
opts := c.srv.getOpts()
|
|
maxTrace := opts.MaxTracedMsgLen
|
|
headersOnly := opts.TraceHeaders
|
|
suffix := LEN_CR_LF
|
|
|
|
// If TraceHeaders is enabled, extract only the header portion of the msg.
|
|
// If a header is present, it ends with an additional trailing CRLF.
|
|
if headersOnly {
|
|
if hdrSize > 0 && len(msg) >= hdrSize {
|
|
msg = msg[:hdrSize]
|
|
suffix += LEN_CR_LF
|
|
} else {
|
|
// No headers present, so nothing to trace.
|
|
return
|
|
}
|
|
}
|
|
|
|
// Do not emit a log line for zero-length payloads.
|
|
l := len(msg) - suffix
|
|
if l <= 0 {
|
|
return
|
|
}
|
|
|
|
const (
|
|
traceInPrefix string = "<<-"
|
|
traceOutPrefix string = "->>"
|
|
)
|
|
var prefix string
|
|
if delivered {
|
|
prefix = traceOutPrefix
|
|
} else {
|
|
prefix = traceInPrefix
|
|
}
|
|
if maxTrace > 0 && l > maxTrace {
|
|
tm := fmt.Sprintf("%q", msg[:maxTrace])
|
|
c.Tracef("%s MSG_PAYLOAD: [\"%s...\"]", prefix, tm[1:len(tm)-1])
|
|
} else {
|
|
c.Tracef("%s MSG_PAYLOAD: [%q]", prefix, msg[:l])
|
|
}
|
|
}
|
|
|
|
func (c *client) traceMsg(msg []byte) {
|
|
c.traceMsgInternal(msg, false, c.pa.hdr)
|
|
}
|
|
|
|
func (c *client) traceMsgDelivery(msg []byte, hdrSize int) {
|
|
c.traceMsgInternal(msg, true, hdrSize)
|
|
}
|
|
|
|
// Traces an incoming operation.
|
|
// Will NOT check if tracing is enabled, does NOT need the client lock.
|
|
func (c *client) traceInOp(op string, arg []byte) {
|
|
c.traceOp("<<- %s", op, arg)
|
|
}
|
|
|
|
// Traces an outgoing operation.
|
|
// Will NOT check if tracing is enabled, does NOT need the client lock.
|
|
func (c *client) traceOutOp(op string, arg []byte) {
|
|
c.traceOp("->> %s", op, arg)
|
|
}
|
|
|
|
func (c *client) traceOp(format, op string, arg []byte) {
|
|
opa := []any{}
|
|
if op != _EMPTY_ {
|
|
opa = append(opa, op)
|
|
}
|
|
if arg != nil {
|
|
opa = append(opa, bytesToString(arg))
|
|
}
|
|
c.Tracef(format, opa)
|
|
}
|
|
|
|
// Process the information messages from Clients and other Routes.
|
|
func (c *client) processInfo(arg []byte) error {
|
|
info := Info{}
|
|
if err := json.Unmarshal(arg, &info); err != nil {
|
|
return err
|
|
}
|
|
switch c.kind {
|
|
case ROUTER:
|
|
c.processRouteInfo(&info)
|
|
case GATEWAY:
|
|
c.processGatewayInfo(&info)
|
|
case LEAF:
|
|
c.processLeafnodeInfo(&info)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *client) processErr(errStr string) {
|
|
close := true
|
|
switch c.kind {
|
|
case CLIENT:
|
|
c.Errorf("Client Error %s", errStr)
|
|
case ROUTER:
|
|
c.Errorf("Route Error %s", errStr)
|
|
case GATEWAY:
|
|
c.Errorf("Gateway Error %s", errStr)
|
|
case LEAF:
|
|
c.Errorf("Leafnode Error %s", errStr)
|
|
c.leafProcessErr(errStr)
|
|
close = false
|
|
case JETSTREAM:
|
|
c.Errorf("JetStream Error %s", errStr)
|
|
}
|
|
if close {
|
|
c.closeConnection(ParseError)
|
|
}
|
|
}
|
|
|
|
// Matcher for pass/password and auth_token fields.
|
|
var prefixAuthPat = regexp.MustCompile(`"?\s*(?:auth_token\S*?|pass\S*?)"?\s*[:=]\s*"?([^",\r\n}]*)`)
|
|
|
|
// Exact matcher for fields sig, proxy_sig and nkey.
|
|
// Overlapping field "sig" does not match inside "proxy_sig".
|
|
var exactAuthPat = regexp.MustCompile(`(?:^|[^A-Za-z0-9_])"?\s*(?:proxy_sig|nkey|sig)"?\s*[:=]\s*"?([^",\r\n}]*)`)
|
|
|
|
// removeSecretsFromTrace removes any notion of passwords/tokens from trace
|
|
// messages for logging.
|
|
func removeSecretsFromTrace(arg []byte) []byte {
|
|
buf := redact(prefixAuthPat, arg)
|
|
return redact(exactAuthPat, buf)
|
|
}
|
|
|
|
func redact(pat *regexp.Regexp, proto []byte) []byte {
|
|
m := pat.FindAllSubmatchIndex(proto, -1)
|
|
if len(m) == 0 {
|
|
return proto
|
|
}
|
|
// Take a copy of the connect proto just for the trace message.
|
|
var _arg [4096]byte
|
|
buf := append(_arg[:0], proto...)
|
|
redactedPass := []byte("[REDACTED]")
|
|
for i := len(m) - 1; i >= 0; i-- {
|
|
match := m[i]
|
|
if len(match) < 4 {
|
|
continue
|
|
}
|
|
start, end := match[2], match[3]
|
|
// Replace value substring.
|
|
buf = append(buf[:start], append(redactedPass, buf[end:]...)...)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// Returns the RTT by computing the elapsed time since now and `start`.
|
|
// On Windows VM where I (IK) run tests, time.Since() will return 0
|
|
// (I suspect some time granularity issues). So return at minimum 1ns.
|
|
func computeRTT(start time.Time) time.Duration {
|
|
rtt := time.Since(start)
|
|
if rtt <= 0 {
|
|
rtt = time.Nanosecond
|
|
}
|
|
return rtt
|
|
}
|
|
|
|
// processConnect will process a client connect op.
|
|
func (c *client) processConnect(arg []byte) error {
|
|
supportsHeaders := c.srv.supportsHeaders()
|
|
c.mu.Lock()
|
|
// If we can't stop the timer because the callback is in progress...
|
|
if !c.clearAuthTimer() {
|
|
// wait for it to finish and handle sending the failure back to
|
|
// the client.
|
|
for !c.isClosed() {
|
|
c.mu.Unlock()
|
|
time.Sleep(25 * time.Millisecond)
|
|
c.mu.Lock()
|
|
}
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
c.last = time.Now().UTC()
|
|
// Estimate RTT to start.
|
|
if c.kind == CLIENT {
|
|
c.rtt = computeRTT(c.start)
|
|
if c.srv != nil {
|
|
c.clearPingTimer()
|
|
c.setFirstPingTimer()
|
|
}
|
|
}
|
|
kind := c.kind
|
|
srv := c.srv
|
|
|
|
// Moved unmarshalling of clients' Options under the lock.
|
|
// The client has already been added to the server map, so it is possible
|
|
// that other routines lookup the client, and access its options under
|
|
// the client's lock, so unmarshalling the options outside of the lock
|
|
// would cause data RACEs.
|
|
if err := json.Unmarshal(arg, &c.opts); err != nil {
|
|
c.mu.Unlock()
|
|
return err
|
|
}
|
|
// Indicate that the CONNECT protocol has been received, and that the
|
|
// server now knows which protocol this client supports.
|
|
firstConnect := !c.flags.isSet(connectReceived)
|
|
c.flags.set(connectReceived)
|
|
// Capture these under lock
|
|
c.echo = c.opts.Echo
|
|
proto := c.opts.Protocol
|
|
verbose := c.opts.Verbose
|
|
lang := c.opts.Lang
|
|
account := c.opts.Account
|
|
accountNew := c.opts.AccountNew
|
|
|
|
// if websocket client, maybe some options through cookies
|
|
if ws := c.ws; ws != nil {
|
|
// if JWT not in the CONNECT, use the cookie JWT (possibly empty).
|
|
if c.opts.JWT == _EMPTY_ {
|
|
c.opts.JWT = ws.cookieJwt
|
|
}
|
|
// if user not in the CONNECT, use the cookie user (possibly empty)
|
|
if c.opts.Username == _EMPTY_ {
|
|
c.opts.Username = ws.cookieUsername
|
|
}
|
|
// if pass not in the CONNECT, use the cookie password (possibly empty).
|
|
if c.opts.Password == _EMPTY_ {
|
|
c.opts.Password = ws.cookiePassword
|
|
}
|
|
// if token not in the CONNECT, use the cookie token (possibly empty).
|
|
if c.opts.Token == _EMPTY_ {
|
|
c.opts.Token = ws.cookieToken
|
|
}
|
|
}
|
|
|
|
// when not in operator mode, discard the jwt
|
|
if srv != nil && srv.trustedKeys == nil {
|
|
c.opts.JWT = _EMPTY_
|
|
}
|
|
ujwt := c.opts.JWT
|
|
|
|
// For headers both client and server need to support.
|
|
c.headers = supportsHeaders && c.opts.Headers
|
|
c.mu.Unlock()
|
|
|
|
if srv != nil {
|
|
// Applicable to clients only:
|
|
// As soon as c.opts is unmarshalled and if the proto is at
|
|
// least ClientProtoInfo, we need to increment the following counter.
|
|
// This is decremented when client is removed from the server's
|
|
// clients map.
|
|
if kind == CLIENT && proto >= ClientProtoInfo && firstConnect {
|
|
srv.mu.Lock()
|
|
srv.cproto++
|
|
srv.mu.Unlock()
|
|
}
|
|
|
|
// A second CONNECT may move the client into a different account via
|
|
// checkAuthentication. Drop any previously-registered subscriptions
|
|
// from the current account first so they don't leak in that account's
|
|
// sublist after the client switches.
|
|
if !firstConnect {
|
|
c.clearAccountSubs(false)
|
|
}
|
|
|
|
// Check for Auth
|
|
if ok := srv.checkAuthentication(c); !ok {
|
|
// We may fail here because we reached max limits on an account.
|
|
if ujwt != _EMPTY_ {
|
|
c.mu.Lock()
|
|
acc := c.acc
|
|
c.mu.Unlock()
|
|
srv.mu.Lock()
|
|
tooManyAccCons := acc != nil && acc != srv.gacc
|
|
srv.mu.Unlock()
|
|
if tooManyAccCons {
|
|
return ErrTooManyAccountConnections
|
|
}
|
|
}
|
|
c.authViolation()
|
|
return ErrAuthentication
|
|
}
|
|
|
|
// Check for Account designation, we used to have this as an optional feature for dynamic
|
|
// sandbox environments. Now its considered an error.
|
|
if accountNew || account != _EMPTY_ {
|
|
c.authViolation()
|
|
return ErrAuthentication
|
|
}
|
|
|
|
// If no account designation.
|
|
// Do this only for CLIENT and LEAF connections.
|
|
if c.acc == nil && (c.kind == CLIENT || c.kind == LEAF) {
|
|
// By default register with the global account.
|
|
c.registerWithAccount(srv.globalAccount())
|
|
}
|
|
|
|
// Initialize user info used in logs.
|
|
c.mu.Lock()
|
|
acc := c.acc
|
|
if c.getRawAuthUser() != _EMPTY_ {
|
|
c.ncsUser.Store(c.getAuthUserLabel())
|
|
}
|
|
c.mu.Unlock()
|
|
if acc != nil {
|
|
acc.mu.RLock()
|
|
c.ncsAcc.Store(acc.traceLabel())
|
|
acc.mu.RUnlock()
|
|
}
|
|
|
|
// Enable logging connection details and auth info for this client.
|
|
if c.kind == CLIENT && firstConnect && c.srv != nil {
|
|
var ncs string
|
|
if c.opts.Version != _EMPTY_ {
|
|
ncs = fmt.Sprintf("v%s", c.opts.Version)
|
|
}
|
|
if c.opts.Lang != _EMPTY_ {
|
|
if c.opts.Version == _EMPTY_ {
|
|
ncs = c.opts.Lang
|
|
} else {
|
|
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang)
|
|
}
|
|
}
|
|
if c.opts.Name != _EMPTY_ {
|
|
if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ {
|
|
ncs = c.opts.Name
|
|
} else {
|
|
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name)
|
|
}
|
|
}
|
|
var acs string
|
|
accl := c.ncsAcc.Load()
|
|
authUser := c.ncsUser.Load()
|
|
if accl != nil && authUser != nil {
|
|
acs = fmt.Sprintf("%s/%s", accl, authUser)
|
|
}
|
|
switch {
|
|
case ncs != _EMPTY_ && acs != _EMPTY_:
|
|
c.ncs.Store(fmt.Sprintf("%s - %q - %q", c, ncs, acs))
|
|
case ncs != _EMPTY_:
|
|
c.ncs.Store(fmt.Sprintf("%s - %q", c, ncs))
|
|
case acs != _EMPTY_:
|
|
c.ncs.Store(fmt.Sprintf("%s - %q", c, acs))
|
|
}
|
|
}
|
|
}
|
|
|
|
switch kind {
|
|
case CLIENT:
|
|
// Check client protocol request if it exists.
|
|
if proto < ClientProtoZero || proto > ClientProtoInfo {
|
|
c.sendErr(ErrBadClientProtocol.Error())
|
|
c.closeConnection(BadClientProtocolVersion)
|
|
return ErrBadClientProtocol
|
|
}
|
|
// Check to see that if no_responders is requested
|
|
// they have header support on as well.
|
|
c.mu.Lock()
|
|
misMatch := c.opts.NoResponders && !c.headers
|
|
c.mu.Unlock()
|
|
if misMatch {
|
|
c.sendErr(ErrNoRespondersRequiresHeaders.Error())
|
|
c.closeConnection(NoRespondersRequiresHeaders)
|
|
return ErrNoRespondersRequiresHeaders
|
|
}
|
|
if verbose {
|
|
c.sendOK()
|
|
}
|
|
case ROUTER:
|
|
// Delegate the rest of processing to the route
|
|
return c.processRouteConnect(srv, arg, lang)
|
|
case GATEWAY:
|
|
// Delegate the rest of processing to the gateway
|
|
return c.processGatewayConnect(arg)
|
|
case LEAF:
|
|
// Delegate the rest of processing to the leaf node
|
|
return c.processLeafNodeConnect(srv, arg, lang)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *client) sendErrAndErr(err string) {
|
|
c.sendErr(err)
|
|
c.RateLimitErrorf(err)
|
|
}
|
|
|
|
func (c *client) sendErrAndDebug(err string) {
|
|
c.sendErr(err)
|
|
c.RateLimitDebugf(err)
|
|
}
|
|
|
|
func (c *client) authTimeout() {
|
|
c.sendErrAndDebug("Authentication Timeout")
|
|
c.closeConnection(AuthenticationTimeout)
|
|
}
|
|
|
|
func (c *client) authExpired() {
|
|
c.sendErrAndDebug("User Authentication Expired")
|
|
c.closeConnection(AuthenticationExpired)
|
|
}
|
|
|
|
func (c *client) accountAuthExpired() {
|
|
c.sendErrAndDebug("Account Authentication Expired")
|
|
c.closeConnection(AuthenticationExpired)
|
|
}
|
|
|
|
func (c *client) authViolation() {
|
|
authErr := c.getAuthError()
|
|
if authErr == nil {
|
|
authErr = ErrAuthentication
|
|
}
|
|
reason := getAuthErrClosedState(authErr)
|
|
|
|
var s *Server
|
|
if s = c.srv; s != nil {
|
|
defer s.sendAuthErrorEvent(c, reason.String())
|
|
if c.getRawAuthUser() != _EMPTY_ {
|
|
c.Errorf("%v - %s", ErrAuthentication, c.getAuthUser())
|
|
} else {
|
|
c.Errorf(ErrAuthentication.Error())
|
|
}
|
|
}
|
|
if c.isMqtt() {
|
|
c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false)
|
|
} else {
|
|
// Send this to client, regardless of the authErr override.
|
|
c.sendErr("Authorization Violation")
|
|
}
|
|
c.closeConnection(reason)
|
|
}
|
|
|
|
func (c *client) maxAccountConnExceeded() {
|
|
c.sendErrAndErr(ErrTooManyAccountConnections.Error())
|
|
c.closeConnection(MaxAccountConnectionsExceeded)
|
|
}
|
|
|
|
func (c *client) maxConnExceeded() {
|
|
c.sendErrAndErr(ErrTooManyConnections.Error())
|
|
c.closeConnection(MaxConnectionsExceeded)
|
|
}
|
|
|
|
func (c *client) maxSubsExceeded() {
|
|
if c.acc.shouldLogMaxSubErr() {
|
|
c.Errorf(ErrTooManySubs.Error())
|
|
}
|
|
c.sendErr(ErrTooManySubs.Error())
|
|
}
|
|
|
|
func (c *client) maxPayloadViolation(sz int, max int32) {
|
|
c.Errorf("%s: %d vs %d", ErrMaxPayload.Error(), sz, max)
|
|
c.sendErr("Maximum Payload Violation")
|
|
c.closeConnection(MaxPayloadExceeded)
|
|
}
|
|
|
|
// queueOutbound queues data for a clientconnection.
|
|
// Lock should be held.
|
|
func (c *client) queueOutbound(data []byte) {
|
|
// Do not keep going if closed
|
|
if c.isClosed() {
|
|
return
|
|
}
|
|
|
|
// Add to pending bytes total.
|
|
c.out.pb += int64(len(data))
|
|
|
|
// Take a copy of the slice ref so that we can chop bits off the beginning
|
|
// without affecting the original "data" slice.
|
|
toBuffer := data
|
|
|
|
// All of the queued []byte have a fixed capacity, so if there's a []byte
|
|
// at the tail of the buffer list that isn't full yet, we should top that
|
|
// up first. This helps to ensure we aren't pulling more []bytes from the
|
|
// pool than we need to.
|
|
if len(c.out.nb) > 0 {
|
|
last := &c.out.nb[len(c.out.nb)-1]
|
|
if free := cap(*last) - len(*last); free > 0 {
|
|
if l := len(toBuffer); l < free {
|
|
free = l
|
|
}
|
|
*last = append(*last, toBuffer[:free]...)
|
|
toBuffer = toBuffer[free:]
|
|
}
|
|
}
|
|
|
|
// Now we can push the rest of the data into new []bytes from the pool
|
|
// in fixed size chunks. This ensures we don't go over the capacity of any
|
|
// of the buffers and end up reallocating.
|
|
for len(toBuffer) > 0 {
|
|
new := nbPoolGet(len(toBuffer))
|
|
n := copy(new[:cap(new)], toBuffer)
|
|
c.out.nb = append(c.out.nb, new[:n])
|
|
toBuffer = toBuffer[n:]
|
|
}
|
|
|
|
// Check for slow consumer via pending bytes limit.
|
|
// ok to return here, client is going away.
|
|
if c.kind == CLIENT && c.out.pb > c.out.mp {
|
|
// Perf wise, it looks like it is faster to optimistically add than
|
|
// checking current pb+len(data) and then add to pb.
|
|
c.out.pb -= int64(len(data))
|
|
|
|
// Increment the total and client's slow consumer counters.
|
|
atomic.AddInt64(&c.srv.slowConsumers, 1)
|
|
c.srv.scStats.clients.Add(1)
|
|
if c.acc != nil {
|
|
c.acc.stats.Lock()
|
|
c.acc.stats.slowConsumers++
|
|
c.acc.stats.Unlock()
|
|
}
|
|
c.Noticef("Slow Consumer Detected: MaxPending of %d Exceeded", c.out.mp)
|
|
c.markConnAsClosed(SlowConsumerPendingBytes)
|
|
return
|
|
}
|
|
|
|
// Check here if we should create a stall channel if we are falling behind.
|
|
// We do this here since if we wait for consumer's writeLoop it could be
|
|
// too late with large number of fan in producers.
|
|
// If the outbound connection is > 75% of maximum pending allowed, create a stall gate.
|
|
if c.out.pb > c.out.mp/4*3 && c.out.stc == nil {
|
|
c.out.stc = make(chan struct{})
|
|
}
|
|
}
|
|
|
|
// Assume the lock is held upon entry.
|
|
func (c *client) enqueueProtoAndFlush(proto []byte, doFlush bool) {
|
|
if c.isClosed() {
|
|
return
|
|
}
|
|
c.queueOutbound(proto)
|
|
if !(doFlush && c.flushOutbound()) {
|
|
c.flushSignal()
|
|
}
|
|
}
|
|
|
|
// Queues and then flushes the connection. This should only be called when
|
|
// the writeLoop cannot be started yet. Use enqueueProto() otherwise.
|
|
// Lock is held on entry.
|
|
func (c *client) sendProtoNow(proto []byte) {
|
|
c.enqueueProtoAndFlush(proto, true)
|
|
}
|
|
|
|
// Enqueues the given protocol and signal the writeLoop if necessary.
|
|
// Lock is held on entry.
|
|
func (c *client) enqueueProto(proto []byte) {
|
|
c.enqueueProtoAndFlush(proto, false)
|
|
}
|
|
|
|
// Assume the lock is held upon entry.
|
|
func (c *client) sendPong() {
|
|
if c.trace {
|
|
c.traceOutOp("PONG", nil)
|
|
}
|
|
c.enqueueProto([]byte(pongProto))
|
|
}
|
|
|
|
// Used to kick off a RTT measurement for latency tracking.
|
|
func (c *client) sendRTTPing() bool {
|
|
c.mu.Lock()
|
|
sent := c.sendRTTPingLocked()
|
|
c.mu.Unlock()
|
|
return sent
|
|
}
|
|
|
|
// Used to kick off a RTT measurement for latency tracking.
|
|
// This is normally called only when the caller has checked that
|
|
// the c.rtt is 0 and wants to force an update by sending a PING.
|
|
// Client lock held on entry.
|
|
func (c *client) sendRTTPingLocked() bool {
|
|
if c.isMqtt() {
|
|
return false
|
|
}
|
|
// Most client libs send a CONNECT+PING and wait for a PONG from the
|
|
// server. So if firstPongSent flag is set, it is ok for server to
|
|
// send the PING. But in case we have client libs that don't do that,
|
|
// allow the send of the PING if more than 2 secs have elapsed since
|
|
// the client TCP connection was accepted.
|
|
if !c.isClosed() &&
|
|
(c.flags.isSet(firstPongSent) || time.Since(c.start) > maxNoRTTPingBeforeFirstPong) {
|
|
c.sendPing()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Assume the lock is held upon entry.
|
|
func (c *client) sendPing() {
|
|
c.rttStart = time.Now().UTC()
|
|
c.ping.out++
|
|
if c.trace {
|
|
c.traceOutOp("PING", nil)
|
|
}
|
|
c.enqueueProto([]byte(pingProto))
|
|
}
|
|
|
|
// Generates the INFO to be sent to the client with the client ID included.
|
|
// info arg will be copied since passed by value.
|
|
// Assume lock is held.
|
|
func (c *client) generateClientInfoJSON(info Info, includeClientIP bool) []byte {
|
|
info.CID = c.cid
|
|
if includeClientIP {
|
|
info.ClientIP = c.host
|
|
}
|
|
info.MaxPayload = c.mpay
|
|
if c.isWebsocket() {
|
|
info.ClientConnectURLs = info.WSConnectURLs
|
|
// Otherwise lame duck info can panic
|
|
if c.srv != nil {
|
|
ws := &c.srv.websocket
|
|
info.TLSAvailable, info.TLSRequired = ws.tls, ws.tls
|
|
info.Host, info.Port = ws.host, ws.port
|
|
}
|
|
}
|
|
info.WSConnectURLs = nil
|
|
return generateInfoJSON(&info)
|
|
}
|
|
|
|
func (c *client) sendErr(err string) {
|
|
c.mu.Lock()
|
|
if c.trace {
|
|
c.traceOutOp("-ERR", []byte(err))
|
|
}
|
|
if !c.isMqtt() {
|
|
c.enqueueProto([]byte(fmt.Sprintf(errProto, err)))
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *client) sendOK() {
|
|
c.mu.Lock()
|
|
if c.trace {
|
|
c.traceOutOp("OK", nil)
|
|
}
|
|
c.enqueueProto([]byte(okProto))
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *client) processPing() {
|
|
c.mu.Lock()
|
|
|
|
if c.isClosed() {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
c.sendPong()
|
|
|
|
// Record this to suppress us sending one if this
|
|
// is within a given time interval for activity.
|
|
c.lastIn = time.Now()
|
|
|
|
// If not a CLIENT, we are done. Also the CONNECT should
|
|
// have been received, but make sure it is so before proceeding
|
|
if c.kind != CLIENT || !c.flags.isSet(connectReceived) {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// If we are here, the CONNECT has been received so we know
|
|
// if this client supports async INFO or not.
|
|
var (
|
|
sendConnectInfo bool
|
|
srv = c.srv
|
|
)
|
|
// For the first PING (so firstPongSet is false) and for clients
|
|
// that support async INFO protocols, we will send one with ConnectInfo=true,
|
|
// the name of the account the client is bound to, and if the
|
|
// account is the system account.
|
|
if !c.flags.isSet(firstPongSent) {
|
|
// Flip the flag.
|
|
c.flags.set(firstPongSent)
|
|
// Evaluate if we should send the INFO protocol.
|
|
sendConnectInfo = srv != nil && c.opts.Protocol >= ClientProtoInfo
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if sendConnectInfo {
|
|
srv.mu.Lock()
|
|
info := srv.copyInfo()
|
|
c.mu.Lock()
|
|
info.RemoteAccount = c.acc.Name
|
|
info.IsSystemAccount = c.acc == srv.SystemAccount()
|
|
info.ConnectInfo = true
|
|
c.enqueueProto(c.generateClientInfoJSON(info, true))
|
|
c.mu.Unlock()
|
|
srv.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func (c *client) processPong() {
|
|
c.mu.Lock()
|
|
c.ping.out = 0
|
|
c.rtt = computeRTT(c.rttStart)
|
|
srv := c.srv
|
|
reorderGWs := c.kind == GATEWAY && c.gw.outbound
|
|
var ri *routeInfo
|
|
// For a route with pooling, we may be instructed to start a new route.
|
|
if c.kind == ROUTER && c.route != nil && c.route.startNewRoute != nil {
|
|
ri = c.route.startNewRoute
|
|
c.route.startNewRoute = nil
|
|
}
|
|
// If compression is currently active for a route/leaf connection, if the
|
|
// compression configuration is s2_auto, check if we should change
|
|
// the compression level.
|
|
if c.kind == ROUTER && needsCompression(c.route.compression) {
|
|
c.updateS2AutoCompressionLevel(&srv.getOpts().Cluster.Compression, &c.route.compression)
|
|
} else if c.kind == LEAF && needsCompression(c.leaf.compression) {
|
|
var co *CompressionOpts
|
|
if r := c.leaf.remote; r != nil {
|
|
co = &r.Compression
|
|
} else {
|
|
co = &srv.getOpts().LeafNode.Compression
|
|
}
|
|
c.updateS2AutoCompressionLevel(co, &c.leaf.compression)
|
|
}
|
|
c.mu.Unlock()
|
|
if reorderGWs {
|
|
srv.gateway.orderOutboundConnections()
|
|
}
|
|
if ri != nil {
|
|
srv.startGoRoutine(func() {
|
|
srv.connectToRoute(ri.url, ri.rtype, true, ri.gossipMode, _EMPTY_)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Select the s2 compression level based on the client's current RTT and the configured
|
|
// RTT thresholds slice. If current level is different than selected one, save the
|
|
// new compression level string and create a new s2 writer.
|
|
// Lock held on entry.
|
|
func (c *client) updateS2AutoCompressionLevel(co *CompressionOpts, compression *string) {
|
|
if co.Mode != CompressionS2Auto {
|
|
return
|
|
}
|
|
if cm := selectS2AutoModeBasedOnRTT(c.rtt, co.RTTThresholds); cm != *compression {
|
|
*compression = cm
|
|
c.out.cw = s2.NewWriter(nil, s2WriterOptions(cm)...)
|
|
}
|
|
}
|
|
|
|
// Will return the parts from the raw wire msg.
|
|
// We return the `hdr` as a slice that is capped to the length of the headers
|
|
// so that if the caller later tries to append to the returned header slice it
|
|
// does not affect the message content.
|
|
func (c *client) msgParts(data []byte) (hdr []byte, msg []byte) {
|
|
if c != nil && c.pa.hdr > 0 {
|
|
return data[:c.pa.hdr:c.pa.hdr], data[c.pa.hdr:]
|
|
}
|
|
return nil, data
|
|
}
|
|
|
|
// Header pubs take form HPUB <subject> [reply] <hdr_len> <total_len>\r\n
|
|
func (c *client) processHeaderPub(arg, remaining []byte) error {
|
|
if !c.headers {
|
|
return ErrMsgHeadersNotSupported
|
|
}
|
|
|
|
// Unroll splitArgs to avoid runtime/heap issues
|
|
a := [MAX_HPUB_ARGS][]byte{}
|
|
args := a[:0]
|
|
start := -1
|
|
for i, b := range arg {
|
|
switch b {
|
|
case ' ', '\t':
|
|
if start >= 0 {
|
|
args = append(args, arg[start:i])
|
|
start = -1
|
|
}
|
|
default:
|
|
if start < 0 {
|
|
start = i
|
|
}
|
|
}
|
|
}
|
|
if start >= 0 {
|
|
args = append(args, arg[start:])
|
|
}
|
|
|
|
c.pa.arg = arg
|
|
switch len(args) {
|
|
case 3:
|
|
c.pa.subject = args[0]
|
|
c.pa.reply = nil
|
|
c.pa.hdr = parseSize(args[1])
|
|
c.pa.size = parseSize(args[2])
|
|
c.pa.hdb = args[1]
|
|
c.pa.szb = args[2]
|
|
case 4:
|
|
c.pa.subject = args[0]
|
|
c.pa.reply = args[1]
|
|
c.pa.hdr = parseSize(args[2])
|
|
c.pa.size = parseSize(args[3])
|
|
c.pa.hdb = args[2]
|
|
c.pa.szb = args[3]
|
|
default:
|
|
return fmt.Errorf("processHeaderPub Parse Error: %q", arg)
|
|
}
|
|
if c.pa.hdr < 0 {
|
|
return fmt.Errorf("processHeaderPub Bad or Missing Header Size: %q", arg)
|
|
}
|
|
// If number overruns an int64, parseSize() will have returned a negative value
|
|
if c.pa.size < 0 {
|
|
return fmt.Errorf("processHeaderPub Bad or Missing Total Size: %q", arg)
|
|
}
|
|
if c.pa.hdr > c.pa.size {
|
|
return fmt.Errorf("processHeaderPub Header Size larger then TotalSize: %q", arg)
|
|
}
|
|
maxPayload := atomic.LoadInt32(&c.mpay)
|
|
// Use int64() to avoid int32 overrun...
|
|
if maxPayload != jwt.NoLimit && int64(c.pa.size) > int64(maxPayload) {
|
|
// If we are given the remaining read buffer (since we do blind reads
|
|
// we may have the beginning of the message header/payload), we will
|
|
// look for the tracing header and if found, we will generate a
|
|
// trace event with the max payload ingress error.
|
|
// Do this only for CLIENT connections.
|
|
if c.kind == CLIENT && c.pa.hdr > 0 && len(remaining) > 0 {
|
|
hdr := remaining[:min(len(remaining), c.pa.hdr)]
|
|
if td, ok := c.allowedMsgTraceDest(hdr, false); ok && td != _EMPTY_ {
|
|
c.initAndSendIngressErrEvent(hdr, td, ErrMaxPayload)
|
|
}
|
|
}
|
|
c.maxPayloadViolation(c.pa.size, maxPayload)
|
|
return ErrMaxPayload
|
|
}
|
|
if c.opts.Pedantic && !IsValidLiteralSubject(bytesToString(c.pa.subject)) {
|
|
c.sendErr("Invalid Publish Subject")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *client) processPub(arg []byte) error {
|
|
// Unroll splitArgs to avoid runtime/heap issues
|
|
a := [MAX_PUB_ARGS][]byte{}
|
|
args := a[:0]
|
|
start := -1
|
|
for i, b := range arg {
|
|
switch b {
|
|
case ' ', '\t':
|
|
if start >= 0 {
|
|
args = append(args, arg[start:i])
|
|
start = -1
|
|
}
|
|
default:
|
|
if start < 0 {
|
|
start = i
|
|
}
|
|
}
|
|
}
|
|
if start >= 0 {
|
|
args = append(args, arg[start:])
|
|
}
|
|
|
|
c.pa.arg = arg
|
|
switch len(args) {
|
|
case 2:
|
|
c.pa.subject = args[0]
|
|
c.pa.reply = nil
|
|
c.pa.size = parseSize(args[1])
|
|
c.pa.szb = args[1]
|
|
case 3:
|
|
c.pa.subject = args[0]
|
|
c.pa.reply = args[1]
|
|
c.pa.size = parseSize(args[2])
|
|
c.pa.szb = args[2]
|
|
default:
|
|
return fmt.Errorf("processPub Parse Error: %q", arg)
|
|
}
|
|
// If number overruns an int64, parseSize() will have returned a negative value
|
|
if c.pa.size < 0 {
|
|
return fmt.Errorf("processPub Bad or Missing Size: %q", arg)
|
|
}
|
|
maxPayload := atomic.LoadInt32(&c.mpay)
|
|
// Use int64() to avoid int32 overrun...
|
|
if maxPayload != jwt.NoLimit && int64(c.pa.size) > int64(maxPayload) {
|
|
c.maxPayloadViolation(c.pa.size, maxPayload)
|
|
return ErrMaxPayload
|
|
}
|
|
if c.opts.Pedantic && !IsValidLiteralSubject(bytesToString(c.pa.subject)) {
|
|
c.sendErr("Invalid Publish Subject")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func splitArg(arg []byte) [][]byte {
|
|
a := [MAX_MSG_ARGS][]byte{}
|
|
args := a[:0]
|
|
start := -1
|
|
for i, b := range arg {
|
|
switch b {
|
|
case ' ', '\t', '\r', '\n':
|
|
if start >= 0 {
|
|
args = append(args, arg[start:i])
|
|
start = -1
|
|
}
|
|
default:
|
|
if start < 0 {
|
|
start = i
|
|
}
|
|
}
|
|
}
|
|
if start >= 0 {
|
|
args = append(args, arg[start:])
|
|
}
|
|
return args
|
|
}
|
|
|
|
func (c *client) parseSub(argo []byte, noForward bool) error {
|
|
// Copy so we do not reference a potentially large buffer
|
|
// FIXME(dlc) - make more efficient.
|
|
arg := make([]byte, len(argo))
|
|
copy(arg, argo)
|
|
args := splitArg(arg)
|
|
var (
|
|
subject []byte
|
|
queue []byte
|
|
sid []byte
|
|
)
|
|
switch len(args) {
|
|
case 2:
|
|
subject = args[0]
|
|
queue = nil
|
|
sid = args[1]
|
|
case 3:
|
|
subject = args[0]
|
|
queue = args[1]
|
|
sid = args[2]
|
|
default:
|
|
return fmt.Errorf("processSub Parse Error: %q", arg)
|
|
}
|
|
// If there was an error, it has been sent to the client. We don't return an
|
|
// error here to not close the connection as a parsing error.
|
|
c.processSub(subject, queue, sid, nil, noForward)
|
|
return nil
|
|
}
|
|
|
|
func (c *client) processSub(subject, queue, bsid []byte, cb msgHandler, noForward bool) (*subscription, error) {
|
|
return c.processSubEx(subject, queue, bsid, cb, noForward, false, false)
|
|
}
|
|
|
|
func (c *client) processSubEx(subject, queue, bsid []byte, cb msgHandler, noForward, si, rsi bool) (*subscription, error) {
|
|
// Create the subscription
|
|
sub := &subscription{client: c, subject: subject, queue: queue, sid: bsid, icb: cb, si: si, rsi: rsi}
|
|
|
|
c.mu.Lock()
|
|
|
|
// Indicate activity.
|
|
c.in.subs++
|
|
|
|
// Grab connection type, account and server info.
|
|
kind := c.kind
|
|
acc := c.acc
|
|
srv := c.srv
|
|
|
|
sid := bytesToString(sub.sid)
|
|
|
|
// This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...)
|
|
// When a connection is closed though, we set c.subs to nil. So check for the map to not be nil.
|
|
if (c.isClosed() && !isInternalClient(kind)) || (c.subs == nil) {
|
|
c.mu.Unlock()
|
|
return nil, ErrConnectionClosed
|
|
}
|
|
|
|
// Check permissions if applicable.
|
|
if kind == CLIENT {
|
|
// First do a pass whether queue subscription is valid. This does not necessarily
|
|
// mean that it will not be able to plain subscribe.
|
|
//
|
|
// allow = ["foo"] -> can subscribe or queue subscribe to foo using any queue
|
|
// allow = ["foo v1"] -> can only queue subscribe to 'foo v1', no plain subs allowed.
|
|
// allow = ["foo", "foo v1"] -> can subscribe to 'foo' but can only queue subscribe to 'foo v1'
|
|
//
|
|
if sub.queue != nil {
|
|
if !c.canSubscribe(string(sub.subject), string(sub.queue)) || string(sub.queue) == sysGroup {
|
|
c.mu.Unlock()
|
|
c.subPermissionViolation(sub)
|
|
return nil, ErrSubscribePermissionViolation
|
|
}
|
|
} else if !c.canSubscribe(string(sub.subject)) {
|
|
c.mu.Unlock()
|
|
c.subPermissionViolation(sub)
|
|
return nil, ErrSubscribePermissionViolation
|
|
}
|
|
|
|
if opts := srv.getOpts(); opts != nil && opts.MaxSubTokens > 0 {
|
|
if len(bytes.Split(sub.subject, []byte(tsep))) > int(opts.MaxSubTokens) {
|
|
c.mu.Unlock()
|
|
c.maxTokensViolation(sub)
|
|
return nil, ErrTooManySubTokens
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we have a maximum on the number of subscriptions.
|
|
if c.subsAtLimit() {
|
|
c.mu.Unlock()
|
|
c.maxSubsExceeded()
|
|
return nil, ErrTooManySubs
|
|
}
|
|
|
|
var updateGWs bool
|
|
var err error
|
|
|
|
// Subscribe here.
|
|
es := c.subs[sid]
|
|
if es == nil {
|
|
c.subs[sid] = sub
|
|
if acc != nil && acc.sl != nil {
|
|
err = acc.sl.Insert(sub)
|
|
if err != nil {
|
|
delete(c.subs, sid)
|
|
} else {
|
|
updateGWs = c.srv.gateway.enabled
|
|
}
|
|
}
|
|
}
|
|
// Unlocked from here onward
|
|
c.mu.Unlock()
|
|
|
|
if err != nil {
|
|
c.sendErr("Invalid Subject")
|
|
return nil, ErrMalformedSubject
|
|
} else if c.opts.Verbose && kind != SYSTEM {
|
|
c.sendOK()
|
|
}
|
|
|
|
// If it was already registered, return it.
|
|
if es != nil {
|
|
return es, nil
|
|
}
|
|
|
|
// No account just return.
|
|
if acc == nil {
|
|
return sub, nil
|
|
}
|
|
|
|
if err := c.addShadowSubscriptions(acc, sub); err != nil {
|
|
c.Errorf(err.Error())
|
|
}
|
|
|
|
if noForward {
|
|
return sub, nil
|
|
}
|
|
|
|
// If we are routing and this is a local sub, add to the route map for the associated account.
|
|
if kind == CLIENT || kind == SYSTEM || kind == JETSTREAM || kind == ACCOUNT {
|
|
srv.updateRouteSubscriptionMap(acc, sub, 1)
|
|
if updateGWs {
|
|
srv.gatewayUpdateSubInterest(acc.Name, sub, 1)
|
|
}
|
|
}
|
|
// Now check on leafnode updates.
|
|
acc.updateLeafNodes(sub, 1)
|
|
return sub, nil
|
|
}
|
|
|
|
// Used to pass stream import matches to addShadowSub
|
|
type ime struct {
|
|
im *streamImport
|
|
overlapSubj string
|
|
dyn bool
|
|
}
|
|
|
|
// If the client's account has stream imports and there are matches for this
|
|
// subscription's subject, then add shadow subscriptions in the other accounts
|
|
// that export this subject.
|
|
func (c *client) addShadowSubscriptions(acc *Account, sub *subscription) error {
|
|
if acc == nil {
|
|
return ErrMissingAccount
|
|
}
|
|
|
|
var (
|
|
_ims [16]ime
|
|
ims = _ims[:0]
|
|
imTsa [32]string
|
|
tokens []string
|
|
tsa [32]string
|
|
hasWC bool
|
|
tokensModified bool
|
|
)
|
|
|
|
acc.mu.RLock()
|
|
// If this is from a service import, ignore.
|
|
if sub.si {
|
|
acc.mu.RUnlock()
|
|
return nil
|
|
}
|
|
subj := bytesToString(sub.subject)
|
|
if len(acc.imports.streams) > 0 {
|
|
tokens = tokenizeSubjectIntoSlice(tsa[:0], subj)
|
|
for _, tk := range tokens {
|
|
if tk == pwcs {
|
|
hasWC = true
|
|
break
|
|
}
|
|
}
|
|
if !hasWC && tokens[len(tokens)-1] == fwcs {
|
|
hasWC = true
|
|
}
|
|
}
|
|
// Loop over the import subjects. We have 4 scenarios. If we have an
|
|
// exact match or a superset match we should use the from field from
|
|
// the import. If we are a subset or overlap, we have to dynamically calculate
|
|
// the subject. On overlap, ime requires the overlap subject.
|
|
for _, im := range acc.imports.streams {
|
|
if im.invalid {
|
|
continue
|
|
}
|
|
if subj == im.to {
|
|
ims = append(ims, ime{im, _EMPTY_, false})
|
|
continue
|
|
}
|
|
if tokensModified {
|
|
// re-tokenize subj to overwrite modifications from a previous iteration
|
|
tokens = tokenizeSubjectIntoSlice(tsa[:0], subj)
|
|
tokensModified = false
|
|
}
|
|
imTokens := tokenizeSubjectIntoSlice(imTsa[:0], im.to)
|
|
|
|
if isSubsetMatchTokenized(tokens, imTokens) {
|
|
ims = append(ims, ime{im, _EMPTY_, true})
|
|
} else if hasWC {
|
|
if isSubsetMatchTokenized(imTokens, tokens) {
|
|
ims = append(ims, ime{im, _EMPTY_, false})
|
|
} else {
|
|
imTokensLen := len(imTokens)
|
|
for i, t := range tokens {
|
|
if i >= imTokensLen {
|
|
break
|
|
}
|
|
if t == pwcs && imTokens[i] != fwcs {
|
|
tokens[i] = imTokens[i]
|
|
tokensModified = true
|
|
}
|
|
}
|
|
tokensLen := len(tokens)
|
|
lastIdx := tokensLen - 1
|
|
if tokens[lastIdx] == fwcs {
|
|
if imTokensLen >= tokensLen {
|
|
// rewrite ">" in tokens to be more specific
|
|
tokens[lastIdx] = imTokens[lastIdx]
|
|
tokensModified = true
|
|
if imTokensLen > tokensLen {
|
|
// copy even more specific parts from import
|
|
tokens = append(tokens, imTokens[tokensLen:]...)
|
|
}
|
|
}
|
|
}
|
|
if isSubsetMatchTokenized(tokens, imTokens) {
|
|
// As isSubsetMatchTokenized was already called with tokens and imTokens,
|
|
// we wouldn't be here if it where not for tokens being modified.
|
|
// Hence, Join to re compute the subject string
|
|
ims = append(ims, ime{im, strings.Join(tokens, tsep), true})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
acc.mu.RUnlock()
|
|
|
|
var shadow []*subscription
|
|
|
|
if len(ims) > 0 {
|
|
shadow = make([]*subscription, 0, len(ims))
|
|
}
|
|
|
|
// Now walk through collected stream imports that matched.
|
|
for i := 0; i < len(ims); i++ {
|
|
ime := &ims[i]
|
|
// We will create a shadow subscription.
|
|
nsub, err := c.addShadowSub(sub, ime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
shadow = append(shadow, nsub)
|
|
}
|
|
|
|
if shadow != nil {
|
|
c.mu.Lock()
|
|
sub.shadow = shadow
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Add in the shadow subscription.
|
|
func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error) {
|
|
c.mu.Lock()
|
|
nsub := *sub // copy
|
|
c.mu.Unlock()
|
|
|
|
im := ime.im
|
|
nsub.im = im
|
|
|
|
if !im.usePub && ime.dyn && im.tr != nil {
|
|
if im.rtr == nil {
|
|
im.rtr = im.tr.reverse()
|
|
}
|
|
s := bytesToString(nsub.subject)
|
|
if ime.overlapSubj != _EMPTY_ {
|
|
s = ime.overlapSubj
|
|
}
|
|
subj := im.rtr.TransformSubject(s)
|
|
|
|
nsub.subject = []byte(subj)
|
|
} else if !im.usePub || (im.usePub && ime.overlapSubj != _EMPTY_) || !ime.dyn {
|
|
if ime.overlapSubj != _EMPTY_ {
|
|
nsub.subject = []byte(ime.overlapSubj)
|
|
} else {
|
|
nsub.subject = []byte(im.from)
|
|
}
|
|
}
|
|
// Else use original subject
|
|
|
|
c.Debugf("Creating import subscription on %q from account %q", nsub.subject, im.acc.Name)
|
|
|
|
if err := im.acc.sl.Insert(&nsub); err != nil {
|
|
errs := fmt.Sprintf("Could not add shadow import subscription for account %q", im.acc.Name)
|
|
c.Debugf(errs)
|
|
return nil, errors.New(errs)
|
|
}
|
|
|
|
// Update our route map here. But only if we are not a leaf node or a hub leafnode.
|
|
if c.kind != LEAF || c.isHubLeafNode() {
|
|
c.srv.updateRemoteSubscription(im.acc, &nsub, 1)
|
|
} else if c.kind == LEAF {
|
|
// Update all leafnodes that connect to this server. Note that we could have
|
|
// used the updateLeafNodes() function since when it does invoke updateSmap()
|
|
// this function already takes care of not sending to a spoke leafnode since
|
|
// the `nsub` here is already from a spoke leafnode, but to be explicit, we
|
|
// use this version that updates only leafnodes that connect to this server.
|
|
im.acc.updateLeafNodesEx(&nsub, 1, true)
|
|
}
|
|
|
|
return &nsub, nil
|
|
}
|
|
|
|
// canSubscribe determines if the client is authorized to subscribe to the
|
|
// given subject. Assumes caller is holding lock.
|
|
func (c *client) canSubscribe(subject string, optQueue ...string) bool {
|
|
if c.perms == nil {
|
|
return true
|
|
}
|
|
|
|
allowed, checkAllow := true, true
|
|
|
|
// Optional queue group.
|
|
var queue string
|
|
if len(optQueue) > 0 {
|
|
queue = optQueue[0]
|
|
}
|
|
|
|
// For CLIENT connections that are MQTT we will implicitly allow anything that starts with
|
|
// the "$MQTT.sub." or "$MQTT.deliver.pubrel." prefix. For other types of connections, we
|
|
// will implicitly allow anything that starts with the full "$MQTT." prefix. However,
|
|
// we don't just return here, we skip the check for "allow" but will check "deny".
|
|
if (c.isMqtt() && (strings.HasPrefix(subject, mqttSubPrefix) || strings.HasPrefix(subject, mqttPubRelDeliverySubjectPrefix))) ||
|
|
(c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix)) {
|
|
checkAllow = false
|
|
}
|
|
// Check allow list. If no allow list that means all are allowed. Deny can overrule.
|
|
if checkAllow && c.perms.sub.allow != nil {
|
|
r := c.perms.sub.allow.Match(subject)
|
|
allowed = len(r.psubs) > 0
|
|
if queue != _EMPTY_ && len(r.qsubs) > 0 {
|
|
// If the queue appears in the allow list, then DO allow.
|
|
allowed = queueMatches(queue, r.qsubs)
|
|
}
|
|
// Leafnodes operate slightly differently in that they allow broader scoped subjects.
|
|
// They will prune based on publish perms before sending to a leafnode client.
|
|
if !allowed && c.kind == LEAF && subjectHasWildcard(subject) {
|
|
r := c.perms.sub.allow.ReverseMatch(subject)
|
|
allowed = len(r.psubs) != 0
|
|
}
|
|
}
|
|
// If we have a deny list and we think we are allowed, check that as well.
|
|
if allowed && c.perms.sub.deny != nil {
|
|
r := c.perms.sub.deny.Match(subject)
|
|
allowed = len(r.psubs) == 0
|
|
|
|
if allowed && queue != _EMPTY_ && len(r.qsubs) > 0 {
|
|
// If the queue appears in the deny list, then DO NOT allow.
|
|
allowed = !queueMatches(queue, r.qsubs)
|
|
}
|
|
|
|
// We use the actual subscription to signal us to spin up the deny mperms
|
|
// and cache. We check if the subject is a wildcard that intersects any of
|
|
// the deny clauses.
|
|
// FIXME(dlc) - We could be smarter and track when these go away and remove.
|
|
if allowed && c.mperms == nil && subjectHasWildcard(subject) {
|
|
// Whip through the deny array and check if this wildcard subject can
|
|
// overlap with any denied deliveries.
|
|
for _, sub := range c.darray {
|
|
if SubjectsCollide(sub, subject) {
|
|
c.loadMsgDenyFilter()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return allowed
|
|
}
|
|
|
|
func queueMatches(queue string, qsubs [][]*subscription) bool {
|
|
if len(qsubs) == 0 {
|
|
return true
|
|
}
|
|
for _, qsub := range qsubs {
|
|
qs := qsub[0]
|
|
qname := bytesToString(qs.queue)
|
|
|
|
// NOTE: '*' and '>' tokens can also be valid
|
|
// queue names so we first check against the
|
|
// literal name. e.g. v1.* == v1.*
|
|
if queue == qname || (subjectHasWildcard(qname) && subjectIsSubsetMatch(queue, qname)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Low level unsubscribe for a given client.
|
|
func (c *client) unsubscribe(acc *Account, sub *subscription, force, remove bool) {
|
|
if s := c.srv; s != nil && s.isShuttingDown() {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
if !force && sub.max > 0 && sub.nm < sub.max {
|
|
c.Debugf("Deferring actual UNSUB(%s): %d max, %d received", sub.subject, sub.max, sub.nm)
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
if c.trace {
|
|
c.traceOp("<-> %s", "DELSUB", sub.sid)
|
|
}
|
|
|
|
// Remove accounting if requested. This will be false when we close a connection
|
|
// with open subscriptions.
|
|
if remove {
|
|
delete(c.subs, bytesToString(sub.sid))
|
|
if acc != nil {
|
|
acc.sl.Remove(sub)
|
|
}
|
|
}
|
|
|
|
// Check to see if we have shadow subscriptions.
|
|
var updateRoute bool
|
|
var isSpokeLeaf bool
|
|
shadowSubs := sub.shadow
|
|
sub.shadow = nil
|
|
if len(shadowSubs) > 0 {
|
|
isSpokeLeaf = c.isSpokeLeafNode()
|
|
updateRoute = !isSpokeLeaf && (c.kind == CLIENT || c.kind == SYSTEM || c.kind == LEAF || c.kind == JETSTREAM) && c.srv != nil
|
|
}
|
|
sub.close()
|
|
c.mu.Unlock()
|
|
|
|
// Process shadow subs if we have them.
|
|
for _, nsub := range shadowSubs {
|
|
if err := nsub.im.acc.sl.Remove(nsub); err != nil {
|
|
c.Debugf("Could not remove shadow import subscription for account %q", nsub.im.acc.Name)
|
|
}
|
|
if updateRoute {
|
|
c.srv.updateRemoteSubscription(nsub.im.acc, nsub, -1)
|
|
} else if isSpokeLeaf {
|
|
nsub.im.acc.updateLeafNodesEx(nsub, -1, true)
|
|
}
|
|
}
|
|
|
|
// Now check to see if this was part of a respMap entry for service imports.
|
|
// We can skip subscriptions on reserved replies.
|
|
if acc != nil && !isReservedReply(sub.subject) {
|
|
acc.checkForReverseEntry(string(sub.subject), nil, true)
|
|
}
|
|
}
|
|
|
|
func (c *client) processUnsub(arg []byte) error {
|
|
args := splitArg(arg)
|
|
var sid []byte
|
|
max := int64(-1)
|
|
|
|
switch len(args) {
|
|
case 1:
|
|
sid = args[0]
|
|
case 2:
|
|
sid = args[0]
|
|
max = int64(parseSize(args[1]))
|
|
default:
|
|
return fmt.Errorf("processUnsub Parse Error: %q", arg)
|
|
}
|
|
|
|
var sub *subscription
|
|
var ok, unsub bool
|
|
|
|
c.mu.Lock()
|
|
|
|
// Indicate activity.
|
|
c.in.subs++
|
|
|
|
// Grab connection type.
|
|
kind := c.kind
|
|
srv := c.srv
|
|
var acc *Account
|
|
|
|
updateGWs := false
|
|
if sub, ok = c.subs[string(sid)]; ok {
|
|
acc = c.acc
|
|
if max > 0 && max > sub.nm {
|
|
sub.max = max
|
|
} else {
|
|
// Clear it here to override
|
|
sub.max = 0
|
|
unsub = true
|
|
}
|
|
updateGWs = srv.gateway.enabled
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if c.opts.Verbose {
|
|
c.sendOK()
|
|
}
|
|
|
|
if unsub {
|
|
c.unsubscribe(acc, sub, false, true)
|
|
if acc != nil && (kind == CLIENT || kind == SYSTEM || kind == ACCOUNT || kind == JETSTREAM) {
|
|
srv.updateRouteSubscriptionMap(acc, sub, -1)
|
|
if updateGWs {
|
|
srv.gatewayUpdateSubInterest(acc.Name, sub, -1)
|
|
}
|
|
}
|
|
// Now check on leafnode updates.
|
|
acc.updateLeafNodes(sub, -1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDenySub will check if we are allowed to deliver this message in the
|
|
// presence of deny clauses for subscriptions. Deny clauses will not prevent
|
|
// larger scoped wildcard subscriptions, so we need to check at delivery time.
|
|
// Lock should be held.
|
|
func (c *client) checkDenySub(subject string) bool {
|
|
if denied, ok := c.mperms.dcache[subject]; ok {
|
|
return denied
|
|
} else if np, _ := c.mperms.deny.NumInterest(subject); np != 0 {
|
|
c.mperms.dcache[subject] = true
|
|
return true
|
|
} else {
|
|
c.mperms.dcache[subject] = false
|
|
}
|
|
if len(c.mperms.dcache) > maxDenyPermCacheSize {
|
|
c.pruneDenyCache()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Create a message header for routes or leafnodes. Header and origin cluster aware.
|
|
func (c *client) msgHeaderForRouteOrLeaf(subj, reply []byte, rt *routeTarget, acc *Account) []byte {
|
|
hasHeader := c.pa.hdr > 0
|
|
subclient := rt.sub.client
|
|
canReceiveHeader := subclient.headers
|
|
|
|
mh := c.msgb[:msgHeadProtoLen]
|
|
kind := subclient.kind
|
|
var lnoc bool
|
|
|
|
if kind == ROUTER {
|
|
// If we are coming from a leaf with an origin cluster we need to handle differently
|
|
// if we can. We will send a route based LMSG which has origin cluster and headers
|
|
// by default.
|
|
if c.kind == LEAF && c.remoteCluster() != _EMPTY_ {
|
|
subclient.mu.Lock()
|
|
lnoc = subclient.route.lnoc
|
|
subclient.mu.Unlock()
|
|
}
|
|
if lnoc {
|
|
mh[0] = 'L'
|
|
mh = append(mh, c.remoteCluster()...)
|
|
mh = append(mh, ' ')
|
|
} else {
|
|
// Router (and Gateway) nodes are RMSG. Set here since leafnodes may rewrite.
|
|
mh[0] = 'R'
|
|
}
|
|
if len(subclient.route.accName) == 0 {
|
|
mh = append(mh, acc.Name...)
|
|
mh = append(mh, ' ')
|
|
}
|
|
} else {
|
|
// Leaf nodes are LMSG
|
|
mh[0] = 'L'
|
|
// Remap subject if its a shadow subscription, treat like a normal client.
|
|
if rt.sub.im != nil {
|
|
if rt.sub.im.tr != nil {
|
|
to := rt.sub.im.tr.TransformSubject(bytesToString(subj))
|
|
subj = []byte(to)
|
|
} else if !rt.sub.im.usePub {
|
|
subj = []byte(rt.sub.im.to)
|
|
}
|
|
}
|
|
}
|
|
mh = append(mh, subj...)
|
|
mh = append(mh, ' ')
|
|
|
|
if len(rt.qs) > 0 {
|
|
if len(reply) > 0 {
|
|
mh = append(mh, "+ "...) // Signal that there is a reply.
|
|
mh = append(mh, reply...)
|
|
mh = append(mh, ' ')
|
|
} else {
|
|
mh = append(mh, "| "...) // Only queues
|
|
}
|
|
mh = append(mh, rt.qs...)
|
|
} else if len(reply) > 0 {
|
|
mh = append(mh, reply...)
|
|
mh = append(mh, ' ')
|
|
}
|
|
|
|
if lnoc {
|
|
// leafnode origin LMSG always have a header entry even if zero.
|
|
if c.pa.hdr <= 0 {
|
|
mh = append(mh, '0')
|
|
} else {
|
|
mh = append(mh, c.pa.hdb...)
|
|
}
|
|
mh = append(mh, ' ')
|
|
mh = append(mh, c.pa.szb...)
|
|
} else if hasHeader {
|
|
if canReceiveHeader {
|
|
mh[0] = 'H'
|
|
mh = append(mh, c.pa.hdb...)
|
|
mh = append(mh, ' ')
|
|
mh = append(mh, c.pa.szb...)
|
|
} else {
|
|
// If we are here we need to truncate the payload size
|
|
nsz := strconv.Itoa(c.pa.size - c.pa.hdr)
|
|
mh = append(mh, nsz...)
|
|
}
|
|
} else {
|
|
mh = append(mh, c.pa.szb...)
|
|
}
|
|
return append(mh, _CRLF_...)
|
|
}
|
|
|
|
// Create a message header for clients. Header aware.
|
|
func (c *client) msgHeader(subj, reply []byte, sub *subscription) []byte {
|
|
// See if we should do headers. We have to have a headers msg and
|
|
// the client we are going to deliver to needs to support headers as well.
|
|
hasHeader := c.pa.hdr > 0
|
|
canReceiveHeader := sub.client != nil && sub.client.headers
|
|
|
|
var mh []byte
|
|
if hasHeader && canReceiveHeader {
|
|
mh = c.msgb[:msgHeadProtoLen]
|
|
mh[0] = 'H'
|
|
} else {
|
|
mh = c.msgb[1:msgHeadProtoLen]
|
|
}
|
|
mh = append(mh, subj...)
|
|
mh = append(mh, ' ')
|
|
|
|
if len(sub.sid) > 0 {
|
|
mh = append(mh, sub.sid...)
|
|
mh = append(mh, ' ')
|
|
}
|
|
if reply != nil {
|
|
mh = append(mh, reply...)
|
|
mh = append(mh, ' ')
|
|
}
|
|
if hasHeader {
|
|
if canReceiveHeader {
|
|
mh = append(mh, c.pa.hdb...)
|
|
mh = append(mh, ' ')
|
|
mh = append(mh, c.pa.szb...)
|
|
} else {
|
|
// If we are here we need to truncate the payload size
|
|
nsz := strconv.Itoa(c.pa.size - c.pa.hdr)
|
|
mh = append(mh, nsz...)
|
|
}
|
|
} else {
|
|
mh = append(mh, c.pa.szb...)
|
|
}
|
|
mh = append(mh, _CRLF_...)
|
|
return mh
|
|
}
|
|
|
|
func (c *client) stalledWait(producer *client) {
|
|
// Check to see if we have exceeded our total wait time per readLoop invocation.
|
|
if producer.in.tst > stallTotalAllowed {
|
|
return
|
|
}
|
|
|
|
// Grab stall channel which the slow consumer will close when caught up.
|
|
stall := c.out.stc
|
|
|
|
// Calculate stall time.
|
|
ttl := stallClientMinDuration
|
|
if c.out.pb >= c.out.mp {
|
|
ttl = stallClientMaxDuration
|
|
}
|
|
|
|
c.mu.Unlock()
|
|
defer c.mu.Lock()
|
|
|
|
// Track per client and total client stalls.
|
|
atomic.AddInt64(&c.stalls, 1)
|
|
if c.srv != nil {
|
|
atomic.AddInt64(&c.srv.stalls, 1)
|
|
}
|
|
|
|
// Now check if we are close to total allowed.
|
|
if producer.in.tst+ttl > stallTotalAllowed {
|
|
ttl = stallTotalAllowed - producer.in.tst
|
|
}
|
|
delay := time.NewTimer(ttl)
|
|
defer delay.Stop()
|
|
|
|
start := time.Now()
|
|
select {
|
|
case <-stall:
|
|
case <-delay.C:
|
|
producer.Debugf("Timed out of fast producer stall (%v)", ttl)
|
|
}
|
|
producer.in.tst += time.Since(start)
|
|
}
|
|
|
|
// Used to treat maps as efficient set
|
|
var needFlush = struct{}{}
|
|
|
|
// deliverMsg will deliver a message to a matching subscription and its underlying client.
|
|
// We process all connection/client types. mh is the part that will be protocol/client specific.
|
|
func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, subject, reply, mh, msg []byte, gwrply bool) bool {
|
|
// Check if message tracing is enabled.
|
|
mt, traceOnly := c.isMsgTraceEnabled()
|
|
|
|
client := sub.client
|
|
// Check sub client and check echo. Only do this if not a service import.
|
|
if client == nil || (c == client && !client.echo && !sub.si) {
|
|
if client != nil && mt != nil {
|
|
client.mu.Lock()
|
|
mt.addEgressEvent(client, sub, errMsgTraceNoEcho)
|
|
client.mu.Unlock()
|
|
}
|
|
return false
|
|
}
|
|
|
|
client.mu.Lock()
|
|
|
|
// Check if we have a subscribe deny clause. This will trigger us to check the subject
|
|
// for a match against the denied subjects.
|
|
if client.mperms != nil && client.checkDenySub(string(subject)) {
|
|
mt.addEgressEvent(client, sub, errMsgTraceSubDeny)
|
|
client.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// New race detector forces this now.
|
|
if sub.isClosed() {
|
|
mt.addEgressEvent(client, sub, errMsgTraceSubClosed)
|
|
client.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// Check if we are a leafnode and have perms to check.
|
|
if client.kind == LEAF && client.perms != nil {
|
|
subjectToCheck, _ := getGWRoutedSubjectOrSelf(subject)
|
|
if !client.pubAllowedFullCheck(string(subjectToCheck), true, true) {
|
|
mt.addEgressEvent(client, sub, errMsgTracePubViolation)
|
|
client.mu.Unlock()
|
|
client.Debugf("Not permitted to deliver to %q", subjectToCheck)
|
|
return false
|
|
}
|
|
}
|
|
|
|
var mtErr string
|
|
if mt != nil {
|
|
// For non internal subscription, and if the remote does not support
|
|
// the tracing feature...
|
|
if sub.icb == nil && !client.msgTraceSupport() {
|
|
if traceOnly {
|
|
// We are not sending the message at all because the user
|
|
// expects a trace-only and the remote does not support
|
|
// tracing, which means that it would process/deliver this
|
|
// message, which may break applications.
|
|
// Add the Egress with the no-support error message.
|
|
mt.addEgressEvent(client, sub, errMsgTraceOnlyNoSupport)
|
|
client.mu.Unlock()
|
|
return false
|
|
}
|
|
// If we are doing delivery, we will still forward the message,
|
|
// but we add an error to the Egress event to hint that one should
|
|
// not expect a tracing event from that remote.
|
|
mtErr = errMsgTraceNoSupport
|
|
}
|
|
// For ROUTER, GATEWAY and LEAF, even if we intend to do tracing only,
|
|
// we will still deliver the message. The remote side will
|
|
// generate an event based on what happened on that server.
|
|
if traceOnly && (client.kind == ROUTER || client.kind == GATEWAY || client.kind == LEAF) {
|
|
traceOnly = false
|
|
}
|
|
// If we skip delivery and this is not for a service import, we are done.
|
|
if traceOnly && (sub.icb == nil || c.noIcb) {
|
|
mt.addEgressEvent(client, sub, _EMPTY_)
|
|
client.mu.Unlock()
|
|
// Although the message is not actually delivered, for the
|
|
// purpose of "didDeliver", we need to return "true" here.
|
|
return true
|
|
}
|
|
}
|
|
|
|
srv := client.srv
|
|
|
|
// We don't want to bump the number of delivered messages to the subscription
|
|
// if we are doing trace-only (since really we are not sending it to the sub).
|
|
if !traceOnly {
|
|
sub.nm++
|
|
}
|
|
|
|
// Check if we should auto-unsubscribe.
|
|
if sub.max > 0 {
|
|
if client.kind == ROUTER && sub.nm >= sub.max {
|
|
// The only router based messages that we will see here are remoteReplies.
|
|
// We handle these slightly differently.
|
|
defer client.removeReplySub(sub)
|
|
} else {
|
|
// For routing..
|
|
shouldForward := client.kind == CLIENT || client.kind == SYSTEM && client.srv != nil
|
|
// If we are at the exact number, unsubscribe but
|
|
// still process the message in hand, otherwise
|
|
// unsubscribe and drop message on the floor.
|
|
if sub.nm == sub.max {
|
|
client.Debugf("Auto-unsubscribe limit of %d reached for sid '%s'", sub.max, sub.sid)
|
|
// Due to defer, reverse the code order so that execution
|
|
// is consistent with other cases where we unsubscribe.
|
|
if shouldForward {
|
|
defer srv.updateRemoteSubscription(client.acc, sub, -1)
|
|
}
|
|
defer client.unsubscribe(client.acc, sub, true, true)
|
|
} else if sub.nm > sub.max {
|
|
client.Debugf("Auto-unsubscribe limit [%d] exceeded", sub.max)
|
|
mt.addEgressEvent(client, sub, errMsgTraceAutoSubExceeded)
|
|
client.mu.Unlock()
|
|
client.unsubscribe(client.acc, sub, true, true)
|
|
if shouldForward {
|
|
srv.updateRemoteSubscription(client.acc, sub, -1)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check here if we have a header with our message. If this client can not
|
|
// support we need to strip the headers from the payload.
|
|
// The actual header would have been processed correctly for us, so just
|
|
// need to update payload.
|
|
hdrSize := c.pa.hdr
|
|
if c.pa.hdr > 0 && !sub.client.headers {
|
|
msg = msg[c.pa.hdr:]
|
|
}
|
|
|
|
// Update statistics
|
|
|
|
// The msg includes the CR_LF, so pull back out for accounting.
|
|
msgSize := int64(len(msg))
|
|
// MQTT producers send messages without CR_LF, so don't remove it for them.
|
|
if !prodIsMQTT {
|
|
msgSize -= int64(LEN_CR_LF)
|
|
}
|
|
|
|
// We do not update the outbound stats if we are doing trace only since
|
|
// this message will not be sent out.
|
|
// Also do not update on internal callbacks.
|
|
if !traceOnly && sub.icb == nil {
|
|
// No atomic needed since accessed under client lock.
|
|
// Monitor is reading those also under client's lock.
|
|
client.outMsgs++
|
|
client.outBytes += msgSize
|
|
}
|
|
|
|
// Check for internal subscriptions.
|
|
if sub.icb != nil && !c.noIcb {
|
|
if gwrply {
|
|
// We will store in the account, not the client since it will likely
|
|
// be a different client that will send the reply.
|
|
srv.trackGWReply(nil, client.acc, reply, c.pa.reply)
|
|
}
|
|
client.mu.Unlock()
|
|
|
|
// For service imports, track if we delivered.
|
|
didDeliver := true
|
|
|
|
// Internal account clients are for service imports and need the '\r\n'.
|
|
start := time.Now()
|
|
if client.kind == ACCOUNT {
|
|
sub.icb(sub, c, acc, string(subject), string(reply), msg)
|
|
// If we are a service import check to make sure we delivered the message somewhere.
|
|
if sub.si {
|
|
didDeliver = c.pa.delivered
|
|
}
|
|
} else {
|
|
sub.icb(sub, c, acc, string(subject), string(reply), msg[:msgSize])
|
|
}
|
|
if dur := time.Since(start); dur >= readLoopReportThreshold {
|
|
srv.Warnf("Internal subscription on %q took too long: %v", subject, dur)
|
|
}
|
|
|
|
return didDeliver
|
|
}
|
|
|
|
// If we are a client and we detect that the consumer we are
|
|
// sending to is in a stalled state, go ahead and wait here
|
|
// with a limit.
|
|
if c.kind == CLIENT && client.out.stc != nil {
|
|
if srv.getOpts().NoFastProducerStall {
|
|
mt.addEgressEvent(client, sub, errMsgTraceFastProdNoStall)
|
|
client.mu.Unlock()
|
|
return false
|
|
}
|
|
client.stalledWait(c)
|
|
}
|
|
|
|
// Check for closed connection
|
|
if client.isClosed() {
|
|
mt.addEgressEvent(client, sub, errMsgTraceClientClosed)
|
|
client.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// We have passed cases where we could possibly fail to deliver.
|
|
// Do not call for service-import.
|
|
if mt != nil && sub.icb == nil {
|
|
mt.addEgressEvent(client, sub, mtErr)
|
|
}
|
|
|
|
// Do a fast check here to see if we should be tracking this from a latency
|
|
// perspective. This will be for a request being received for an exported service.
|
|
// This needs to be from a non-client (otherwise tracking happens at requestor).
|
|
//
|
|
// Also this check captures if the original reply (c.pa.reply) is a GW routed
|
|
// reply (since it is known to be > minReplyLen). If that is the case, we need to
|
|
// track the binding between the routed reply and the reply set in the message
|
|
// header (which is c.pa.reply without the GNR routing prefix).
|
|
if client.kind == CLIENT && len(c.pa.reply) > minReplyLen {
|
|
if gwrply {
|
|
// Note that we keep track of the GW routed reply in the destination
|
|
// connection (`client`). The routed reply subject is in `c.pa.reply`,
|
|
// should that change, we would have to pass the GW routed reply as
|
|
// a parameter of deliverMsg().
|
|
srv.trackGWReply(client, nil, reply, c.pa.reply)
|
|
}
|
|
|
|
// If we do not have a registered RTT queue that up now.
|
|
if client.rtt == 0 {
|
|
client.sendRTTPingLocked()
|
|
}
|
|
// FIXME(dlc) - We may need to optimize this.
|
|
// We will have tagged this with a suffix ('.T') if we are tracking. This is
|
|
// needed from sampling. Not all will be tracked.
|
|
if c.kind != CLIENT && isTrackedReply(c.pa.reply) {
|
|
client.trackRemoteReply(string(subject), string(c.pa.reply))
|
|
}
|
|
}
|
|
|
|
// Queue to outbound buffer
|
|
client.queueOutbound(mh)
|
|
client.queueOutbound(msg)
|
|
if prodIsMQTT {
|
|
// Need to add CR_LF since MQTT producers don't send CR_LF
|
|
client.queueOutbound([]byte(CR_LF))
|
|
}
|
|
|
|
// If we are tracking dynamic publish permissions that track reply subjects,
|
|
// do that accounting here. We only look at client.replies which will be non-nil.
|
|
// Only reply subject permissions if the client is not already allowed to publish to the reply subject.
|
|
if client.replies != nil && len(reply) > 0 && !client.pubAllowedFullCheck(string(reply), true, true) {
|
|
client.replies[string(reply)] = &resp{time.Now(), 0}
|
|
client.repliesSincePrune++
|
|
if client.repliesSincePrune > replyPermLimit || time.Since(client.lastReplyPrune) > replyPruneTime {
|
|
client.pruneReplyPerms()
|
|
}
|
|
}
|
|
|
|
// Check outbound threshold and queue IO flush if needed.
|
|
// This is specifically looking at situations where we are getting behind and may want
|
|
// to intervene before this producer goes back to top of readloop. We are in the producer's
|
|
// readloop go routine at this point.
|
|
// FIXME(dlc) - We may call this alot, maybe suppress after first call?
|
|
if len(client.out.nb) != 0 {
|
|
client.flushSignal()
|
|
}
|
|
|
|
// Add the data size we are responsible for here. This will be processed when we
|
|
// return to the top of the readLoop.
|
|
c.addToPCD(client)
|
|
|
|
if client.trace {
|
|
client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil)
|
|
client.traceMsgDelivery(msg, hdrSize)
|
|
}
|
|
|
|
client.mu.Unlock()
|
|
|
|
return true
|
|
}
|
|
|
|
// Add the given sub's client to the list of clients that need flushing.
|
|
// This must be invoked from `c`'s readLoop. No lock for c is required,
|
|
// however, `client` lock must be held on entry. This holds true even
|
|
// if `client` is same than `c`.
|
|
func (c *client) addToPCD(client *client) {
|
|
if _, ok := c.pcd[client]; !ok {
|
|
client.out.fsp++
|
|
c.pcd[client] = needFlush
|
|
}
|
|
}
|
|
|
|
// This will track a remote reply for an exported service that has requested
|
|
// latency tracking.
|
|
// Lock assumed to be held.
|
|
func (c *client) trackRemoteReply(subject, reply string) {
|
|
a := c.acc
|
|
if a == nil {
|
|
return
|
|
}
|
|
|
|
var lrt time.Duration
|
|
var respThresh time.Duration
|
|
|
|
a.mu.RLock()
|
|
se := a.getServiceExport(subject)
|
|
if se != nil {
|
|
lrt = a.lowestServiceExportResponseTime()
|
|
respThresh = se.respThresh
|
|
}
|
|
a.mu.RUnlock()
|
|
|
|
if se == nil {
|
|
return
|
|
}
|
|
|
|
if c.rrTracking == nil {
|
|
c.rrTracking = &rrTracking{
|
|
rmap: make(map[string]*remoteLatency),
|
|
ptmr: time.AfterFunc(lrt, c.pruneRemoteTracking),
|
|
lrt: lrt,
|
|
}
|
|
}
|
|
rl := remoteLatency{
|
|
Account: a.Name,
|
|
ReqId: reply,
|
|
respThresh: respThresh,
|
|
}
|
|
rl.M2.RequestStart = time.Now().UTC()
|
|
c.rrTracking.rmap[reply] = &rl
|
|
}
|
|
|
|
// pruneRemoteTracking will prune any remote tracking objects
|
|
// that are too old. These are orphaned when a service is not
|
|
// sending reponses etc.
|
|
// Lock should be held upon entry.
|
|
func (c *client) pruneRemoteTracking() {
|
|
c.mu.Lock()
|
|
if c.rrTracking == nil {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
now := time.Now()
|
|
for subject, rl := range c.rrTracking.rmap {
|
|
if now.After(rl.M2.RequestStart.Add(rl.respThresh)) {
|
|
delete(c.rrTracking.rmap, subject)
|
|
}
|
|
}
|
|
if len(c.rrTracking.rmap) > 0 {
|
|
t := c.rrTracking.ptmr
|
|
t.Stop()
|
|
t.Reset(c.rrTracking.lrt)
|
|
} else {
|
|
c.rrTracking.ptmr.Stop()
|
|
c.rrTracking = nil
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// pruneReplyPerms will remove any stale or expired entries
|
|
// in our reply cache. We make sure to not check too often.
|
|
func (c *client) pruneReplyPerms() {
|
|
// Make sure we do not check too often.
|
|
if c.perms.resp == nil {
|
|
return
|
|
}
|
|
|
|
mm := c.perms.resp.MaxMsgs
|
|
ttl := c.perms.resp.Expires
|
|
now := time.Now()
|
|
|
|
for k, resp := range c.replies {
|
|
if mm > 0 && resp.n >= mm {
|
|
delete(c.replies, k)
|
|
} else if ttl > 0 && now.Sub(resp.t) > ttl {
|
|
delete(c.replies, k)
|
|
}
|
|
}
|
|
|
|
c.repliesSincePrune = 0
|
|
c.lastReplyPrune = now
|
|
}
|
|
|
|
// pruneDenyCache will prune the deny cache via randomly
|
|
// deleting items. Doing so pruneSize items at a time.
|
|
// Lock must be held for this one since it is shared under
|
|
// deliverMsg.
|
|
func (c *client) pruneDenyCache() {
|
|
r := 0
|
|
for subject := range c.mperms.dcache {
|
|
delete(c.mperms.dcache, subject)
|
|
if r++; r > pruneSize {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// prunePubPermsCache will prune the cache via randomly
|
|
// deleting items. Doing so pruneSize items at a time.
|
|
func (c *client) prunePubPermsCache() {
|
|
// With parallel additions to the cache, it is possible that this function
|
|
// would not be able to reduce the cache to its max size in one go. We
|
|
// will try a few times but will release/reacquire the "lock" at each
|
|
// attempt to give a chance to another go routine to take over and not
|
|
// have this go routine do too many attempts.
|
|
for i := 0; i < 5; i++ {
|
|
// There is a case where we can invoke this from multiple go routines,
|
|
// (in deliverMsg() if sub.client is a LEAF), so we make sure to prune
|
|
// from only one go routine at a time.
|
|
if !atomic.CompareAndSwapInt32(&c.perms.prun, 0, 1) {
|
|
return
|
|
}
|
|
const maxPruneAtOnce = 1000
|
|
r := 0
|
|
c.perms.pcache.Range(func(k, _ any) bool {
|
|
c.perms.pcache.Delete(k)
|
|
if r++; (r > pruneSize && atomic.LoadInt32(&c.perms.pcsz) < int32(maxPermCacheSize)) ||
|
|
(r > maxPruneAtOnce) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
n := atomic.AddInt32(&c.perms.pcsz, -int32(r))
|
|
atomic.StoreInt32(&c.perms.prun, 0)
|
|
if n <= int32(maxPermCacheSize) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// pubAllowed checks on publish permissioning.
|
|
// Lock should not be held.
|
|
func (c *client) pubAllowed(subject string) bool {
|
|
return c.pubAllowedFullCheck(subject, true, false)
|
|
}
|
|
|
|
// allowedMsgTraceDest returns the trace destination if present and authorized.
|
|
// It only considers static publish permissions and does not consume dynamic
|
|
// reply permissions because the client is not publishing the trace event itself.
|
|
func (c *client) allowedMsgTraceDest(hdr []byte, hasLock bool) (string, bool) {
|
|
if len(hdr) == 0 {
|
|
return _EMPTY_, true
|
|
}
|
|
td := sliceHeader(MsgTraceDest, hdr)
|
|
if len(td) == 0 || bytes.Equal(td, traceDestDisabledAsBytes) {
|
|
return _EMPTY_, true
|
|
}
|
|
dest := bytesToString(td)
|
|
if c.kind == CLIENT {
|
|
if hasGWRoutedReplyPrefix(td) {
|
|
return dest, false
|
|
}
|
|
var acc *Account
|
|
var srv *Server
|
|
if !hasLock {
|
|
c.mu.Lock()
|
|
}
|
|
acc, srv = c.acc, c.srv
|
|
if !hasLock {
|
|
c.mu.Unlock()
|
|
}
|
|
if bytes.HasPrefix(td, clientNRGPrefix) && srv != nil && acc != srv.SystemAccount() {
|
|
return dest, false
|
|
}
|
|
}
|
|
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowedFullCheck(dest, false, hasLock) {
|
|
return dest, false
|
|
}
|
|
return dest, true
|
|
}
|
|
|
|
// pubAllowedFullCheck checks on all publish permissioning depending
|
|
// on the flag for dynamic reply permissions.
|
|
func (c *client) pubAllowedFullCheck(subject string, fullCheck, hasLock bool) bool {
|
|
if c.perms == nil || (c.perms.pub.allow == nil && c.perms.pub.deny == nil) {
|
|
return true
|
|
}
|
|
// Check if published subject is allowed if we have permissions in place.
|
|
v, ok := c.perms.pcache.Load(subject)
|
|
if ok {
|
|
return v.(bool)
|
|
}
|
|
allowed, checkAllow := true, true
|
|
// For any connections, other than CLIENT, we will implicitly allow anything that
|
|
// starts with the "$MQTT." prefix. However, we don't just return here,
|
|
// we skip the check for "allow" but will check "deny".
|
|
if c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix) {
|
|
checkAllow = false
|
|
}
|
|
// Cache miss, check allow then deny as needed.
|
|
if checkAllow && c.perms.pub.allow != nil {
|
|
np, _ := c.perms.pub.allow.NumInterest(subject)
|
|
allowed = np != 0
|
|
}
|
|
// If we have a deny list and are currently allowed, check that as well.
|
|
if allowed && c.perms.pub.deny != nil {
|
|
np, _ := c.perms.pub.deny.NumInterest(subject)
|
|
allowed = np == 0
|
|
}
|
|
|
|
// If we are tracking reply subjects
|
|
// dynamically, check to see if we are allowed here but avoid pcache.
|
|
// We need to acquire the lock though.
|
|
if !allowed && fullCheck && c.perms.resp != nil {
|
|
if !hasLock {
|
|
c.mu.Lock()
|
|
}
|
|
allowed = c.responseAllowed(subject)
|
|
if !hasLock {
|
|
c.mu.Unlock()
|
|
}
|
|
} else {
|
|
// Update our cache here.
|
|
c.perms.pcache.Store(subject, allowed)
|
|
if n := atomic.AddInt32(&c.perms.pcsz, 1); n > maxPermCacheSize {
|
|
c.prunePubPermsCache()
|
|
}
|
|
}
|
|
return allowed
|
|
}
|
|
|
|
// Returns true if this subject matches a tracked dynamic reply permission.
|
|
// Lock must be held.
|
|
func (c *client) responseAllowed(subject string) bool {
|
|
if c.perms == nil || c.perms.resp == nil {
|
|
return false
|
|
}
|
|
if resp := c.replies[subject]; resp != nil {
|
|
resp.n++
|
|
if c.perms.resp.MaxMsgs > 0 && resp.n > c.perms.resp.MaxMsgs {
|
|
delete(c.replies, subject)
|
|
} else if c.perms.resp.Expires > 0 && time.Since(resp.t) > c.perms.resp.Expires {
|
|
delete(c.replies, subject)
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Test whether a reply subject is a service import reply.
|
|
func isServiceReply(reply []byte) bool {
|
|
// This function is inlined and checking this way is actually faster
|
|
// than byte-by-byte comparison.
|
|
return len(reply) > 3 && bytesToString(reply[:4]) == replyPrefix
|
|
}
|
|
|
|
// Test whether a subject is a JetStream ACK.
|
|
func isJSAckSubject(subject []byte) bool {
|
|
return len(subject) > jsAckPreLen && bytesToString(subject[:jsAckPreLen]) == jsAckPre
|
|
}
|
|
|
|
// jsAckDeliverIdx returns the byte offset of the `@` separator in an encoded
|
|
// `$JS.ACK....@<deliver>` reply, or -1 if reply is not in that form. Stream,
|
|
// consumer, and subject tokens may legally contain `@`, so we accept only the
|
|
// first `@` that follows the eight dots of the JS ACK token:
|
|
//
|
|
// $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>@<deliver>
|
|
func jsAckDeliverIdx(reply []byte) int {
|
|
if !isJSAckSubject(reply) {
|
|
return -1
|
|
}
|
|
dots := 0
|
|
for i, b := range reply {
|
|
switch b {
|
|
case '.':
|
|
dots++
|
|
case '@':
|
|
if dots >= 8 {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// replyHasJSAckSuffix reports whether reply is already in `$JS.ACK....@<deliver>`
|
|
// form, so callers don't double-append the suffix on a re-entrant pass
|
|
// (service-import or chained JS push).
|
|
func replyHasJSAckSuffix(reply []byte) bool {
|
|
return jsAckDeliverIdx(reply) != -1
|
|
}
|
|
|
|
// Test whether a reply subject is a service import or a gateway routed reply.
|
|
func isReservedReply(reply []byte) bool {
|
|
if isServiceReply(reply) {
|
|
return true
|
|
}
|
|
// Faster to check with string([:]) than byte-by-byte
|
|
if isJSAckSubject(reply) {
|
|
return true
|
|
} else if len(reply) > gwReplyPrefixLen && bytesToString(reply[:gwReplyPrefixLen]) == gwReplyPrefix {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// This will decide to call the client code or router code.
|
|
func (c *client) processInboundMsg(msg []byte) {
|
|
switch c.kind {
|
|
case CLIENT:
|
|
c.processInboundClientMsg(msg)
|
|
case ROUTER:
|
|
c.processInboundRoutedMsg(msg)
|
|
case GATEWAY:
|
|
c.processInboundGatewayMsg(msg)
|
|
case LEAF:
|
|
c.processInboundLeafMsg(msg)
|
|
}
|
|
}
|
|
|
|
// selectMappedSubject will choose the mapped subject based on the client's inbound subject.
|
|
func (c *client) selectMappedSubject() bool {
|
|
nsubj, changed := c.acc.selectMappedSubject(bytesToString(c.pa.subject))
|
|
if changed {
|
|
c.pa.mapped = c.pa.subject
|
|
c.pa.subject = []byte(nsubj)
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// clientNRGPrefix is used in processInboundClientMsg to detect if publishes
|
|
// are being made from normal clients to NRG subjects.
|
|
var clientNRGPrefix = []byte("$NRG.")
|
|
|
|
// processInboundClientMsg is called to process an inbound msg from a client.
|
|
// Return if the message was delivered, and if the message was not delivered
|
|
// due to a permission issue.
|
|
func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
|
|
// Update statistics
|
|
// The msg includes the CR_LF, so pull back out for accounting.
|
|
c.in.msgs++
|
|
c.in.bytes += int32(len(msg) - LEN_CR_LF)
|
|
|
|
// Check that client (could be here with SYSTEM) is not publishing on reserved "$GNR" prefix.
|
|
if c.kind == CLIENT && hasGWRoutedReplyPrefix(c.pa.subject) {
|
|
c.pubPermissionViolation(c.pa.subject)
|
|
return false, true
|
|
}
|
|
|
|
// Mostly under testing scenarios.
|
|
c.mu.Lock()
|
|
if c.srv == nil || c.acc == nil {
|
|
c.mu.Unlock()
|
|
return false, false
|
|
}
|
|
acc := c.acc
|
|
genidAddr := &acc.sl.genid
|
|
|
|
// Check pub permissions
|
|
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) {
|
|
if !c.pubAllowedFullCheck(string(c.pa.subject), true, true) {
|
|
c.mu.Unlock()
|
|
c.pubPermissionViolation(c.pa.subject)
|
|
return false, true
|
|
}
|
|
}
|
|
if c.pa.hdr > 0 {
|
|
if td, ok := c.allowedMsgTraceDest(msg[:c.pa.hdr], true); !ok {
|
|
c.mu.Unlock()
|
|
c.pubPermissionViolation(stringToBytes(td))
|
|
return false, true
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
// Check if the client is trying to publish to reserved NRG subjects.
|
|
// Doesn't apply to NRGs themselves as they use SYSTEM-kind clients instead.
|
|
if c.kind == CLIENT && bytes.HasPrefix(c.pa.subject, clientNRGPrefix) && acc != c.srv.SystemAccount() {
|
|
c.pubPermissionViolation(c.pa.subject)
|
|
return false, true
|
|
}
|
|
|
|
// Now check for reserved replies. These are used for service imports.
|
|
if c.kind == CLIENT && len(c.pa.reply) > 0 && isReservedReply(c.pa.reply) {
|
|
c.replySubjectViolation(c.pa.reply)
|
|
return false, true
|
|
}
|
|
|
|
if c.opts.Verbose {
|
|
c.sendOK()
|
|
}
|
|
|
|
// If MQTT client, check for retain flag now that we have passed permissions check
|
|
if c.isMqtt() {
|
|
c.mqttHandlePubRetain()
|
|
}
|
|
|
|
// Doing this inline as opposed to create a function (which otherwise has a measured
|
|
// performance impact reported in our bench)
|
|
var isGWRouted bool
|
|
if c.kind != CLIENT {
|
|
if atomic.LoadInt32(&acc.gwReplyMapping.check) > 0 {
|
|
acc.mu.RLock()
|
|
c.pa.subject, isGWRouted = acc.gwReplyMapping.get(c.pa.subject)
|
|
acc.mu.RUnlock()
|
|
}
|
|
} else if atomic.LoadInt32(&c.gwReplyMapping.check) > 0 {
|
|
c.mu.Lock()
|
|
c.pa.subject, isGWRouted = c.gwReplyMapping.get(c.pa.subject)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// If we have an exported service and we are doing remote tracking, check this subject
|
|
// to see if we need to report the latency.
|
|
if c.rrTracking != nil {
|
|
c.mu.Lock()
|
|
rl := c.rrTracking.rmap[string(c.pa.subject)]
|
|
if rl != nil {
|
|
delete(c.rrTracking.rmap, bytesToString(c.pa.subject))
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if rl != nil {
|
|
sl := &rl.M2
|
|
// Fill this in and send it off to the other side.
|
|
sl.Status = 200
|
|
sl.Responder = c.getClientInfo(true)
|
|
sl.ServiceLatency = time.Since(sl.RequestStart) - sl.Responder.RTT
|
|
sl.TotalLatency = sl.ServiceLatency + sl.Responder.RTT
|
|
sanitizeLatencyMetric(sl)
|
|
lsub := remoteLatencySubjectForResponse(c.pa.subject)
|
|
c.srv.sendInternalAccountMsg(nil, lsub, rl) // Send to SYS account
|
|
}
|
|
}
|
|
|
|
// If the subject was converted to the gateway routed subject, then handle it now
|
|
// and be done with the rest of this function.
|
|
if isGWRouted {
|
|
c.handleGWReplyMap(msg)
|
|
return true, false
|
|
}
|
|
|
|
// Match the subscriptions. We will use our own L1 map if
|
|
// it's still valid, avoiding contention on the shared sublist.
|
|
var r *SublistResult
|
|
var ok bool
|
|
|
|
genid := atomic.LoadUint64(genidAddr)
|
|
if genid == c.in.genid && c.in.results != nil {
|
|
r, ok = c.in.results[string(c.pa.subject)]
|
|
} else {
|
|
// Reset our L1 completely.
|
|
c.in.results = make(map[string]*SublistResult)
|
|
c.in.genid = genid
|
|
}
|
|
|
|
// Go back to the sublist data structure.
|
|
if !ok {
|
|
// Match may use the subject here to populate a cache, so can not use bytesToString here.
|
|
r = acc.sl.Match(string(c.pa.subject))
|
|
if len(r.psubs)+len(r.qsubs) > 0 {
|
|
// Prune the results cache. Keeps us from unbounded growth. Random delete.
|
|
if len(c.in.results) >= maxResultCacheSize {
|
|
n := 0
|
|
for subject := range c.in.results {
|
|
delete(c.in.results, subject)
|
|
if n++; n > pruneSize {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Then add the new cache entry.
|
|
c.in.results[string(c.pa.subject)] = r
|
|
}
|
|
}
|
|
|
|
// Indication if we attempted to deliver the message to anyone.
|
|
var didDeliver bool
|
|
var qnames [][]byte
|
|
|
|
// Check for no interest, short circuit if so.
|
|
// This is the fanout scale.
|
|
if len(r.psubs)+len(r.qsubs) > 0 {
|
|
flag := pmrNoFlag
|
|
// If there are matching queue subs and we are in gateway mode,
|
|
// we need to keep track of the queue names the messages are
|
|
// delivered to. When sending to the GWs, the RMSG will include
|
|
// those names so that the remote clusters do not deliver messages
|
|
// to their queue subs of the same names.
|
|
if len(r.qsubs) > 0 && c.srv.gateway.enabled &&
|
|
atomic.LoadInt64(&c.srv.gateway.totalQSubs) > 0 {
|
|
flag |= pmrCollectQueueNames
|
|
}
|
|
didDeliver, qnames = c.processMsgResults(acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, flag)
|
|
}
|
|
|
|
// Now deal with gateways
|
|
if c.srv.gateway.enabled {
|
|
reply := c.pa.reply
|
|
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
|
|
reply = append(reply, '@')
|
|
reply = append(reply, c.pa.deliver...)
|
|
}
|
|
didDeliver = c.sendMsgToGateways(acc, msg, c.pa.subject, reply, qnames, false) || didDeliver
|
|
}
|
|
|
|
// Check to see if we did not deliver to anyone and the client has a reply subject set
|
|
// and wants notification of no_responders.
|
|
if !didDeliver && len(c.pa.reply) > 0 {
|
|
c.mu.Lock()
|
|
if c.opts.NoResponders {
|
|
if sub := c.subForReply(c.pa.reply); sub != nil {
|
|
hdrLen := 32 /* header without the subject */ + len(c.pa.subject)
|
|
proto := fmt.Sprintf("HMSG %s %s %d %d\r\nNATS/1.0 503\r\nNats-Subject: %s\r\n\r\n\r\n", c.pa.reply, sub.sid, hdrLen, hdrLen, c.pa.subject)
|
|
c.queueOutbound([]byte(proto))
|
|
c.addToPCD(c)
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
return didDeliver, false
|
|
}
|
|
|
|
// Return the subscription for this reply subject. Only look at normal subs for this client.
|
|
func (c *client) subForReply(reply []byte) *subscription {
|
|
r := c.acc.sl.Match(string(reply))
|
|
for _, sub := range r.psubs {
|
|
if sub.client == c {
|
|
return sub
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// This is invoked knowing that c.pa.subject has been set to the gateway routed subject.
|
|
// This function will send the message to possibly LEAFs and directly back to the origin
|
|
// gateway.
|
|
func (c *client) handleGWReplyMap(msg []byte) bool {
|
|
// Check for leaf nodes
|
|
if c.srv.gwLeafSubs.Count() > 0 {
|
|
if r := c.srv.gwLeafSubs.MatchBytes(c.pa.subject); len(r.psubs) > 0 {
|
|
c.processMsgResults(c.acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, pmrNoFlag)
|
|
}
|
|
}
|
|
if c.srv.gateway.enabled {
|
|
reply := c.pa.reply
|
|
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
|
|
reply = append(reply, '@')
|
|
reply = append(reply, c.pa.deliver...)
|
|
}
|
|
c.sendMsgToGateways(c.acc, msg, c.pa.subject, reply, nil, false)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Used to setup the response map for a service import request that has a reply subject.
|
|
func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tracking bool, header http.Header) *serviceImport {
|
|
rsi := si.acc.addRespServiceImport(acc, string(c.pa.reply), si, tracking, header)
|
|
if si.latency != nil {
|
|
if c.rtt == 0 {
|
|
// We have a service import that we are tracking but have not established RTT.
|
|
c.sendRTTPing()
|
|
}
|
|
si.acc.mu.Lock()
|
|
rsi.rc = c
|
|
si.acc.mu.Unlock()
|
|
}
|
|
return rsi
|
|
}
|
|
|
|
// Will remove a status and description from the header if present.
|
|
func removeHeaderStatusIfPresent(hdr []byte) []byte {
|
|
k := []byte("NATS/1.0")
|
|
kl, i := len(k), bytes.IndexByte(hdr, '\r')
|
|
if !bytes.HasPrefix(hdr, k) || i <= kl {
|
|
return hdr
|
|
}
|
|
hdr = append(hdr[:kl], hdr[i:]...)
|
|
if len(hdr) == len(emptyHdrLine) {
|
|
return nil
|
|
}
|
|
return hdr
|
|
}
|
|
|
|
// Will remove a header if present.
|
|
func removeHeaderIfPresent(hdr []byte, key string) []byte {
|
|
for {
|
|
start := getHeaderKeyIndex(key, hdr)
|
|
// key can't be first and we want to check that it is preceded by a '\n'
|
|
if start < 1 || hdr[start-1] != '\n' {
|
|
return hdr
|
|
}
|
|
index := start + len(key)
|
|
if index >= len(hdr) || hdr[index] != ':' {
|
|
return hdr
|
|
}
|
|
end := bytes.Index(hdr[start:], []byte(_CRLF_))
|
|
if end < 0 {
|
|
return hdr
|
|
}
|
|
hdr = append(hdr[:start], hdr[start+end+len(_CRLF_):]...)
|
|
if len(hdr) <= len(emptyHdrLine) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeHeaderIfPrefixPresent(hdr []byte, prefix string) []byte {
|
|
var index int
|
|
for {
|
|
if index >= len(hdr) {
|
|
return hdr
|
|
}
|
|
|
|
start := bytes.Index(hdr[index:], []byte(prefix))
|
|
if start < 0 {
|
|
return hdr
|
|
}
|
|
index += start
|
|
if index < 1 || hdr[index-1] != '\n' {
|
|
return hdr
|
|
}
|
|
|
|
end := bytes.Index(hdr[index+len(prefix):], []byte(_CRLF_))
|
|
if end < 0 {
|
|
return hdr
|
|
}
|
|
|
|
hdr = append(hdr[:index], hdr[index+end+len(prefix)+len(_CRLF_):]...)
|
|
if len(hdr) <= len(emptyHdrLine) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate a new header based on optional original header and key value.
|
|
// More used in JetStream layers.
|
|
func genHeader(hdr []byte, key, value string) []byte {
|
|
var bb bytes.Buffer
|
|
if len(hdr) > LEN_CR_LF {
|
|
bb.Write(hdr[:len(hdr)-LEN_CR_LF])
|
|
} else {
|
|
bb.WriteString(hdrLine)
|
|
}
|
|
http.Header{key: []string{value}}.Write(&bb)
|
|
bb.WriteString(CR_LF)
|
|
return bb.Bytes()
|
|
}
|
|
|
|
// This will set a header for the message.
|
|
// Lock does not need to be held but this should only be called
|
|
// from the inbound go routine. We will update the pubArgs.
|
|
// This will replace any previously set header and not add to it per normal spec.
|
|
func (c *client) setHeader(key, value string, msg []byte) []byte {
|
|
var bb bytes.Buffer
|
|
var omi int
|
|
// Write original header if present.
|
|
if c.pa.hdr > LEN_CR_LF {
|
|
omi = c.pa.hdr
|
|
// Need to copy since we're removing the header in place.
|
|
hdr := removeHeaderIfPresent(copyBytes(msg[:c.pa.hdr-LEN_CR_LF]), key)
|
|
if len(hdr) == 0 {
|
|
bb.WriteString(hdrLine)
|
|
} else {
|
|
bb.Write(hdr)
|
|
}
|
|
} else {
|
|
bb.WriteString(hdrLine)
|
|
}
|
|
http.Header{key: []string{value}}.Write(&bb)
|
|
bb.WriteString(CR_LF)
|
|
nhdr := bb.Len()
|
|
// Put the original message back.
|
|
// FIXME(dlc) - This is inefficient.
|
|
bb.Write(msg[omi:])
|
|
nsize := bb.Len() - LEN_CR_LF
|
|
// MQTT producers don't have CRLF, so add it back.
|
|
if c.isMqtt() {
|
|
nsize += LEN_CR_LF
|
|
}
|
|
// Update pubArgs
|
|
// If others will use this later we need to save and restore original.
|
|
c.pa.hdr = nhdr
|
|
c.pa.size = nsize
|
|
c.pa.hdb = []byte(strconv.Itoa(nhdr))
|
|
c.pa.szb = []byte(strconv.Itoa(nsize))
|
|
return bb.Bytes()
|
|
}
|
|
|
|
// Will return a copy of the value for the header denoted by key or nil if it does not exist.
|
|
// If you know that it is safe to refer to the underlying hdr slice for the period that the
|
|
// return value is used, then sliceHeader() will be faster.
|
|
func getHeader(key string, hdr []byte) []byte {
|
|
v := sliceHeader(key, hdr)
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return append(make([]byte, 0, len(v)), v...)
|
|
}
|
|
|
|
// Will return the sliced value for the header denoted by key or nil if it does not exists.
|
|
// This function ignores errors and tries to achieve speed and no additional allocations.
|
|
func sliceHeader(key string, hdr []byte) []byte {
|
|
if len(hdr) == 0 {
|
|
return nil
|
|
}
|
|
index := getHeaderKeyIndex(key, hdr)
|
|
if index == -1 {
|
|
return nil
|
|
}
|
|
// Skip over the key and the : separator.
|
|
index += len(key) + 1
|
|
hdrLen := len(hdr)
|
|
// Skip over whitespace before the value.
|
|
for index < hdrLen && hdr[index] == ' ' {
|
|
index++
|
|
}
|
|
// Collect together the rest of the value until we hit a CRLF.
|
|
start := index
|
|
for index < hdrLen {
|
|
if hdr[index] == '\r' && index < hdrLen-1 && hdr[index+1] == '\n' {
|
|
break
|
|
}
|
|
index++
|
|
}
|
|
return hdr[start:index:index]
|
|
}
|
|
|
|
// getHeaderKeyIndex returns an index into the header slice for the given key.
|
|
// Returns -1 if not found.
|
|
func getHeaderKeyIndex(key string, hdr []byte) int {
|
|
if len(hdr) == 0 {
|
|
return -1
|
|
}
|
|
bkey := stringToBytes(key)
|
|
keyLen, hdrLen := len(key), len(hdr)
|
|
var offset int
|
|
for {
|
|
index := bytes.Index(hdr[offset:], bkey)
|
|
// Check that we have enough characters, this will handle the -1 case of the key not
|
|
// being found and will also handle not having enough characters for trailing CRLF.
|
|
if index < 2 {
|
|
return -1
|
|
}
|
|
index += offset
|
|
// There should be a terminating CRLF.
|
|
if index >= hdrLen-1 || hdr[index-1] != '\n' || hdr[index-2] != '\r' {
|
|
offset = index + keyLen
|
|
continue
|
|
}
|
|
// The key should be immediately followed by a : separator.
|
|
if index+keyLen >= hdrLen {
|
|
return -1
|
|
}
|
|
if hdr[index+keyLen] != ':' {
|
|
offset = index + keyLen
|
|
continue
|
|
}
|
|
return index
|
|
}
|
|
}
|
|
|
|
// setHeader will replace the value of the first existing key `key`
|
|
// with the given value `val`, or add this new key at the end of
|
|
// the headers.
|
|
//
|
|
// Note: If the key does not exist, or if it exists but the new value
|
|
// would make the resulting byte slice larger than the original one,
|
|
// a new byte slice is returned and the original is left untouched.
|
|
// This is to prevent situations where caller may have a `hdr` and
|
|
// `msg` that are the parts of an underlying buffer. Extending the
|
|
// `hdr` would otherwise overwrite the `msg` part.
|
|
//
|
|
// If the new value is smaller, then the original `hdr` byte slice
|
|
// is modified.
|
|
func setHeader(key, val string, hdr []byte) []byte {
|
|
start := getHeaderKeyIndex(key, hdr)
|
|
if start >= 0 {
|
|
valStart := start + len(key) + 1
|
|
// Preserve single whitespace if used.
|
|
hdrLen := len(hdr)
|
|
if valStart < hdrLen && hdr[valStart] == ' ' {
|
|
valStart++
|
|
}
|
|
valEnd := bytes.Index(hdr[valStart:], []byte("\r"))
|
|
if valEnd < 0 {
|
|
return hdr // malformed headers
|
|
}
|
|
valEnd += valStart
|
|
// Length of the existing value (before the `\r`)
|
|
oldValLen := valEnd - valStart
|
|
// This is how many extra bytes we need for the new value.
|
|
// If <= 0, it means that we need less and so will reuse the `hdr` buffer.
|
|
if extra := len(val) - oldValLen; extra > 0 {
|
|
// Check that we don't overflow an "int".
|
|
if rem := math.MaxInt - hdrLen; rem < extra {
|
|
// We don't grow, and return the existing header.
|
|
return hdr
|
|
}
|
|
// The new size is the old size plus the extra bytes.
|
|
newHdrSize := hdrLen + extra
|
|
newHdr := make([]byte, newHdrSize)
|
|
// Copy the parts from `hdr` and `val` into the new buffer.
|
|
n := copy(newHdr, hdr[:valStart])
|
|
n += copy(newHdr[n:], val)
|
|
copy(newHdr[n:], hdr[valEnd:])
|
|
return newHdr
|
|
}
|
|
// We can write in place since it fits in the existing `hdr` buffer.
|
|
n := copy(hdr[valStart:], val)
|
|
n += copy(hdr[valStart+n:], hdr[valEnd:])
|
|
hdr = hdr[:valStart+n]
|
|
return hdr
|
|
}
|
|
if len(hdr) > 0 && bytes.HasSuffix(hdr, []byte("\r\n")) {
|
|
hdr = hdr[:len(hdr)-2]
|
|
val += "\r\n"
|
|
}
|
|
// Create the new buffer based on length of existing one and
|
|
// length of the new "<key>: <value>\r\n". Protect against "int" overflow.
|
|
newSize := uint64(len(hdr)) + uint64(len(key)) + 1 + 1 + uint64(len(val)) + 2
|
|
if newSize > uint64(math.MaxInt) {
|
|
// We don't grow, and return the existing header.
|
|
return hdr
|
|
}
|
|
newHdr := make([]byte, 0, int(newSize))
|
|
newHdr = append(newHdr, hdr...)
|
|
return fmt.Appendf(newHdr, "%s: %s\r\n", key, val)
|
|
}
|
|
|
|
// For bytes.HasPrefix below.
|
|
var (
|
|
jsRequestNextPreB = []byte(jsRequestNextPre)
|
|
jsDirectGetPreB = []byte(jsDirectGetPre)
|
|
)
|
|
|
|
// processServiceImport is an internal callback when a subscription matches an imported service
|
|
// from another account. This includes response mappings as well.
|
|
func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byte) bool {
|
|
// If we are a GW and this is not a direct serviceImport ignore.
|
|
isResponse := si.isRespServiceImport()
|
|
if (c.kind == GATEWAY || c.kind == ROUTER) && !isResponse {
|
|
return false
|
|
}
|
|
// Detect cycles and ignore (return) when we detect one.
|
|
if len(c.pa.psi) > 0 {
|
|
for i := len(c.pa.psi) - 1; i >= 0; i-- {
|
|
if psi := c.pa.psi[i]; psi.se == si.se {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
acc.mu.RLock()
|
|
var checkJS bool
|
|
shouldReturn := si.invalid || acc.sl == nil
|
|
if !shouldReturn && !isResponse && si.to == jsAllAPI {
|
|
if bytes.HasPrefix(c.pa.subject, jsDirectGetPreB) || bytes.HasPrefix(c.pa.subject, jsRequestNextPreB) {
|
|
checkJS = true
|
|
}
|
|
}
|
|
siAcc := si.acc
|
|
allowTrace := si.atrc
|
|
acc.mu.RUnlock()
|
|
|
|
// We have a special case where JetStream pulls in all service imports through one export.
|
|
// However the GetNext for consumers and DirectGet for streams are a no-op and causes buildups of service imports,
|
|
// response service imports and rrMap entries which all will need to simply expire.
|
|
// TODO(dlc) - Come up with something better.
|
|
if shouldReturn || (checkJS && si.se != nil && si.se.acc == c.srv.SystemAccount()) {
|
|
return false
|
|
}
|
|
|
|
mt, traceOnly := c.isMsgTraceEnabled()
|
|
|
|
var nrr []byte
|
|
var rsi *serviceImport
|
|
|
|
// Check if there is a reply present and set up a response.
|
|
tracking, headers := shouldSample(si.latency, c)
|
|
if len(c.pa.reply) > 0 {
|
|
// Special case for now, need to formalize.
|
|
// TODO(dlc) - Formalize as a service import option for reply rewrite.
|
|
// For now we can't do $JS.ACK since that breaks pull consumers across accounts.
|
|
if !bytes.HasPrefix(c.pa.reply, []byte(jsAckPre)) {
|
|
if rsi = c.setupResponseServiceImport(acc, si, tracking, headers); rsi != nil {
|
|
nrr = []byte(rsi.from)
|
|
}
|
|
} else {
|
|
// This only happens when we do a pull subscriber that trampolines through another account.
|
|
// Normally this code is not called.
|
|
nrr = c.pa.reply
|
|
}
|
|
} else if !isResponse && si.latency != nil && tracking {
|
|
// Check to see if this was a bad request with no reply and we were supposed to be tracking.
|
|
siAcc.sendBadRequestTrackingLatency(si, c, headers)
|
|
}
|
|
|
|
// Send tracking info here if we are tracking this response.
|
|
// This is always a response.
|
|
var didSendTL bool
|
|
if si.tracking && !si.didDeliver {
|
|
// Stamp that we attempted delivery.
|
|
si.didDeliver = true
|
|
didSendTL = acc.sendTrackingLatency(si, c)
|
|
}
|
|
|
|
// Pick correct "to" subject. If we matched on a wildcard use the literal publish subject.
|
|
to, subject := si.to, string(c.pa.subject)
|
|
|
|
if si.tr != nil {
|
|
// FIXME(dlc) - This could be slow, may want to look at adding cache to bare transforms?
|
|
to = si.tr.TransformSubject(subject)
|
|
} else if si.usePub {
|
|
to = subject
|
|
}
|
|
|
|
// Copy our pubArg since this gets modified as we process the service import itself.
|
|
pacopy := c.pa
|
|
|
|
// Now check to see if this account has mappings that could affect the service import.
|
|
// Can't use non-locked trick like in processInboundClientMsg, so just call into selectMappedSubject
|
|
// so we only lock once.
|
|
nsubj, changed := siAcc.selectMappedSubject(to)
|
|
if changed {
|
|
c.pa.mapped = []byte(to)
|
|
to = nsubj
|
|
}
|
|
|
|
// Set previous service import to detect chaining.
|
|
lpsi := len(c.pa.psi)
|
|
hadPrevSi, share := lpsi > 0, si.share
|
|
if hadPrevSi {
|
|
share = c.pa.psi[lpsi-1].share
|
|
}
|
|
c.pa.psi = append(c.pa.psi, si)
|
|
|
|
// Place our client info for the request in the original message.
|
|
// This will survive going across routes, etc.
|
|
if !isResponse {
|
|
isSysImport := siAcc == c.srv.SystemAccount()
|
|
var ci *ClientInfo
|
|
var cis *ClientInfo
|
|
if c.pa.hdr >= 0 {
|
|
var hci ClientInfo
|
|
if err := json.Unmarshal(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr]), &hci); err == nil {
|
|
cis = &hci
|
|
}
|
|
}
|
|
if c.kind == LEAF && c.pa.hdr >= 0 && len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) > 0 {
|
|
// Leaf nodes may forward a Nats-Request-Info from a remote domain,
|
|
// but the local server must replace it with the identity of the
|
|
// authenticated leaf connection instead of trusting forwarded values.
|
|
ci = c.getClientInfo(share)
|
|
if hadPrevSi && cis != nil && cis.Reply != _EMPTY_ {
|
|
ci.Reply = cis.Reply
|
|
} else if bytes.HasSuffix(c.pa.reply, []byte(FastBatchSuffix)) {
|
|
// Fast batch requires knowledge of the original reply subject.
|
|
ci.Reply = bytesToString(c.pa.reply)
|
|
}
|
|
if hadPrevSi {
|
|
ci.Service = acc.Name
|
|
if !share && (si.share || isSysImport) {
|
|
c.addServerAndClusterInfo(ci)
|
|
}
|
|
} else if !share && isSysImport {
|
|
c.addServerAndClusterInfo(ci)
|
|
}
|
|
} else if hadPrevSi && cis != nil {
|
|
ci = cis
|
|
ci.Service = acc.Name
|
|
// Check if we are moving into a share details account from a non-shared
|
|
// and add in server and cluster details.
|
|
if !share && (si.share || isSysImport) {
|
|
c.addServerAndClusterInfo(ci)
|
|
}
|
|
} else if c.kind != LEAF || c.pa.hdr < 0 || len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) == 0 {
|
|
ci = c.getClientInfo(share)
|
|
// Fast batch requires knowledge of the original reply subject.
|
|
if bytes.HasSuffix(c.pa.reply, []byte(FastBatchSuffix)) {
|
|
ci.Reply = bytesToString(c.pa.reply)
|
|
}
|
|
// If we did not share but the imports destination is the system account add in the server and cluster info.
|
|
if !share && isSysImport {
|
|
c.addServerAndClusterInfo(ci)
|
|
}
|
|
}
|
|
// Set clientInfo if present.
|
|
if ci != nil {
|
|
if b, _ := json.Marshal(ci); b != nil {
|
|
msg = c.setHeader(ClientInfoHdr, bytesToString(b), msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set our optional subject(to) and reply.
|
|
if !isResponse && to != subject {
|
|
c.pa.subject = []byte(to)
|
|
}
|
|
c.pa.reply = nrr
|
|
|
|
if changed && c.isMqtt() && c.pa.hdr > 0 {
|
|
c.srv.mqttStoreQoSMsgForAccountOnNewSubject(c.pa.hdr, msg, siAcc.GetName(), to)
|
|
}
|
|
|
|
// FIXME(dlc) - Do L1 cache trick like normal client?
|
|
rr := siAcc.sl.Match(to)
|
|
|
|
// If we are a route or gateway or leafnode and this message is flipped to a queue subscriber we
|
|
// need to handle that since the processMsgResults will want a queue filter.
|
|
flags := pmrMsgImportedFromService
|
|
if c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF {
|
|
flags |= pmrIgnoreEmptyQueueFilter
|
|
}
|
|
|
|
// We will be calling back into processMsgResults since we are now being called as a normal sub.
|
|
// We need to take care of the c.in.rts, so save off what is there and use a local version. We
|
|
// will put back what was there after.
|
|
|
|
orts := c.in.rts
|
|
|
|
var lrts [routeTargetInit]routeTarget
|
|
c.in.rts = lrts[:0]
|
|
|
|
var skipProcessing bool
|
|
// If message tracing enabled, add the service import trace.
|
|
if mt != nil {
|
|
mt.addServiceImportEvent(siAcc.GetName(), string(pacopy.subject), to)
|
|
// If we are not allowing tracing and doing trace only, we stop at this level.
|
|
if !allowTrace {
|
|
if traceOnly {
|
|
skipProcessing = true
|
|
} else {
|
|
// We are going to do normal processing, and possibly chainning
|
|
// with other server imports, but the rest won't be traced.
|
|
// We do so by setting the c.pa.trace to nil (it will be restored
|
|
// with c.pa = pacopy).
|
|
c.pa.trace = nil
|
|
// We also need to disable the message trace headers so that
|
|
// if the message is routed, it does not initialize tracing in the
|
|
// remote.
|
|
msg = c.setHeader(MsgTraceDest, MsgTraceDestDisabled, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
var didDeliver bool
|
|
|
|
if !skipProcessing {
|
|
// If this is not a gateway connection but gateway is enabled,
|
|
// try to send this converted message to all gateways.
|
|
if c.srv.gateway.enabled {
|
|
flags |= pmrCollectQueueNames
|
|
var queues [][]byte
|
|
didDeliver, queues = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
|
|
didDeliver = c.sendMsgToGateways(siAcc, msg, []byte(to), nrr, queues, false) || didDeliver
|
|
} else {
|
|
didDeliver, _ = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
|
|
}
|
|
}
|
|
|
|
// Restore to original values.
|
|
c.in.rts = orts
|
|
c.pa = pacopy
|
|
|
|
// Before we undo didDeliver based on tracing and last mile, mark in the c.pa which informs us of no responders status.
|
|
// If we override due to tracing and traceOnly we do not want to send back a no responders.
|
|
c.pa.delivered = didDeliver
|
|
|
|
// If this was a message trace but we skip last-mile delivery, we need to
|
|
// do the remove, so:
|
|
if mt != nil && traceOnly && didDeliver {
|
|
didDeliver = false
|
|
}
|
|
|
|
// Determine if we should remove this service import. This is for response service imports.
|
|
// We will remove if we did not deliver, or if we are a response service import and we are
|
|
// a singleton, or we have an EOF message.
|
|
shouldRemove := !didDeliver || (isResponse && (si.rt == Singleton || len(msg) == LEN_CR_LF))
|
|
// If we are tracking and we did not actually send the latency info we need to suppress the removal.
|
|
if si.tracking && !didSendTL {
|
|
shouldRemove = false
|
|
}
|
|
// If we are streamed or chunked we need to update our timestamp to avoid cleanup.
|
|
if si.rt != Singleton && didDeliver {
|
|
acc.mu.Lock()
|
|
si.ts = time.Now().UnixNano()
|
|
acc.mu.Unlock()
|
|
}
|
|
|
|
// Cleanup of a response service import
|
|
if shouldRemove {
|
|
reason := rsiOk
|
|
if !didDeliver {
|
|
reason = rsiNoDelivery
|
|
}
|
|
if isResponse {
|
|
acc.removeRespServiceImport(si, reason)
|
|
} else {
|
|
// This is a main import and since we could not even deliver to the exporting account
|
|
// go ahead and remove the respServiceImport we created above.
|
|
siAcc.removeRespServiceImport(rsi, reason)
|
|
}
|
|
}
|
|
|
|
return didDeliver
|
|
}
|
|
|
|
func (c *client) addSubToRouteTargets(sub *subscription) {
|
|
if c.in.rts == nil {
|
|
c.in.rts = make([]routeTarget, 0, routeTargetInit)
|
|
}
|
|
|
|
for i := range c.in.rts {
|
|
rt := &c.in.rts[i]
|
|
if rt.sub.client == sub.client {
|
|
if sub.queue != nil {
|
|
rt.qs = append(rt.qs, sub.queue...)
|
|
rt.qs = append(rt.qs, ' ')
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
var rt *routeTarget
|
|
lrts := len(c.in.rts)
|
|
|
|
// If we are here we do not have the sub yet in our list
|
|
// If we have to grow do so here.
|
|
if lrts == cap(c.in.rts) {
|
|
c.in.rts = append(c.in.rts, routeTarget{})
|
|
}
|
|
|
|
c.in.rts = c.in.rts[:lrts+1]
|
|
rt = &c.in.rts[lrts]
|
|
rt.sub = sub
|
|
rt.qs = rt._qs[:0]
|
|
if sub.queue != nil {
|
|
rt.qs = append(rt.qs, sub.queue...)
|
|
rt.qs = append(rt.qs, ' ')
|
|
}
|
|
}
|
|
|
|
// This processes the sublist results for a given message.
|
|
// Returns if the message was delivered to at least target and queue filters.
|
|
func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver, subject, reply []byte, flags int) (bool, [][]byte) {
|
|
// For sending messages across routes and leafnodes.
|
|
// Reset if we have one since we reuse this data structure.
|
|
if c.in.rts != nil {
|
|
c.in.rts = c.in.rts[:0]
|
|
}
|
|
|
|
var rplyHasGWPrefix bool
|
|
var creply = reply
|
|
|
|
// If the reply subject is a GW routed reply, we will perform some
|
|
// tracking in deliverMsg(). We also want to send to the user the
|
|
// reply without the prefix. `creply` will be set to that and be
|
|
// used to create the message header for client connections.
|
|
if rplyHasGWPrefix = isGWRoutedReply(reply); rplyHasGWPrefix {
|
|
creply = reply[gwSubjectOffset:]
|
|
}
|
|
|
|
// With JetStream we now have times where we want to match a subscription
|
|
// on one subject, but deliver it with another. e.g. JetStream deliverables.
|
|
// This only works for last mile, meaning to a client. For other types we need
|
|
// to use the original subject.
|
|
subj := subject
|
|
if len(deliver) > 0 {
|
|
subj = deliver
|
|
}
|
|
|
|
// Check for JetStream encoded reply subjects.
|
|
// For now these will only be on $JS.ACK prefixed reply subjects.
|
|
var remapped bool
|
|
if len(creply) > 0 && c.kind != CLIENT && !isInternalClient(c.kind) {
|
|
// We need to rewrite the subject and the reply.
|
|
if li := jsAckDeliverIdx(creply); li != -1 && li < len(creply)-1 {
|
|
remapped = true
|
|
subj, creply = creply[li+1:], creply[:li]
|
|
}
|
|
}
|
|
|
|
var didDeliver bool
|
|
|
|
// delivery subject for clients
|
|
var dsubj []byte
|
|
// Used as scratch if mapping
|
|
var _dsubj [128]byte
|
|
|
|
// For stats, we will keep track of the number of messages that have been
|
|
// delivered and then multiply by the size of that message and update
|
|
// server and account stats in a "single" operation (instead of per-sub).
|
|
// However, we account for situations where the message is possibly changed
|
|
// by having an extra size
|
|
var dlvMsgs int64
|
|
var dlvExtraSize int64
|
|
var dlvRouteMsgs int64
|
|
var dlvLeafMsgs int64
|
|
|
|
// We need to know if this is a MQTT producer because they send messages
|
|
// without CR_LF (we otherwise remove the size of CR_LF from message size).
|
|
prodIsMQTT := c.isMqtt()
|
|
|
|
updateStats := func() {
|
|
if dlvMsgs == 0 {
|
|
return
|
|
}
|
|
|
|
totalBytes := dlvMsgs*int64(len(msg)) + dlvExtraSize
|
|
routeBytes := dlvRouteMsgs*int64(len(msg)) + dlvExtraSize
|
|
leafBytes := dlvLeafMsgs*int64(len(msg)) + dlvExtraSize
|
|
|
|
// For non MQTT producers, remove the CR_LF * number of messages
|
|
if !prodIsMQTT {
|
|
totalBytes -= dlvMsgs * int64(LEN_CR_LF)
|
|
routeBytes -= dlvRouteMsgs * int64(LEN_CR_LF)
|
|
leafBytes -= dlvLeafMsgs * int64(LEN_CR_LF)
|
|
}
|
|
|
|
if acc != nil {
|
|
acc.stats.Lock()
|
|
acc.stats.outMsgs += dlvMsgs
|
|
acc.stats.outBytes += totalBytes
|
|
if dlvRouteMsgs > 0 {
|
|
acc.stats.rt.outMsgs += dlvRouteMsgs
|
|
acc.stats.rt.outBytes += routeBytes
|
|
}
|
|
if dlvLeafMsgs > 0 {
|
|
acc.stats.ln.outMsgs += dlvLeafMsgs
|
|
acc.stats.ln.outBytes += leafBytes
|
|
}
|
|
acc.stats.Unlock()
|
|
}
|
|
|
|
if srv := c.srv; srv != nil {
|
|
atomic.AddInt64(&srv.outMsgs, dlvMsgs)
|
|
atomic.AddInt64(&srv.outBytes, totalBytes)
|
|
}
|
|
}
|
|
|
|
mt, traceOnly := c.isMsgTraceEnabled()
|
|
|
|
// Loop over all normal subscriptions that match.
|
|
for _, sub := range r.psubs {
|
|
// Check if this is a send to a ROUTER. We now process
|
|
// these after everything else.
|
|
switch sub.client.kind {
|
|
case ROUTER:
|
|
if (c.kind != ROUTER && !c.isSpokeLeafNode()) || (flags&pmrAllowSendFromRouteToRoute != 0) {
|
|
c.addSubToRouteTargets(sub)
|
|
}
|
|
continue
|
|
case GATEWAY:
|
|
// Never send to gateway from here.
|
|
continue
|
|
case LEAF:
|
|
// We handle similarly to routes and use the same data structures.
|
|
// Leaf node delivery audience is different however.
|
|
// Also leaf nodes are always no echo, so we make sure we are not
|
|
// going to send back to ourselves here. For messages from routes we want
|
|
// to suppress in general unless we know from the hub or its a service reply.
|
|
if c != sub.client && (c.kind != ROUTER || sub.client.isHubLeafNode() || isServiceReply(c.pa.subject)) {
|
|
c.addSubToRouteTargets(sub)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Assume delivery subject is the normal subject to this point.
|
|
dsubj = subj
|
|
|
|
// We may need to disable tracing, by setting c.pa.trace to `nil`
|
|
// before the call to deliverMsg, if so, this will indicate that
|
|
// we need to put it back.
|
|
var restorePaTrace bool
|
|
|
|
// Check for stream import mapped subs (shadow subs). These apply to local subs only.
|
|
if sub.im != nil {
|
|
// If this message was a service import do not re-export to an exported stream.
|
|
if flags&pmrMsgImportedFromService != 0 {
|
|
continue
|
|
}
|
|
if sub.im.tr != nil {
|
|
to := sub.im.tr.TransformSubject(bytesToString(subject))
|
|
dsubj = append(_dsubj[:0], to...)
|
|
} else if sub.im.usePub {
|
|
dsubj = append(_dsubj[:0], subj...)
|
|
} else {
|
|
dsubj = append(_dsubj[:0], sub.im.to...)
|
|
}
|
|
|
|
if mt != nil {
|
|
mt.addStreamExportEvent(sub.client, dsubj)
|
|
// If allow_trace is false...
|
|
if !sub.im.atrc {
|
|
// If we are doing only message tracing, we can move to the
|
|
// next sub.
|
|
if traceOnly {
|
|
// Although the message was not delivered, for the purpose
|
|
// of didDeliver, we need to set to true (to avoid possible
|
|
// no responders).
|
|
didDeliver = true
|
|
continue
|
|
}
|
|
// If we are delivering the message, we need to disable tracing
|
|
// before calling deliverMsg().
|
|
c.pa.trace, restorePaTrace = nil, true
|
|
}
|
|
}
|
|
|
|
// Make sure deliver is set if inbound from a route.
|
|
if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) {
|
|
deliver = subj
|
|
}
|
|
// If we are mapping for a deliver subject we will reverse roles.
|
|
// The original subj we set from above is correct for the msg header,
|
|
// but we need to transform the deliver subject to properly route.
|
|
if len(deliver) > 0 {
|
|
dsubj, subj = subj, dsubj
|
|
}
|
|
}
|
|
|
|
// Remap to the original subject if internal.
|
|
if sub.icb != nil && sub.rsi {
|
|
dsubj = subject
|
|
}
|
|
|
|
// Normal delivery
|
|
mh := c.msgHeader(dsubj, creply, sub)
|
|
if c.deliverMsg(prodIsMQTT, sub, acc, dsubj, creply, mh, msg, rplyHasGWPrefix) {
|
|
// We don't count internal deliveries, so do only when sub.icb is nil.
|
|
if sub.icb == nil {
|
|
dlvMsgs++
|
|
}
|
|
didDeliver = true
|
|
}
|
|
if restorePaTrace {
|
|
c.pa.trace = mt
|
|
}
|
|
}
|
|
|
|
// Set these up to optionally filter based on the queue lists.
|
|
// This is for messages received from routes which will have directed
|
|
// guidance on which queue groups we should deliver to.
|
|
qf := c.pa.queues
|
|
|
|
// Declared here because of goto.
|
|
var queues [][]byte
|
|
|
|
var leafOrigin string
|
|
switch c.kind {
|
|
case ROUTER:
|
|
if len(c.pa.origin) > 0 {
|
|
// Picture a message sent from a leafnode to a server that then routes
|
|
// this message: CluserA -leaf-> HUB1 -route-> HUB2
|
|
// Here we are in HUB2, so c.kind is a ROUTER, but the message will
|
|
// contain a c.pa.origin set to "ClusterA" to indicate that this message
|
|
// originated from that leafnode cluster.
|
|
leafOrigin = bytesToString(c.pa.origin)
|
|
}
|
|
case LEAF:
|
|
leafOrigin = c.remoteCluster()
|
|
}
|
|
|
|
// For all routes/leaf/gateway connections, we may still want to send messages to
|
|
// leaf nodes or routes even if there are no queue filters since we collect
|
|
// them above and do not process inline like normal clients.
|
|
// However, do select queue subs if asked to ignore empty queue filter.
|
|
if (c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY) && len(qf) == 0 && flags&pmrIgnoreEmptyQueueFilter == 0 {
|
|
goto sendToRoutesOrLeafs
|
|
}
|
|
|
|
// Process queue subs
|
|
for i := 0; i < len(r.qsubs); i++ {
|
|
qsubs := r.qsubs[i]
|
|
// If we have a filter check that here. We could make this a map or someting more
|
|
// complex but linear search since we expect queues to be small. Should be faster
|
|
// and more cache friendly.
|
|
if qf != nil && len(qsubs) > 0 {
|
|
tqn := qsubs[0].queue
|
|
for _, qn := range qf {
|
|
if bytes.Equal(qn, tqn) {
|
|
goto selectQSub
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
selectQSub:
|
|
// We will hold onto remote or lead qsubs when we are coming from
|
|
// a route or a leaf node just in case we can no longer do local delivery.
|
|
var rsub, sub *subscription
|
|
var _ql [32]*subscription
|
|
|
|
src := c.kind
|
|
// If we just came from a route we want to prefer local subs.
|
|
// So only select from local subs but remember the first rsub
|
|
// in case all else fails.
|
|
if src == ROUTER {
|
|
ql := _ql[:0]
|
|
for i := 0; i < len(qsubs); i++ {
|
|
sub = qsubs[i]
|
|
if dst := sub.client.kind; dst == LEAF || dst == ROUTER {
|
|
// If the destination is a LEAF, we first need to make sure
|
|
// that we would not pick one that was the origin of this
|
|
// message.
|
|
if dst == LEAF && leafOrigin != _EMPTY_ && leafOrigin == sub.client.remoteCluster() {
|
|
continue
|
|
}
|
|
// If we have assigned a ROUTER rsub already, replace if
|
|
// the destination is a LEAF since we want to favor that.
|
|
if rsub == nil || (rsub.client.kind == ROUTER && dst == LEAF) {
|
|
rsub = sub
|
|
} else if dst == LEAF {
|
|
// We already have a LEAF and this is another one.
|
|
// Flip a coin to see if we swap it or not.
|
|
// See https://github.com/nats-io/nats-server/issues/6040
|
|
if fastrand.Uint32()%2 == 1 {
|
|
rsub = sub
|
|
}
|
|
}
|
|
} else {
|
|
ql = append(ql, sub)
|
|
}
|
|
}
|
|
qsubs = ql
|
|
}
|
|
|
|
sindex := 0
|
|
lqs := len(qsubs)
|
|
if lqs > 1 {
|
|
sindex = int(fastrand.Uint32() % uint32(lqs))
|
|
}
|
|
|
|
// Find a subscription that is able to deliver this message starting at a random index.
|
|
// Note that if the message came from a ROUTER, we will only have CLIENT or LEAF
|
|
// queue subs here, otherwise we can have all types.
|
|
for i := 0; i < lqs; i++ {
|
|
if sindex+i < lqs {
|
|
sub = qsubs[sindex+i]
|
|
} else {
|
|
sub = qsubs[(sindex+i)%lqs]
|
|
}
|
|
if sub == nil {
|
|
continue
|
|
}
|
|
|
|
// If we are a spoke leaf node make sure to not forward across routes.
|
|
// This mimics same behavior for normal subs above.
|
|
if c.kind == LEAF && c.isSpokeLeafNode() && sub.client.kind == ROUTER {
|
|
continue
|
|
}
|
|
|
|
// We have taken care of preferring local subs for a message from a route above.
|
|
// Here we just care about a client or leaf and skipping a leaf and preferring locals.
|
|
if dst := sub.client.kind; dst == ROUTER || dst == LEAF {
|
|
if (src == LEAF || src == CLIENT) && dst == LEAF {
|
|
// If we come from a LEAF and are about to pick a LEAF connection,
|
|
// make sure this is not the same leaf cluster.
|
|
if src == LEAF && leafOrigin != _EMPTY_ && leafOrigin == sub.client.remoteCluster() {
|
|
continue
|
|
}
|
|
// Remember that leaf in case we don't find any other candidate.
|
|
// We already start randomly in lqs slice, so we don't need
|
|
// to do a random swap if we already have an rsub like we do
|
|
// when src == ROUTER above.
|
|
if rsub == nil {
|
|
rsub = sub
|
|
}
|
|
continue
|
|
} else {
|
|
// We want to favor qsubs in our own cluster. If the routed
|
|
// qsub has an origin, it means that is on behalf of a leaf.
|
|
// We need to treat it differently.
|
|
if len(sub.origin) > 0 {
|
|
// If we already have an rsub, nothing to do. Also, do
|
|
// not pick a routed qsub for a LEAF origin cluster
|
|
// that is the same than where the message comes from.
|
|
if rsub == nil && (leafOrigin == _EMPTY_ || leafOrigin != bytesToString(sub.origin)) {
|
|
rsub = sub
|
|
}
|
|
continue
|
|
}
|
|
// This is a qsub that is local on the remote server (or
|
|
// we are connected to an older server and we don't know).
|
|
// Pick this one and be done.
|
|
rsub = sub
|
|
break
|
|
}
|
|
}
|
|
|
|
// Assume delivery subject is normal subject to this point.
|
|
dsubj = subj
|
|
|
|
// We may need to disable tracing, by setting c.pa.trace to `nil`
|
|
// before the call to deliverMsg, if so, this will indicate that
|
|
// we need to put it back.
|
|
var restorePaTrace bool
|
|
var skipDelivery bool
|
|
|
|
// Check for stream import mapped subs. These apply to local subs only.
|
|
if sub.im != nil {
|
|
// If this message was a service import do not re-export to an exported stream.
|
|
if flags&pmrMsgImportedFromService != 0 {
|
|
continue
|
|
}
|
|
if sub.im.tr != nil {
|
|
to := sub.im.tr.TransformSubject(bytesToString(subject))
|
|
dsubj = append(_dsubj[:0], to...)
|
|
} else if sub.im.usePub {
|
|
dsubj = append(_dsubj[:0], subj...)
|
|
} else {
|
|
dsubj = append(_dsubj[:0], sub.im.to...)
|
|
}
|
|
|
|
if mt != nil {
|
|
mt.addStreamExportEvent(sub.client, dsubj)
|
|
// If allow_trace is false...
|
|
if !sub.im.atrc {
|
|
// If we are doing only message tracing, we are done
|
|
// with this queue group.
|
|
if traceOnly {
|
|
skipDelivery = true
|
|
} else {
|
|
// If we are delivering, we need to disable tracing
|
|
// before the call to deliverMsg()
|
|
c.pa.trace, restorePaTrace = nil, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure deliver is set if inbound from a route.
|
|
if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) {
|
|
deliver = subj
|
|
}
|
|
// If we are mapping for a deliver subject we will reverse roles.
|
|
// The original subj we set from above is correct for the msg header,
|
|
// but we need to transform the deliver subject to properly route.
|
|
if len(deliver) > 0 {
|
|
dsubj, subj = subj, dsubj
|
|
}
|
|
}
|
|
|
|
var delivered bool
|
|
if !skipDelivery {
|
|
mh := c.msgHeader(dsubj, creply, sub)
|
|
delivered = c.deliverMsg(prodIsMQTT, sub, acc, subject, creply, mh, msg, rplyHasGWPrefix)
|
|
if restorePaTrace {
|
|
c.pa.trace = mt
|
|
}
|
|
}
|
|
if skipDelivery || delivered {
|
|
// Update only if not skipped.
|
|
if !skipDelivery && sub.icb == nil {
|
|
dlvMsgs++
|
|
switch sub.client.kind {
|
|
case ROUTER:
|
|
dlvRouteMsgs++
|
|
case LEAF:
|
|
dlvLeafMsgs++
|
|
}
|
|
}
|
|
// Do the rest even when message delivery was skipped.
|
|
didDeliver = true
|
|
// Clear rsub
|
|
rsub = nil
|
|
if flags&pmrCollectQueueNames != 0 {
|
|
queues = append(queues, sub.queue)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if rsub != nil {
|
|
// We are here if we have selected a leaf or route as the destination,
|
|
// or if we tried to deliver to a local qsub but failed.
|
|
c.addSubToRouteTargets(rsub)
|
|
if flags&pmrCollectQueueNames != 0 {
|
|
queues = append(queues, rsub.queue)
|
|
}
|
|
}
|
|
}
|
|
|
|
sendToRoutesOrLeafs:
|
|
|
|
// If no messages for routes or leafnodes return here.
|
|
if len(c.in.rts) == 0 {
|
|
updateStats()
|
|
return didDeliver, queues
|
|
}
|
|
|
|
// If we do have a deliver subject we need to do something with it.
|
|
// Again this is when JetStream (but possibly others) wants the system
|
|
// to rewrite the delivered subject. The way we will do that is place it
|
|
// at the end of the reply subject if it exists. But only if this wasn't
|
|
// already performed, otherwise we'd end up with a duplicate '@' suffix
|
|
// resulting in a protocol error.
|
|
if len(deliver) > 0 && len(reply) > 0 && !remapped && !replyHasJSAckSuffix(reply) {
|
|
reply = append(reply, '@')
|
|
reply = append(reply, deliver...)
|
|
}
|
|
|
|
// Copy off original pa in case it changes.
|
|
pa := c.pa
|
|
|
|
if mt != nil {
|
|
// We are going to replace "pa" with our copy of c.pa, but to restore
|
|
// to the original copy of c.pa, we need to save it again.
|
|
cpa := pa
|
|
msg = mt.setOriginAccountHeaderIfNeeded(c, acc, msg)
|
|
defer func() { c.pa = cpa }()
|
|
// Update pa with our current c.pa state.
|
|
pa = c.pa
|
|
}
|
|
|
|
// We address by index to avoid struct copy.
|
|
// We have inline structs for memory layout and cache coherency.
|
|
for i := range c.in.rts {
|
|
rt := &c.in.rts[i]
|
|
dc := rt.sub.client
|
|
dmsg, hset := msg, false
|
|
|
|
// Check if we have an origin cluster set from a leafnode message.
|
|
// If so make sure we do not send it back to the same cluster for a different
|
|
// leafnode. Cluster wide no echo.
|
|
if dc.kind == LEAF {
|
|
// Check two scenarios. One is inbound from a route (c.pa.origin),
|
|
// and the other is leaf to leaf. In both case, leafOrigin is the one
|
|
// to use for the comparison.
|
|
if leafOrigin != _EMPTY_ && leafOrigin == dc.remoteCluster() {
|
|
continue
|
|
}
|
|
|
|
// We need to check if this is a request that has a stamped client information header.
|
|
// This will contain an account but will represent the account from the leafnode. If
|
|
// they are not named the same this would cause an account lookup failure trying to
|
|
// process the request for something like JetStream or other system services that rely
|
|
// on the client info header. We can just check for reply and the presence of a header
|
|
// to avoid slow downs for all traffic.
|
|
if len(c.pa.reply) > 0 && c.pa.hdr >= 0 {
|
|
dmsg, hset = c.checkLeafClientInfoHeader(msg)
|
|
}
|
|
}
|
|
|
|
if mt != nil {
|
|
dmsg = mt.setHopHeader(c, dmsg)
|
|
hset = true
|
|
}
|
|
|
|
mh := c.msgHeaderForRouteOrLeaf(subject, reply, rt, acc)
|
|
if c.deliverMsg(prodIsMQTT, rt.sub, acc, subject, reply, mh, dmsg, false) {
|
|
if rt.sub.icb == nil {
|
|
dlvMsgs++
|
|
switch dc.kind {
|
|
case ROUTER:
|
|
dlvRouteMsgs++
|
|
case LEAF:
|
|
dlvLeafMsgs++
|
|
}
|
|
dlvExtraSize += int64(len(dmsg) - len(msg))
|
|
}
|
|
didDeliver = true
|
|
}
|
|
|
|
// If we set the header reset the origin pub args.
|
|
if hset {
|
|
c.pa = pa
|
|
}
|
|
}
|
|
updateStats()
|
|
return didDeliver, queues
|
|
}
|
|
|
|
// Check and swap accounts on a client info header destined across a leafnode.
|
|
func (c *client) checkLeafClientInfoHeader(msg []byte) (dmsg []byte, setHdr bool) {
|
|
if c.pa.hdr < 0 || len(msg) < c.pa.hdr {
|
|
return msg, false
|
|
}
|
|
cir := sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])
|
|
if len(cir) == 0 {
|
|
return msg, false
|
|
}
|
|
|
|
dmsg = msg
|
|
|
|
var ci ClientInfo
|
|
if err := json.Unmarshal(cir, &ci); err == nil {
|
|
if v, _ := c.srv.leafRemoteAccounts.Load(ci.Account); v != nil {
|
|
remoteAcc := v.(string)
|
|
if ci.Account != remoteAcc {
|
|
ci.Account = remoteAcc
|
|
if b, _ := json.Marshal(ci); b != nil {
|
|
dmsg, setHdr = c.setHeader(ClientInfoHdr, bytesToString(b), msg), true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return dmsg, setHdr
|
|
}
|
|
|
|
func (c *client) pubPermissionViolation(subject []byte) {
|
|
errTxt := fmt.Sprintf("Permissions Violation for Publish to %q", subject)
|
|
if mt, _ := c.isMsgTraceEnabled(); mt != nil {
|
|
mt.setIngressError(errTxt)
|
|
}
|
|
c.sendErr(errTxt)
|
|
c.Errorf("Publish Violation - Subject %q", subject)
|
|
}
|
|
|
|
func (c *client) subPermissionViolation(sub *subscription) {
|
|
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject)
|
|
logTxt := fmt.Sprintf("Subscription Violation - Subject %q, SID %s", sub.subject, sub.sid)
|
|
|
|
if sub.queue != nil {
|
|
errTxt = fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q", sub.subject, sub.queue)
|
|
logTxt = fmt.Sprintf("Subscription Violation - Subject %q, Queue: %q, SID %s", sub.subject, sub.queue, sub.sid)
|
|
}
|
|
|
|
c.sendErr(errTxt)
|
|
c.Errorf(logTxt)
|
|
}
|
|
|
|
func (c *client) replySubjectViolation(reply []byte) {
|
|
errTxt := fmt.Sprintf("Permissions Violation for Publish with Reply of %q", reply)
|
|
if mt, _ := c.isMsgTraceEnabled(); mt != nil {
|
|
mt.setIngressError(errTxt)
|
|
}
|
|
c.sendErr(errTxt)
|
|
c.Errorf("Publish Violation - Reply %q", reply)
|
|
}
|
|
|
|
func (c *client) maxTokensViolation(sub *subscription) {
|
|
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q, too many tokens", sub.subject)
|
|
logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - Subject %q, SID %s", sub.subject, sub.sid)
|
|
c.sendErr(errTxt)
|
|
c.Errorf(logTxt)
|
|
}
|
|
|
|
func (c *client) processPingTimer() {
|
|
c.mu.Lock()
|
|
c.ping.tmr = nil
|
|
// Check if connection is still opened
|
|
if c.isClosed() {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
c.Debugf("%s Ping Timer", c.kindString())
|
|
|
|
var sendPing bool
|
|
|
|
opts := c.srv.getOpts()
|
|
pingInterval := opts.PingInterval
|
|
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
|
pingInterval = opts.Cluster.PingInterval
|
|
}
|
|
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
|
pingInterval = opts.Websocket.PingInterval
|
|
}
|
|
pingInterval = adjustPingInterval(c.kind, pingInterval)
|
|
now := time.Now()
|
|
needRTT := c.rtt == 0 || now.Sub(c.rttStart) > DEFAULT_RTT_MEASUREMENT_INTERVAL
|
|
|
|
// Do not delay PINGs for ROUTER, GATEWAY or spoke LEAF connections.
|
|
if c.kind == ROUTER || c.kind == GATEWAY || c.isSpokeLeafNode() {
|
|
sendPing = true
|
|
} else {
|
|
// If we received client data or a ping from the other side within the PingInterval,
|
|
// then there is no need to send a ping.
|
|
if delta := now.Sub(c.lastIn); delta < pingInterval && !needRTT {
|
|
c.Debugf("Delaying PING due to remote client data or ping %v ago", delta.Round(time.Second))
|
|
} else {
|
|
sendPing = true
|
|
}
|
|
}
|
|
|
|
if sendPing {
|
|
// Check for violation
|
|
maxPingsOut := opts.MaxPingsOut
|
|
if c.kind == ROUTER && opts.Cluster.MaxPingsOut > 0 {
|
|
maxPingsOut = opts.Cluster.MaxPingsOut
|
|
}
|
|
if c.ping.out+1 > maxPingsOut {
|
|
c.Debugf("Stale Client Connection - Closing")
|
|
c.enqueueProto([]byte(fmt.Sprintf(errProto, "Stale Connection")))
|
|
c.mu.Unlock()
|
|
c.closeConnection(StaleConnection)
|
|
return
|
|
}
|
|
// Send PING
|
|
c.sendPing()
|
|
}
|
|
|
|
// Reset to fire again.
|
|
c.setPingTimer()
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Returns the smallest value between the given `d` and some max value
|
|
// based on the connection kind.
|
|
func adjustPingInterval(kind int, d time.Duration) time.Duration {
|
|
switch kind {
|
|
case ROUTER:
|
|
if d > routeMaxPingInterval {
|
|
return routeMaxPingInterval
|
|
}
|
|
case GATEWAY:
|
|
if d > gatewayMaxPingInterval {
|
|
return gatewayMaxPingInterval
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// This is used when a connection cannot yet start to send PINGs because
|
|
// the remote would not be able to handle them (case of compression,
|
|
// or outbound gateway, etc...), but we still want to close the connection
|
|
// if the timer has not been reset by the time we reach the time equivalent
|
|
// to have sent the max number of pings.
|
|
//
|
|
// Lock should be held
|
|
func (c *client) watchForStaleConnection(pingInterval time.Duration, pingMax int) {
|
|
c.ping.tmr = time.AfterFunc(pingInterval*time.Duration(pingMax+1), func() {
|
|
c.mu.Lock()
|
|
c.Debugf("Stale Client Connection - Closing")
|
|
c.enqueueProto([]byte(fmt.Sprintf(errProto, "Stale Connection")))
|
|
c.mu.Unlock()
|
|
c.closeConnection(StaleConnection)
|
|
})
|
|
}
|
|
|
|
// Lock should be held
|
|
func (c *client) setPingTimer() {
|
|
if c.srv == nil {
|
|
return
|
|
}
|
|
opts := c.srv.getOpts()
|
|
d := opts.PingInterval
|
|
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
|
d = opts.Cluster.PingInterval
|
|
}
|
|
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
|
d = opts.Websocket.PingInterval
|
|
}
|
|
d = adjustPingInterval(c.kind, d)
|
|
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
|
|
}
|
|
|
|
// Lock should be held
|
|
func (c *client) clearPingTimer() {
|
|
if c.ping.tmr == nil {
|
|
return
|
|
}
|
|
c.ping.tmr.Stop()
|
|
c.ping.tmr = nil
|
|
}
|
|
|
|
func (c *client) clearTlsToTimer() {
|
|
if c.tlsTo == nil {
|
|
return
|
|
}
|
|
c.tlsTo.Stop()
|
|
c.tlsTo = nil
|
|
}
|
|
|
|
// Lock should be held
|
|
func (c *client) setAuthTimer(d time.Duration) {
|
|
c.atmr = time.AfterFunc(d, c.authTimeout)
|
|
}
|
|
|
|
// Lock should be held
|
|
func (c *client) clearAuthTimer() bool {
|
|
if c.atmr == nil {
|
|
return true
|
|
}
|
|
stopped := c.atmr.Stop()
|
|
c.atmr = nil
|
|
return stopped
|
|
}
|
|
|
|
// Track whether the parser should still enforce pre-CONNECT rules.
|
|
// This is handshake state, not timer state, since some handshakes
|
|
// use a different timer while still expecting CONNECT.
|
|
// Lock assume held on entry.
|
|
func (c *client) awaitingAuth() bool {
|
|
return c.flags.isSet(expectConnect) && !c.flags.isSet(connectReceived)
|
|
}
|
|
|
|
// This will set the atmr for the JWT expiration time.
|
|
// We will lock on entry.
|
|
func (c *client) setExpirationTimer(d time.Duration) {
|
|
c.mu.Lock()
|
|
c.setExpirationTimerUnlocked(d)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// This will set the atmr for the JWT expiration time. client lock should be held before call
|
|
func (c *client) setExpirationTimerUnlocked(d time.Duration) {
|
|
c.atmr = time.AfterFunc(d, c.authExpired)
|
|
// This is an JWT expiration.
|
|
if c.flags.isSet(connectReceived) {
|
|
c.expires = time.Now().Add(d).Truncate(time.Second)
|
|
}
|
|
}
|
|
|
|
// Return when this client expires via a claim, or 0 if not set.
|
|
func (c *client) claimExpiration() time.Duration {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.expires.IsZero() {
|
|
return 0
|
|
}
|
|
return time.Until(c.expires).Truncate(time.Second)
|
|
}
|
|
|
|
// Possibly flush the connection and then close the low level connection.
|
|
// The boolean `minimalFlush` indicates if the flush operation should have a
|
|
// minimal write deadline.
|
|
// Lock is held on entry.
|
|
func (c *client) flushAndClose(minimalFlush bool) {
|
|
if !c.flags.isSet(skipFlushOnClose) && c.out.pb > 0 {
|
|
if minimalFlush {
|
|
const lowWriteDeadline = 100 * time.Millisecond
|
|
|
|
// Reduce the write deadline if needed.
|
|
if c.out.wdl > lowWriteDeadline {
|
|
c.out.wdl = lowWriteDeadline
|
|
}
|
|
}
|
|
c.flushOutbound()
|
|
}
|
|
for i := range c.out.nb {
|
|
nbPoolPut(c.out.nb[i])
|
|
}
|
|
c.out.nb = nil
|
|
// We can't touch c.out.wnb when a flushOutbound is in progress since it
|
|
// is accessed outside the lock there. If in progress, the cleanup will be
|
|
// done in flushOutbound when detecting that connection is closed.
|
|
if !c.flags.isSet(flushOutbound) {
|
|
for i := range c.out.wnb {
|
|
nbPoolPut(c.out.wnb[i])
|
|
}
|
|
c.out.wnb = nil
|
|
}
|
|
// This seem to be important (from experimentation) for the GC to release
|
|
// the connection.
|
|
c.out.sg = nil
|
|
|
|
// Close the low level connection.
|
|
if c.nc != nil {
|
|
// Starting with Go 1.16, the low level close will set its own deadline
|
|
// of 5 seconds, so setting our own deadline does not work. Instead,
|
|
// we will close the TLS connection in separate go routine.
|
|
nc := c.nc
|
|
c.nc = nil
|
|
if _, ok := nc.(*tls.Conn); ok {
|
|
go func() { nc.Close() }()
|
|
} else {
|
|
nc.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
var kindStringMap = map[int]string{
|
|
CLIENT: "Client",
|
|
ROUTER: "Router",
|
|
GATEWAY: "Gateway",
|
|
LEAF: "Leafnode",
|
|
JETSTREAM: "JetStream",
|
|
ACCOUNT: "Account",
|
|
SYSTEM: "System",
|
|
}
|
|
|
|
func (c *client) kindString() string {
|
|
if kindStringVal, ok := kindStringMap[c.kind]; ok {
|
|
return kindStringVal
|
|
}
|
|
return "Unknown Type"
|
|
}
|
|
|
|
// swapAccountAfterReload will check to make sure the bound account for this client
|
|
// is current. Under certain circumstances after a reload we could be pointing to
|
|
// an older one.
|
|
func (c *client) swapAccountAfterReload() {
|
|
c.mu.Lock()
|
|
srv := c.srv
|
|
an := c.acc.GetName()
|
|
c.mu.Unlock()
|
|
if srv == nil {
|
|
return
|
|
}
|
|
if acc, _ := srv.LookupAccount(an); acc != nil {
|
|
c.mu.Lock()
|
|
if c.acc != acc {
|
|
c.acc = acc
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// processSubsOnConfigReload removes any subscriptions the client has that are no
|
|
// longer authorized, and checks for imports (accounts) due to a config reload.
|
|
func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) {
|
|
c.mu.Lock()
|
|
var (
|
|
checkPerms = c.perms != nil
|
|
checkAcc = c.acc != nil
|
|
acc = c.acc
|
|
)
|
|
if !checkPerms && !checkAcc {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
var (
|
|
_subs [32]*subscription
|
|
subs = _subs[:0]
|
|
_removed [32]*subscription
|
|
removed = _removed[:0]
|
|
srv = c.srv
|
|
)
|
|
if checkAcc {
|
|
// We actually only want to check if stream imports have changed.
|
|
if _, ok := awcsti[acc.Name]; !ok {
|
|
checkAcc = false
|
|
}
|
|
}
|
|
// We will clear any mperms we have here. It will rebuild on the fly with canSubscribe,
|
|
// so we do that here as we collect them. We will check result down below.
|
|
c.mperms = nil
|
|
// Collect client's subs under the lock
|
|
for _, sub := range c.subs {
|
|
// Just checking to rebuild mperms under the lock, will collect removed though here.
|
|
// Only collect under subs array of canSubscribe and checkAcc true.
|
|
canSub := c.canSubscribe(string(sub.subject))
|
|
canQSub := sub.queue != nil && c.canSubscribe(string(sub.subject), string(sub.queue))
|
|
|
|
if !canSub && !canQSub {
|
|
removed = append(removed, sub)
|
|
} else if checkAcc {
|
|
subs = append(subs, sub)
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
// This list is all subs who are allowed and we need to check accounts.
|
|
for _, sub := range subs {
|
|
c.mu.Lock()
|
|
oldShadows := sub.shadow
|
|
sub.shadow = nil
|
|
c.mu.Unlock()
|
|
c.addShadowSubscriptions(acc, sub)
|
|
for _, nsub := range oldShadows {
|
|
nsub.im.acc.sl.Remove(nsub)
|
|
}
|
|
}
|
|
|
|
// Unsubscribe all that need to be removed and report back to client and logs.
|
|
for _, sub := range removed {
|
|
c.unsubscribe(acc, sub, true, true)
|
|
c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)", sub.subject, sub.sid))
|
|
srv.Noticef("Removed sub %q (sid %q) for %s - not authorized", sub.subject, sub.sid, c.getAuthUser())
|
|
}
|
|
}
|
|
|
|
// Allows us to count up all the queue subscribers during close.
|
|
type qsub struct {
|
|
sub *subscription
|
|
n int32
|
|
}
|
|
|
|
func (c *client) closeConnection(reason ClosedState) {
|
|
c.mu.Lock()
|
|
if c.flags.isSet(closeConnection) {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
// Note that we may have markConnAsClosed() invoked before closeConnection(),
|
|
// so don't set this to 1, instead bump the count.
|
|
c.rref++
|
|
c.flags.set(closeConnection)
|
|
c.clearAuthTimer()
|
|
c.clearPingTimer()
|
|
c.clearTlsToTimer()
|
|
c.markConnAsClosed(reason)
|
|
|
|
// Unblock anyone who is potentially stalled waiting on us.
|
|
if c.out.stc != nil {
|
|
close(c.out.stc)
|
|
c.out.stc = nil
|
|
}
|
|
|
|
// If we have remote latency tracking running shut that down.
|
|
if c.rrTracking != nil {
|
|
c.rrTracking.ptmr.Stop()
|
|
c.rrTracking = nil
|
|
}
|
|
|
|
// If we are shutting down, no need to do all the accounting on subs, etc.
|
|
// During LDM we'll still do the accounting, otherwise account limits could close others after this reconnects.
|
|
if reason == ServerShutdown && c.srv.isShuttingDown() {
|
|
s := c.srv
|
|
c.mu.Unlock()
|
|
if s != nil {
|
|
// Unregister
|
|
s.removeClient(c)
|
|
}
|
|
return
|
|
}
|
|
|
|
var (
|
|
kind = c.kind
|
|
srv = c.srv
|
|
noReconnect = c.flags.isSet(noReconnect)
|
|
acc = c.acc
|
|
)
|
|
c.mu.Unlock()
|
|
|
|
if kind == ROUTER {
|
|
c.removeRemoteSubs()
|
|
}
|
|
if srv != nil {
|
|
// Unregister
|
|
srv.removeClient(c)
|
|
|
|
if acc != nil {
|
|
// Update remote subscriptions.
|
|
if kind == CLIENT || kind == LEAF || kind == JETSTREAM {
|
|
// Remove client's subscriptions from the account and unregister
|
|
// client from that account. Keep c.subs populated because
|
|
// saveClosedClient (started as a goroutine in markConnAsClosed)
|
|
// still needs to read it.
|
|
c.clearAccountSubs(true)
|
|
}
|
|
// Always remove from the account, otherwise we can leak clients.
|
|
// Note that SYSTEM and ACCOUNT types from above cleanup their own subs.
|
|
if prev := acc.removeClient(c); prev == 1 {
|
|
srv.decActiveAccounts()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now that we are done with subscriptions, clear the field so that the
|
|
// connection can be released and gc'ed.
|
|
if kind == CLIENT || kind == LEAF {
|
|
c.mu.Lock()
|
|
c.subs = nil
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Don't reconnect connections that have been marked with
|
|
// the no reconnect flag.
|
|
if noReconnect {
|
|
return
|
|
}
|
|
|
|
c.reconnect()
|
|
}
|
|
|
|
// clearAccountSubs removes the client's subscriptions from its current account
|
|
// and unregisters it from that account. If close is true, c.subs is left
|
|
// populated for saveClosedClient; otherwise c.subs is cleared and c.acc
|
|
// registered back to the global account.
|
|
// Client lock MUST NOT be held on entry.
|
|
func (c *client) clearAccountSubs(close bool) {
|
|
c.mu.Lock()
|
|
kind := c.kind
|
|
srv := c.srv
|
|
acc := c.acc
|
|
if acc == nil || (kind != CLIENT && kind != LEAF && kind != JETSTREAM) {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
var _subs [32]*subscription
|
|
subs := _subs[:0]
|
|
// Do not set c.subs to nil or delete the sub from c.subs here because
|
|
// it will be needed in saveClosedClient (which has been started as a
|
|
// go routine in markConnAsClosed). Cleanup will be done there.
|
|
for _, sub := range c.subs {
|
|
// Auto-unsubscribe subscriptions must be unsubscribed forcibly.
|
|
sub.max = 0
|
|
sub.close()
|
|
subs = append(subs, sub)
|
|
if !close {
|
|
delete(c.subs, string(sub.sid))
|
|
}
|
|
}
|
|
spoke := c.isSpokeLeafNode()
|
|
c.mu.Unlock()
|
|
|
|
acc.sl.RemoveBatch(subs)
|
|
|
|
if srv != nil {
|
|
qsubs := map[string]*qsub{}
|
|
for _, sub := range subs {
|
|
// Call unsubscribe here to cleanup shadow subscriptions and such.
|
|
c.unsubscribe(acc, sub, true, false)
|
|
// Update route as normal for a normal subscriber.
|
|
if sub.queue == nil {
|
|
if !spoke {
|
|
srv.updateRouteSubscriptionMap(acc, sub, -1)
|
|
if srv.gateway.enabled {
|
|
srv.gatewayUpdateSubInterest(acc.Name, sub, -1)
|
|
}
|
|
}
|
|
acc.updateLeafNodes(sub, -1)
|
|
} else {
|
|
// We handle queue subscribers special in case we
|
|
// have a bunch we can just send one update to the
|
|
// connected routes.
|
|
num := int32(1)
|
|
if kind == LEAF {
|
|
num = sub.qw
|
|
}
|
|
key := keyFromSub(sub)
|
|
if esub, ok := qsubs[key]; ok {
|
|
esub.n += num
|
|
} else {
|
|
qsubs[key] = &qsub{sub, num}
|
|
}
|
|
}
|
|
}
|
|
// Process any qsubs here.
|
|
for _, esub := range qsubs {
|
|
if !spoke {
|
|
srv.updateRouteSubscriptionMap(acc, esub.sub, -(esub.n))
|
|
if srv.gateway.enabled {
|
|
srv.gatewayUpdateSubInterest(acc.Name, esub.sub, -(esub.n))
|
|
}
|
|
}
|
|
acc.updateLeafNodes(esub.sub, -(esub.n))
|
|
}
|
|
}
|
|
|
|
if !close {
|
|
// Register back to global account, mimicking the state after client initialization.
|
|
c.registerWithAccount(srv.globalAccount())
|
|
}
|
|
}
|
|
|
|
// Depending on the kind of connections, this may attempt to recreate a connection.
|
|
// The actual reconnect attempt will be started in a go routine.
|
|
func (c *client) reconnect() {
|
|
var (
|
|
retryImplicit bool
|
|
gwName string
|
|
gwIsOutbound bool
|
|
gwCfg *gatewayCfg
|
|
leafCfg *leafNodeCfg
|
|
)
|
|
|
|
c.mu.Lock()
|
|
// Decrease the ref count and perform the reconnect only if == 0.
|
|
c.rref--
|
|
if c.flags.isSet(noReconnect) || c.rref > 0 {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
if c.route != nil {
|
|
// A route is marked as solicited if it was given an URL to connect to,
|
|
// which would be the case even with implicit (due to gossip), so mark this
|
|
// as a retry for a route that is solicited and not explicit.
|
|
retryImplicit = c.route.retry || (c.route.didSolicit && c.route.routeType == Implicit)
|
|
}
|
|
kind := c.kind
|
|
switch kind {
|
|
case GATEWAY:
|
|
gwName = c.gw.name
|
|
gwIsOutbound = c.gw.outbound
|
|
gwCfg = c.gw.cfg
|
|
case LEAF:
|
|
if c.isSolicitedLeafNode() {
|
|
leafCfg = c.leaf.remote
|
|
}
|
|
}
|
|
srv := c.srv
|
|
c.mu.Unlock()
|
|
|
|
// Check for a solicited route. If it was, start up a reconnect unless
|
|
// we are already connected to the other end.
|
|
if didSolicit := c.isSolicitedRoute(); didSolicit || retryImplicit {
|
|
srv.mu.Lock()
|
|
defer srv.mu.Unlock()
|
|
|
|
// Capture these under lock
|
|
c.mu.Lock()
|
|
rid := c.route.remoteID
|
|
rtype := c.route.routeType
|
|
rurl := c.route.url
|
|
accName := string(c.route.accName)
|
|
checkRID := accName == _EMPTY_ && srv.getOpts().Cluster.PoolSize < 1 && rid != _EMPTY_
|
|
c.mu.Unlock()
|
|
|
|
// It is possible that the server is being shutdown.
|
|
// If so, don't try to reconnect
|
|
if !srv.isRunning() {
|
|
return
|
|
}
|
|
|
|
if checkRID && srv.routes[rid] != nil {
|
|
// This is the case of "no pool". Make sure that the registered one
|
|
// is upgraded to solicited if the connection trying to reconnect
|
|
// was a solicited one.
|
|
if didSolicit {
|
|
if remote := srv.routes[rid][0]; remote != nil {
|
|
upgradeRouteToSolicited(remote, rurl, rtype)
|
|
}
|
|
}
|
|
srv.Debugf("Not attempting reconnect for solicited route, already connected to %q", rid)
|
|
return
|
|
} else if rid == srv.info.ID {
|
|
srv.Debugf("Detected route to self, ignoring %q", rurl.Redacted())
|
|
return
|
|
} else if rtype != Implicit || retryImplicit {
|
|
srv.Debugf("Attempting reconnect for solicited route %q", rurl.Redacted())
|
|
// Keep track of this go-routine so we can wait for it on
|
|
// server shutdown.
|
|
srv.startGoRoutine(func() { srv.reConnectToRoute(rurl, rtype, accName) })
|
|
}
|
|
} else if srv != nil && kind == GATEWAY && gwIsOutbound {
|
|
if gwCfg != nil {
|
|
srv.Debugf("Attempting reconnect for gateway %q", gwName)
|
|
// Run this as a go routine since we may be called within
|
|
// the solicitGateway itself if there was an error during
|
|
// the creation of the gateway connection.
|
|
srv.startGoRoutine(func() { srv.reconnectGateway(gwCfg) })
|
|
} else {
|
|
srv.Debugf("Gateway %q not in configuration, not attempting reconnect", gwName)
|
|
}
|
|
} else if leafCfg != nil {
|
|
// This is a solicited leaf node. Start up a reconnect.
|
|
srv.startGoRoutine(func() { srv.reConnectToRemoteLeafNode(leafCfg) })
|
|
}
|
|
}
|
|
|
|
// Set the noReconnect flag. This is used before a call to closeConnection()
|
|
// to prevent the connection to reconnect (routes, gateways).
|
|
func (c *client) setNoReconnect() {
|
|
c.mu.Lock()
|
|
c.flags.set(noReconnect)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Returns the client's RTT value with the protection of the client's lock.
|
|
func (c *client) getRTTValue() time.Duration {
|
|
c.mu.Lock()
|
|
rtt := c.rtt
|
|
c.mu.Unlock()
|
|
return rtt
|
|
}
|
|
|
|
// This function is used by ROUTER and GATEWAY connections to
|
|
// look for a subject on a given account (since these type of
|
|
// connections are not bound to a specific account).
|
|
// If the c.pa.subject is found in the cache, the cached result
|
|
// is returned, otherwse, we match the account's sublist and update
|
|
// the cache. The cache is pruned if reaching a certain size.
|
|
func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) {
|
|
var (
|
|
acc *Account
|
|
pac *perAccountCache
|
|
r *SublistResult
|
|
ok bool
|
|
)
|
|
// Check our cache.
|
|
if pac, ok = c.in.pacache[string(c.pa.pacache)]; ok {
|
|
// Check the genid to see if it's still valid.
|
|
// Since v2.10.0, the config reload of accounts has been fixed
|
|
// and an account's sublist pointer should not change, so no need to
|
|
// lock to access it.
|
|
sl := pac.acc.sl
|
|
|
|
if genid := atomic.LoadUint64(&sl.genid); genid != pac.genid {
|
|
ok = false
|
|
clear(c.in.pacache)
|
|
} else {
|
|
acc = pac.acc
|
|
r = pac.results
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
if c.kind == ROUTER && len(c.route.accName) > 0 {
|
|
if acc = c.acc; acc == nil {
|
|
return nil, nil
|
|
}
|
|
} else {
|
|
// Match correct account and sublist.
|
|
if acc, _ = c.srv.LookupAccount(bytesToString(c.pa.account)); acc == nil {
|
|
return nil, nil
|
|
}
|
|
}
|
|
sl := acc.sl
|
|
|
|
// Match against the account sublist.
|
|
r = sl.MatchBytes(c.pa.subject)
|
|
|
|
// Check if we need to prune. This should give us a perAccountCache struct
|
|
// to reuse instead of having to allocate a new one.
|
|
// Previously we would have removed multiple entries but now we will only
|
|
// prune the minimum number required to maintain the cache size, so that
|
|
// we reduce the amount of GC pressure and maintain cache stability as best
|
|
// as possible.
|
|
if len(c.in.pacache) >= maxPerAccountCacheSize {
|
|
for cacheKey, p := range c.in.pacache {
|
|
delete(c.in.pacache, cacheKey)
|
|
pac = p
|
|
if len(c.in.pacache) < maxPerAccountCacheSize {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we can reuse the pac from earlier (i.e. we loaded one but it was an
|
|
// old generation or we pruned the cache) then do so.
|
|
if pac == nil {
|
|
pac = &perAccountCache{}
|
|
}
|
|
pac.acc = acc
|
|
pac.results = r
|
|
pac.genid = atomic.LoadUint64(&sl.genid)
|
|
|
|
// Store in our cache,make sure to do so after we prune.
|
|
c.in.pacache[string(c.pa.pacache)] = pac
|
|
}
|
|
return acc, r
|
|
}
|
|
|
|
// Account will return the associated account for this client.
|
|
func (c *client) Account() *Account {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
acc := c.acc
|
|
c.mu.Unlock()
|
|
return acc
|
|
}
|
|
|
|
// pruneClosedSubFromPerAccountCache remove entries that contain subscriptions
|
|
// that have been closed.
|
|
func (c *client) pruneClosedSubFromPerAccountCache() {
|
|
for cacheKey, pac := range c.in.pacache {
|
|
for _, sub := range pac.results.psubs {
|
|
if sub.isClosed() {
|
|
goto REMOVE
|
|
}
|
|
}
|
|
for _, qsub := range pac.results.qsubs {
|
|
for _, sub := range qsub {
|
|
if sub.isClosed() {
|
|
goto REMOVE
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
REMOVE:
|
|
delete(c.in.pacache, cacheKey)
|
|
}
|
|
}
|
|
|
|
// Returns our service account for this request.
|
|
func (ci *ClientInfo) serviceAccount() string {
|
|
if ci == nil {
|
|
return _EMPTY_
|
|
}
|
|
if ci.Service != _EMPTY_ {
|
|
return ci.Service
|
|
}
|
|
return ci.Account
|
|
}
|
|
|
|
// Add in our server and cluster information to this client info.
|
|
func (c *client) addServerAndClusterInfo(ci *ClientInfo) {
|
|
if ci == nil {
|
|
return
|
|
}
|
|
// Server
|
|
if c.kind != LEAF {
|
|
ci.Server = c.srv.Name()
|
|
} else if c.kind == LEAF {
|
|
ci.Server = c.leaf.remoteServer
|
|
}
|
|
// Cluster
|
|
ci.Cluster = c.srv.cachedClusterName()
|
|
// If we have gateways fill in cluster alternates.
|
|
// These will be in RTT asc order.
|
|
if c.srv.gateway.enabled {
|
|
var gws []*client
|
|
c.srv.getOutboundGatewayConnections(&gws)
|
|
for _, c := range gws {
|
|
c.mu.Lock()
|
|
cn := c.gw.name
|
|
c.mu.Unlock()
|
|
ci.Alternates = append(ci.Alternates, cn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Grabs the information for this client.
|
|
func (c *client) getClientInfo(detailed bool) *ClientInfo {
|
|
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
|
|
return nil
|
|
}
|
|
|
|
// Result
|
|
var ci ClientInfo
|
|
|
|
if detailed {
|
|
c.addServerAndClusterInfo(&ci)
|
|
}
|
|
|
|
c.mu.Lock()
|
|
// RTT and Account are always added.
|
|
ci.Account = accForClient(c)
|
|
ci.RTT = c.rtt
|
|
// Detailed signals additional opt in.
|
|
if detailed {
|
|
ci.Start = &c.start
|
|
ci.Host = c.host
|
|
ci.ID = c.cid
|
|
ci.Name = c.opts.Name
|
|
ci.User = c.getRawAuthUser()
|
|
ci.Lang = c.opts.Lang
|
|
ci.Version = c.opts.Version
|
|
ci.Jwt = c.opts.JWT
|
|
ci.IssuerKey = issuerForClient(c)
|
|
ci.NameTag = c.nameTag
|
|
ci.Tags = c.tags
|
|
ci.Kind = c.kindString()
|
|
ci.ClientType = c.clientTypeString()
|
|
}
|
|
c.mu.Unlock()
|
|
return &ci
|
|
}
|
|
|
|
func (c *client) doTLSServerHandshake(typ string, tlsConfig *tls.Config, timeout float64, pCerts PinnedCertSet) error {
|
|
_, err := c.doTLSHandshake(typ, false, nil, tlsConfig, _EMPTY_, timeout, pCerts)
|
|
return err
|
|
}
|
|
|
|
func (c *client) doTLSClientHandshake(typ string, url *url.URL, tlsConfig *tls.Config, tlsName string, timeout float64, pCerts PinnedCertSet) (bool, error) {
|
|
return c.doTLSHandshake(typ, true, url, tlsConfig, tlsName, timeout, pCerts)
|
|
}
|
|
|
|
// Performs either server or client side (if solicit is true) TLS Handshake.
|
|
// On error, the TLS handshake error has been logged and the connection
|
|
// has been closed.
|
|
//
|
|
// Lock is held on entry.
|
|
func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfig *tls.Config, tlsName string, timeout float64, pCerts PinnedCertSet) (bool, error) {
|
|
var host string
|
|
var resetTLSName bool
|
|
var err error
|
|
|
|
// Capture kind for some debug/error statements.
|
|
kind := c.kind
|
|
|
|
// If we solicited, we will act like the client, otherwise the server.
|
|
if solicit {
|
|
c.Debugf("Starting TLS %s client handshake", typ)
|
|
if tlsConfig.ServerName == _EMPTY_ {
|
|
// If the given url is a hostname, use this hostname for the
|
|
// ServerName. If it is an IP, use the cfg's tlsName. If none
|
|
// is available, resort to current IP.
|
|
host = url.Hostname()
|
|
if tlsName != _EMPTY_ && net.ParseIP(host) != nil {
|
|
host = tlsName
|
|
}
|
|
tlsConfig.ServerName = host
|
|
}
|
|
c.nc = tls.Client(c.nc, tlsConfig)
|
|
} else {
|
|
if kind == CLIENT {
|
|
c.Debugf("Starting TLS client connection handshake")
|
|
} else {
|
|
c.Debugf("Starting TLS %s server handshake", typ)
|
|
}
|
|
c.nc = tls.Server(c.nc, tlsConfig)
|
|
}
|
|
|
|
conn := c.nc.(*tls.Conn)
|
|
|
|
// Setup the timeout
|
|
ttl := secondsToDuration(timeout)
|
|
c.tlsTo = time.AfterFunc(ttl, func() { tlsTimeout(c, conn) })
|
|
conn.SetReadDeadline(time.Now().Add(ttl))
|
|
|
|
c.mu.Unlock()
|
|
if err = conn.Handshake(); err != nil {
|
|
if solicit {
|
|
// Based on type of error, possibly clear the saved tlsName
|
|
// See: https://github.com/nats-io/nats-server/issues/1256
|
|
// NOTE: As of Go 1.20, the HostnameError is wrapped so cannot
|
|
// type assert to check directly.
|
|
var hostnameErr x509.HostnameError
|
|
if errors.As(err, &hostnameErr) {
|
|
if host == tlsName {
|
|
resetTLSName = true
|
|
}
|
|
}
|
|
}
|
|
} else if !c.matchesPinnedCert(pCerts) {
|
|
err = ErrCertNotPinned
|
|
}
|
|
|
|
if err != nil {
|
|
var detail string
|
|
var subjs []string
|
|
if ve, ok := err.(*tls.CertificateVerificationError); ok {
|
|
for _, cert := range ve.UnverifiedCertificates {
|
|
fp := sha256.Sum256(cert.Raw)
|
|
fph := hex.EncodeToString(fp[:])
|
|
subjs = append(subjs, fmt.Sprintf("%s SHA-256: %s", cert.Subject.String(), fph))
|
|
}
|
|
}
|
|
if len(subjs) > 0 {
|
|
detail = fmt.Sprintf(" (%s)", strings.Join(subjs, "; "))
|
|
}
|
|
if kind == CLIENT {
|
|
c.Errorf("TLS handshake error: %v%s", err, detail)
|
|
} else {
|
|
c.Errorf("TLS %s handshake error: %v%s", typ, err, detail)
|
|
}
|
|
c.closeConnection(TLSHandshakeError)
|
|
|
|
// Grab the lock before returning since the caller was holding the lock on entry
|
|
c.mu.Lock()
|
|
// Returning any error is fine. Since the connection is closed ErrConnectionClosed
|
|
// is appropriate.
|
|
return resetTLSName, ErrConnectionClosed
|
|
}
|
|
|
|
// Reset the read deadline
|
|
conn.SetReadDeadline(time.Time{})
|
|
|
|
// Re-Grab lock
|
|
c.mu.Lock()
|
|
|
|
// To be consistent with client, set this flag to indicate that handshake is done
|
|
c.flags.set(handshakeComplete)
|
|
|
|
// The connection still may have been closed on success handshake due
|
|
// to a race with tls timeout. If that the case, return error indicating
|
|
// that the connection is closed.
|
|
if c.isClosed() {
|
|
err = ErrConnectionClosed
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
// getRawAuthUserLock returns the raw auth user for the client.
|
|
// Will acquire the client lock.
|
|
func (c *client) getRawAuthUserLock() string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.getRawAuthUser()
|
|
}
|
|
|
|
// getRawAuthUser returns the raw auth user for the client.
|
|
// Lock should be held.
|
|
func (c *client) getRawAuthUser() string {
|
|
switch {
|
|
case c.opts.Nkey != _EMPTY_:
|
|
return c.opts.Nkey
|
|
case c.opts.Username != _EMPTY_:
|
|
return c.opts.Username
|
|
case c.opts.JWT != _EMPTY_:
|
|
return c.pubKey
|
|
case c.opts.Token != _EMPTY_:
|
|
return "[REDACTED]"
|
|
default:
|
|
return _EMPTY_
|
|
}
|
|
}
|
|
|
|
// getAuthUser returns the auth user for the client.
|
|
// Lock should be held.
|
|
func (c *client) getAuthUser() string {
|
|
switch {
|
|
case c.opts.Nkey != _EMPTY_:
|
|
return fmt.Sprintf("Nkey %q", c.opts.Nkey)
|
|
case c.opts.Username != _EMPTY_:
|
|
return fmt.Sprintf("User %q", c.opts.Username)
|
|
case c.opts.JWT != _EMPTY_:
|
|
return fmt.Sprintf("JWT User %q", c.pubKey)
|
|
case c.opts.Token != _EMPTY_:
|
|
return fmt.Sprintf("Token %q", "[REDACTED]")
|
|
default:
|
|
return `User "N/A"`
|
|
}
|
|
}
|
|
|
|
// getAuthUserLabel returns a label for the auth user for the client.
|
|
func (c *client) getAuthUserLabel() string {
|
|
switch {
|
|
case c.opts.Nkey != _EMPTY_:
|
|
return fmt.Sprintf("nkey:%s", c.opts.Nkey)
|
|
case c.opts.Username != _EMPTY_:
|
|
return fmt.Sprintf("user:%s", c.opts.Username)
|
|
case c.opts.JWT != _EMPTY_:
|
|
return fmt.Sprintf("jwt:%s", c.pubKey)
|
|
case c.opts.Token != _EMPTY_:
|
|
return "token"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Given an array of strings, this function converts it to a map as long
|
|
// as all the content (converted to upper-case) matches some constants.
|
|
|
|
// Converts the given array of strings to a map of string.
|
|
// The strings are converted to upper-case and added to the map only
|
|
// if the server recognize them as valid connection types.
|
|
// If there are unknown connection types, the map of valid ones is returned
|
|
// along with an error that contains the name of the unknown.
|
|
func convertAllowedConnectionTypes(cts []string) (map[string]struct{}, error) {
|
|
var unknown []string
|
|
m := make(map[string]struct{}, len(cts))
|
|
for _, i := range cts {
|
|
i = strings.ToUpper(i)
|
|
switch i {
|
|
case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
|
|
jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
|
|
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS,
|
|
jwt.ConnectionTypeInProcess:
|
|
m[i] = struct{}{}
|
|
default:
|
|
unknown = append(unknown, i)
|
|
}
|
|
}
|
|
var err error
|
|
// We will still return the map of valid ones.
|
|
if len(unknown) != 0 {
|
|
err = fmt.Errorf("invalid connection types %q", unknown)
|
|
}
|
|
return m, err
|
|
}
|
|
|
|
// This will return true if the connection is of a type present in the given `acts` map.
|
|
// Note that so far this is used only for CLIENT or LEAF connections.
|
|
// But a CLIENT can be standard or websocket (and other types in the future).
|
|
func (c *client) connectionTypeAllowed(acts map[string]struct{}) bool {
|
|
// Empty means all type of clients are allowed
|
|
if len(acts) == 0 {
|
|
return true
|
|
}
|
|
var want string
|
|
switch c.kind {
|
|
case CLIENT:
|
|
switch c.clientType() {
|
|
case NATS:
|
|
if c.iproc {
|
|
want = jwt.ConnectionTypeInProcess
|
|
} else {
|
|
want = jwt.ConnectionTypeStandard
|
|
}
|
|
case WS:
|
|
want = jwt.ConnectionTypeWebsocket
|
|
case MQTT:
|
|
if c.isWebsocket() {
|
|
want = jwt.ConnectionTypeMqttWS
|
|
} else {
|
|
want = jwt.ConnectionTypeMqtt
|
|
}
|
|
}
|
|
case LEAF:
|
|
if c.isWebsocket() {
|
|
want = jwt.ConnectionTypeLeafnodeWS
|
|
} else {
|
|
want = jwt.ConnectionTypeLeafnode
|
|
}
|
|
}
|
|
_, ok := acts[want]
|
|
return ok
|
|
}
|
|
|
|
// isClosed returns true if either closeConnection or connMarkedClosed
|
|
// flag have been set, or if `nc` is nil, which may happen in tests.
|
|
func (c *client) isClosed() bool {
|
|
return c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed) || c.nc == nil
|
|
}
|
|
|
|
func (c *client) format(format string) string {
|
|
if s := c.String(); s != _EMPTY_ {
|
|
return fmt.Sprintf("%s - %s", s, format)
|
|
} else {
|
|
return format
|
|
}
|
|
}
|
|
|
|
func (c *client) formatNoClientInfo(format string) string {
|
|
acc := c.ncsAcc.Load()
|
|
if acc != nil {
|
|
return fmt.Sprintf("%s - Account:%s", format, acc)
|
|
} else {
|
|
return format
|
|
}
|
|
}
|
|
|
|
func (c *client) formatClientSuffix() string {
|
|
user := c.ncsUser.Load()
|
|
if user == nil || user.(string) == _EMPTY_ {
|
|
return _EMPTY_
|
|
}
|
|
return fmt.Sprintf(" - %s", user)
|
|
}
|
|
|
|
// Logging functionality scoped to a client or route.
|
|
func (c *client) Error(err error) {
|
|
c.srv.Errorf(c.format(err.Error()))
|
|
}
|
|
|
|
func (c *client) Errorf(format string, v ...any) {
|
|
c.srv.Errorf(c.format(format), v...)
|
|
}
|
|
|
|
func (c *client) Debugf(format string, v ...any) {
|
|
c.srv.Debugf(c.format(format), v...)
|
|
}
|
|
|
|
func (c *client) Noticef(format string, v ...any) {
|
|
c.srv.Noticef(c.format(format), v...)
|
|
}
|
|
|
|
func (c *client) Tracef(format string, v ...any) {
|
|
c.srv.Tracef(c.format(format), v...)
|
|
}
|
|
|
|
func (c *client) Warnf(format string, v ...any) {
|
|
c.srv.Warnf(c.format(format), v...)
|
|
}
|
|
|
|
func (c *client) RateLimitErrorf(format string, v ...any) {
|
|
// Do the check before adding the client info to the format...
|
|
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
|
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
|
return
|
|
}
|
|
if s := c.String(); s != _EMPTY_ {
|
|
c.srv.Errorf("%s - %s%s", c, statement, c.formatClientSuffix())
|
|
} else {
|
|
c.srv.Errorf("%s%s", statement, c.formatClientSuffix())
|
|
}
|
|
}
|
|
|
|
func (c *client) rateLimitFormatWarnf(format string, v ...any) {
|
|
// Do the check before adding the client info to the format...
|
|
format = c.formatNoClientInfo(format)
|
|
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(format, time.Now()); loaded {
|
|
return
|
|
}
|
|
statement := fmt.Sprintf(format, v...)
|
|
if s := c.String(); s != _EMPTY_ {
|
|
c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix())
|
|
} else {
|
|
c.srv.Warnf("%s%s", statement, c.formatClientSuffix())
|
|
}
|
|
}
|
|
|
|
func (c *client) RateLimitWarnf(format string, v ...any) {
|
|
// Do the check before adding the client info to the format...
|
|
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
|
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
|
return
|
|
}
|
|
if s := c.String(); s != _EMPTY_ {
|
|
c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix())
|
|
} else {
|
|
c.srv.Warnf("%s%s", statement, c.formatClientSuffix())
|
|
}
|
|
}
|
|
|
|
func (c *client) RateLimitDebugf(format string, v ...any) {
|
|
// Do the check before adding the client info to the format...
|
|
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
|
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
|
return
|
|
}
|
|
if s := c.String(); s != _EMPTY_ {
|
|
c.srv.Debugf("%s - %s%s", c, statement, c.formatClientSuffix())
|
|
} else {
|
|
c.srv.Debugf("%s%s", statement, c.formatClientSuffix())
|
|
}
|
|
}
|
|
|
|
// Set the very first PING to a lower interval to capture the initial RTT.
|
|
// After that the PING interval will be set to the user defined value.
|
|
// Client lock should be held.
|
|
func (c *client) setFirstPingTimer() {
|
|
s := c.srv
|
|
if s == nil {
|
|
return
|
|
}
|
|
opts := s.getOpts()
|
|
d := opts.PingInterval
|
|
|
|
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
|
|
d = opts.Cluster.PingInterval
|
|
}
|
|
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
|
|
d = opts.Websocket.PingInterval
|
|
}
|
|
if !opts.DisableShortFirstPing {
|
|
if c.kind != CLIENT {
|
|
if d > firstPingInterval {
|
|
d = firstPingInterval
|
|
}
|
|
d = adjustPingInterval(c.kind, d)
|
|
} else if d > firstClientPingInterval {
|
|
d = firstClientPingInterval
|
|
}
|
|
}
|
|
// We randomize the first one by an offset up to 20%, e.g. 2m ~= max 24s.
|
|
addDelay := rand.Int63n(int64(d / 5))
|
|
d += time.Duration(addDelay)
|
|
// In the case of ROUTER/LEAF and when compression is configured, it is possible
|
|
// that this timer was already set, but just to detect a stale connection
|
|
// since we have to delay the first PING after compression negotiation
|
|
// occurred.
|
|
if c.ping.tmr != nil {
|
|
c.ping.tmr.Stop()
|
|
}
|
|
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
|
|
}
|
|
|
|
// Sets this error as the authentication error. To be used in authViolation()
|
|
// to report an error different of `ErrAuthentication`.
|
|
func (c *client) setAuthError(err error) {
|
|
c.mu.Lock()
|
|
c.authErr = err
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Returns the authentication error set in the connection, possibly nil.
|
|
func (c *client) getAuthError() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.authErr
|
|
}
|