Files
nats-server/server/util.go
Ivan Kozlovic 5fc9e0e1cc [FIXED] Gateway URLs gossip and /varz report issues
- When detecting duplicate route, it was possible that a server
would lose track of the peer's gateway URL, which would prevent
it from gossiping that URL to inbound gateway connections
- When a server has gateways enabled and has as a remote its
own gateway, the monitoring endpoint `/varz` would include it
but without the "urls" array.

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>
2021-10-28 12:05:30 -06:00

274 lines
7.3 KiB
Go

// Copyright 2012-2019 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 (
"context"
"errors"
"fmt"
"math"
"net"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
// This map is used to store URLs string as the key with a reference count as
// the value. This is used to handle gossiped URLs such as connect_urls, etc..
type refCountedUrlSet map[string]int
// Ascii numbers 0-9
const (
asciiZero = 48
asciiNine = 57
)
// parseSize expects decimal positive numbers. We
// return -1 to signal error.
func parseSize(d []byte) (n int) {
const maxParseSizeLen = 9 //999M
l := len(d)
if l == 0 || l > maxParseSizeLen {
return -1
}
var (
i int
dec byte
)
// Note: Use `goto` here to avoid for loop in order
// to have the function be inlined.
// See: https://github.com/golang/go/issues/14768
loop:
dec = d[i]
if dec < asciiZero || dec > asciiNine {
return -1
}
n = n*10 + (int(dec) - asciiZero)
i++
if i < l {
goto loop
}
return n
}
// parseInt64 expects decimal positive numbers. We
// return -1 to signal error
func parseInt64(d []byte) (n int64) {
if len(d) == 0 {
return -1
}
for _, dec := range d {
if dec < asciiZero || dec > asciiNine {
return -1
}
n = n*10 + (int64(dec) - asciiZero)
}
return n
}
// Helper to move from float seconds to time.Duration
func secondsToDuration(seconds float64) time.Duration {
ttl := seconds * float64(time.Second)
return time.Duration(ttl)
}
// Parse a host/port string with a default port to use
// if none (or 0 or -1) is specified in `hostPort` string.
func parseHostPort(hostPort string, defaultPort int) (host string, port int, err error) {
if hostPort != "" {
host, sPort, err := net.SplitHostPort(hostPort)
if ae, ok := err.(*net.AddrError); ok && strings.Contains(ae.Err, "missing port") {
// try appending the current port
host, sPort, err = net.SplitHostPort(fmt.Sprintf("%s:%d", hostPort, defaultPort))
}
if err != nil {
return "", -1, err
}
port, err = strconv.Atoi(strings.TrimSpace(sPort))
if err != nil {
return "", -1, err
}
if port == 0 || port == -1 {
port = defaultPort
}
return strings.TrimSpace(host), port, nil
}
return "", -1, errors.New("no hostport specified")
}
// Returns true if URL u1 represents the same URL than u2,
// false otherwise.
func urlsAreEqual(u1, u2 *url.URL) bool {
return reflect.DeepEqual(u1, u2)
}
// comma produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. comma(834142) -> 834,142
//
// This function was copied from the github.com/dustin/go-humanize
// package and is Copyright Dustin Sallings <dustin@spy.net>
func comma(v int64) string {
sign := ""
// Min int64 can't be negated to a usable value, so it has to be special cased.
if v == math.MinInt64 {
return "-9,223,372,036,854,775,808"
}
if v < 0 {
sign = "-"
v = 0 - v
}
parts := []string{"", "", "", "", "", "", ""}
j := len(parts) - 1
for v > 999 {
parts[j] = strconv.FormatInt(v%1000, 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
v = v / 1000
j--
}
parts[j] = strconv.Itoa(int(v))
return sign + strings.Join(parts[j:], ",")
}
// Adds urlStr to the given map. If the string was already present, simply
// bumps the reference count.
// Returns true only if it was added for the first time.
func (m refCountedUrlSet) addUrl(urlStr string) bool {
m[urlStr]++
return m[urlStr] == 1
}
// Removes urlStr from the given map. If the string is not present, nothing
// is done and false is returned.
// If the string was present, its reference count is decreased. Returns true
// if this was the last reference, false otherwise.
func (m refCountedUrlSet) removeUrl(urlStr string) bool {
removed := false
if ref, ok := m[urlStr]; ok {
if ref == 1 {
removed = true
delete(m, urlStr)
} else {
m[urlStr]--
}
}
return removed
}
// Returns the unique URLs in this map as a slice
func (m refCountedUrlSet) getAsStringSlice() []string {
a := make([]string, 0, len(m))
for u := range m {
a = append(a, u)
}
return a
}
// natsListenConfig provides a common configuration to match the one used by
// net.Listen() but with our own defaults.
// Go 1.13 introduced default-on TCP keepalives with aggressive timings and
// there's no sane portable way in Go with stdlib to split the initial timer
// from the retry timer. Linux/BSD defaults are 2hrs/75s and Go sets both
// to 15s; the issue re making them indepedently tunable has been open since
// 2014 and this code here is being written in 2020.
// The NATS protocol has its own L7 PING/PONG keepalive system and the Go
// defaults are inappropriate for IoT deployment scenarios.
// Replace any NATS-protocol calls to net.Listen(...) with
// natsListenConfig.Listen(ctx,...) or use natsListen(); leave calls for HTTP
// monitoring, etc, on the default.
var natsListenConfig = &net.ListenConfig{
KeepAlive: -1,
}
// natsListen() is the same as net.Listen() except that TCP keepalives are
// disabled (to match Go's behavior before Go 1.13).
func natsListen(network, address string) (net.Listener, error) {
return natsListenConfig.Listen(context.Background(), network, address)
}
// natsDialTimeout is the same as net.DialTimeout() except the TCP keepalives
// are disabled (to match Go's behavior before Go 1.13).
func natsDialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
d := net.Dialer{
Timeout: timeout,
KeepAlive: -1,
}
return d.Dial(network, address)
}
// redactURLList() returns a copy of a list of URL pointers where each item
// in the list will either be the same pointer if the URL does not contain a
// password, or to a new object if there is a password.
// The intended use-case is for logging lists of URLs safely.
func redactURLList(unredacted []*url.URL) []*url.URL {
r := make([]*url.URL, len(unredacted))
// In the common case of no passwords, if we don't let the new object leave
// this function then GC should be easier.
needCopy := false
for i := range unredacted {
if unredacted[i] == nil {
r[i] = nil
continue
}
if _, has := unredacted[i].User.Password(); !has {
r[i] = unredacted[i]
continue
}
needCopy = true
ru := *unredacted[i]
ru.User = url.UserPassword(ru.User.Username(), "xxxxx")
r[i] = &ru
}
if needCopy {
return r
}
return unredacted
}
// redactURLString() attempts to redact a URL string.
func redactURLString(raw string) string {
if !strings.ContainsRune(raw, '@') {
return raw
}
u, err := url.Parse(raw)
if err != nil {
return raw
}
return u.Redacted()
}
// getURLsAsString returns a slice of u.Host from the given slice of url.URL's
func getURLsAsString(urls []*url.URL) []string {
a := make([]string, 0, len(urls))
for _, u := range urls {
a = append(a, u.Host)
}
return a
}