Repository: nats-io/nats-server
Pull request #2707 (2021-11-22 18:09:56)

[FIXED/CHANGED] Add leafnode websocket connection type
This was missing since WEBSOCKET allowed connection type is really
used for client connections.
If one wants to limit a configured user to leafnode connections,
including if the connection is over websocket, but does not
want an application to connect over websocket using this user,
this would have been impossible to configure.

The JWT library has been updated to add LEAFNODE_WS and MQTT_WS for
future work.

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>



Patch:
diff --git a/go.mod b/go.mod
index be6b70e9..7e921fa2 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/golang/protobuf v1.4.2 // indirect
 	github.com/klauspost/compress v1.13.4
 	github.com/minio/highwayhash v1.0.1
-	github.com/nats-io/jwt/v2 v2.1.0
+	github.com/nats-io/jwt/v2 v2.2.0
 	github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483
 	github.com/nats-io/nkeys v0.3.0
 	github.com/nats-io/nuid v1.0.1
diff --git a/go.sum b/go.sum
index b7b2c150..b5c80979 100644
--- a/go.sum
+++ b/go.sum
@@ -14,8 +14,8 @@ github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEE
 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0=
 github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
-github.com/nats-io/jwt/v2 v2.1.0 h1:1UbfD5g1xTdWmSeRV8bh/7u+utTiBsRtWhLl1PixZp4=
-github.com/nats-io/jwt/v2 v2.1.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
+github.com/nats-io/jwt/v2 v2.2.0 h1:Yg/4WFK6vsqMudRg91eBb7Dh6XeVcDMPHycDE8CfltE=
+github.com/nats-io/jwt/v2 v2.2.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
 github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483 h1:GMx3ZOcMEVM5qnUItQ4eJyQ6ycwmIEB/VC/UxvdevE0=
 github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
 github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
