• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nats-io / nats-server / 24949216239

24 Apr 2026 08:34AM UTC coverage: 80.645% (-2.4%) from 83.05%
24949216239

push

github

web-flow
(2.14) [ADDED] `RemoteLeafOpts.IgnoreDiscoveredServers` option (#8067)

For a given leafnode remote, if this is set to true, this remote will
ignore any server leafnode URLs returned by the hub, allowing the user
to fully manage the servers this remote can connect to.

Resolves #8002

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>

74685 of 92610 relevant lines covered (80.64%)

632737.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.65
/server/util.go
1
// Copyright 2012-2025 The NATS Authors
2
// Licensed under the Apache License, Version 2.0 (the "License");
3
// you may not use this file except in compliance with the License.
4
// You may obtain a copy of the License at
5
//
6
// http://www.apache.org/licenses/LICENSE-2.0
7
//
8
// Unless required by applicable law or agreed to in writing, software
9
// distributed under the License is distributed on an "AS IS" BASIS,
10
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
// See the License for the specific language governing permissions and
12
// limitations under the License.
13

14
package server
15

16
import (
17
        "bytes"
18
        "context"
19
        "encoding/json"
20
        "errors"
21
        "fmt"
22
        "math"
23
        "math/bits"
24
        "net"
25
        "net/url"
26
        "reflect"
27
        "runtime"
28
        "strconv"
29
        "strings"
30
        "time"
31
)
32

33
// This map is used to store URLs string as the key with a reference count as
34
// the value. This is used to handle gossiped URLs such as connect_urls, etc..
35
type refCountedUrlSet map[string]int
36

37
// Ascii numbers 0-9
38
const (
39
        asciiZero = 48
40
        asciiNine = 57
41
)
42

43
func versionComponents(version string) (major, minor, patch int, err error) {
7,864✔
44
        m := semVerRe.FindStringSubmatch(version)
7,864✔
45
        if len(m) == 0 {
7,866✔
46
                return 0, 0, 0, errors.New("invalid semver")
2✔
47
        }
2✔
48
        major, err = strconv.Atoi(m[1])
7,862✔
49
        if err != nil {
7,862✔
50
                return -1, -1, -1, err
×
51
        }
×
52
        minor, err = strconv.Atoi(m[2])
7,862✔
53
        if err != nil {
7,862✔
54
                return -1, -1, -1, err
×
55
        }
×
56
        patch, err = strconv.Atoi(m[3])
7,862✔
57
        if err != nil {
7,862✔
58
                return -1, -1, -1, err
×
59
        }
×
60
        return major, minor, patch, err
7,862✔
61
}
62

63
func versionAtLeastCheckError(version string, emajor, eminor, epatch int) (bool, error) {
7,545✔
64
        major, minor, patch, err := versionComponents(version)
7,545✔
65
        if err != nil {
7,547✔
66
                return false, err
2✔
67
        }
2✔
68
        if major > emajor ||
7,543✔
69
                (major == emajor && minor > eminor) ||
7,543✔
70
                (major == emajor && minor == eminor && patch >= epatch) {
15,083✔
71
                return true, nil
7,540✔
72
        }
7,540✔
73
        return false, err
3✔
74
}
75

76
func versionAtLeast(version string, emajor, eminor, epatch int) bool {
7,536✔
77
        res, _ := versionAtLeastCheckError(version, emajor, eminor, epatch)
7,536✔
78
        return res
7,536✔
79
}
7,536✔
80

81
// parseSize expects decimal positive numbers. We
82
// return -1 to signal error.
83
func parseSize(d []byte) (n int) {
31,196,406✔
84
        const maxParseSizeLen = 9 //999M
31,196,406✔
85

31,196,406✔
86
        l := len(d)
31,196,406✔
87
        if l == 0 || l > maxParseSizeLen {
31,196,407✔
88
                return -1
1✔
89
        }
1✔
90
        var (
31,196,405✔
91
                i   int
31,196,405✔
92
                dec byte
31,196,405✔
93
        )
31,196,405✔
94

31,196,405✔
95
        // Note: Use `goto` here to avoid for loop in order
31,196,405✔
96
        // to have the function be inlined.
31,196,405✔
97
        // See: https://github.com/golang/go/issues/14768
31,196,405✔
98
loop:
31,196,405✔
99
        dec = d[i]
61,871,509✔
100
        if dec < asciiZero || dec > asciiNine {
61,871,509✔
101
                return -1
×
102
        }
×
103
        n = n*10 + (int(dec) - asciiZero)
61,871,509✔
104

61,871,509✔
105
        i++
61,871,509✔
106
        if i < l {
92,546,613✔
107
                goto loop
30,675,104✔
108
        }
109
        return n
31,196,405✔
110
}
111

112
// parseInt64 expects decimal positive numbers. We
113
// return -1 to signal error
114
func parseInt64(d []byte) (n int64) {
4,861✔
115
        if len(d) == 0 {
4,861✔
116
                return -1
×
117
        }
×
118
        for _, dec := range d {
13,900✔
119
                if dec < asciiZero || dec > asciiNine {
9,049✔
120
                        return -1
10✔
121
                }
10✔
122
                n = n*10 + (int64(dec) - asciiZero)
9,029✔
123
        }
124
        return n
4,851✔
125
}
126

127
// Helper to move from float seconds to time.Duration
128
func secondsToDuration(seconds float64) time.Duration {
11,490✔
129
        ttl := seconds * float64(time.Second)
11,490✔
130
        return time.Duration(ttl)
11,490✔
131
}
11,490✔
132

133
// Parse a host/port string with a default port to use
134
// if none (or 0 or -1) is specified in `hostPort` string.
135
func parseHostPort(hostPort string, defaultPort int) (host string, port int, err error) {
47✔
136
        if hostPort != "" {
94✔
137
                host, sPort, err := net.SplitHostPort(hostPort)
47✔
138
                if ae, ok := err.(*net.AddrError); ok && strings.Contains(ae.Err, "missing port") {
79✔
139
                        // try appending the current port
32✔
140
                        host, sPort, err = net.SplitHostPort(fmt.Sprintf("%s:%d", hostPort, defaultPort))
32✔
141
                }
32✔
142
                if err != nil {
47✔
143
                        return "", -1, err
×
144
                }
×
145
                port, err = strconv.Atoi(strings.TrimSpace(sPort))
47✔
146
                if err != nil {
48✔
147
                        return "", -1, err
1✔
148
                }
1✔
149
                if port == 0 || port == -1 {
47✔
150
                        port = defaultPort
1✔
151
                }
1✔
152
                return strings.TrimSpace(host), port, nil
46✔
153
        }
154
        return "", -1, errors.New("no hostport specified")
×
155
}
156

157
// Returns true if URL u1 represents the same URL than u2,
158
// false otherwise.
159
func urlsAreEqual(u1, u2 *url.URL) bool {
269,168✔
160
        return reflect.DeepEqual(u1, u2)
269,168✔
161
}
269,168✔
162

163
// comma produces a string form of the given number in base 10 with
164
// commas after every three orders of magnitude.
165
//
166
// e.g. comma(834142) -> 834,142
167
//
168
// This function was copied from the github.com/dustin/go-humanize
169
// package (MIT License) and is Copyright Dustin Sallings <dustin@spy.net>
170
func comma(v int64) string {
514✔
171
        sign := ""
514✔
172

514✔
173
        // Min int64 can't be negated to a usable value, so it has to be special cased.
514✔
174
        if v == math.MinInt64 {
514✔
175
                return "-9,223,372,036,854,775,808"
×
176
        }
×
177

178
        if v < 0 {
514✔
179
                sign = "-"
×
180
                v = 0 - v
×
181
        }
×
182

183
        parts := []string{"", "", "", "", "", "", ""}
514✔
184
        j := len(parts) - 1
514✔
185

514✔
186
        for v > 999 {
536✔
187
                parts[j] = strconv.FormatInt(v%1000, 10)
22✔
188
                switch len(parts[j]) {
22✔
189
                case 2:
1✔
190
                        parts[j] = "0" + parts[j]
1✔
191
                case 1:
19✔
192
                        parts[j] = "00" + parts[j]
19✔
193
                }
194
                v = v / 1000
22✔
195
                j--
22✔
196
        }
197
        parts[j] = strconv.Itoa(int(v))
514✔
198
        return sign + strings.Join(parts[j:], ",")
514✔
199
}
200

201
// Adds urlStr to the given map. If the string was already present, simply
202
// bumps the reference count.
203
// Returns true only if it was added for the first time.
204
func (m refCountedUrlSet) addUrl(urlStr string) bool {
18,722✔
205
        m[urlStr]++
18,722✔
206
        return m[urlStr] == 1
18,722✔
207
}
18,722✔
208

209
// Removes urlStr from the given map. If the string is not present, nothing
210
// is done and false is returned.
211
// If the string was present, its reference count is decreased. Returns true
212
// if this was the last reference, false otherwise.
213
func (m refCountedUrlSet) removeUrl(urlStr string) bool {
14,675✔
214
        removed := false
14,675✔
215
        if ref, ok := m[urlStr]; ok {
28,217✔
216
                if ref == 1 {
27,078✔
217
                        removed = true
13,536✔
218
                        delete(m, urlStr)
13,536✔
219
                } else {
13,542✔
220
                        m[urlStr]--
6✔
221
                }
6✔
222
        }
223
        return removed
14,675✔
224
}
225

226
// Returns the unique URLs in this map as a slice
227
func (m refCountedUrlSet) getAsStringSlice() []string {
34,221✔
228
        a := make([]string, 0, len(m))
34,221✔
229
        for u := range m {
75,113✔
230
                a = append(a, u)
40,892✔
231
        }
40,892✔
232
        return a
34,221✔
233
}
234

235
// natsListenConfig provides a common configuration to match the one used by
236
// net.Listen() but with our own defaults.
237
// Go 1.13 introduced default-on TCP keepalives with aggressive timings and
238
// there's no sane portable way in Go with stdlib to split the initial timer
239
// from the retry timer.  Linux/BSD defaults are 2hrs/75s and Go sets both
240
// to 15s; the issue re making them indepedently tunable has been open since
241
// 2014 and this code here is being written in 2020.
242
// The NATS protocol has its own L7 PING/PONG keepalive system and the Go
243
// defaults are inappropriate for IoT deployment scenarios.
244
// Replace any NATS-protocol calls to net.Listen(...) with
245
// natsListenConfig.Listen(ctx,...) or use natsListen(); leave calls for HTTP
246
// monitoring, etc, on the default.
247
var natsListenConfig = &net.ListenConfig{
248
        KeepAlive: -1,
249
}
250

251
// natsListen() is the same as net.Listen() except that TCP keepalives are
252
// disabled (to match Go's behavior before Go 1.13).
253
func natsListen(network, address string) (net.Listener, error) {
16,410✔
254
        return natsListenConfig.Listen(context.Background(), network, address)
16,410✔
255
}
16,410✔
256

257
// natsDialTimeout is the same as net.DialTimeout() except the TCP keepalives
258
// are disabled (to match Go's behavior before Go 1.13).
259
func natsDialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
133,831✔
260
        d := net.Dialer{
133,831✔
261
                Timeout:   timeout,
133,831✔
262
                KeepAlive: -1,
133,831✔
263
        }
133,831✔
264
        return d.Dial(network, address)
133,831✔
265
}
133,831✔
266

267
// redactURLList() returns a copy of a list of URL pointers where each item
268
// in the list will either be the same pointer if the URL does not contain a
269
// password, or to a new object if there is a password.
270
// The intended use-case is for logging lists of URLs safely.
271
func redactURLList(unredacted []*url.URL) []*url.URL {
16✔
272
        r := make([]*url.URL, len(unredacted))
16✔
273
        // In the common case of no passwords, if we don't let the new object leave
16✔
274
        // this function then GC should be easier.
16✔
275
        needCopy := false
16✔
276
        for i := range unredacted {
42✔
277
                if unredacted[i] == nil {
26✔
278
                        r[i] = nil
×
279
                        continue
×
280
                }
281
                if _, has := unredacted[i].User.Password(); !has {
44✔
282
                        r[i] = unredacted[i]
18✔
283
                        continue
18✔
284
                }
285
                needCopy = true
8✔
286
                ru := *unredacted[i]
8✔
287
                ru.User = url.UserPassword(ru.User.Username(), "xxxxx")
8✔
288
                r[i] = &ru
8✔
289
        }
290
        if needCopy {
24✔
291
                return r
8✔
292
        }
8✔
293
        return unredacted
8✔
294
}
295

296
// redactURLString() attempts to redact a URL string.
297
func redactURLString(raw string) string {
2✔
298
        if !strings.ContainsRune(raw, '@') {
4✔
299
                return raw
2✔
300
        }
2✔
301
        u, err := url.Parse(raw)
×
302
        if err != nil {
×
303
                return raw
×
304
        }
×
305
        return u.Redacted()
×
306
}
307