diff --git a/server/auth.go b/server/auth.go
index 5f5c9298..2e5031d2 100644
--- a/server/auth.go
+++ b/server/auth.go
@@ -1104,7 +1104,9 @@ func validateAllowedConnectionTypes(m map[string]struct{}) error {
 	for ct := range m {
 		ctuc := strings.ToUpper(ct)
 		switch ctuc {
-		case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt:
+		case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
+			jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
+			jwt.ConnectionTypeMqtt:
 		default:
 			return fmt.Errorf("unknown connection type %q", ct)
 		}
diff --git a/server/client.go b/server/client.go
index 11fe8759..aeb0d6e2 100644
--- a/server/client.go
+++ b/server/client.go
@@ -5172,7 +5172,9 @@ func convertAllowedConnectionTypes(cts []string) (map[string]struct{}, error) {
 	for _, i := range cts {
 		i = strings.ToUpper(i)
 		switch i {
-		case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt:
+		case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
+			jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
+			jwt.ConnectionTypeMqtt:
 			m[i] = struct{}{}
 		default:
 			unknown = append(unknown, i)
@@ -5206,7 +5208,11 @@ func (c *client) connectionTypeAllowed(acts map[string]struct{}) bool {
 			want = jwt.ConnectionTypeMqtt
 		}
 	case LEAF:
-		want = jwt.ConnectionTypeLeafnode
+		if c.isWebsocket() {
+			want = jwt.ConnectionTypeLeafnodeWS
+		} else {
+			want = jwt.ConnectionTypeLeafnode
+		}
 	}
 	_, ok := acts[want]
 	return ok
diff --git a/vendor/github.com/nats-io/jwt/v2/header.go b/vendor/github.com/nats-io/jwt/v2/header.go
index 3dece6a1..198bf306 100644
--- a/vendor/github.com/nats-io/jwt/v2/header.go
+++ b/vendor/github.com/nats-io/jwt/v2/header.go
@@ -23,7 +23,7 @@ import (
 
 const (
 	// Version is semantic version.
-	Version = "2.1.0"
+	Version = "2.2.0"
 
 	// TokenTypeJwt is the JWT token type supported JWT tokens
 	// encoded and decoded by this library
diff --git a/vendor/github.com/nats-io/jwt/v2/signingkeys.go b/vendor/github.com/nats-io/jwt/v2/signingkeys.go
index 87fc951a..0d8a5b93 100644
--- a/vendor/github.com/nats-io/jwt/v2/signingkeys.go
+++ b/vendor/github.com/nats-io/jwt/v2/signingkeys.go
@@ -120,9 +120,12 @@ func (sk SigningKeys) Validate(vr *ValidationResults) {
 }
 
 // MarshalJSON serializes the scoped signing keys as an array
-func (sk SigningKeys) MarshalJSON() ([]byte, error) {
+func (sk *SigningKeys) MarshalJSON() ([]byte, error) {
+	if sk == nil {
+		return nil, nil
+	}
 	var a []interface{}
-	for k, v := range sk {
+	for k, v := range *sk {
 		if v != nil {
 			a = append(a, v)
 		} else {
@@ -132,7 +135,10 @@ func (sk SigningKeys) MarshalJSON() ([]byte, error) {
 	return json.Marshal(a)
 }
 
-func (sk SigningKeys) UnmarshalJSON(data []byte) error {
+func (sk *SigningKeys) UnmarshalJSON(data []byte) error {
+	if *sk == nil {
+		*sk = make(SigningKeys)
+	}
 	// read an array - we can have a string or an map
 	var a []interface{}
 	if err := json.Unmarshal(data, &a); err != nil {
@@ -141,7 +147,7 @@ func (sk SigningKeys) UnmarshalJSON(data []byte) error {
 	for _, i := range a {
 		switch v := i.(type) {
 		case string:
-			sk[v] = nil
+			(*sk)[v] = nil
 		case map[string]interface{}:
 			d, err := json.Marshal(v)
 			if err != nil {
@@ -153,7 +159,7 @@ func (sk SigningKeys) UnmarshalJSON(data []byte) error {
 				if err := json.Unmarshal(d, &us); err != nil {
 					return err
 				}
-				sk[us.Key] = us
+				(*sk)[us.Key] = us
 			default:
 				return fmt.Errorf("unknown signing key scope %q", v["type"])
 			}
diff --git a/vendor/github.com/nats-io/jwt/v2/user_claims.go b/vendor/github.com/nats-io/jwt/v2/user_claims.go
index 0c8ccbeb..d8a3a6c9 100644
--- a/vendor/github.com/nats-io/jwt/v2/user_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/user_claims.go
@@ -23,10 +23,12 @@ import (
 )
 
 const (
-	ConnectionTypeStandard  = "STANDARD"
-	ConnectionTypeWebsocket = "WEBSOCKET"
-	ConnectionTypeLeafnode  = "LEAFNODE"
-	ConnectionTypeMqtt      = "MQTT"
+	ConnectionTypeStandard   = "STANDARD"
+	ConnectionTypeWebsocket  = "WEBSOCKET"
+	ConnectionTypeLeafnode   = "LEAFNODE"
+	ConnectionTypeLeafnodeWS = "LEAFNODE_WS"
+	ConnectionTypeMqtt       = "MQTT"
+	ConnectionTypeMqttWS     = "MQTT_WS"
 )
 
 type UserPermissionLimits struct {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a71b7fe2..8a790725 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -6,7 +6,7 @@ github.com/klauspost/compress/s2
 # github.com/minio/highwayhash v1.0.1
 ## explicit
 github.com/minio/highwayhash
-# github.com/nats-io/jwt/v2 v2.1.0
+# github.com/nats-io/jwt/v2 v2.2.0
 ## explicit
 github.com/nats-io/jwt/v2
 # github.com/nats-io/nats.go v1.13.1-0.20211018182449-f2416a8b1483


Test patch:
diff --git a/server/leafnode_test.go b/server/leafnode_test.go
index 5d1e19b5..feb6dc5d 100644
--- a/server/leafnode_test.go
+++ b/server/leafnode_test.go
@@ -22,6 +22,7 @@ import (
 	"math/rand"
 	"net"
 	"net/url"
+	"os"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -3032,12 +3033,12 @@ func TestLeafNodeWSFailedConnection(t *testing.T) {
 }
 
 func TestLeafNodeWSAuth(t *testing.T) {
-	conf := createConfFile(t, []byte(fmt.Sprintf(`
+	template := `
 		port: -1
 		authorization {
 			users [
 				{user: "user", pass: "puser", connection_types: ["%s"]}
-				{user: "leaf", pass: "pleaf", connection_types: ["%s"]}
+				{user: "leaf", pass: "pleaf", connection_types: ["%s"%s]}
 			]
 		}
 		websocket {
@@ -3047,20 +3048,40 @@ func TestLeafNodeWSAuth(t *testing.T) {
 		leafnodes {
 			port: -1
 		}
-	`, jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode)))
-	defer removeFile(t, conf)
-	o, err := ProcessConfigFile(conf)
-	if err != nil {
-		t.Fatalf("Error processing config file: %v", err)
-	}
-	o.NoLog, o.NoSigs = true, true
-	s := RunServer(o)
+	`
+	s, o, conf := runReloadServerWithContent(t,
+		[]byte(fmt.Sprintf(template, jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, "")))
+	defer os.Remove(conf)
 	defer s.Shutdown()
 
+	l := &captureErrorLogger{errCh: make(chan string, 10)}
+	s.SetLogger(l, false, false)
+
 	lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
+	u, _ := url.Parse(fmt.Sprintf("ws://leaf:pleaf@127.0.0.1:%d", o.Websocket.Port))
+	remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
+	lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
+	lo.LeafNode.ReconnectInterval = 50 * time.Millisecond
 	ln := RunServer(lo)
 	defer ln.Shutdown()
 
+	var lasterr string
+	tm := time.NewTimer(2 * time.Second)
+	for done := false; !done; {
+		select {
+		case lasterr = <-l.errCh:
+			if strings.Contains(lasterr, "authentication") {
+				done = true
+			}
+		case <-tm.C:
+			t.Fatalf("Expected auth error, got %v", lasterr)
+		}
+	}
+
+	ws := fmt.Sprintf(`, "%s"`, jwt.ConnectionTypeLeafnodeWS)
+	reloadUpdateConfig(t, s, conf, fmt.Sprintf(template,
+		jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, ws))
+
 	checkLeafNodeConnected(t, s)
 	checkLeafNodeConnected(t, ln)