308
// getURLsAsString returns a slice of u.Host from the given slice of url.URL's
309
func getURLsAsString(urls []*url.URL) []string {
458✔
310
        a := make([]string, 0, len(urls))
458✔
311
        for _, u := range urls {
2,000✔
312
                a = append(a, u.Host)
1,542✔
313
        }
1,542✔
314
        return a
458✔
315
}
316

317
// copyBytes make a new slice of the same size as `src` and copy its content.
318
// If `src` is nil or its length is 0, then this returns `nil`
319
func copyBytes(src []byte) []byte {
10,378,841✔
320
        if len(src) == 0 {
10,666,298✔
321
                return nil
287,457✔
322
        }
287,457✔
323
        dst := make([]byte, len(src))
10,091,384✔
324
        copy(dst, src)
10,091,384✔
325
        return dst
10,091,384✔
326
}
327

328
// copyStrings make a new slice of the same size than `src` and copy its content.
329
// If `src` is nil, then this returns `nil`
330
func copyStrings(src []string) []string {
35,732✔
331
        if src == nil {
63,516✔
332
                return nil
27,784✔
333
        }
27,784✔
334
        dst := make([]string, len(src))
7,948✔
335
        copy(dst, src)
7,948✔
336
        return dst
7,948✔
337
}
338

339
// Returns a byte slice for the INFO protocol.
340
func generateInfoJSON(info *Info) []byte {
85,541✔
341
        b, _ := json.Marshal(info)
85,541✔
342
        pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
85,541✔
343
        return bytes.Join(pcs, []byte(" "))
85,541✔
344
}
85,541✔
345

346
// parallelTaskQueue starts a number of goroutines and returns a channel
347
// which functions can be sent to for queued parallel execution. The
348
// goroutines will stop running when the returned channel is closed and
349
// all queued tasks have completed. The passed in mp limits concurrency,
350
// or a value <= 0 will default to GOMAXPROCS.
351
func parallelTaskQueue(mp int) chan<- func() {
4,289✔
352
        if rmp := runtime.GOMAXPROCS(-1); mp <= 0 {
4,289✔
353
                mp = rmp
×
354
        } else {
4,289✔
355
                mp = max(rmp, mp)
4,289✔
356
        }
4,289✔
357
        tq := make(chan func(), mp)
4,289✔
358
        for range mp {
21,445✔
359
                go func() {
34,312✔
360
                        for fn := range tq {
17,699✔
361
                                fn()
543✔
362
                        }
543✔
363
                }()
364
        }
365
        return tq
4,289✔
366
}
367

368
// addSaturate returns a + b, saturating at math.MaxInt64.
369
// Both a and b must be non-negative.
370
func addSaturate(a, b int64) int64 {
12,612✔
371
        sum, carry := bits.Add64(uint64(a), uint64(b), 0)
12,612✔
372
        if carry != 0 || sum > uint64(math.MaxInt64) {
12,614✔
373
                return math.MaxInt64
2✔
374
        }
2✔
375
        return int64(sum)
12,610✔
376
}
377

378
// mulSaturate returns a * b, saturating at math.MaxInt64.
379
// Both a and b must be non-negative.
380
func mulSaturate(a, b int64) int64 {
96✔
381
        hi, lo := bits.Mul64(uint64(a), uint64(b))
96✔
382
        if hi != 0 || lo > uint64(math.MaxInt64) {
97✔
383
                return math.MaxInt64
1✔
384
        }
1✔
385
        return int64(lo)
95✔
386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc