Repository: nats-io/nats-server
Pull request #1309 (2020-03-09 23:55:20)

[ADDED] Websocket support
Websocket support can be enabled with a new websocket
configuration block:

```
websocket {
    # Specify a host and port to listen for websocket connections
    # listen: "host:port"

    # It can also be configured with individual parameters,
    # namely host and port.
    # host: "hostname"
    # port: 4443

    # This will optionally specify what host:port for websocket
    # connections to be advertised in the cluster
    # advertise: "host:port"

    # TLS configuration is required
    tls {
      cert_file: "/path/to/cert.pem"
      key_file: "/path/to/key.pem"
    }

    # If same_origin is true, then the Origin header of the
    # client request must match the request's Host.
    # same_origin: true

    # This list specifies the only accepted values for
    # the client's request Origin header. The scheme,
    # host and port must match. By convention, the
    # absence of port for an http:// scheme will be 80,
    # and for https:// will be 443.
    # allowed_origins [
    #    "http://www.example.com"
    #    "https://www.other-example.com"
    # ]

    # This enables support for compressed websocket frames
    # in the server. For compression to be used, both server
    # and client have to support it.
    # compression: true

    # This is the total time allowed for the server to
    # read the client request and write the response back
    # to the client. This include the time needed for the
    # TLS handshake.
    # handshake_timeout: "2s"
}
```

Signed-off-by: Ivan Kozlovic <ivan@synadia.com>


wallyqs: A couple of items to add:

- Currently when `ws` is enabled and you connect to a NATS cluster, the `connect_urls` will be using the port for the clients (like `"connect_urls":["172.31.10.166:4222","172.31.10.124:4222"]` ).  Disabling advertise would disable this for both the ws and regular connections though.  Thinking that maybe there will be a use case were we want external ip gossip client enabled for both websockets and regular nats clients so we should not advertise the client port to websocket clients.

- Something slightly different with accepting websocket connections and accepting regular NATS connections is that we do not account for websocket connection attempts, so if a client makes many bad requests (e.g. connecting without TLS) there will be no trace in the logs either for example. There is a `ConnState func(net.Conn, ConnState)` hook that we could implement in the `http.Server` that could help for debug/trace when a new websocket connection attempt is done.
kozlovic: > Currently when ws is enabled and you connect to a NATS cluster, the connect_urls will be using the port for the clients (like "connect_urls":["172.31.10.166:4222","172.31.10.124:4222"] ).

Keep in mind that websocket{} can define its own host so possibly not the same that the client. I was thinking that if we want to do advertise for WS clients, it would have to be a new field in the INFO. Unless we force the websocket{} to accept only port and fallback to same host than client, in which case we could have connect_urls actually contain only host and have new fields for client port, ws port, this only when sending to WS clients (otherwise it would break backward compatibility).

> Something slightly different with accepting websocket connections and accepting regular NATS connections is ...

Good to know, will look into that.
wallyqs: Ah ok, then for example right now if you connect to a cluster on the websocket port you would get the `"connect_urls"` entries with the client port (4222), but since we could use a different field for advertising websockets then clients that connect using the ws port would ignore that one and use the other field with the websocket urls.
kozlovic: Yes, to be clear, there is no advertise implemented for Websockets as of now, so the connect_urls is the one that you normally get for (regular) client connections.
derekcollison: Feels like we should no? Its a ws client, so should only have connect_urls that are applicable to its client type. Or empty to start with. Most likely these will be behind a LB so might not be as critical.
kozlovic: > Feels like we should no? Its a ws client, so should only have connect_urls that are applicable to its client type. Or empty to start with. Most likely these will be behind a LB so might not be as critical.

Yes, but the point is that nodes in the cluster do exchange their client's listen spec, which means that this need to be implemented. As I said, if we allow user to specify a different "host" for the listen, then we have to re-implement the current client host:port gossip between servers, not only from server to WS clients.
derekcollison: Understand regarding host. 
derekcollison: Thinking with MQTT we may need to support it..

Patch:
diff --git a/server/client.go b/server/client.go
index d2e9af97..b90c6eff 100644
--- a/server/client.go
+++ b/server/client.go
@@ -232,6 +232,7 @@ type client struct {
 	route *route
 	gw    *gateway
 	leaf  *leaf
+	ws    *websocket
 
 	// To keep track of gateway replies mapping
 	gwrm map[string]*gwReplyMap
@@ -270,7 +271,6 @@ type outbound struct {
 	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.
-	lwb int32         // Last byte size of Write.
 }
 
 type perm struct {
@@ -484,16 +484,23 @@ func (c *client) initClient() {
 
 	// snapshot the string version of the connection
 	var conn string
-	if ip, ok := c.nc.(*net.TCPConn); ok {
-		conn = ip.RemoteAddr().String()
-		host, port, _ := net.SplitHostPort(conn)
-		iPort, _ := strconv.Atoi(port)
-		c.host, c.port = host, uint16(iPort)
+	if c.nc != nil {
+		if addr := c.nc.RemoteAddr(); addr != nil {
+			if conn = addr.String(); conn != _EMPTY_ {
+				host, port, _ := net.SplitHostPort(conn)
+				iPort, _ := strconv.Atoi(port)
+				c.host, c.port = host, uint16(iPort)
+			}
+		}
 	}
 
 	switch c.kind {
 	case CLIENT:
-		c.ncs = fmt.Sprintf("%s - cid:%d", conn, c.cid)
+		name := "cid"
+		if c.ws != nil {
+			name = "wid"
+		}
+		c.ncs = fmt.Sprintf("%s - %s:%d", conn, name, c.cid)
 	case ROUTER:
 		c.ncs = fmt.Sprintf("%s - rid:%d", conn, c.cid)
 	case GATEWAY:
@@ -873,6 +880,7 @@ func (c *client) readLoop() {
 		return
 	}
 	nc := c.nc
+	ws := c.ws != nil
 	c.in.rsz = startBufSize
 	// 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
@@ -898,6 +906,19 @@ func (c *client) readLoop() {
 	// 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{}
+		wsr.init()
+	}
+
 	for {
 		n, err := nc.Read(b)
 		// If we have any data we will try to parse and exit at the end.
@@ -905,6 +926,19 @@ func (c *client) readLoop() {
 			c.closeConnection(closedStateForErr(err))
 			return
 		}
+		if ws {
+			bufs, err = c.wsRead(wsr, nc, b[:n])
+			if bufs == nil && err != nil {
+				if err != io.EOF {
+					c.Errorf("read error: %v", err)
+				}
+				c.closeConnection(closedStateForErr(err))
+			} else if bufs == nil {
+				continue
+			}
+		} else {
+			bufs[0] = b[:n]
+		}
 		start := time.Now()
 
 		// Clear inbound stats cache
@@ -914,20 +948,22 @@ func (c *client) readLoop() {
 
 		// Main call into parser for inbound data. This will generate callouts
 		// to process messages, etc.
-		if err := c.parse(b[:n]); err != nil {
-			if dur := time.Since(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)
+		for i := 0; i < len(bufs); i++ {
+			if err := c.parse(bufs[i]); err != nil {
+				if dur := time.Since(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
 			}
-			return
 		}
 
 		// Updates stats for client and server that were collected
@@ -1011,19 +1047,26 @@ func closedStateForErr(err error) ClosedState {
 
 // collapsePtoNB will place primary onto nb buffer as needed in prep for WriteTo.
 // This will return a copy on purpose.
-func (c *client) collapsePtoNB() net.Buffers {
+func (c *client) collapsePtoNB() (net.Buffers, int64) {
+	if c.ws != nil {
+		return c.wsCollapsePtoNB()
+	}
 	if c.out.p != nil {
 		p := c.out.p
 		c.out.p = nil
-		return append(c.out.nb, p)
+		return append(c.out.nb, p), c.out.pb
 	}
-	return c.out.nb
+	return c.out.nb, c.out.pb
 }
 
 // This will handle the fixup needed on a partial write.
 // Assume pending has been already calculated correctly.
 func (c *client) handlePartialWrite(pnb net.Buffers) {
-	nb := c.collapsePtoNB()
+	if c.ws != nil {
+		c.ws.frames = append(pnb, c.ws.frames...)
+		return
+	}
+	nb, _ := c.collapsePtoNB()
 	// The partial needs to be first, so append nb to pnb
 	c.out.nb = append(pnb, nb...)
 }
@@ -1050,8 +1093,11 @@ func (c *client) flushOutbound() bool {
 	}
 
 	// Place primary on nb, assign primary to secondary, nil out nb and secondary.
-	nb := c.collapsePtoNB()
+	nb, attempted := c.collapsePtoNB()
 	c.out.p, c.out.nb, c.out.s = c.out.s, nil, nil
+	if nb == nil {
+		return true
+	}
 
 	// For selecting primary replacement.
 	cnb := nb
@@ -1062,7 +1108,6 @@ func (c *client) flushOutbound() bool {
 
 	// In case it goes away after releasing the lock.
 	nc := c.nc
-	attempted := c.out.pb
 	apm := c.out.pm
 
 	// Capture this (we change the value in some tests)
@@ -1086,7 +1131,8 @@ func (c *client) flushOutbound() bool {
 	// Re-acquire client lock.
 	c.mu.Lock()
 
-	if err != nil {
+	// Ignore ErrShortWrite errors, they will be handled as partials.
+	if err != nil && err != io.ErrShortWrite {
 		// Handle timeout error (slow consumer) differently
 		if ne, ok := err.(net.Error); ok && ne.Timeout() {
 			if closed := c.handleWriteTimeout(n, attempted, len(cnb)); closed {
@@ -1100,29 +1146,31 @@ func (c *client) flushOutbound() bool {
 				report = c.Errorf
 			}
 			report("Error flushing: %v", err)
-			c.markConnAsClosed(WriteError, true)
+			c.markConnAsClosed(WriteError)
 			return true
 		}
 	}
 
 	// Update flush time statistics.
 	c.out.lft = lft
-	c.out.lwb = int32(n)
 
 	// Subtract from pending bytes and messages.
-	c.out.pb -= int64(c.out.lwb)
+	c.out.pb -= n
+	if c.ws != nil {
+		c.ws.fs -= n
+	}
 	c.out.pm -= apm // FIXME(dlc) - this will not be totally accurate on partials.
 
 	// Check for partial writes
 	// TODO(dlc) - zero write with no error will cause lost message and the writeloop to spin.
-	if int64(c.out.lwb) != attempted && n > 0 {
+	if n != attempted && n > 0 {
 		c.handlePartialWrite(nb)
-	} else if c.out.lwb >= c.out.sz {
+	} else if int32(n) >= c.out.sz {
 		c.out.sws = 0
 	}
 
 	// Adjust based on what we wrote plus any pending.
-	pt := int64(c.out.lwb) + c.out.pb
+	pt := n + c.out.pb
 
 	// Adjust sz as needed downward, keeping power of 2.
 	// We do this at a slower rate.
@@ -1158,7 +1206,7 @@ func (c *client) flushOutbound() bool {
 
 	// 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 && (int64(c.out.lwb) == attempted || c.out.pb < c.out.mp/2) {
+	if c.out.stc != nil && (n == attempted || c.out.pb < c.out.mp/2) {
 		close(c.out.stc)
 		c.out.stc = nil
 	}
@@ -1173,7 +1221,7 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
 	if tlsConn, ok := c.nc.(*tls.Conn); ok {
 		if !tlsConn.ConnectionState().HandshakeComplete {
 			// Likely a TLSTimeout error instead...
-			c.markConnAsClosed(TLSHandshakeError, true)
+			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.
@@ -1184,7 +1232,7 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
 		// 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, true)
+		c.markConnAsClosed(SlowConsumerWriteDeadline)
 		return true
 	}
 
@@ -1195,26 +1243,40 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
 
 	// We always close CLIENT connections, or when nothing was written at all...
 	if c.kind == CLIENT || written == 0 {
-		c.markConnAsClosed(SlowConsumerWriteDeadline, true)
+		c.markConnAsClosed(SlowConsumerWriteDeadline)
 		return true
 	}
 	return false
 }
 
 // Marks this connection has closed with the given reason.
-// Sets the closeConnection flag and skipFlushOnClose flag if asked.
+// Sets the closeConnection 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/close/teardown 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, skipFlush bool) bool {
+func (c *client) markConnAsClosed(reason ClosedState) bool {
+	// 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
+	}
 	if c.flags.isSet(closeConnection) {
 		return false
 	}
 	c.flags.set(closeConnection)
-	if skipFlush {
-		c.flags.set(skipFlushOnClose)
+	// For a websocket client, unless we are told not to flush, enqueue
+	// a websocket CloseMessage based on the reason.
+	if !skipFlush && c.ws != nil && !c.ws.closeSent {
+		c.wsEnqueueCloseMessage(reason)
 	}
 	// Be consistent with the creation: for routes and gateways,
 	// we use Noticef on create, so use that too for delete.
@@ -1610,7 +1672,7 @@ func (c *client) queueOutbound(data []byte) bool {
 		c.out.pb -= int64(len(data))
 		atomic.AddInt64(&c.srv.slowConsumers, 1)
 		c.Noticef("Slow Consumer Detected: MaxPending of %d Exceeded", c.out.mp)
-		c.markConnAsClosed(SlowConsumerPendingBytes, true)
+		c.markConnAsClosed(SlowConsumerPendingBytes)
 		return referenced
 	}
 
@@ -1755,6 +1817,10 @@ func (c *client) generateClientInfoJSON(info Info) []byte {
 	info.CID = c.cid
 	info.ClientIP = c.host
 	info.MaxPayload = c.mpay
+	if c.ws != nil {
+		info.ClientConnectURLs = info.WSConnectURLs
+	}
+	info.WSConnectURLs = nil
 	// Generate the info json
 	b, _ := json.Marshal(info)
 	pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
@@ -3803,7 +3869,7 @@ func (c *client) closeConnection(reason ClosedState) {
 	// This will set the closeConnection flag and save the connection, etc..
 	// Will return true if no writeLoop was started and TCP connection was
 	// closed in place, in which case we need to do the teardown.
-	teardownNow := c.markConnAsClosed(reason, false)
+	teardownNow := c.markConnAsClosed(reason)
 	c.mu.Unlock()
 
 	if teardownNow {
@@ -3841,6 +3907,7 @@ func (c *client) teardownConn() {
 	var (
 		retryImplicit bool
 		connectURLs   []string
+		wsConnectURLs []string
 		gwName        string
 		gwIsOutbound  bool
 		gwCfg         *gatewayCfg
@@ -3870,6 +3937,7 @@ func (c *client) teardownConn() {
 			retryImplicit = c.route.retry
 		}
 		connectURLs = c.route.connectURLs
+		wsConnectURLs = c.route.wsConnURLs
 	}
 	if kind == GATEWAY {
 		gwName = c.gw.name
@@ -3894,11 +3962,11 @@ func (c *client) teardownConn() {
 
 	if srv != nil {
 		// This is a route that disconnected, but we are not in lame duck mode...
-		if len(connectURLs) > 0 && !srv.isLameDuckMode() {
+		if (len(connectURLs) > 0 || len(wsConnectURLs) > 0) && !srv.isLameDuckMode() {
 			// Unless disabled, possibly update the server's INFO protocol
 			// and send to clients that know how to handle async INFOs.
 			if !srv.getOpts().Cluster.NoAdvertise {
-				srv.removeClientConnectURLsAndSendINFOToClients(connectURLs)
+				srv.removeConnectURLsAndSendINFOToClients(connectURLs, wsConnectURLs)
 			}
 		}
 
diff --git a/server/monitor.go b/server/monitor.go
index 46c86a01..116e7f6e 100644
--- a/server/monitor.go
+++ b/server/monitor.go
@@ -969,6 +969,7 @@ type Varz struct {
 	TLSVerify         bool              `json:"tls_verify,omitempty"`
 	IP                string            `json:"ip,omitempty"`
 	ClientConnectURLs []string          `json:"connect_urls,omitempty"`
+	WSConnectURLs     []string          `json:"ws_connect_urls,omitempty"`
 	MaxConn           int               `json:"max_connections"`
 	MaxSubs           int               `json:"max_subscriptions,omitempty"`
 	PingInterval      time.Duration     `json:"ping_interval"`
@@ -1286,8 +1287,10 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
 	v.Mem = rss
 	v.CPU = pcpu
 	if l := len(s.info.ClientConnectURLs); l > 0 {
-		v.ClientConnectURLs = make([]string, l)
-		copy(v.ClientConnectURLs, s.info.ClientConnectURLs)
+		v.ClientConnectURLs = append([]string(nil), s.info.ClientConnectURLs...)
+	}
+	if l := len(s.info.WSConnectURLs); l > 0 {
+		v.WSConnectURLs = append([]string(nil), s.info.WSConnectURLs...)
 	}
 	v.Connections = len(s.clients)
 	v.TotalConnections = s.totalClients
diff --git a/server/opts.go b/server/opts.go
index a0b52e5e..afa49a94 100644
--- a/server/opts.go
+++ b/server/opts.go
@@ -189,6 +189,7 @@ type Options struct {
 	JetStreamMaxMemory    int64         `json:"-"`
 	JetStreamMaxStore     int64         `json:"-"`
 	StoreDir              string        `json:"-"`
+	Websocket             WebsocketOpts `json:"-"`
 	ProfPort              int           `json:"-"`
 	PidFile               string        `json:"-"`
 	PortsFileDir          string        `json:"-"`
@@ -246,6 +247,37 @@ type Options struct {
 	routeProto           int
 }
 
+// WebsocketOpts ...
+type WebsocketOpts struct {
+	// The server will accept websocket client connections on this hostname/IP.
+	Host string
+	// The server will accept websocket client connections on this port.
+	Port int
+	// The host:port to advertise to websocket clients in the cluster.
+	Advertise string
+
+	// TLS configuration is required.
+	TLSConfig *tls.Config
+
+	// If true, the Origin header must match the request's host.
+	SameOrigin bool
+
+	// Only origins in this list will be accepted. If empty and
+	// SameOrigin is false, any origin is accepted.
+	AllowedOrigins []string
+
+	// If set to true, the server will negotiate with clients
+	// if compression can be used. If this is false, no compression
+	// will be used (both in server and clients) since it has to
+	// be negotiated between both endpoints
+	Compression bool
+
+	// Total time allowed for the server to read the client request
+	// and write the response back to the client. This include the
+	// time needed for the TLS Handshake.
+	HandshakeTimeout time.Duration
+}
+
 type netResolver interface {
 	LookupHost(ctx context.Context, host string) ([]string, error)
 }
@@ -843,6 +875,11 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
 		o.ConnectErrorReports = int(v.(int64))
 	case "reconnect_error_reports":
 		o.ReconnectErrorReports = int(v.(int64))
+	case "websocket", "ws":
+		if err := parseWebsocket(tk, o, errors, warnings); err != nil {
+			*errors = append(*errors, err)
+			return
+		}
 	default:
 		if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() {
 			err := &unknownConfigFieldErr{
@@ -912,6 +949,8 @@ func parseListen(v interface{}) (*hostPort, error) {
 			return nil, fmt.Errorf("could not parse port %q", port)
 		}
 		hp.host = host
+	default:
+		return nil, fmt.Errorf("expected port or host:port, got %T", vv)
 	}
 	return hp, nil
 }
@@ -2940,6 +2979,100 @@ func parseTLS(v interface{}) (t *TLSConfigOpts, retErr error) {
 	return &tc, nil
 }
 
+func parseWebsocket(v interface{}, o *Options, errors *[]error, warnings *[]error) error {
+	var lt token
+	defer convertPanicToErrorList(&lt, errors)
+
+	tk, v := unwrapValue(v, &lt)
+	gm, ok := v.(map[string]interface{})
+	if !ok {
+		return &configErr{tk, fmt.Sprintf("Expected websocket to be a map, got %T", v)}
+	}
+	for mk, mv := range gm {
+		// Again, unwrap token value if line check is required.
+		tk, mv = unwrapValue(mv, &lt)
+		switch strings.ToLower(mk) {
+		case "listen":
+			hp, err := parseListen(mv)
+			if err != nil {
+				err := &configErr{tk, err.Error()}
+				*errors = append(*errors, err)
+				continue
+			}
+			o.Websocket.Host = hp.host
+			o.Websocket.Port = hp.port
+		case "port":
+			o.Websocket.Port = int(mv.(int64))
+		case "host", "net":
+			o.Websocket.Host = mv.(string)
+		case "advertise":
+			o.Websocket.Advertise = mv.(string)
+		case "tls":
+			config, _, err := getTLSConfig(tk)
+			if err != nil {
+				*errors = append(*errors, err)
+				continue
+			}
+			o.Websocket.TLSConfig = config
+		case "same_origin":
+			o.Websocket.SameOrigin = mv.(bool)
+		case "allowed_origins", "allowed_origin", "allow_origins", "allow_origin", "origins", "origin":
+			switch mv := mv.(type) {
+			case string:
+				o.Websocket.AllowedOrigins = []string{mv}
+			case []interface{}:
+				keys := make([]string, 0, len(mv))
+				for _, val := range mv {
+					tk, val = unwrapValue(val, &lt)
+					if key, ok := val.(string); ok {
+						keys = append(keys, key)
+					} else {
+						err := &configErr{tk, fmt.Sprintf("error parsing allowed origins: unsupported type in array %T", val)}
+						*errors = append(*errors, err)
+						continue
+					}
+				}
+				o.Websocket.AllowedOrigins = keys
+			default:
+				err := &configErr{tk, fmt.Sprintf("error parsing allowed origins: unsupported type %T", mv)}
+				*errors = append(*errors, err)
+			}
+		case "handshake_timeout":
+			ht := time.Duration(0)
+			switch mv := mv.(type) {
+			case int64:
+				ht = time.Duration(mv) * time.Second
+			case string:
+				var err error
+				ht, err = time.ParseDuration(mv)
+				if err != nil {
+					err := &configErr{tk, err.Error()}
+					*errors = append(*errors, err)
+					continue
+				}
+			default:
+				err := &configErr{tk, fmt.Sprintf("error parsing handshake timeout: unsupported type %T", mv)}
+				*errors = append(*errors, err)
+			}
+			o.Websocket.HandshakeTimeout = ht
+		case "compression":
+			o.Websocket.Compression = mv.(bool)
+		default:
+			if !tk.IsUsedVariable() {
+				err := &unknownConfigFieldErr{
+					field: mk,
+					configErr: configErr{
+						token: tk,
+					},
+				}
+				*errors = append(*errors, err)
+				continue
+			}
+		}
+	}
+	return nil
+}
+
 // GenTLSConfig loads TLS related configuration parameters.
 func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
 	// Create the tls.Config from our options before including the certs.
@@ -3275,6 +3408,11 @@ func setBaselineOptions(opts *Options) {
 	if opts.ReconnectErrorReports == 0 {
 		opts.ReconnectErrorReports = DEFAULT_RECONNECT_ERROR_REPORTS
 	}
+	if opts.Websocket.Port != 0 {
+		if opts.Websocket.Host == "" {
+			opts.Websocket.Host = DEFAULT_HOST
+		}
+	}
 	// JetStream
 	if opts.JetStreamMaxMemory == 0 {
 		opts.JetStreamMaxMemory = -1
diff --git a/server/reload.go b/server/reload.go
index b67a6425..9b6a9d49 100644
--- a/server/reload.go
+++ b/server/reload.go
@@ -306,8 +306,10 @@ func (c *clusterOption) Apply(server *Server) {
 	server.routeInfo.AuthRequired = c.newValue.Username != ""
 	if c.newValue.NoAdvertise {
 		server.routeInfo.ClientConnectURLs = nil
+		server.routeInfo.WSConnectURLs = nil
 	} else {
 		server.routeInfo.ClientConnectURLs = server.clientConnectURLs
+		server.routeInfo.WSConnectURLs = server.websocket.connectURLs
 	}
 	server.setRouteInfoHostPortAndIP()
 	server.mu.Unlock()
@@ -523,7 +525,7 @@ type clientAdvertiseOption struct {
 // Apply the setting by updating the server info and regenerate the infoJSON byte array.
 func (c *clientAdvertiseOption) Apply(server *Server) {
 	server.mu.Lock()
-	server.setInfoHostPortAndGenerateJSON()
+	server.setInfoHostPort()
 	server.mu.Unlock()
 	server.Noticef("Reload: client_advertise = %s", c.newValue)
 }
@@ -627,6 +629,7 @@ func (s *Server) Reload() error {
 	clusterOrgPort := curOpts.Cluster.Port
 	gatewayOrgPort := curOpts.Gateway.Port
 	leafnodesOrgPort := curOpts.LeafNode.Port
+	websocketOrgPort := curOpts.Websocket.Port
 
 	s.mu.Unlock()
 
@@ -656,6 +659,9 @@ func (s *Server) Reload() error {
 	if newOpts.LeafNode.Port == -1 {
 		newOpts.LeafNode.Port = leafnodesOrgPort
 	}
+	if newOpts.Websocket.Port == -1 {
+		newOpts.Websocket.Port = websocketOrgPort
+	}
 
 	if err := s.reloadOptions(curOpts, newOpts); err != nil {
 		return err
@@ -756,6 +762,10 @@ func imposeOrder(value interface{}) error {
 		sort.Slice(value.Gateways, func(i, j int) bool {
 			return value.Gateways[i].Name < value.Gateways[j].Name
 		})
+	case WebsocketOpts:
+		sort.Slice(value.AllowedOrigins, func(i, j int) bool {
+			return value.AllowedOrigins[i] < value.AllowedOrigins[j]
+		})
 	case string, bool, int, int32, int64, time.Duration, float64, nil,
 		LeafNodeOpts, ClusterOpts, *tls.Config, *URLAccResolver, *MemAccResolver, Authentication:
 		// explicitly skipped types
@@ -914,6 +924,18 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
 			return nil, fmt.Errorf("config reload not supported for jetstream max memory")
 		case "jetstreammaxstore":
 			return nil, fmt.Errorf("config reload not supported for jetstream max storage")
+		case "websocket":
+			// Similar to gateways
+			tmpOld := oldValue.(WebsocketOpts)
+			tmpNew := newValue.(WebsocketOpts)
+			tmpOld.TLSConfig = nil
+			tmpNew.TLSConfig = nil
+			// If there is really a change prevents reload.
+			if !reflect.DeepEqual(tmpOld, tmpNew) {
+				// See TODO(ik) note below about printing old/new values.
+				return nil, fmt.Errorf("config reload not supported for %s: old=%v, new=%v",
+					field.Name, oldValue, newValue)
+			}
 		case "connecterrorreports":
 			diffOpts = append(diffOpts, &connectErrorReports{newValue: newValue.(int)})
 		case "reconnecterrorreports":
diff --git a/server/route.go b/server/route.go
index c4dfccea..330ce80e 100644
--- a/server/route.go
+++ b/server/route.go
@@ -73,6 +73,7 @@ type route struct {
 	authRequired bool
 	tlsRequired  bool
 	connectURLs  []string
+	wsConnURLs   []string
 	replySubs    map[*subscription]*time.Timer
 	gatewayURL   string
 	leafnodeURL  string
@@ -524,7 +525,7 @@ func (c *client) processRouteInfo(info *Info) {
 		// Unless disabled, possibly update the server's INFO protocol
 		// and send to clients that know how to handle async INFOs.
 		if !s.getOpts().Cluster.NoAdvertise {
-			s.addClientConnectURLsAndSendINFOToClients(info.ClientConnectURLs)
+			s.addConnectURLsAndSendINFOToClients(info.ClientConnectURLs, info.WSConnectURLs)
 		}
 	} else {
 		c.Debugf("Detected duplicate remote route %q", info.ID)
@@ -570,7 +571,7 @@ func (c *client) updateRemoteRoutePerms(sl *Sublist, info *Info) {
 // sendAsyncInfoToClients sends an INFO protocol to all
 // connected clients that accept async INFO updates.
 // The server lock is held on entry.
-func (s *Server) sendAsyncInfoToClients() {
+func (s *Server) sendAsyncInfoToClients(regCli, wsCli bool) {
 	// If there are no clients supporting async INFO protocols, we are done.
 	// Also don't send if we are shutting down...
 	if s.cproto == 0 || s.shutdown {
@@ -583,7 +584,9 @@ func (s *Server) sendAsyncInfoToClients() {
 		// registered (server has received CONNECT and first PING). For
 		// clients that are not at this stage, this will happen in the
 		// processing of the first PING (see client.processPing)
-		if c.opts.Protocol >= ClientProtoInfo && c.flags.isSet(firstPongSent) {
+		if ((regCli && c.ws == nil) || (wsCli && c.ws != nil)) &&
+			c.opts.Protocol >= ClientProtoInfo &&
+			c.flags.isSet(firstPongSent) {
 			// sendInfo takes care of checking if the connection is still
 			// valid or not, so don't duplicate tests here.
 			c.enqueueProto(c.generateClientInfoJSON(s.copyInfo()))
@@ -1236,6 +1239,7 @@ func (s *Server) addRoute(c *client, info *Info) (bool, bool) {
 		s.remotes[id] = c
 		c.mu.Lock()
 		c.route.connectURLs = info.ClientConnectURLs
+		c.route.wsConnURLs = info.WSConnectURLs
 		cid := c.cid
 		hash := string(c.route.hash)
 		c.mu.Unlock()
@@ -1495,6 +1499,7 @@ func (s *Server) routeAcceptLoop(ch chan struct{}) {
 	// Set this if only if advertise is not disabled
 	if !opts.Cluster.NoAdvertise {
 		info.ClientConnectURLs = s.clientConnectURLs
+		info.WSConnectURLs = s.websocket.connectURLs
 	}
 	// If we have selected a random port...
 	if port == 0 {
diff --git a/server/server.go b/server/server.go
index 3b590130..0028a38b 100644
--- a/server/server.go
+++ b/server/server.go
@@ -80,7 +80,8 @@ type Info struct {
 	ClientIP          string   `json:"client_ip,omitempty"`
 	Nonce             string   `json:"nonce,omitempty"`
 	Cluster           string   `json:"cluster,omitempty"`
-	ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to.
+	ClientConnectURLs []string `json:"connect_urls,omitempty"`    // Contains URLs a client can connect to.
+	WSConnectURLs     []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to.
 
 	// Route Specific
 	Import *SubjectPermission `json:"import,omitempty"`
@@ -214,6 +215,9 @@ type Server struct {
 
 	// For eventIDs
 	eventIds *nuid.NUID
+
+	// Websocket structure
+	websocket srvWebsocket
 }
 
 // Make sure all are 64bits for atomic use
@@ -301,6 +305,9 @@ func NewServer(opts *Options) (*Server, error) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	// Used internally for quick look-ups.
+	s.websocket.connectURLsMap = make(map[string]struct{})
+
 	// Ensure that non-exported options (used in tests) are properly set.
 	s.setLeafNodeNonExportedOptions()
 
@@ -322,7 +329,7 @@ func NewServer(opts *Options) (*Server, error) {
 	// listener has been created (possibly with random port),
 	// but since some tests may expect the INFO to be properly
 	// set after New(), let's do it now.
-	s.setInfoHostPortAndGenerateJSON()
+	s.setInfoHostPort()
 
 	// For tracking clients
 	s.clients = make(map[uint64]*client)
@@ -434,7 +441,10 @@ func validateOptions(o *Options) error {
 	}
 	// Check that gateway is properly configured. Returns no error
 	// if there is no gateway defined.
-	return validateGatewayOptions(o)
+	if err := validateGatewayOptions(o); err != nil {
+		return err
+	}
+	return validateWebsocketOptions(o)
 }
 
 func (s *Server) getOpts() *Options {
@@ -1319,6 +1329,13 @@ func (s *Server) Start() {
 	// port to be opened and potential ephemeral port selected.
 	clientListenReady := make(chan struct{})
 
+	// Start websocket server if needed. Do this before starting the routes,
+	// because we want to resolve the gateway host:port so that this information
+	// can be sent to other routes.
+	if opts.Websocket.Port != 0 {
+		s.startWebsocketServer()
+	}
+
 	// Start up routing as well if needed.
 	if opts.Cluster.Port != 0 {
 		s.startGoRoutine(func() {
@@ -1402,6 +1419,14 @@ func (s *Server) Shutdown() {
 		s.listener = nil
 	}
 
+	// Kick websocket server
+	if s.websocket.server != nil {
+		doneExpected++
+		s.websocket.server.Close()
+		s.websocket.server = nil
+		s.websocket.listener = nil
+	}
+
 	// Kick leafnodes AcceptLoop()
 	if s.leafNodeListener != nil {
 		doneExpected++
@@ -1512,7 +1537,6 @@ func (s *Server) AcceptLoop(clr chan struct{}) {
 
 	// Setup state that can enable shutdown
 	s.mu.Lock()
-	s.listener = l
 
 	// If server was started with RANDOM_PORT (-1), opts.Port would be equal
 	// to 0 at the beginning this function. So we need to get the actual port
@@ -1523,14 +1547,16 @@ func (s *Server) AcceptLoop(clr chan struct{}) {
 
 	// Now that port has been set (if it was set to RANDOM), set the
 	// server's info Host/Port with either values from Options or
-	// ClientAdvertise. Also generate the JSON byte array.
-	if err := s.setInfoHostPortAndGenerateJSON(); err != nil {
+	// ClientAdvertise.
+	if err := s.setInfoHostPort(); err != nil {
 		s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", s.opts.ClientAdvertise, err)
+		l.Close()
 		s.mu.Unlock()
 		return
 	}
 	// Keep track of client connect URLs. We may need them later.
 	s.clientConnectURLs = s.getClientConnectURLs()
+	s.listener = l
 	s.mu.Unlock()
 
 	// Let the caller know that we are ready
@@ -1554,7 +1580,7 @@ func (s *Server) AcceptLoop(clr chan struct{}) {
 		}
 		tmpDelay = ACCEPT_MIN_SLEEP
 		s.startGoRoutine(func() {
-			s.createClient(conn)
+			s.createClient(conn, nil)
 			s.grWG.Done()
 		})
 	}
@@ -1565,8 +1591,7 @@ func (s *Server) AcceptLoop(clr chan struct{}) {
 // Note that this function may be called during config reload, this is why
 // Host/Port may be reset to original Options if the ClientAdvertise option
 // is not set (since it may have previously been).
-// The function then generates the server infoJSON.
-func (s *Server) setInfoHostPortAndGenerateJSON() error {
+func (s *Server) setInfoHostPort() error {
 	// When this function is called, opts.Port is set to the actual listen
 	// port (if option was originally set to RANDOM), even during a config
 	// reload. So use of s.opts.Port is safe.
@@ -1799,14 +1824,16 @@ func (s *Server) HTTPHandler() http.Handler {
 	return s.httpHandler
 }
 
-// Perform a conditional deep copy due to reference nature of ClientConnectURLs.
+// Perform a conditional deep copy due to reference nature of [Client|WS]ConnectURLs.
 // If updates are made to Info, this function should be consulted and updated.
 // Assume lock is held.
 func (s *Server) copyInfo() Info {
 	info := s.info
-	if info.ClientConnectURLs != nil {
-		info.ClientConnectURLs = make([]string, len(s.info.ClientConnectURLs))
-		copy(info.ClientConnectURLs, s.info.ClientConnectURLs)
+	if len(info.ClientConnectURLs) > 0 {
+		info.ClientConnectURLs = append([]string(nil), s.info.ClientConnectURLs...)
+	}
+	if len(info.WSConnectURLs) > 0 {
+		info.WSConnectURLs = append([]string(nil), s.info.WSConnectURLs...)
 	}
 	if s.nonceRequired() {
 		// Nonce handling
@@ -1818,7 +1845,7 @@ func (s *Server) copyInfo() Info {
 	return info
 }
 
-func (s *Server) createClient(conn net.Conn) *client {
+func (s *Server) createClient(conn net.Conn, ws *websocket) *client {
 	// Snapshot server options.
 	opts := s.getOpts()
 
@@ -1830,7 +1857,7 @@ func (s *Server) createClient(conn net.Conn) *client {
 	}
 	now := time.Now()
 
-	c := &client{srv: s, nc: conn, opts: defaultOpts, mpay: maxPay, msubs: maxSubs, start: now, last: now}
+	c := &client{srv: s, nc: conn, opts: defaultOpts, mpay: maxPay, msubs: maxSubs, start: now, last: now, ws: ws}
 
 	c.registerWithAccount(s.globalAccount())
 
@@ -1857,6 +1884,7 @@ func (s *Server) createClient(conn net.Conn) *client {
 	// TLS handshake is done (if applicable).
 	c.sendProtoNow(c.generateClientInfoJSON(info))
 
+	tlsRequired := ws == nil && info.TLSRequired
 	// Unlock to register
 	c.mu.Unlock()
 
@@ -1885,7 +1913,7 @@ func (s *Server) createClient(conn net.Conn) *client {
 	c.mu.Lock()
 
 	// Check for TLS
-	if info.TLSRequired {
+	if tlsRequired {
 		c.Debugf("Starting TLS client connection handshake")
 		c.nc = tls.Server(c.nc, opts.TLSConfig)
 		conn := c.nc.(*tls.Conn)
@@ -1942,7 +1970,7 @@ func (s *Server) createClient(conn net.Conn) *client {
 	// Spin up the write loop.
 	s.startGoRoutine(func() { c.writeLoop() })
 
-	if info.TLSRequired {
+	if tlsRequired {
 		c.Debugf("TLS handshake complete")
 		cs := c.nc.(*tls.Conn).ConnectionState()
 		c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite))
@@ -1990,57 +2018,66 @@ func (s *Server) saveClosedClient(c *client, nc net.Conn, reason ClosedState) {
 	s.mu.Unlock()
 }
 
-// Adds the given array of urls to the server's INFO.ClientConnectURLs
-// array. The server INFO JSON is regenerated.
-// Note that a check is made to ensure that given URLs are not
-// already present. So the INFO JSON is regenerated only if new ULRs
-// were added.
+// Adds to the list of client and websocket clients connect URLs.
 // If there was a change, an INFO protocol is sent to registered clients
 // that support async INFO protocols.
-func (s *Server) addClientConnectURLsAndSendINFOToClients(urls []string) {
-	s.updateServerINFOAndSendINFOToClients(urls, true)
+func (s *Server) addConnectURLsAndSendINFOToClients(curls, wsurls []string) {
+	s.updateServerINFOAndSendINFOToClients(curls, wsurls, true)
 }
 
-// Removes the given array of urls from the server's INFO.ClientConnectURLs
-// array. The server INFO JSON is regenerated if needed.
+// Removes from the list of client and websocket clients connect URLs.
 // If there was a change, an INFO protocol is sent to registered clients
 // that support async INFO protocols.
-func (s *Server) removeClientConnectURLsAndSendINFOToClients(urls []string) {
-	s.updateServerINFOAndSendINFOToClients(urls, false)
+func (s *Server) removeConnectURLsAndSendINFOToClients(curls, wsurls []string) {
+	s.updateServerINFOAndSendINFOToClients(curls, wsurls, false)
 }
 
-// Updates the server's Info object with the given array of URLs and re-generate
-// the infoJSON byte array, then send an (async) INFO protocol to clients that
-// support it.
-func (s *Server) updateServerINFOAndSendINFOToClients(urls []string, add bool) {
+// Updates the list of client and websocket clients connect URLs and if any change
+// sends an async INFO update to clients that support it.
+func (s *Server) updateServerINFOAndSendINFOToClients(curls, wsurls []string, add bool) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
 	// Will be set to true if we alter the server's Info object.
-	wasUpdated := false
 	remove := !add
-	for _, url := range urls {
-		_, present := s.clientConnectURLsMap[url]
-		if add && !present {
-			s.clientConnectURLsMap[url] = struct{}{}
-			wasUpdated = true
-		} else if remove && present {
-			delete(s.clientConnectURLsMap, url)
-			wasUpdated = true
-		}
-	}
-	if wasUpdated {
-		// Recreate the info.ClientConnectURL array from the map
-		s.info.ClientConnectURLs = s.info.ClientConnectURLs[:0]
+	checkMap := func(urls []string, m map[string]struct{}) bool {
+		wasUpdated := false
+		for _, url := range urls {
+			_, present := m[url]
+			if add && !present {
+				m[url] = struct{}{}
+				wasUpdated = true
+			} else if remove && present {
+				delete(m, url)
+				wasUpdated = true
+			}
+		}
+		return wasUpdated
+	}
+	cliUpdated := checkMap(curls, s.clientConnectURLsMap)
+	wsUpdated := checkMap(wsurls, s.websocket.connectURLsMap)
+
+	updateInfo := func(infoURLs *[]string, urls []string, m map[string]struct{}) {
+		// Recreate the info's slice from the map
+		*infoURLs = (*infoURLs)[:0]
 		// Add this server client connect ULRs first...
-		s.info.ClientConnectURLs = append(s.info.ClientConnectURLs, s.clientConnectURLs...)
-		for url := range s.clientConnectURLsMap {
-			s.info.ClientConnectURLs = append(s.info.ClientConnectURLs, url)
+		*infoURLs = append(*infoURLs, urls...)
+		// Then the ones from the map
+		for url := range m {
+			*infoURLs = append(*infoURLs, url)
 		}
+	}
+	if cliUpdated {
+		updateInfo(&s.info.ClientConnectURLs, s.clientConnectURLs, s.clientConnectURLsMap)
+	}
+	if wsUpdated {
+		updateInfo(&s.info.WSConnectURLs, s.websocket.connectURLs, s.websocket.connectURLsMap)
+	}
+	if cliUpdated || wsUpdated {
 		// Update the time of this update
 		s.lastCURLsUpdate = time.Now().UnixNano()
 		// Send to all registered clients that support async INFO protocols.
-		s.sendAsyncInfoToClients()
+		s.sendAsyncInfoToClients(cliUpdated, wsUpdated)
 	}
 }
 
@@ -2265,7 +2302,11 @@ func (s *Server) ReadyForConnections(dur time.Duration) bool {
 	end := time.Now().Add(dur)
 	for time.Now().Before(end) {
 		s.mu.Lock()
-		ok := s.listener != nil && (opts.Cluster.Port == 0 || s.routeListener != nil) && (opts.Gateway.Name == "" || s.gatewayListener != nil)
+		ok := s.listener != nil &&
+			(opts.Cluster.Port == 0 || s.routeListener != nil) &&
+			(opts.Gateway.Name == "" || s.gatewayListener != nil) &&
+			(opts.LeafNode.Port == 0 || s.leafNodeListener != nil) &&
+			(opts.Websocket.Port == 0 || s.websocket.listener != nil)
 		s.mu.Unlock()
 		if ok {
 			return true
@@ -2332,16 +2373,28 @@ func (s *Server) closedClients() []*closedClient {
 func (s *Server) getClientConnectURLs() []string {
 	// Snapshot server options.
 	opts := s.getOpts()
+	// Ignore error here since we know that if there is client advertise, the
+	// parseHostPort is correct because we did it right before calling this
+	// function in Server.New().
+	urls, _ := s.getConnectURLs(opts.ClientAdvertise, opts.Host, opts.Port)
+	return urls
+}
 
+// Generic version that will return an array of URLs based on the given
+// advertise, host and port values.
+func (s *Server) getConnectURLs(advertise, host string, port int) ([]string, error) {
 	urls := make([]string, 0, 1)
 
-	// short circuit if client advertise is set
-	if opts.ClientAdvertise != "" {
-		// just use the info host/port. This is updated in s.New()
-		urls = append(urls, net.JoinHostPort(s.info.Host, strconv.Itoa(s.info.Port)))
+	// short circuit if advertise is set
+	if advertise != "" {
+		h, p, err := parseHostPort(advertise, port)
+		if err != nil {
+			return nil, err
+		}
+		urls = append(urls, net.JoinHostPort(h, strconv.Itoa(p)))
 	} else {
-		sPort := strconv.Itoa(opts.Port)
-		_, ips, err := s.getNonLocalIPsIfHostIsIPAny(opts.Host, true)
+		sPort := strconv.Itoa(port)
+		_, ips, err := s.getNonLocalIPsIfHostIsIPAny(host, true)
 		for _, ip := range ips {
 			urls = append(urls, net.JoinHostPort(ip, sPort))
 		}
@@ -2352,14 +2405,14 @@ func (s *Server) getClientConnectURLs() []string {
 			// and not add any address in the array in the loop above, and we
 			// ended-up returning 0.0.0.0, which is problematic for Windows clients.
 			// Check for 0.0.0.0 or :: specifically, and ignore if that's the case.
-			if opts.Host == "0.0.0.0" || opts.Host == "::" {
-				s.Errorf("Address %q can not be resolved properly", opts.Host)
+			if host == "0.0.0.0" || host == "::" {
+				s.Errorf("Address %q can not be resolved properly", host)
 			} else {
-				urls = append(urls, net.JoinHostPort(opts.Host, sPort))
+				urls = append(urls, net.JoinHostPort(host, sPort))
 			}
 		}
 	}
-	return urls
+	return urls, nil
 }
 
 // Returns an array of non local IPs if the provided host is
@@ -2450,6 +2503,7 @@ type Ports struct {
 	Monitoring []string `json:"monitoring,omitempty"`
 	Cluster    []string `json:"cluster,omitempty"`
 	Profile    []string `json:"profile,omitempty"`
+	WebSocket  []string `json:"websocket,omitempty"`
 }
 
 // PortsInfo attempts to resolve all the ports. If after maxWait the ports are not
@@ -2460,18 +2514,20 @@ func (s *Server) PortsInfo(maxWait time.Duration) *Ports {
 		opts := s.getOpts()
 
 		s.mu.Lock()
-		info := s.copyInfo()
+		tls := s.info.TLSRequired
 		listener := s.listener
 		httpListener := s.http
 		clusterListener := s.routeListener
 		profileListener := s.profiler
+		wsListener := s.websocket.listener
+		wss := s.websocket.tls
 		s.mu.Unlock()
 
 		ports := Ports{}
 
 		if listener != nil {
 			natsProto := "nats"
-			if info.TLSRequired {
+			if tls {
 				natsProto = "tls"
 			}
 			ports.Nats = formatURL(natsProto, listener)
@@ -2497,6 +2553,14 @@ func (s *Server) PortsInfo(maxWait time.Duration) *Ports {
 			ports.Profile = formatURL("http", profileListener)
 		}
 
+		if wsListener != nil {
+			protocol := "ws"
+			if wss {
+				protocol = "wss"
+			}
+			ports.WebSocket = formatURL(protocol, wsListener)
+		}
+
 		return &ports
 	}
 
@@ -2600,6 +2664,9 @@ func (s *Server) serviceListeners() []net.Listener {
 	if opts.ProfPort != 0 {
 		listeners = append(listeners, s.profiler)
 	}
+	if opts.Websocket.Port != 0 {
+		listeners = append(listeners, s.websocket.listener)
+	}
 	return listeners
 }
 
diff --git a/server/util.go b/server/util.go
index 43e7e336..51cbf3aa 100644
--- a/server/util.go
+++ b/server/util.go
@@ -86,8 +86,7 @@ func secondsToDuration(seconds float64) time.Duration {
 func parseHostPort(hostPort string, defaultPort int) (host string, port int, err error) {
 	if hostPort != "" {
 		host, sPort, err := net.SplitHostPort(hostPort)
-		switch err.(type) {
-		case *net.AddrError:
+		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))
 		}
diff --git a/server/websocket.go b/server/websocket.go
new file mode 100644
index 00000000..be9ef817
--- /dev/null
+++ b/server/websocket.go
@@ -0,0 +1,977 @@
+// Copyright 2020 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"
+	"compress/flate"
+	"crypto/sha1"
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+	"unicode/utf8"
+)
+
+type wsOpCode int
+
+const (
+	// From https://tools.ietf.org/html/rfc6455#section-5.2
+	wsTextMessage   = wsOpCode(1)
+	wsBinaryMessage = wsOpCode(2)
+	wsCloseMessage  = wsOpCode(8)
+	wsPingMessage   = wsOpCode(9)
+	wsPongMessage   = wsOpCode(10)
+
+	wsFinalBit = 1 << 7
+	wsRsv1Bit  = 1 << 6 // Used for compression, from https://tools.ietf.org/html/rfc7692#section-6
+	wsRsv2Bit  = 1 << 5
+	wsRsv3Bit  = 1 << 4
+
+	wsMaskBit = 1 << 7
+
+	wsContinuationFrame     = 0
+	wsMaxFrameHeaderSize    = 10 // For a server-to-client frame
+	wsMaxControlPayloadSize = 125
+	wsFrameSizeForBrowsers  = 4096 // From experiment, webrowsers behave better with limited frame size
+
+	// From https://tools.ietf.org/html/rfc6455#section-11.7
+	wsCloseStatusNormalClosure      = 1000
+	wsCloseStatusGoingAway          = 1001
+	wsCloseStatusProtocolError      = 1002
+	wsCloseStatusUnsupportedData    = 1003
+	wsCloseStatusNoStatusReceived   = 1005
+	wsCloseStatusAbnormalClosure    = 1006
+	wsCloseStatusInvalidPayloadData = 1007
+	wsCloseStatusPolicyViolation    = 1008
+	wsCloseStatusMessageTooBig      = 1009
+	wsCloseStatusInternalSrvError   = 1011
+	wsCloseStatusTLSHandshake       = 1015
+
+	wsFirstFrame        = true
+	wsContFrame         = false
+	wsFinalFrame        = true
+	wsCompressedFrame   = true
+	wsUncompressedFrame = false
+)
+
+var decompressorPool sync.Pool
+
+// From https://tools.ietf.org/html/rfc6455#section-1.3
+var wsGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
+
+type websocket struct {
+	frames     net.Buffers
+	fs         int64
+	closeMsg   []byte
+	compress   bool
+	closeSent  bool
+	browser    bool
+	compressor *flate.Writer
+}
+
+type srvWebsocket struct {
+	mu             sync.RWMutex
+	server         *http.Server
+	listener       net.Listener
+	tls            bool
+	allowedOrigins map[string]*allowedOrigin // host will be the key
+	sameOrigin     bool
+	connectURLs    []string
+	connectURLsMap map[string]struct{}
+}
+
+type allowedOrigin struct {
+	scheme string
+	port   string
+}
+
+type wsUpgradeResult struct {
+	conn net.Conn
+	ws   *websocket
+}
+
+type wsReadInfo struct {
+	rem   int
+	fs    bool
+	ff    bool
+	fc    bool
+	mkpos byte
+	mkey  [4]byte
+	buf   []byte
+}
+
+func (r *wsReadInfo) init() {
+	r.fs, r.ff = true, true
+}
+
+// Returns a slice containing `needed` bytes from the given buffer `buf`
+// starting at position `pos`, and possibly read from the given reader `r`.
+// When bytes are present in `buf`, the `pos` is incremented by the number
+// of bytes found up to `needed` and the new position is returned. If not
+// enough bytes are found, the bytes found in `buf` are copied to the returned
+// slice and the remaning bytes are read from `r`.
+func wsGet(r io.Reader, buf []byte, pos, needed int) ([]byte, int, error) {
+	avail := len(buf) - pos
+	if avail >= needed {
+		return buf[pos : pos+needed], pos + needed, nil
+	}
+	b := make([]byte, needed)
+	start := copy(b, buf[pos:])
+	for start != needed {
+		n, err := r.Read(b[start:cap(b)])
+		if err != nil {
+			return nil, 0, err
+		}
+		start += n
+	}
+	return b, pos + avail, nil
+}
+
+// Returns a slice of byte slices corresponding to payload of websocket frames.
+// The byte slice `buf` is filled with bytes from the connection's read loop.
+// This function will decode the frame headers and unmask the payload(s).
+// It is possible that the returned slices point to the given `buf` slice, so
+// `buf` should not be overwritten until the returned slices have been parsed.
+//
+// Client lock MUST NOT be held on entry.
+func (c *client) wsRead(r *wsReadInfo, ior io.Reader, buf []byte) ([][]byte, error) {
+	var (
+		bufs   [][]byte
+		tmpBuf []byte
+		err    error
+		pos    int
+		max    = len(buf)
+	)
+	for pos != max {
+		if r.fs {
+			b0 := buf[pos]
+			frameType := wsOpCode(b0 & 0xF)
+			final := b0&wsFinalBit != 0
+			compressed := b0&wsRsv1Bit != 0
+			pos++
+
+			tmpBuf, pos, err = wsGet(ior, buf, pos, 1)
+			if err != nil {
+				return bufs, err
+			}
+			b1 := tmpBuf[0]
+
+			// Clients MUST set the mask bit. If not set, reject.
+			if b1&wsMaskBit == 0 {
+				return bufs, c.wsHandleProtocolError("mask bit missing")
+			}
+
+			// Store size in case it is < 125
+			r.rem = int(b1 & 0x7F)
+
+			switch frameType {
+			case wsPingMessage, wsPongMessage, wsCloseMessage:
+				if r.rem > wsMaxControlPayloadSize {
+					return bufs, c.wsHandleProtocolError(
+						fmt.Sprintf("control frame length bigger than maximum allowed of %v bytes",
+							wsMaxControlPayloadSize))
+				}
+				if !final {
+					return bufs, c.wsHandleProtocolError("control frame does not have final bit set")
+				}
+			case wsTextMessage, wsBinaryMessage:
+				if !r.ff {
+					return bufs, c.wsHandleProtocolError("new message started before final frame for previous message was received")
+				}
+				r.ff = final
+				r.fc = compressed
+			case wsContinuationFrame:
+				// Compressed bit must be only set in the first frame
+				if r.ff || compressed {
+					return bufs, c.wsHandleProtocolError("invalid continuation frame")
+				}
+				r.ff = final
+			default:
+				return bufs, c.wsHandleProtocolError(fmt.Sprintf("unknown opcode %v", frameType))
+			}
+
+			switch r.rem {
+			case 126:
+				tmpBuf, pos, err = wsGet(ior, buf, pos, 2)
+				if err != nil {
+					return bufs, err
+				}
+				r.rem = int(binary.BigEndian.Uint16(tmpBuf))
+			case 127:
+				tmpBuf, pos, err = wsGet(ior, buf, pos, 8)
+				if err != nil {
+					return bufs, err
+				}
+				r.rem = int(binary.BigEndian.Uint64(tmpBuf))
+			}
+
+			// Read masking key
+			tmpBuf, pos, err = wsGet(ior, buf, pos, 4)
+			if err != nil {
+				return bufs, err
+			}
+			copy(r.mkey[:], tmpBuf)
+			r.mkpos = 0
+
+			// Handle control messages in place...
+			if wsIsControlFrame(frameType) {
+				pos, err = c.wsHandleControlFrame(r, frameType, ior, buf, pos)
+				if err != nil {
+					return bufs, err
+				}
+				continue
+			}
+
+			// Done with the frame header
+			r.fs = false
+		}
+		if pos < max {
+			var b []byte
+			var n int
+
+			n = r.rem
+			if pos+n > max {
+				n = max - pos
+			}
+			b = buf[pos : pos+n]
+			pos += n
+			r.rem -= n
+			if r.fc {
+				r.buf = append(r.buf, b...)
+				b = r.buf
+			}
+			if !r.fc || r.rem == 0 {
+				r.unmask(b)
+				if r.fc {
+					// As per https://tools.ietf.org/html/rfc7692#section-7.2.2
+					// add 0x00, 0x00, 0xff, 0xff and then a final block so that flate reader
+					// does not report unexpected EOF.
+					b = append(b, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff)
+					br := bytes.NewBuffer(b)
+					d, _ := decompressorPool.Get().(io.ReadCloser)
+					if d == nil {
+						d = flate.NewReader(br)
+					} else {
+						d.(flate.Resetter).Reset(br, nil)
+					}
+					b, err = ioutil.ReadAll(d)
+					decompressorPool.Put(d)
+					if err != nil {
+						return bufs, err
+					}
+				}
+				bufs = append(bufs, b)
+				if r.rem == 0 {
+					r.fs, r.fc, r.buf = true, false, nil
+				}
+			}
+		}
+	}
+	return bufs, nil
+}
+
+// Handles the PING, PONG and CLOSE websocket control frames.
+//
+// Client lock MUST NOT be held on entry.
+func (c *client) wsHandleControlFrame(r *wsReadInfo, frameType wsOpCode, nc io.Reader, buf []byte, pos int) (int, error) {
+	var payload []byte
+	var err error
+
+	statusPos := pos
+	if r.rem > 0 {
+		payload, pos, err = wsGet(nc, buf, pos, r.rem)
+		if err != nil {
+			return pos, err
+		}
+		r.unmask(payload)
+		r.rem = 0
+	}
+	switch frameType {
+	case wsCloseMessage:
+		status := wsCloseStatusNoStatusReceived
+		body := ""
+		// If there is a payload, it should contain 2 unsigned bytes
+		// that represent the status code and then optional payload.
+		if len(payload) >= 2 {
+			status = int(binary.BigEndian.Uint16(buf[statusPos : statusPos+2]))
+			body = string(buf[statusPos+2 : statusPos+len(payload)])
+			if body != "" && !utf8.ValidString(body) {
+				// https://tools.ietf.org/html/rfc6455#section-5.5.1
+				// If body is present, it must be a valid utf8
+				status = wsCloseStatusInvalidPayloadData
+				body = "invalid utf8 body in close frame"
+			}
+		}
+		c.wsEnqueueControlMessage(wsCloseMessage, wsCreateCloseMessage(status, body))
+		// Return io.EOF so that readLoop will close the connection as ClientClosed
+		// after processing pending buffers.
+		return pos, io.EOF
+	case wsPingMessage:
+		c.wsEnqueueControlMessage(wsPongMessage, payload)
+	case wsPongMessage:
+		// Nothing to do..
+	}
+	return pos, nil
+}
+
+// Unmask the given slice.
+func (r *wsReadInfo) unmask(buf []byte) {
+	p := int(r.mkpos)
+	if len(buf) < 16 {
+		for i := 0; i < len(buf); i++ {
+			buf[i] ^= r.mkey[p&3]
+			p++
+		}
+		r.mkpos = byte(p & 3)
+		return
+	}
+	var k [8]byte
+	for i := 0; i < 8; i++ {
+		k[i] = r.mkey[(p+i)&3]
+	}
+	km := binary.BigEndian.Uint64(k[:])
+	n := (len(buf) / 8) * 8
+	for i := 0; i < n; i += 8 {
+		tmp := binary.BigEndian.Uint64(buf[i : i+8])
+		tmp ^= km
+		binary.BigEndian.PutUint64(buf[i:], tmp)
+	}
+	buf = buf[n:]
+	for i := 0; i < len(buf); i++ {
+		buf[i] ^= r.mkey[p&3]
+		p++
+	}
+	r.mkpos = byte(p & 3)
+}
+
+// Returns true if the op code corresponds to a control frame.
+func wsIsControlFrame(frameType wsOpCode) bool {
+	return frameType >= wsCloseMessage
+}
+
+// Create the frame header.
+// Encodes the frame type and optional compression flag, and the size of the payload.
+func wsCreateFrameHeader(compressed bool, frameType wsOpCode, l int) []byte {
+	fh := make([]byte, wsMaxFrameHeaderSize)
+	n := wsFillFrameHeader(fh, wsFirstFrame, wsFinalFrame, compressed, frameType, l)
+	return fh[:n]
+}
+
+func wsFillFrameHeader(fh []byte, first, final, compressed bool, frameType wsOpCode, l int) int {
+	var n int
+	var b byte
+	if first {
+		b = byte(frameType)
+	}
+	if final {
+		b |= wsFinalBit
+	}
+	if compressed {
+		b |= wsRsv1Bit
+	}
+	switch {
+	case l <= 125:
+		n = 2
+		fh[0] = b
+		fh[1] = byte(l)
+	case l < 65536:
+		n = 4
+		fh[0] = b
+		fh[1] = 126
+		binary.BigEndian.PutUint16(fh[2:], uint16(l))
+	default:
+		n = 10
+		fh[0] = b
+		fh[1] = 127
+		binary.BigEndian.PutUint64(fh[2:], uint64(l))
+	}
+	return n
+}
+
+// Invokes wsEnqueueControlMessageLocked under client lock.
+//
+// Client lock MUST NOT be held on entry
+func (c *client) wsEnqueueControlMessage(controlMsg wsOpCode, payload []byte) {
+	c.mu.Lock()
+	c.wsEnqueueControlMessageLocked(controlMsg, payload)
+	c.mu.Unlock()
+}
+
+// Enqueues a websocket control message.
+// If the control message is a wsCloseMessage, then marks this client
+// has having sent the close message (since only one should be sent).
+// This will prevent the generic closeConnection() to enqueue one.
+//
+// Client lock held on entry.
+func (c *client) wsEnqueueControlMessageLocked(controlMsg wsOpCode, payload []byte) {
+	// Control messages are never compressed and their size will be
+	// less than wsMaxControlPayloadSize, which means the frame header
+	// will be only 2 bytes.
+	cm := make([]byte, 2+len(payload))
+	wsFillFrameHeader(cm, wsFirstFrame, wsFinalFrame, wsUncompressedFrame, controlMsg, len(payload))
+	// Note that payload is optional.
+	if len(payload) > 0 {
+		copy(cm[2:], payload)
+	}
+	c.out.pb += int64(len(cm))
+	if controlMsg == wsCloseMessage {
+		// We can't add the close message to the frames buffers
+		// now. It will be done on a flushOutbound() when there
+		// are no more pending buffers to send.
+		c.ws.closeSent = true
+		c.ws.closeMsg = cm
+	} else {
+		c.ws.frames = append(c.ws.frames, cm)
+		c.ws.fs += int64(len(cm))
+	}
+	c.flushSignal()
+}
+
+// Enqueues a websocket close message with a status mapped from the given `reason`.
+//
+// Client lock held on entry
+func (c *client) wsEnqueueCloseMessage(reason ClosedState) {
+	var status int
+	switch reason {
+	case ClientClosed:
+		status = wsCloseStatusNormalClosure
+	case AuthenticationTimeout, AuthenticationViolation, SlowConsumerPendingBytes, SlowConsumerWriteDeadline,
+		MaxAccountConnectionsExceeded, MaxConnectionsExceeded, MaxControlLineExceeded, MaxSubscriptionsExceeded,
+		MissingAccount, AuthenticationExpired, Revocation:
+		status = wsCloseStatusPolicyViolation
+	case TLSHandshakeError:
+		status = wsCloseStatusTLSHandshake
+	case ParseError, ProtocolViolation, BadClientProtocolVersion:
+		status = wsCloseStatusProtocolError
+	case MaxPayloadExceeded:
+		status = wsCloseStatusMessageTooBig
+	case ServerShutdown:
+		status = wsCloseStatusGoingAway
+	case WriteError, ReadError, StaleConnection:
+		status = wsCloseStatusAbnormalClosure
+	default:
+		status = wsCloseStatusInternalSrvError
+	}
+	body := wsCreateCloseMessage(status, reason.String())
+	c.wsEnqueueControlMessageLocked(wsCloseMessage, body)
+}
+
+// Create and then enqueue a close message with a protocol error and the
+// given message. This is invoked when parsing websocket frames.
+//
+// Lock MUST NOT be held on entry.
+func (c *client) wsHandleProtocolError(message string) error {
+	buf := wsCreateCloseMessage(wsCloseStatusProtocolError, message)
+	c.wsEnqueueControlMessage(wsCloseMessage, buf)
+	return fmt.Errorf(message)
+}
+
+// Create a close message with the given `status` and `body`.
+// If the `body` is more than the maximum allows control frame payload size,
+// it is truncated and "..." is added at the end (as a hint that message
+// is not complete).
+func wsCreateCloseMessage(status int, body string) []byte {
+	// Since a control message payload is limited in size, we
+	// will limit the text and add trailing "..." if truncated.
+	// The body of a Close Message must be preceded with 2 bytes,
+	// so take that into account for limiting the body length.
+	if len(body) > wsMaxControlPayloadSize-2 {
+		body = body[:wsMaxControlPayloadSize-5]
+		body += "..."
+	}
+	buf := make([]byte, 2+len(body))
+	// We need to have a 2 byte unsigned int that represents the error status code
+	// https://tools.ietf.org/html/rfc6455#section-5.5.1
+	binary.BigEndian.PutUint16(buf[:2], uint16(status))
+	copy(buf[2:], []byte(body))
+	return buf
+}
+
+// Process websocket client handshake. On success, returns the raw net.Conn that
+// will be used to create a *client object.
+// Invoked from the HTTP server listening on websocket port.
+func (s *Server) wsUpgrade(w http.ResponseWriter, r *http.Request) (*wsUpgradeResult, error) {
+	opts := s.getOpts()
+
+	// From https://tools.ietf.org/html/rfc6455#section-4.2.1
+	// Point 1.
+	if r.Method != "GET" {
+		return nil, wsReturnHTTPError(w, http.StatusMethodNotAllowed, "request method must be GET")
+	}
+	// Point 2.
+	if r.Host == "" {
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "'Host' missing in request")
+	}
+	// Point 3.
+	if !wsHeaderContains(r.Header, "Upgrade", "websocket") {
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "invalid value for header 'Uprade'")
+	}
+	// Point 4.
+	if !wsHeaderContains(r.Header, "Connection", "Upgrade") {
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "invalid value for header 'Connection'")
+	}
+	// Point 5.
+	key := r.Header.Get("Sec-Websocket-Key")
+	if key == "" {
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "key missing")
+	}
+	// Point 6.
+	if !wsHeaderContains(r.Header, "Sec-Websocket-Version", "13") {
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "invalid version")
+	}
+	// Others are optional
+	// Point 7.
+	if err := s.websocket.checkOrigin(r); err != nil {
+		return nil, wsReturnHTTPError(w, http.StatusForbidden, fmt.Sprintf("origin not allowed: %v", err))
+	}
+	// Point 8.
+	// We don't have protocols, so ignore.
+	// Point 9.
+	// Extensions, only support for compression at the moment
+	compress := opts.Websocket.Compression
+	if compress {
+		compress = wsClientSupportsCompression(r.Header)
+	}
+
+	h := w.(http.Hijacker)
+	conn, brw, err := h.Hijack()
+	if err != nil {
+		if conn != nil {
+			conn.Close()
+		}
+		return nil, wsReturnHTTPError(w, http.StatusInternalServerError, err.Error())
+	}
+	if brw.Reader.Buffered() > 0 {
+		conn.Close()
+		return nil, wsReturnHTTPError(w, http.StatusBadRequest, "client sent data before handshake is complete")
+	}
+
+	var buf [1024]byte
+	p := buf[:0]
+
+	// From https://tools.ietf.org/html/rfc6455#section-4.2.2
+	p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
+	p = append(p, wsAcceptKey(key)...)
+	p = append(p, _CRLF_...)
+	if compress {
+		p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
+	}
+	p = append(p, _CRLF_...)
+
+	if _, err = conn.Write(p); err != nil {
+		conn.Close()
+		return nil, err
+	}
+	// If there was a deadline set for the handshake, clear it now.
+	if opts.Websocket.HandshakeTimeout > 0 {
+		conn.SetDeadline(time.Time{})
+	}
+	ws := &websocket{compress: compress}
+	// Indicate if this is likely coming from a browser.
+	if ua := r.Header.Get("User-Agent"); ua != "" && strings.HasPrefix(ua, "Mozilla/") {
+		ws.browser = true
+	}
+	return &wsUpgradeResult{conn: conn, ws: ws}, nil
+}
+
+// Returns true if the header named `name` contains a token with value `value`.
+func wsHeaderContains(header http.Header, name string, value string) bool {
+	for _, s := range header[name] {
+		tokens := strings.Split(s, ",")
+		for _, t := range tokens {
+			t = strings.Trim(t, " \t")
+			if strings.EqualFold(t, value) {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// Return true if the client has "permessage-deflate" in its extensions.
+func wsClientSupportsCompression(header http.Header) bool {
+	for _, extensionList := range header["Sec-Websocket-Extensions"] {
+		extensions := strings.Split(extensionList, ",")
+		for _, extension := range extensions {
+			extension = strings.Trim(extension, " \t")
+			params := strings.Split(extension, ";")
+			for _, p := range params {
+				p = strings.Trim(p, " \t")
+				if strings.EqualFold(p, "permessage-deflate") {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
+// Send an HTTP error with the given `status`` to the given http response writer `w`.
+// Return an error created based on the `reason` string.
+func wsReturnHTTPError(w http.ResponseWriter, status int, reason string) error {
+	err := fmt.Errorf("websocket handshake error: %s", reason)
+	w.Header().Set("Sec-Websocket-Version", "13")
+	http.Error(w, http.StatusText(status), status)
+	return err
+}
+
+// If the server is configured to accept any origin, then this function returns
+// `nil` without checking if the Origin is present and valid.
+// Otherwise, this will check that the Origin matches the same origine or
+// any origin in the allowed list.
+func (w *srvWebsocket) checkOrigin(r *http.Request) error {
+	w.mu.RLock()
+	checkSame := w.sameOrigin
+	listEmpty := len(w.allowedOrigins) == 0
+	w.mu.RUnlock()
+	if !checkSame && listEmpty {
+		return nil
+	}
+	origin := r.Header.Get("Origin")
+	if origin == "" {
+		origin = r.Header.Get("Sec-Websocket-Origin")
+	}
+	if origin == "" {
+		return errors.New("origin not provided")
+	}
+	u, err := url.ParseRequestURI(origin)
+	if err != nil {
+		return err
+	}
+	oh, op, err := wsGetHostAndPort(u.Scheme == "https", u.Host)
+	if err != nil {
+		return err
+	}
+	// If checking same origin, compare with the http's request's Host.
+	if checkSame {
+		rh, rp, err := wsGetHostAndPort(r.TLS != nil, r.Host)
+		if err != nil {
+			return err
+		}
+		if oh != rh || op != rp {
+			return errors.New("not same origin")
+		}
+		// I guess it is possible to have cases where one wants to check
+		// same origin, but also that the origin is in the allowed list.
+		// So continue with the next check.
+	}
+	if !listEmpty {
+		w.mu.RLock()
+		ao := w.allowedOrigins[oh]
+		w.mu.RUnlock()
+		if ao == nil || u.Scheme != ao.scheme || op != ao.port {
+			return errors.New("not in the allowed list")
+		}
+	}
+	return nil
+}
+
+func wsGetHostAndPort(tls bool, hostport string) (string, string, error) {
+	host, port, err := net.SplitHostPort(hostport)
+	if err != nil {
+		// If error is missing port, then use defaults based on the scheme
+		if ae, ok := err.(*net.AddrError); ok && strings.Contains(ae.Err, "missing port") {
+			err = nil
+			host = hostport
+			if tls {
+				port = "443"
+			} else {
+				port = "80"
+			}
+		}
+	}
+	return strings.ToLower(host), port, err
+}
+
+// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
+// and returns it as a based64 encoded string.
+func wsAcceptKey(key string) string {
+	h := sha1.New()
+	h.Write([]byte(key))
+	h.Write(wsGUID)
+	return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
+
+// Validate the websocket related options.
+func validateWebsocketOptions(o *Options) error {
+	wo := &o.Websocket
+	// If no port is defined, we don't care about other options
+	if wo.Port == 0 {
+		return nil
+	}
+	// Enforce TLS...
+	if wo.TLSConfig == nil {
+		return errors.New("websocket requires TLS configuration")
+	}
+	// Make sure that allowed origins, if specified, can be parsed.
+	for _, ao := range wo.AllowedOrigins {
+		if _, err := url.Parse(ao); err != nil {
+			return fmt.Errorf("unable to parse allowed origin: %v", err)
+		}
+	}
+	return nil
+}
+
+// Creates or updates the existing map
+func (s *Server) wsSetOriginOptions(o *WebsocketOpts) {
+	ws := &s.websocket
+	ws.mu.Lock()
+	defer ws.mu.Unlock()
+	// Copy over the option's same origin boolean
+	ws.sameOrigin = o.SameOrigin
+	// Reset the map. Will help for config reload if/when we support it.
+	ws.allowedOrigins = nil
+	if o.AllowedOrigins == nil {
+		return
+	}
+	for _, ao := range o.AllowedOrigins {
+		// We have previously checked (during options validation) that the urls
+		// are parseable, but if we get an error, report and skip.
+		u, err := url.ParseRequestURI(ao)
+		if err != nil {
+			s.Errorf("error parsing allowed origin: %v", err)
+			continue
+		}
+		h, p, _ := wsGetHostAndPort(u.Scheme == "https", u.Host)
+		if ws.allowedOrigins == nil {
+			ws.allowedOrigins = make(map[string]*allowedOrigin, len(o.AllowedOrigins))
+		}
+		ws.allowedOrigins[h] = &allowedOrigin{scheme: u.Scheme, port: p}
+	}
+}
+
+func (s *Server) startWebsocketServer() {
+	sopts := s.getOpts()
+	o := &sopts.Websocket
+
+	s.wsSetOriginOptions(o)
+
+	var hl net.Listener
+	var proto string
+	var err error
+
+	port := o.Port
+	if port == -1 {
+		port = 0
+	}
+	hp := net.JoinHostPort(o.Host, strconv.Itoa(port))
+
+	// We are enforcing (when validating the options) the use of TLS, but the
+	// code was originally supporting both modes. The reason for TLS only is
+	// that we expect users to send JWTs with bearer tokens and we want to
+	// avoid the possibility of it being "intercepted".
+
+	if o.TLSConfig != nil {
+		proto = "wss"
+		config := o.TLSConfig.Clone()
+		config.ClientAuth = tls.NoClientCert
+		hl, err = tls.Listen("tcp", hp, config)
+	} else {
+		proto = "ws"
+		hl, err = net.Listen("tcp", hp)
+	}
+	if err != nil {
+		s.Fatalf("Unable to listen for websocket connections: %v", err)
+		return
+	}
+	s.Noticef("Listening for websocket clients on %s://%s:%d", proto, o.Host, port)
+
+	s.mu.Lock()
+	s.websocket.tls = proto == "wss"
+	if port == 0 {
+		s.opts.Websocket.Port = hl.Addr().(*net.TCPAddr).Port
+	}
+	s.websocket.connectURLs, err = s.getConnectURLs(o.Advertise, o.Host, o.Port)
+	if err != nil {
+		s.Fatalf("Unable to get websocket connect URLs: %v", err)
+		hl.Close()
+		s.mu.Unlock()
+		return
+	}
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		res, err := s.wsUpgrade(w, r)
+		if err != nil {
+			s.Errorf(err.Error())
+			return
+		}
+		s.createClient(res.conn, res.ws)
+	})
+	hs := &http.Server{
+		Addr:        hp,
+		Handler:     mux,
+		ReadTimeout: o.HandshakeTimeout,
+		ErrorLog:    log.New(&wsCaptureHTTPServerLog{s}, "", 0),
+	}
+	s.websocket.server = hs
+	s.websocket.listener = hl
+	s.mu.Unlock()
+
+	s.startGoRoutine(func() {
+		defer s.grWG.Done()
+
+		if err := hs.Serve(hl); err != http.ErrServerClosed {
+			s.Fatalf("websocket listener error: %v", err)
+		}
+		s.done <- true
+	})
+}
+
+type wsCaptureHTTPServerLog struct {
+	s *Server
+}
+
+func (cl *wsCaptureHTTPServerLog) Write(p []byte) (int, error) {
+	var buf [128]byte
+	var b = buf[:0]
+
+	copy(b, []byte("websocket :"))
+	offset := 0
+	if bytes.HasPrefix(p, []byte("http:")) {
+		offset = 6
+	}
+	b = append(b, p[offset:]...)
+	cl.s.Errorf(string(b))
+	return len(p), nil
+}
+
+func (c *client) wsCollapsePtoNB() (net.Buffers, int64) {
+	var nb net.Buffers
+	var total = 0
+	var mfs = 0
+	if c.ws.browser {
+		mfs = wsFrameSizeForBrowsers
+	}
+	if len(c.out.p) > 0 {
+		p := c.out.p
+		c.out.p = nil
+		nb = append(c.out.nb, p)
+	} else if len(c.out.nb) > 0 {
+		nb = c.out.nb
+	}
+	// Start with possible already framed buffers (that we could have
+	// got from partials or control messages such as ws pings or pongs).
+	bufs := c.ws.frames
+	if c.ws.compress && len(nb) > 0 {
+		buf := &bytes.Buffer{}
+
+		cp := c.ws.compressor
+		if cp == nil {
+			c.ws.compressor, _ = flate.NewWriter(buf, flate.BestSpeed)
+			cp = c.ws.compressor
+		} else {
+			cp.Reset(buf)
+		}
+		var usz int
+		var csz int
+		for _, b := range nb {
+			usz += len(b)
+			cp.Write(b)
+		}
+		cp.Close()
+		b := buf.Bytes()
+		p := b[:len(b)-4]
+		if mfs > 0 && len(p) > mfs {
+			for first, final := true, false; len(p) > 0; first = false {
+				lp := len(p)
+				if lp > mfs {
+					lp = mfs
+				} else {
+					final = true
+				}
+				fh := make([]byte, wsMaxFrameHeaderSize)
+				n := wsFillFrameHeader(fh, first, final, wsCompressedFrame, wsBinaryMessage, lp)
+				bufs = append(bufs, fh[:n], p[:lp])
+				csz += n + lp
+				p = p[lp:]
+			}
+		} else {
+			h := wsCreateFrameHeader(true, wsBinaryMessage, len(p))
+			bufs = append(bufs, h, p)
+			csz = len(h) + len(p)
+		}
+		// Add to pb the compressed data size (including headers), but
+		// remove the original uncompressed data size that was added
+		// during the queueing.
+		c.out.pb += int64(csz) - int64(usz)
+		c.ws.fs += int64(csz)
+	} else if len(nb) > 0 {
+		if mfs > 0 {
+			// We are limiting the frame size.
+			startFrame := func() int {
+				bufs = append(bufs, make([]byte, wsMaxFrameHeaderSize))
+				return len(bufs) - 1
+			}
+			endFrame := func(idx, size int) {
+				n := wsFillFrameHeader(bufs[idx], wsFirstFrame, wsFinalFrame, wsUncompressedFrame, wsBinaryMessage, size)
+				c.out.pb += int64(n)
+				c.ws.fs += int64(n + size)
+				bufs[idx] = bufs[idx][:n]
+			}
+
+			fhIdx := startFrame()
+			for i := 0; i < len(nb); i++ {
+				b := nb[i]
+				if total+len(b) <= mfs {
+					bufs = append(bufs, b)
+					total += len(b)
+					continue
+				}
+				for len(b) > 0 {
+					endFrame(fhIdx, total)
+					total = len(b)
+					if total >= mfs {
+						total = mfs
+					}
+					fhIdx = startFrame()
+					bufs = append(bufs, b[:total])
+					b = b[total:]
+				}
+			}
+			if total > 0 {
+				endFrame(fhIdx, total)
+			}
+		} else {
+			// If there is no limit on the frame size, create a single frame for
+			// all pending buffers.
+			for _, b := range nb {
+				total += len(b)
+			}
+			wsfh := wsCreateFrameHeader(false, wsBinaryMessage, total)
+			c.out.pb += int64(len(wsfh))
+			bufs = append(bufs, wsfh)
+			bufs = append(bufs, nb...)
+			c.ws.fs += int64(len(wsfh) + total)
+		}
+	}
+	if len(c.ws.closeMsg) > 0 {
+		bufs = append(bufs, c.ws.closeMsg)
+		c.ws.fs += int64(len(c.ws.closeMsg))
+		c.ws.closeMsg = nil
+	}
+	c.ws.frames = nil
+	return bufs, c.ws.fs
+}


Test patch:
diff --git a/server/client_test.go b/server/client_test.go
index fce36a62..6fd4cb34 100644
--- a/server/client_test.go
+++ b/server/client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2012-2019 The NATS Authors
+// Copyright 2012-2020 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
@@ -72,7 +72,7 @@ func (c *testAsyncClient) parseAndClose(proto []byte) {
 func createClientAsync(ch chan *client, s *Server, cli net.Conn) {
 	s.grWG.Add(1)
 	go func() {
-		c := s.createClient(cli)
+		c := s.createClient(cli, nil)
 		// Must be here to suppress +OK
 		c.opts.Verbose = false
 		go c.writeLoop()
@@ -2163,6 +2163,10 @@ func (c *testConnWritePartial) Write(p []byte) (int, error) {
 	return c.buf.Write(p[:n])
 }
 
+func (c *testConnWritePartial) RemoteAddr() net.Addr {
+	return nil
+}
+
 func (c *testConnWritePartial) SetWriteDeadline(_ time.Time) error {
 	return nil
 }
@@ -2279,7 +2283,7 @@ func TestCloseConnectionVeryEarly(t *testing.T) {
 	// Call again with this closed connection. Alternatively, we
 	// would have to call with a fake connection that implements
 	// net.Conn but returns an error on Write.
-	s.createClient(c)
+	s.createClient(c, nil)
 
 	// This connection should not have been added to the server.
 	checkClientsCount(t, s, 0)
diff --git a/server/gateway_test.go b/server/gateway_test.go
index 2c981a6a..8b77be60 100644
--- a/server/gateway_test.go
+++ b/server/gateway_test.go
@@ -154,7 +154,7 @@ func waitCh(t *testing.T, ch chan bool, errTxt string) {
 	}
 }
 
-func natsConnect(t *testing.T, url string, options ...nats.Option) *nats.Conn {
+func natsConnect(t testing.TB, url string, options ...nats.Option) *nats.Conn {
 	t.Helper()
 	nc, err := nats.Connect(url, options...)
 	if err != nil {
@@ -215,7 +215,7 @@ func natsFlush(t *testing.T, nc *nats.Conn) {
 	}
 }
 
-func natsPub(t *testing.T, nc *nats.Conn, subj string, payload []byte) {
+func natsPub(t testing.TB, nc *nats.Conn, subj string, payload []byte) {
 	t.Helper()
 	if err := nc.Publish(subj, payload); err != nil {
 		t.Fatalf("Error on publish: %v", err)
diff --git a/server/websocket_test.go b/server/websocket_test.go
new file mode 100644
index 00000000..3dc5df96
--- /dev/null
+++ b/server/websocket_test.go
@@ -0,0 +1,2779 @@
+// Copyright 2020 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 (
+	"bufio"
+	"bytes"
+	"compress/flate"
+	"crypto/tls"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"math/rand"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"reflect"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+)
+
+type testReader struct {
+	buf []byte
+	pos int
+	max int
+	err error
+}
+
+func (tr *testReader) Read(p []byte) (int, error) {
+	if tr.err != nil {
+		return 0, tr.err
+	}
+	n := len(tr.buf) - tr.pos
+	if n == 0 {
+		return 0, nil
+	}
+	if n > cap(p) {
+		n = cap(p)
+	}
+	if tr.max > 0 && n > tr.max {
+		n = tr.max
+	}
+	copy(p, tr.buf[tr.pos:tr.pos+n])
+	tr.pos += n
+	return n, nil
+}
+
+func TestWSGet(t *testing.T) {
+	rb := []byte("012345")
+
+	tr := &testReader{buf: []byte("6789")}
+
+	for _, test := range []struct {
+		name   string
+		pos    int
+		needed int
+		newpos int
+		trmax  int
+		result string
+		reterr bool
+	}{
+		{"fromrb1", 0, 3, 3, 4, "012", false},    // Partial from read buffer
+		{"fromrb2", 3, 2, 5, 4, "34", false},     // Partial from read buffer
+		{"fromrb3", 5, 1, 6, 4, "5", false},      // Partial from read buffer
+		{"fromtr1", 4, 4, 6, 4, "4567", false},   // Partial from read buffer + some of ioReader
+		{"fromtr2", 4, 6, 6, 4, "456789", false}, // Partial from read buffer + all of ioReader
+		{"fromtr3", 4, 6, 6, 2, "456789", false}, // Partial from read buffer + all of ioReader with several reads
+		{"fromtr4", 4, 6, 6, 2, "", true},        // ioReader returns error
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			tr.pos = 0
+			tr.max = test.trmax
+			if test.reterr {
+				tr.err = fmt.Errorf("on purpose")
+			}
+			res, np, err := wsGet(tr, rb, test.pos, test.needed)
+			if test.reterr {
+				if err == nil {
+					t.Fatalf("Expected error, got none")
+				}
+				if err.Error() != "on purpose" {
+					t.Fatalf("Unexpected error: %v", err)
+				}
+				if np != 0 || res != nil {
+					t.Fatalf("Unexpected returned values: res=%v n=%v", res, np)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("Error on get: %v", err)
+			}
+			if np != test.newpos {
+				t.Fatalf("Expected pos=%v, got %v", test.newpos, np)
+			}
+			if string(res) != test.result {
+				t.Fatalf("Invalid returned content: %s", res)
+			}
+		})
+	}
+}
+
+func TestWSIsControlFrame(t *testing.T) {
+	for _, test := range []struct {
+		name      string
+		code      wsOpCode
+		isControl bool
+	}{
+		{"binary", wsBinaryMessage, false},
+		{"text", wsTextMessage, false},
+		{"ping", wsPingMessage, true},
+		{"pong", wsPongMessage, true},
+		{"close", wsCloseMessage, true},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			if res := wsIsControlFrame(test.code); res != test.isControl {
+				t.Fatalf("Expected %q isControl to be %v, got %v", test.name, test.isControl, res)
+			}
+		})
+	}
+}
+
+func testWSSimpleMask(key, buf []byte) {
+	for i := 0; i < len(buf); i++ {
+		buf[i] ^= key[i&3]
+	}
+}
+
+func TestWSUnmask(t *testing.T) {
+	key := []byte{1, 2, 3, 4}
+	orgBuf := []byte("this is a clear text")
+
+	mask := func() []byte {
+		t.Helper()
+		buf := append([]byte(nil), orgBuf...)
+		testWSSimpleMask(key, buf)
+		// First ensure that the content is masked.
+		if bytes.Equal(buf, orgBuf) {
+			t.Fatalf("Masking did not do anything: %q", buf)
+		}
+		return buf
+	}
+
+	ri := &wsReadInfo{}
+	ri.init()
+	copy(ri.mkey[:], key)
+
+	buf := mask()
+	// Unmask in one call
+	ri.unmask(buf)
+	if !bytes.Equal(buf, orgBuf) {
+		t.Fatalf("Unmask error, expected %q, got %q", orgBuf, buf)
+	}
+
+	// Unmask in multiple calls
+	buf = mask()
+	ri.mkpos = 0
+	ri.unmask(buf[:3])
+	ri.unmask(buf[3:11])
+	ri.unmask(buf[11:])
+	if !bytes.Equal(buf, orgBuf) {
+		t.Fatalf("Unmask error, expected %q, got %q", orgBuf, buf)
+	}
+}
+
+func TestWSCreateCloseMessage(t *testing.T) {
+	for _, test := range []struct {
+		name      string
+		status    int
+		psize     int
+		truncated bool
+	}{
+		{"fits", wsCloseStatusInternalSrvError, 10, false},
+		{"truncated", wsCloseStatusProtocolError, wsMaxControlPayloadSize + 10, true},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			payload := make([]byte, test.psize)
+			for i := 0; i < len(payload); i++ {
+				payload[i] = byte('A' + (i % 26))
+			}
+			res := wsCreateCloseMessage(test.status, string(payload))
+			if status := binary.BigEndian.Uint16(res[:2]); int(status) != test.status {
+				t.Fatalf("Expected status to be %v, got %v", test.status, status)
+			}
+			psize := len(res) - 2
+			if !test.truncated {
+				if int(psize) != test.psize {
+					t.Fatalf("Expected size to be %v, got %v", test.psize, psize)
+				}
+				if !bytes.Equal(res[2:], payload) {
+					t.Fatalf("Unexpected result: %q", res[2:])
+				}
+				return
+			}
+			// Since the payload of a close message contains a 2 byte status, the
+			// actual max text size will be wsMaxControlPayloadSize-2
+			if int(psize) != wsMaxControlPayloadSize-2 {
+				t.Fatalf("Expected size to be capped to %v, got %v", wsMaxControlPayloadSize-2, psize)
+			}
+			if string(res[len(res)-3:]) != "..." {
+				t.Fatalf("Expected res to have `...` at the end, got %q", res[4:])
+			}
+		})
+	}
+}
+
+func TestWSCreateFrameHeader(t *testing.T) {
+	for _, test := range []struct {
+		name       string
+		frameType  wsOpCode
+		compressed bool
+		len        int
+	}{
+		{"uncompressed 10", wsBinaryMessage, false, 10},
+		{"uncompressed 600", wsTextMessage, false, 600},
+		{"uncompressed 100000", wsTextMessage, false, 100000},
+		{"compressed 10", wsBinaryMessage, true, 10},
+		{"compressed 600", wsBinaryMessage, true, 600},
+		{"compressed 100000", wsTextMessage, true, 100000},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			res := wsCreateFrameHeader(test.compressed, test.frameType, test.len)
+			// The server is always sending the message has a single frame,
+			// so the "final" bit should be set.
+			expected := byte(test.frameType) | wsFinalBit
+			if test.compressed {
+				expected |= wsRsv1Bit
+			}
+			if b := res[0]; b != expected {
+				t.Fatalf("Expected first byte to be %v, got %v", expected, b)
+			}
+			switch {
+			case test.len <= 125:
+				if len(res) != 2 {
+					t.Fatalf("Frame len should be 2, got %v", len(res))
+				}
+				if res[1] != byte(test.len) {
+					t.Fatalf("Expected len to be in second byte and be %v, got %v", test.len, res[1])
+				}
+			case test.len < 65536:
+				// 1+1+2
+				if len(res) != 4 {
+					t.Fatalf("Frame len should be 4, got %v", len(res))
+				}
+				if res[1] != 126 {
+					t.Fatalf("Second byte value should be 126, got %v", res[1])
+				}
+				if rl := binary.BigEndian.Uint16(res[2:]); int(rl) != test.len {
+					t.Fatalf("Expected len to be %v, got %v", test.len, rl)
+				}
+			default:
+				// 1+1+8
+				if len(res) != 10 {
+					t.Fatalf("Frame len should be 10, got %v", len(res))
+				}
+				if res[1] != 127 {
+					t.Fatalf("Second byte value should be 127, got %v", res[1])
+				}
+				if rl := binary.BigEndian.Uint64(res[2:]); int(rl) != test.len {
+					t.Fatalf("Expected len to be %v, got %v", test.len, rl)
+				}
+			}
+		})
+	}
+}
+
+func testWSCreateClientMsg(frameType wsOpCode, frameNum int, final, compressed bool, payload []byte) []byte {
+	if compressed {
+		buf := &bytes.Buffer{}
+		compressor, _ := flate.NewWriter(buf, 1)
+		compressor.Write(payload)
+		compressor.Flush()
+		payload = buf.Bytes()
+		// The last 4 bytes are dropped
+		payload = payload[:len(payload)-4]
+	}
+	frame := make([]byte, 14+len(payload))
+	if frameNum == 1 {
+		frame[0] = byte(frameType)
+	}
+	if final {
+		frame[0] |= wsFinalBit
+	}
+	if compressed {
+		frame[0] |= wsRsv1Bit
+	}
+	pos := 1
+	lenPayload := len(payload)
+	switch {
+	case lenPayload <= 125:
+		frame[pos] = byte(lenPayload) | wsMaskBit
+		pos++
+	case lenPayload < 65536:
+		frame[pos] = 126 | wsMaskBit
+		binary.BigEndian.PutUint16(frame[2:], uint16(lenPayload))
+		pos += 3
+	default:
+		frame[1] = 127 | wsMaskBit
+		binary.BigEndian.PutUint64(frame[2:], uint64(lenPayload))
+		pos += 9
+	}
+	key := []byte{1, 2, 3, 4}
+	copy(frame[pos:], key)
+	pos += 4
+	copy(frame[pos:], payload)
+	testWSSimpleMask(key, frame[pos:])
+	pos += lenPayload
+	return frame[:pos]
+}
+
+func testWSSetupForRead() (*client, *wsReadInfo, *testReader) {
+	ri := &wsReadInfo{}
+	ri.init()
+	tr := &testReader{}
+	opts := DefaultOptions()
+	opts.MaxPending = MAX_PENDING_SIZE
+	s := &Server{opts: opts}
+	c := &client{srv: s, ws: &websocket{}}
+	c.initClient()
+	return c, ri, tr
+}
+
+func TestWSReadUncompressedFrames(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	// Create 2 WS messages
+	pl1 := []byte("first message")
+	wsmsg1 := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, pl1)
+	pl2 := []byte("second message")
+	wsmsg2 := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, pl2)
+	// Add both in single buffer
+	orgrb := append([]byte(nil), wsmsg1...)
+	orgrb = append(orgrb, wsmsg2...)
+
+	rb := append([]byte(nil), orgrb...)
+	bufs, err := c.wsRead(ri, tr, rb)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 2 {
+		t.Fatalf("Expected 2 buffers, got %v", n)
+	}
+	if !bytes.Equal(bufs[0], pl1) {
+		t.Fatalf("Unexpected content for buffer 1: %s", bufs[0])
+	}
+	if !bytes.Equal(bufs[1], pl2) {
+		t.Fatalf("Unexpected content for buffer 2: %s", bufs[1])
+	}
+
+	// Now reset and try with the read buffer not containing full ws frame
+	c, ri, tr = testWSSetupForRead()
+	rb = append([]byte(nil), orgrb...)
+	// Frame is 1+1+4+'first message'. So say we pass with rb of 11 bytes,
+	// then we should get "first"
+	bufs, err = c.wsRead(ri, tr, rb[:11])
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 1 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	if string(bufs[0]) != "first" {
+		t.Fatalf("Unexpected content: %q", bufs[0])
+	}
+	// Call again with more data..
+	bufs, err = c.wsRead(ri, tr, rb[11:32])
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 2 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	if string(bufs[0]) != " message" {
+		t.Fatalf("Unexpected content: %q", bufs[0])
+	}
+	if string(bufs[1]) != "second " {
+		t.Fatalf("Unexpected content: %q", bufs[1])
+	}
+	// Call with the rest
+	bufs, err = c.wsRead(ri, tr, rb[32:])
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 1 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	if string(bufs[0]) != "message" {
+		t.Fatalf("Unexpected content: %q", bufs[0])
+	}
+}
+
+func TestWSReadCompressedFrames(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	uncompressed := []byte("this is the uncompress data")
+	wsmsg1 := testWSCreateClientMsg(wsBinaryMessage, 1, true, true, uncompressed)
+	rb := append([]byte(nil), wsmsg1...)
+	// Call with some but not all of the payload
+	bufs, err := c.wsRead(ri, tr, rb[:10])
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 0 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	// Call with the rest, only then should we get the uncompressed data.
+	bufs, err = c.wsRead(ri, tr, rb[10:])
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 1 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	if !bytes.Equal(bufs[0], uncompressed) {
+		t.Fatalf("Unexpected content: %s", bufs[0])
+	}
+	// Stress the fact that we use a pool and want to make sure
+	// that if we get a decompressor from the pool, it is properly reset
+	// with the buffer to decompress.
+	for i := 0; i < 9; i++ {
+		rb = append(rb, wsmsg1...)
+	}
+	bufs, err = c.wsRead(ri, tr, rb)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 10 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+}
+
+func TestWSReadCompressedFrameCorrupted(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	uncompressed := []byte("this is the uncompress data")
+	wsmsg1 := testWSCreateClientMsg(wsBinaryMessage, 1, true, true, uncompressed)
+	copy(wsmsg1[10:], []byte{1, 2, 3, 4})
+	rb := append([]byte(nil), wsmsg1...)
+	bufs, err := c.wsRead(ri, tr, rb)
+	if err == nil || !strings.Contains(err.Error(), "corrupt") {
+		t.Fatalf("Expected error about corrupted data, got %v", err)
+	}
+	if n := len(bufs); n != 0 {
+		t.Fatalf("Expected no buffer, got %v", n)
+	}
+}
+
+func TestWSReadVariousFrameSizes(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		size int
+	}{
+		{"tiny", 100},
+		{"medium", 1000},
+		{"large", 70000},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			c, ri, tr := testWSSetupForRead()
+			uncompressed := make([]byte, test.size)
+			for i := 0; i < len(uncompressed); i++ {
+				uncompressed[i] = 'A' + byte(i%26)
+			}
+			wsmsg1 := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, uncompressed)
+			rb := append([]byte(nil), wsmsg1...)
+			bufs, err := c.wsRead(ri, tr, rb)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if n := len(bufs); n != 1 {
+				t.Fatalf("Unexpected buffer returned: %v", n)
+			}
+			if !bytes.Equal(bufs[0], uncompressed) {
+				t.Fatalf("Unexpected content: %s", bufs[0])
+			}
+		})
+	}
+}
+
+func TestWSReadFragmentedFrames(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	payloads := []string{"first", "second", "third"}
+	var rb []byte
+	for i := 0; i < len(payloads); i++ {
+		final := i == len(payloads)-1
+		frag := testWSCreateClientMsg(wsBinaryMessage, i+1, final, false, []byte(payloads[i]))
+		rb = append(rb, frag...)
+	}
+	bufs, err := c.wsRead(ri, tr, rb)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 3 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	for i, expected := range payloads {
+		if string(bufs[i]) != expected {
+			t.Fatalf("Unexpected content for buf=%v: %s", i, bufs[i])
+		}
+	}
+}
+
+func TestWSReadPartialFrameHeaderAtEndOfReadBuffer(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	msg1 := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("msg1"))
+	msg2 := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("msg2"))
+	rb := append([]byte(nil), msg1...)
+	rb = append(rb, msg2...)
+	// We will pass the first frame + the first byte of the next frame.
+	rbl := rb[:len(msg1)+1]
+	// Make the io reader return the rest of the frame
+	tr.buf = rb[len(msg1)+1:]
+	bufs, err := c.wsRead(ri, tr, rbl)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 1 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	// We should not have asked to the io reader more than what is needed for reading
+	// the frame header. Since we had already the first byte in the read buffer,
+	// tr.pos should be 1(size)+4(key)=5
+	if tr.pos != 5 {
+		t.Fatalf("Expected reader pos to be 5, got %v", tr.pos)
+	}
+}
+
+func TestWSReadPingFrame(t *testing.T) {
+	for _, test := range []struct {
+		name    string
+		payload []byte
+	}{
+		{"without payload", nil},
+		{"with payload", []byte("optional payload")},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			c, ri, tr := testWSSetupForRead()
+			ping := testWSCreateClientMsg(wsPingMessage, 1, true, false, test.payload)
+			rb := append([]byte(nil), ping...)
+			bufs, err := c.wsRead(ri, tr, rb)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if n := len(bufs); n != 0 {
+				t.Fatalf("Unexpected buffer returned: %v", n)
+			}
+			// A PONG should have been queued with the payload of the ping
+			c.mu.Lock()
+			nb, _ := c.collapsePtoNB()
+			c.mu.Unlock()
+			if n := len(nb); n == 0 {
+				t.Fatalf("Expected buffers, got %v", n)
+			}
+			if expected := 2 + len(test.payload); expected != len(nb[0]) {
+				t.Fatalf("Expected buffer to be %v bytes long, got %v", expected, len(nb[0]))
+			}
+			b := nb[0][0]
+			if b&wsFinalBit == 0 {
+				t.Fatalf("Control frame should have been the final flag, it was not set: %v", b)
+			}
+			if b&byte(wsPongMessage) == 0 {
+				t.Fatalf("Should have been a PONG, it wasn't: %v", b)
+			}
+			if len(test.payload) > 0 {
+				if !bytes.Equal(nb[0][2:], test.payload) {
+					t.Fatalf("Unexpected content: %s", nb[0][2:])
+				}
+			}
+		})
+	}
+}
+
+func TestWSReadPongFrame(t *testing.T) {
+	for _, test := range []struct {
+		name    string
+		payload []byte
+	}{
+		{"without payload", nil},
+		{"with payload", []byte("optional payload")},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			c, ri, tr := testWSSetupForRead()
+			pong := testWSCreateClientMsg(wsPongMessage, 1, true, false, test.payload)
+			rb := append([]byte(nil), pong...)
+			bufs, err := c.wsRead(ri, tr, rb)
+			if err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if n := len(bufs); n != 0 {
+				t.Fatalf("Unexpected buffer returned: %v", n)
+			}
+			// Nothing should be sent...
+			c.mu.Lock()
+			nb, _ := c.collapsePtoNB()
+			c.mu.Unlock()
+			if n := len(nb); n != 0 {
+				t.Fatalf("Expected no buffer, got %v", n)
+			}
+		})
+	}
+}
+
+func TestWSReadCloseFrame(t *testing.T) {
+	for _, test := range []struct {
+		name    string
+		payload []byte
+	}{
+		{"without payload", nil},
+		{"with payload", []byte("optional payload")},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			c, ri, tr := testWSSetupForRead()
+			// a close message has a status in 2 bytes + optional payload
+			payload := make([]byte, 2+len(test.payload))
+			binary.BigEndian.PutUint16(payload[:2], wsCloseStatusNormalClosure)
+			if len(test.payload) > 0 {
+				copy(payload[2:], test.payload)
+			}
+			close := testWSCreateClientMsg(wsCloseMessage, 1, true, false, payload)
+			// Have a normal frame prior to close to make sure that wsRead returns
+			// the normal frame along with io.EOF to indicate that wsCloseMessage was received.
+			msg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("msg"))
+			rb := append([]byte(nil), msg...)
+			rb = append(rb, close...)
+			bufs, err := c.wsRead(ri, tr, rb)
+			// It is expected that wsRead returns io.EOF on processing a close.
+			if err != io.EOF {
+				t.Fatalf("Unexpected error: %v", err)
+			}
+			if n := len(bufs); n != 1 {
+				t.Fatalf("Unexpected buffer returned: %v", n)
+			}
+			if string(bufs[0]) != "msg" {
+				t.Fatalf("Unexpected content: %s", bufs[0])
+			}
+			// A CLOSE should have been queued with the payload of the original close message.
+			c.mu.Lock()
+			nb, _ := c.collapsePtoNB()
+			c.mu.Unlock()
+			if n := len(nb); n == 0 {
+				t.Fatalf("Expected buffers, got %v", n)
+			}
+			if expected := 2 + 2 + len(test.payload); expected != len(nb[0]) {
+				t.Fatalf("Expected buffer to be %v bytes long, got %v", expected, len(nb[0]))
+			}
+			b := nb[0][0]
+			if b&wsFinalBit == 0 {
+				t.Fatalf("Control frame should have been the final flag, it was not set: %v", b)
+			}
+			if b&byte(wsCloseMessage) == 0 {
+				t.Fatalf("Should have been a CLOSE, it wasn't: %v", b)
+			}
+			if status := binary.BigEndian.Uint16(nb[0][2:4]); status != wsCloseStatusNormalClosure {
+				t.Fatalf("Expected status to be %v, got %v", wsCloseStatusNormalClosure, status)
+			}
+			if len(test.payload) > 0 {
+				if !bytes.Equal(nb[0][4:], test.payload) {
+					t.Fatalf("Unexpected content: %s", nb[0][4:])
+				}
+			}
+		})
+	}
+}
+
+func TestWSReadControlFrameBetweebFragmentedFrames(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	frag1 := testWSCreateClientMsg(wsBinaryMessage, 1, false, false, []byte("first"))
+	frag2 := testWSCreateClientMsg(wsBinaryMessage, 2, true, false, []byte("second"))
+	ctrl := testWSCreateClientMsg(wsPongMessage, 1, true, false, nil)
+	rb := append([]byte(nil), frag1...)
+	rb = append(rb, ctrl...)
+	rb = append(rb, frag2...)
+	bufs, err := c.wsRead(ri, tr, rb)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 2 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	if string(bufs[0]) != "first" {
+		t.Fatalf("Unexpected content: %s", bufs[0])
+	}
+	if string(bufs[1]) != "second" {
+		t.Fatalf("Unexpected content: %s", bufs[1])
+	}
+}
+
+func TestWSReadGetErrors(t *testing.T) {
+	tr := &testReader{err: fmt.Errorf("on purpose")}
+	for _, test := range []struct {
+		lenPayload int
+		rbextra    int
+	}{
+		{10, 1},
+		{10, 3},
+		{200, 1},
+		{200, 2},
+		{200, 5},
+		{70000, 1},
+		{70000, 5},
+		{70000, 13},
+	} {
+		t.Run("", func(t *testing.T) {
+			c, ri, _ := testWSSetupForRead()
+			msg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("msg"))
+			frame := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, make([]byte, test.lenPayload))
+			rb := append([]byte(nil), msg...)
+			rb = append(rb, frame...)
+			bufs, err := c.wsRead(ri, tr, rb[:len(msg)+test.rbextra])
+			if err == nil || err.Error() != "on purpose" {
+				t.Fatalf("Expected 'on purpose' error, got %v", err)
+			}
+			if n := len(bufs); n != 1 {
+				t.Fatalf("Unexpected buffer returned: %v", n)
+			}
+			if string(bufs[0]) != "msg" {
+				t.Fatalf("Unexpected content: %s", bufs[0])
+			}
+		})
+	}
+}
+
+func TestWSHandleControlFrameErrors(t *testing.T) {
+	c, ri, tr := testWSSetupForRead()
+	tr.err = fmt.Errorf("on purpose")
+
+	// a close message has a status in 2 bytes + optional payload
+	text := []byte("this is a close message")
+	payload := make([]byte, 2+len(text))
+	binary.BigEndian.PutUint16(payload[:2], wsCloseStatusNormalClosure)
+	copy(payload[2:], text)
+	ctrl := testWSCreateClientMsg(wsCloseMessage, 1, true, false, payload)
+
+	bufs, err := c.wsRead(ri, tr, ctrl[:len(ctrl)-4])
+	if err == nil || err.Error() != "on purpose" {
+		t.Fatalf("Expected 'on purpose' error, got %v", err)
+	}
+	if n := len(bufs); n != 0 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+
+	// Alter the content of close message. It is supposed to be valid utf-8.
+	c, ri, tr = testWSSetupForRead()
+	cp := append([]byte(nil), payload...)
+	cp[10] = 0xF1
+	ctrl = testWSCreateClientMsg(wsCloseMessage, 1, true, false, cp)
+	bufs, err = c.wsRead(ri, tr, ctrl)
+	// We should still receive an EOF but the message enqueued to the client
+	// should contain wsCloseStatusInvalidPayloadData and the error about invalid utf8
+	if err != io.EOF {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if n := len(bufs); n != 0 {
+		t.Fatalf("Unexpected buffer returned: %v", n)
+	}
+	c.mu.Lock()
+	nb, _ := c.collapsePtoNB()
+	c.mu.Unlock()
+	if n := len(nb); n == 0 {
+		t.Fatalf("Expected buffers, got %v", n)
+	}
+	b := nb[0][0]
+	if b&wsFinalBit == 0 {
+		t.Fatalf("Control frame should have been the final flag, it was not set: %v", b)
+	}
+	if b&byte(wsCloseMessage) == 0 {
+		t.Fatalf("Should have been a CLOSE, it wasn't: %v", b)
+	}
+	if status := binary.BigEndian.Uint16(nb[0][2:4]); status != wsCloseStatusInvalidPayloadData {
+		t.Fatalf("Expected status to be %v, got %v", wsCloseStatusInvalidPayloadData, status)
+	}
+	if !bytes.Contains(nb[0][4:], []byte("utf8")) {
+		t.Fatalf("Unexpected content: %s", nb[0][4:])
+	}
+}
+
+func TestWSReadErrors(t *testing.T) {
+	for _, test := range []struct {
+		cframe func() []byte
+		err    string
+		nbufs  int
+	}{
+		{
+			func() []byte {
+				msg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("hello"))
+				msg[1] &= ^byte(wsMaskBit)
+				return msg
+			},
+			"mask bit missing", 1,
+		},
+		{
+			func() []byte {
+				return testWSCreateClientMsg(wsPingMessage, 1, true, false, make([]byte, 200))
+			},
+			"control frame length bigger than maximum allowed", 1,
+		},
+		{
+			func() []byte {
+				return testWSCreateClientMsg(wsPingMessage, 1, false, false, []byte("hello"))
+			},
+			"control frame does not have final bit set", 1,
+		},
+		{
+			func() []byte {
+				frag1 := testWSCreateClientMsg(wsBinaryMessage, 1, false, false, []byte("frag1"))
+				newMsg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("new message"))
+				all := append([]byte(nil), frag1...)
+				all = append(all, newMsg...)
+				return all
+			},
+			"new message started before final frame for previous message was received", 2,
+		},
+		{
+			func() []byte {
+				frag1 := testWSCreateClientMsg(wsBinaryMessage, 1, false, true, []byte("frag1"))
+				frag2 := testWSCreateClientMsg(wsBinaryMessage, 2, false, true, []byte("frag2"))
+				frag2[0] |= wsRsv1Bit
+				all := append([]byte(nil), frag1...)
+				all = append(all, frag2...)
+				return all
+			},
+			"invalid continuation frame", 2,
+		},
+		{
+			func() []byte {
+				return testWSCreateClientMsg(99, 1, false, false, []byte("hello"))
+			},
+			"unknown opcode", 1,
+		},
+	} {
+		t.Run(test.err, func(t *testing.T) {
+			c, ri, tr := testWSSetupForRead()
+			// Add a valid message first
+			msg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("hello"))
+			// Then add the bad frame
+			bad := test.cframe()
+			// Add them both to a read buffer
+			rb := append([]byte(nil), msg...)
+			rb = append(rb, bad...)
+			bufs, err := c.wsRead(ri, tr, rb)
+			if err == nil || !strings.Contains(err.Error(), test.err) {
+				t.Fatalf("Expected error to contain %q, got %q", test.err, err.Error())
+			}
+			if n := len(bufs); n != test.nbufs {
+				t.Fatalf("Unexpected number of buffers: %v", n)
+			}
+			if string(bufs[0]) != "hello" {
+				t.Fatalf("Unexpected content: %s", bufs[0])
+			}
+		})
+	}
+}
+
+func TestWSEnqueueCloseMsg(t *testing.T) {
+	for _, test := range []struct {
+		reason ClosedState
+		status int
+	}{
+		{ClientClosed, wsCloseStatusNormalClosure},
+		{AuthenticationTimeout, wsCloseStatusPolicyViolation},
+		{AuthenticationViolation, wsCloseStatusPolicyViolation},
+		{SlowConsumerPendingBytes, wsCloseStatusPolicyViolation},
+		{SlowConsumerWriteDeadline, wsCloseStatusPolicyViolation},
+		{MaxAccountConnectionsExceeded, wsCloseStatusPolicyViolation},
+		{MaxConnectionsExceeded, wsCloseStatusPolicyViolation},
+		{MaxControlLineExceeded, wsCloseStatusPolicyViolation},
+		{MaxSubscriptionsExceeded, wsCloseStatusPolicyViolation},
+		{MissingAccount, wsCloseStatusPolicyViolation},
+		{AuthenticationExpired, wsCloseStatusPolicyViolation},
+		{Revocation, wsCloseStatusPolicyViolation},
+		{TLSHandshakeError, wsCloseStatusTLSHandshake},
+		{ParseError, wsCloseStatusProtocolError},
+		{ProtocolViolation, wsCloseStatusProtocolError},
+		{BadClientProtocolVersion, wsCloseStatusProtocolError},
+		{MaxPayloadExceeded, wsCloseStatusMessageTooBig},
+		{ServerShutdown, wsCloseStatusGoingAway},
+		{WriteError, wsCloseStatusAbnormalClosure},
+		{ReadError, wsCloseStatusAbnormalClosure},
+		{StaleConnection, wsCloseStatusAbnormalClosure},
+		{ClosedState(254), wsCloseStatusInternalSrvError},
+	} {
+		t.Run(test.reason.String(), func(t *testing.T) {
+			c, _, _ := testWSSetupForRead()
+			c.wsEnqueueCloseMessage(test.reason)
+			c.mu.Lock()
+			nb, _ := c.collapsePtoNB()
+			c.mu.Unlock()
+			if n := len(nb); n != 1 {
+				t.Fatalf("Expected 1 buffer, got %v", n)
+			}
+			b := nb[0][0]
+			if b&wsFinalBit == 0 {
+				t.Fatalf("Control frame should have been the final flag, it was not set: %v", b)
+			}
+			if b&byte(wsCloseMessage) == 0 {
+				t.Fatalf("Should have been a CLOSE, it wasn't: %v", b)
+			}
+			if status := binary.BigEndian.Uint16(nb[0][2:4]); int(status) != test.status {
+				t.Fatalf("Expected status to be %v, got %v", test.status, status)
+			}
+			if string(nb[0][4:]) != test.reason.String() {
+				t.Fatalf("Unexpected content: %s", nb[0][4:])
+			}
+		})
+	}
+}
+
+type testResponseWriter struct {
+	http.ResponseWriter
+	buf     bytes.Buffer
+	headers http.Header
+	err     error
+	brw     *bufio.ReadWriter
+	conn    *testWSFakeNetConn
+}
+
+func (trw *testResponseWriter) Write(p []byte) (int, error) {
+	return trw.buf.Write(p)
+}
+
+func (trw *testResponseWriter) WriteHeader(status int) {
+	trw.buf.WriteString(fmt.Sprintf("%v", status))
+}
+
+func (trw *testResponseWriter) Header() http.Header {
+	if trw.headers == nil {
+		trw.headers = make(http.Header)
+	}
+	return trw.headers
+}
+
+type testWSFakeNetConn struct {
+	net.Conn
+	wbuf            bytes.Buffer
+	err             error
+	wsOpened        bool
+	isClosed        bool
+	deadlineCleared bool
+}
+
+func (c *testWSFakeNetConn) Write(p []byte) (int, error) {
+	if c.err != nil {
+		return 0, c.err
+	}
+	return c.wbuf.Write(p)
+}
+
+func (c *testWSFakeNetConn) SetDeadline(t time.Time) error {
+	if t.IsZero() {
+		c.deadlineCleared = true
+	}
+	return nil
+}
+
+func (c *testWSFakeNetConn) Close() error {
+	c.isClosed = true
+	return nil
+}
+
+func (trw *testResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	if trw.conn == nil {
+		trw.conn = &testWSFakeNetConn{}
+	}
+	trw.conn.wsOpened = true
+	if trw.brw == nil {
+		trw.brw = bufio.NewReadWriter(bufio.NewReader(trw.conn), bufio.NewWriter(trw.conn))
+	}
+	return trw.conn, trw.brw, trw.err
+}
+
+func testWSOptions() *Options {
+	opts := DefaultOptions()
+	opts.DisableShortFirstPing = true
+	opts.Websocket.Host = "127.0.0.1"
+	opts.Websocket.Port = -1
+	var err error
+	tc := &TLSConfigOpts{
+		CertFile: "./configs/certs/server.pem",
+		KeyFile:  "./configs/certs/key.pem",
+	}
+	opts.Websocket.TLSConfig, err = GenTLSConfig(tc)
+	if err != nil {
+		panic(err)
+	}
+	return opts
+}
+
+func testWSCreateValidReq() *http.Request {
+	req := &http.Request{
+		Method: "GET",
+		Host:   "localhost",
+		Proto:  "HTTP/1.1",
+	}
+	req.Header = make(http.Header)
+	req.Header.Set("Upgrade", "websocket")
+	req.Header.Set("Connection", "Upgrade")
+	req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
+	req.Header.Set("Sec-Websocket-Version", "13")
+	return req
+}
+
+func TestWSCheckOrigin(t *testing.T) {
+	notSameOrigin := false
+	sameOrigin := true
+	allowedListEmpty := []string{}
+	someList := []string{"http://host1.com", "http://host2.com:1234"}
+
+	for _, test := range []struct {
+		name       string
+		sameOrigin bool
+		origins    []string
+		reqHost    string
+		reqTLS     bool
+		origin     string
+		err        string
+	}{
+		{"any", notSameOrigin, allowedListEmpty, "", false, "http://any.host.com", ""},
+		{"same origin ok", sameOrigin, allowedListEmpty, "host.com", false, "http://host.com:80", ""},
+		{"same origin bad host", sameOrigin, allowedListEmpty, "host.com", false, "http://other.host.com", "not same origin"},
+		{"same origin bad port", sameOrigin, allowedListEmpty, "host.com", false, "http://host.com:81", "not same origin"},
+		{"same origin bad scheme", sameOrigin, allowedListEmpty, "host.com", true, "http://host.com", "not same origin"},
+		{"same origin bad uri", sameOrigin, allowedListEmpty, "host.com", false, "@@@://invalid:url:1234", "invalid URI"},
+		{"same origin bad url", sameOrigin, allowedListEmpty, "host.com", false, "http://invalid:url:1234", "too many colons"},
+		{"same origin bad req host", sameOrigin, allowedListEmpty, "invalid:url:1234", false, "http://host.com", "too many colons"},
+		{"no origin", sameOrigin, allowedListEmpty, "", false, "", "origin not provided"},
+		{"allowed from list", notSameOrigin, someList, "", false, "http://host2.com:1234", ""},
+		{"allowed with different path", notSameOrigin, someList, "", false, "http://host1.com/some/path", ""},
+		{"list bad port", notSameOrigin, someList, "", false, "http://host1.com:1234", "not in the allowed list"},
+		{"list bad scheme", notSameOrigin, someList, "", false, "https://host2.com:1234", "not in the allowed list"},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			opts := DefaultOptions()
+			opts.Websocket.SameOrigin = test.sameOrigin
+			opts.Websocket.AllowedOrigins = test.origins
+			s := &Server{opts: opts}
+			s.wsSetOriginOptions(&opts.Websocket)
+
+			req := testWSCreateValidReq()
+			req.Host = test.reqHost
+			if test.reqTLS {
+				req.TLS = &tls.ConnectionState{}
+			}
+			if test.origin != "" {
+				req.Header.Set("Origin", test.origin)
+			}
+			err := s.websocket.checkOrigin(req)
+			if test.err == "" && err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			} else if test.err != "" && (err == nil || !strings.Contains(err.Error(), test.err)) {
+				t.Fatalf("Expected error %q, got %v", test.err, err)
+			}
+		})
+	}
+}
+
+func TestWSUpgradeValidationErrors(t *testing.T) {
+	for _, test := range []struct {
+		name   string
+		setup  func() (*Options, *testResponseWriter, *http.Request)
+		err    string
+		status int
+	}{
+		{
+			"bad method",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Method = "POST"
+				return opts, nil, req
+			},
+			"must be GET",
+			http.StatusMethodNotAllowed,
+		},
+		{
+			"no host",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Host = ""
+				return opts, nil, req
+			},
+			"'Host' missing in request",
+			http.StatusBadRequest,
+		},
+		{
+			"invalid upgrade header",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Del("Upgrade")
+				return opts, nil, req
+			},
+			"invalid value for header 'Uprade'",
+			http.StatusBadRequest,
+		},
+		{
+			"invalid connection header",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Del("Connection")
+				return opts, nil, req
+			},
+			"invalid value for header 'Connection'",
+			http.StatusBadRequest,
+		},
+		{
+			"no key",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Del("Sec-Websocket-Key")
+				return opts, nil, req
+			},
+			"key missing",
+			http.StatusBadRequest,
+		},
+		{
+			"empty key",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Set("Sec-Websocket-Key", "")
+				return opts, nil, req
+			},
+			"key missing",
+			http.StatusBadRequest,
+		},
+		{
+			"missing version",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Del("Sec-Websocket-Version")
+				return opts, nil, req
+			},
+			"invalid version",
+			http.StatusBadRequest,
+		},
+		{
+			"wrong version",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				req := testWSCreateValidReq()
+				req.Header.Set("Sec-Websocket-Version", "99")
+				return opts, nil, req
+			},
+			"invalid version",
+			http.StatusBadRequest,
+		},
+		{
+			"origin",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				opts.Websocket.SameOrigin = true
+				req := testWSCreateValidReq()
+				req.Header.Set("Origin", "http://bad.host.com")
+				return opts, nil, req
+			},
+			"origin not allowed",
+			http.StatusForbidden,
+		},
+		{
+			"hijack error",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				rw := &testResponseWriter{err: fmt.Errorf("on purpose")}
+				req := testWSCreateValidReq()
+				return opts, rw, req
+			},
+			"on purpose",
+			http.StatusInternalServerError,
+		},
+		{
+			"hijack buffered data",
+			func() (*Options, *testResponseWriter, *http.Request) {
+				opts := testWSOptions()
+				buf := &bytes.Buffer{}
+				buf.WriteString("some data")
+				rw := &testResponseWriter{
+					conn: &testWSFakeNetConn{},
+					brw:  bufio.NewReadWriter(bufio.NewReader(buf), bufio.NewWriter(nil)),
+				}
+				tmp := [1]byte{}
+				io.ReadAtLeast(rw.brw, tmp[:1], 1)
+				req := testWSCreateValidReq()
+				return opts, rw, req
+			},
+			"client sent data before handshake is complete",
+			http.StatusBadRequest,
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			opts, rw, req := test.setup()
+			if rw == nil {
+				rw = &testResponseWriter{}
+			}
+			s := &Server{opts: opts}
+			s.wsSetOriginOptions(&opts.Websocket)
+			res, err := s.wsUpgrade(rw, req)
+			if err == nil || !strings.Contains(err.Error(), test.err) {
+				t.Fatalf("Should get error %q, got %v", test.err, err)
+			}
+			if res != nil {
+				t.Fatalf("Should not have returned a result, got %v", res)
+			}
+			expected := fmt.Sprintf("%v%s\n", test.status, http.StatusText(test.status))
+			if got := rw.buf.String(); got != expected {
+				t.Fatalf("Expected %q got %q", expected, got)
+			}
+			// Check that if the connection was opened, it is now closed.
+			if rw.conn != nil && rw.conn.wsOpened && !rw.conn.isClosed {
+				t.Fatal("Connection was opened, but has not been closed")
+			}
+		})
+	}
+}
+
+func TestWSUpgradeResponseWriteError(t *testing.T) {
+	opts := testWSOptions()
+	s := &Server{opts: opts}
+	expectedErr := errors.New("on purpose")
+	rw := &testResponseWriter{
+		conn: &testWSFakeNetConn{err: expectedErr},
+	}
+	req := testWSCreateValidReq()
+	res, err := s.wsUpgrade(rw, req)
+	if err != expectedErr {
+		t.Fatalf("Should get error %q, got %v", expectedErr.Error(), err)
+	}
+	if res != nil {
+		t.Fatalf("Should not have returned a result, got %v", res)
+	}
+	if !rw.conn.isClosed {
+		t.Fatal("Connection should have been closed")
+	}
+}
+
+func TestWSUpgradeConnDeadline(t *testing.T) {
+	opts := testWSOptions()
+	opts.Websocket.HandshakeTimeout = time.Second
+	s := &Server{opts: opts}
+	rw := &testResponseWriter{}
+	req := testWSCreateValidReq()
+	res, err := s.wsUpgrade(rw, req)
+	if res == nil || err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	if rw.conn.isClosed {
+		t.Fatal("Connection should NOT have been closed")
+	}
+	if !rw.conn.deadlineCleared {
+		t.Fatal("Connection deadline should have been cleared after handshake")
+	}
+}
+
+func TestWSCompressNegotiation(t *testing.T) {
+	// No compression on the server, but client asks
+	opts := testWSOptions()
+	s := &Server{opts: opts}
+	rw := &testResponseWriter{}
+	req := testWSCreateValidReq()
+	req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate")
+	res, err := s.wsUpgrade(rw, req)
+	if res == nil || err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	// The http response should not contain "permessage-deflate"
+	output := rw.conn.wbuf.String()
+	if strings.Contains(output, "permessage-deflate") {
+		t.Fatalf("Compression disabled in server so response to client should not contain extension, got %s", output)
+	}
+
+	// Option in the server and client, so compression should be negotiated.
+	s.opts.Websocket.Compression = true
+	rw = &testResponseWriter{}
+	res, err = s.wsUpgrade(rw, req)
+	if res == nil || err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	// The http response should not contain "permessage-deflate"
+	output = rw.conn.wbuf.String()
+	if !strings.Contains(output, "permessage-deflate") {
+		t.Fatalf("Compression in server and client request, so response should contain extension, got %s", output)
+	}
+
+	// Option in server but not asked by the client, so response should not contain "permessage-deflate"
+	rw = &testResponseWriter{}
+	req.Header.Del("Sec-Websocket-Extensions")
+	res, err = s.wsUpgrade(rw, req)
+	if res == nil || err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	// The http response should not contain "permessage-deflate"
+	output = rw.conn.wbuf.String()
+	if strings.Contains(output, "permessage-deflate") {
+		t.Fatalf("Compression in server but not in client, so response to client should not contain extension, got %s", output)
+	}
+}
+
+func TestWSParseOptions(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		content  string
+		checkOpt func(*WebsocketOpts) error
+		err      string
+	}{
+		// Negative tests
+		{"bad type", "websocket: []", nil, "to be a map"},
+		{"bad listen", "websocket: { listen: [] }", nil, "port or host:port"},
+		{"bad port", `websocket: { port: "abc" }`, nil, "not int64"},
+		{"bad host", `websocket: { host: 123 }`, nil, "not string"},
+		{"bad advertise type", `websocket: { advertise: 123 }`, nil, "not string"},
+		{"bad tls", `websocket: { tls: 123 }`, nil, "not map[string]interface {}"},
+		{"bad same origin", `websocket: { same_origin: "abc" }`, nil, "not bool"},
+		{"bad allowed origins type", `websocket: { allowed_origins: {} }`, nil, "unsupported type"},
+		{"bad allowed origins values", `websocket: { allowed_origins: [ {} ] }`, nil, "unsupported type in array"},
+		{"bad handshake timeout type", `websocket: { handshake_timeout: [] }`, nil, "unsupported type"},
+		{"bad handshake timeout duration", `websocket: { handshake_timeout: "abc" }`, nil, "invalid duration"},
+		{"unknown field", `websocket: { this_does_not_exist: 123 }`, nil, "unknown"},
+		// Positive tests
+		{"listen port only", `websocket { listen: 1234 }`, func(wo *WebsocketOpts) error {
+			if wo.Port != 1234 {
+				return fmt.Errorf("expected 1234, got %v", wo.Port)
+			}
+			return nil
+		}, ""},
+		{"listen host and port", `websocket { listen: "localhost:1234" }`, func(wo *WebsocketOpts) error {
+			if wo.Host != "localhost" || wo.Port != 1234 {
+				return fmt.Errorf("expected localhost:1234, got %v:%v", wo.Host, wo.Port)
+			}
+			return nil
+		}, ""},
+		{"host", `websocket { host: "localhost" }`, func(wo *WebsocketOpts) error {
+			if wo.Host != "localhost" {
+				return fmt.Errorf("expected localhost, got %v", wo.Host)
+			}
+			return nil
+		}, ""},
+		{"port", `websocket { port: 1234 }`, func(wo *WebsocketOpts) error {
+			if wo.Port != 1234 {
+				return fmt.Errorf("expected 1234, got %v", wo.Port)
+			}
+			return nil
+		}, ""},
+		{"advertise", `websocket { advertise: "host:1234" }`, func(wo *WebsocketOpts) error {
+			if wo.Advertise != "host:1234" {
+				return fmt.Errorf("expected %q, got %q", "host:1234", wo.Advertise)
+			}
+			return nil
+		}, ""},
+		{"same origin", `websocket { same_origin: true }`, func(wo *WebsocketOpts) error {
+			if !wo.SameOrigin {
+				return fmt.Errorf("expected same_origin==true, got %v", wo.SameOrigin)
+			}
+			return nil
+		}, ""},
+		{"allowed origins one only", `websocket { allowed_origins: "https://host.com/" }`, func(wo *WebsocketOpts) error {
+			expected := []string{"https://host.com/"}
+			if !reflect.DeepEqual(wo.AllowedOrigins, expected) {
+				return fmt.Errorf("expected allowed origins to be %q, got %q", expected, wo.AllowedOrigins)
+			}
+			return nil
+		}, ""},
+		{"allowed origins array",
+			`
+			websocket {
+				allowed_origins: [
+					"https://host1.com/"
+					"https://host2.com/"
+				]
+			}
+			`, func(wo *WebsocketOpts) error {
+				expected := []string{"https://host1.com/", "https://host2.com/"}
+				if !reflect.DeepEqual(wo.AllowedOrigins, expected) {
+					return fmt.Errorf("expected allowed origins to be %q, got %q", expected, wo.AllowedOrigins)
+				}
+				return nil
+			}, ""},
+		{"handshake timeout in whole seconds", `websocket { handshake_timeout: 3 }`, func(wo *WebsocketOpts) error {
+			if wo.HandshakeTimeout != 3*time.Second {
+				return fmt.Errorf("expected handshake to be 3s, got %v", wo.HandshakeTimeout)
+			}
+			return nil
+		}, ""},
+		{"handshake timeout n duration", `websocket { handshake_timeout: "4s" }`, func(wo *WebsocketOpts) error {
+			if wo.HandshakeTimeout != 4*time.Second {
+				return fmt.Errorf("expected handshake to be 4s, got %v", wo.HandshakeTimeout)
+			}
+			return nil
+		}, ""},
+		{"tls config",
+			`
+			websocket {
+				tls {
+					cert_file: "./configs/certs/server.pem"
+					key_file: "./configs/certs/key.pem"
+				}
+			}
+			`, func(wo *WebsocketOpts) error {
+				if wo.TLSConfig == nil {
+					return fmt.Errorf("TLSConfig should have been set")
+				}
+				return nil
+			}, ""},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			conf := createConfFile(t, []byte(test.content))
+			defer os.Remove(conf)
+			o, err := ProcessConfigFile(conf)
+			if test.err != _EMPTY_ {
+				if err == nil || !strings.Contains(err.Error(), test.err) {
+					t.Fatalf("For content: %q, expected error about %q, got %v", test.content, test.err, err)
+				}
+				return
+			} else if err != nil {
+				t.Fatalf("Unexpected error for content %q: %v", test.content, err)
+			}
+			if err := test.checkOpt(&o.Websocket); err != nil {
+				t.Fatalf("Incorrect option for content %q: %v", test.content, err.Error())
+			}
+		})
+	}
+}
+
+func TestWSValidateOptions(t *testing.T) {
+	nwso := DefaultOptions()
+	wso := testWSOptions()
+	for _, test := range []struct {
+		name    string
+		getOpts func() *Options
+		err     string
+	}{
+		{"websocket disabled", func() *Options { return nwso.Clone() }, ""},
+		{"no tls", func() *Options { o := wso.Clone(); o.Websocket.TLSConfig = nil; return o }, "requires TLS configuration"},
+		{"bad url in allowed list", func() *Options {
+			o := wso.Clone()
+			o.Websocket.AllowedOrigins = []string{"http://this:is:bad:url"}
+			return o
+		}, "unable to parse"},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			err := validateWebsocketOptions(test.getOpts())
+			if test.err == "" && err != nil {
+				t.Fatalf("Unexpected error: %v", err)
+			} else if test.err != "" && (err == nil || !strings.Contains(err.Error(), test.err)) {
+				t.Fatalf("Expected error to contain %q, got %v", test.err, err)
+			}
+		})
+	}
+}
+
+func TestWSSetOriginOptions(t *testing.T) {
+	o := testWSOptions()
+	for _, test := range []struct {
+		content string
+		err     string
+	}{
+		{"@@@://host.com/", "invalid URI"},
+		{"http://this:is:bad:url/", "invalid port"},
+	} {
+		t.Run(test.err, func(t *testing.T) {
+			o.Websocket.AllowedOrigins = []string{test.content}
+			s := &Server{}
+			l := &captureErrorLogger{errCh: make(chan string, 1)}
+			s.SetLogger(l, false, false)
+			s.wsSetOriginOptions(&o.Websocket)
+			select {
+			case e := <-l.errCh:
+				if !strings.Contains(e, test.err) {
+					t.Fatalf("Unexpected error: %v", e)
+				}
+			case <-time.After(50 * time.Millisecond):
+				t.Fatalf("Did not get the error")
+			}
+
+		})
+	}
+}
+
+type captureFatalLogger struct {
+	DummyLogger
+	fatalCh chan string
+}
+
+func (l *captureFatalLogger) Fatalf(format string, v ...interface{}) {
+	select {
+	case l.fatalCh <- fmt.Sprintf(format, v...):
+	default:
+	}
+}
+
+func TestWSFailureToStartServer(t *testing.T) {
+	// Create a listener to use a port
+	l, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("Error listening: %v", err)
+	}
+	defer l.Close()
+
+	o := testWSOptions()
+	o.Websocket.Port = l.Addr().(*net.TCPAddr).Port
+	s, err := NewServer(o)
+	if err != nil {
+		t.Fatalf("Error creating server: %v", err)
+	}
+	defer s.Shutdown()
+	logger := &captureFatalLogger{fatalCh: make(chan string, 1)}
+	s.SetLogger(logger, false, false)
+
+	wg := sync.WaitGroup{}
+	wg.Add(1)
+	go func() {
+		s.Start()
+		wg.Done()
+	}()
+
+	select {
+	case e := <-logger.fatalCh:
+		if !strings.Contains(e, "Unable to listen") {
+			t.Fatalf("Unexpected error: %v", e)
+		}
+	case <-time.After(2 * time.Second):
+		t.Fatalf("Should have reported a fatal error")
+	}
+	s.Shutdown()
+	wg.Wait()
+}
+
+func TestWSAbnormalFailureOfWebServer(t *testing.T) {
+	o := testWSOptions()
+	s := RunServer(o)
+	defer s.Shutdown()
+	logger := &captureFatalLogger{fatalCh: make(chan string, 1)}
+	s.SetLogger(logger, false, false)
+
+	// Now close the WS listener to cause a WebServer error
+	s.mu.Lock()
+	s.websocket.listener.Close()
+	s.mu.Unlock()
+
+	select {
+	case e := <-logger.fatalCh:
+		if !strings.Contains(e, "websocket listener error") {
+			t.Fatalf("Unexpected error: %v", e)
+		}
+	case <-time.After(2 * time.Second):
+		t.Fatalf("Should have reported a fatal error")
+	}
+}
+
+func testWSCreateClient(t testing.TB, compress, web bool, host string, port int) (net.Conn, *bufio.Reader) {
+	t.Helper()
+	addr := fmt.Sprintf("%s:%d", host, port)
+	wsc, err := net.Dial("tcp", addr)
+	if err != nil {
+		t.Fatalf("Error creating ws connection: %v", err)
+	}
+	wsc = tls.Client(wsc, &tls.Config{InsecureSkipVerify: true})
+	if err := wsc.(*tls.Conn).Handshake(); err != nil {
+		t.Fatalf("Error during handshake: %v", err)
+	}
+	req := testWSCreateValidReq()
+	if compress {
+		req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate")
+	}
+	if web {
+		req.Header.Set("User-Agent", "Mozilla/5.0")
+	}
+	req.URL, _ = url.Parse("wss://" + addr)
+	if err := req.Write(wsc); err != nil {
+		t.Fatalf("Error sending request: %v", err)
+	}
+	br := bufio.NewReader(wsc)
+	resp, err := http.ReadResponse(br, req)
+	if err != nil {
+		t.Fatalf("Error reading response: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusSwitchingProtocols {
+		t.Fatalf("Expected response status %v, got %v", http.StatusSwitchingProtocols, resp.StatusCode)
+	}
+	// Wait for the INFO
+	if msg := testWSReadFrame(t, br); !bytes.HasPrefix(msg, []byte("INFO {")) {
+		t.Fatalf("Expected INFO, got %s", msg)
+	}
+	// Send CONNECT and PING
+	wsmsg := testWSCreateClientMsg(wsBinaryMessage, 1, true, compress, []byte("CONNECT {\"verbose\":false,\"protocol\":1}\r\nPING\r\n"))
+	if _, err := wsc.Write(wsmsg); err != nil {
+		t.Fatalf("Error sending message: %v", err)
+	}
+	// Wait for the PONG
+	if msg := testWSReadFrame(t, br); !bytes.HasPrefix(msg, []byte("PONG\r\n")) {
+		t.Fatalf("Expected INFO, got %s", msg)
+	}
+	return wsc, br
+}
+
+func testWSReadFrame(t testing.TB, br *bufio.Reader) []byte {
+	t.Helper()
+	fh := [2]byte{}
+	if _, err := io.ReadAtLeast(br, fh[:2], 2); err != nil {
+		t.Fatalf("Error reading frame: %v", err)
+	}
+	fc := fh[0]&wsRsv1Bit != 0
+	sb := fh[1]
+	size := 0
+	switch {
+	case sb <= 125:
+		size = int(sb)
+	case sb == 126:
+		tmp := [2]byte{}
+		if _, err := io.ReadAtLeast(br, tmp[:2], 2); err != nil {
+			t.Fatalf("Error reading frame: %v", err)
+		}
+		size = int(binary.BigEndian.Uint16(tmp[:2]))
+	case sb == 127:
+		tmp := [8]byte{}
+		if _, err := io.ReadAtLeast(br, tmp[:8], 8); err != nil {
+			t.Fatalf("Error reading frame: %v", err)
+		}
+		size = int(binary.BigEndian.Uint64(tmp[:8]))
+	}
+	buf := make([]byte, size)
+	if _, err := io.ReadAtLeast(br, buf, size); err != nil {
+		t.Fatalf("Error reading frame: %v", err)
+	}
+	if !fc {
+		return buf
+	}
+	buf = append(buf, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff)
+	dbr := bytes.NewBuffer(buf)
+	d := flate.NewReader(dbr)
+	uncompressed, err := ioutil.ReadAll(d)
+	if err != nil {
+		t.Fatalf("Error reading frame: %v", err)
+	}
+	return uncompressed
+}
+
+func TestWSPubSub(t *testing.T) {
+	for _, test := range []struct {
+		name        string
+		compression bool
+	}{
+		{"no compression", false},
+		{"compression", true},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			o := testWSOptions()
+			if test.compression {
+				o.Websocket.Compression = true
+			}
+			s := RunServer(o)
+			defer s.Shutdown()
+
+			// Create a regular client to subscribe
+			nc := natsConnect(t, s.ClientURL())
+			defer nc.Close()
+			nsub := natsSubSync(t, nc, "foo")
+			checkExpectedSubs(t, 1, s)
+
+			// Now create a WS client and send a message on "foo"
+			wsc, br := testWSCreateClient(t, test.compression, false, o.Websocket.Host, o.Websocket.Port)
+			defer wsc.Close()
+
+			// Send a WS message for "PUB foo 2\r\nok\r\n"
+			wsmsg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("PUB foo 7\r\nfrom ws\r\n"))
+			if _, err := wsc.Write(wsmsg); err != nil {
+				t.Fatalf("Error sending message: %v", err)
+			}
+
+			// Now check that message is received
+			msg := natsNexMsg(t, nsub, time.Second)
+			if string(msg.Data) != "from ws" {
+				t.Fatalf("Expected message to be %q, got %q", "ok", string(msg.Data))
+			}
+
+			// Now do reverse, create a subscription on WS client on bar
+			wsmsg = testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("SUB bar 1\r\n"))
+			if _, err := wsc.Write(wsmsg); err != nil {
+				t.Fatalf("Error sending subscription: %v", err)
+			}
+			// Wait for it to be registered on server
+			checkExpectedSubs(t, 2, s)
+			// Now publish from NATS connection and verify received on WS client
+			natsPub(t, nc, "bar", []byte("from nats"))
+			natsFlush(t, nc)
+
+			// Check for the "from nats" message...
+			// Set some deadline so we are not stuck forever on failure
+			wsc.SetReadDeadline(time.Now().Add(10 * time.Second))
+			ok := 0
+			for {
+				line, _, err := br.ReadLine()
+				if err != nil {
+					t.Fatalf("Error reading: %v", err)
+				}
+				// Note that this works even in compression test because those
+				// texts are likely not to be compressed, but compression code is
+				// still executed.
+				if ok == 0 && bytes.Contains(line, []byte("MSG bar 1 9")) {
+					ok = 1
+					continue
+				} else if ok == 1 && bytes.Contains(line, []byte("from nats")) {
+					ok = 2
+					break
+				}
+			}
+		})
+	}
+}
+
+func TestWSTLSConnection(t *testing.T) {
+	o := testWSOptions()
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	addr := fmt.Sprintf("%s:%d", o.Websocket.Host, o.Websocket.Port)
+
+	for _, test := range []struct {
+		name   string
+		useTLS bool
+		status int
+	}{
+		{"client uses TLS", true, http.StatusSwitchingProtocols},
+		{"client does not use TLS", false, http.StatusBadRequest},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			wsc, err := net.Dial("tcp", addr)
+			if err != nil {
+				t.Fatalf("Error creating ws connection: %v", err)
+			}
+			defer wsc.Close()
+			if test.useTLS {
+				wsc = tls.Client(wsc, &tls.Config{InsecureSkipVerify: true})
+				if err := wsc.(*tls.Conn).Handshake(); err != nil {
+					t.Fatalf("Error during handshake: %v", err)
+				}
+			}
+			req := testWSCreateValidReq()
+			var scheme string
+			if test.useTLS {
+				scheme = "s"
+			}
+			req.URL, _ = url.Parse("ws" + scheme + "://" + addr)
+			if err := req.Write(wsc); err != nil {
+				t.Fatalf("Error sending request: %v", err)
+			}
+			br := bufio.NewReader(wsc)
+			resp, err := http.ReadResponse(br, req)
+			if err != nil {
+				t.Fatalf("Error reading response: %v", err)
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode != test.status {
+				t.Fatalf("Expected status %v, got %v", test.status, resp.StatusCode)
+			}
+		})
+	}
+}
+
+func TestWSHandshakeTimeout(t *testing.T) {
+	o := testWSOptions()
+	o.Websocket.HandshakeTimeout = time.Millisecond
+	tc := &TLSConfigOpts{
+		CertFile: "./configs/certs/server.pem",
+		KeyFile:  "./configs/certs/key.pem",
+	}
+	o.Websocket.TLSConfig, _ = GenTLSConfig(tc)
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	logger := &captureErrorLogger{errCh: make(chan string, 1)}
+	s.SetLogger(logger, false, false)
+
+	addr := fmt.Sprintf("%s:%d", o.Websocket.Host, o.Websocket.Port)
+	wsc, err := net.Dial("tcp", addr)
+	if err != nil {
+		t.Fatalf("Error creating ws connection: %v", err)
+	}
+	defer wsc.Close()
+
+	// Delay the handshake
+	wsc = tls.Client(wsc, &tls.Config{InsecureSkipVerify: true})
+	time.Sleep(20 * time.Millisecond)
+	// We expect error since the server should have cut us off
+	if err := wsc.(*tls.Conn).Handshake(); err == nil {
+		t.Fatal("Expected error during handshake")
+	}
+
+	// Check that server logs error
+	select {
+	case e := <-logger.errCh:
+		if !strings.Contains(e, "timeout") {
+			t.Fatalf("Unexpected error: %v", e)
+		}
+	case <-time.After(time.Second):
+		t.Fatalf("Should have timed-out")
+	}
+}
+
+func TestWSServerReportUpgradeFailure(t *testing.T) {
+	o := testWSOptions()
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	logger := &captureErrorLogger{errCh: make(chan string, 1)}
+	s.SetLogger(logger, false, false)
+
+	addr := fmt.Sprintf("%s:%d", o.Websocket.Host, o.Websocket.Port)
+	req := testWSCreateValidReq()
+	req.URL, _ = url.Parse("wss://" + addr)
+
+	wsc, err := net.Dial("tcp", addr)
+	if err != nil {
+		t.Fatalf("Error creating ws connection: %v", err)
+	}
+	defer wsc.Close()
+	wsc = tls.Client(wsc, &tls.Config{InsecureSkipVerify: true})
+	if err := wsc.(*tls.Conn).Handshake(); err != nil {
+		t.Fatalf("Error during handshake: %v", err)
+	}
+	// Remove a required field from the request to have it fail
+	req.Header.Del("Connection")
+	// Send the request
+	if err := req.Write(wsc); err != nil {
+		t.Fatalf("Error sending request: %v", err)
+	}
+	br := bufio.NewReader(wsc)
+	resp, err := http.ReadResponse(br, req)
+	if err != nil {
+		t.Fatalf("Error reading response: %v", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusBadRequest {
+		t.Fatalf("Expected status %v, got %v", http.StatusBadRequest, resp.StatusCode)
+	}
+
+	// Check that server logs error
+	select {
+	case e := <-logger.errCh:
+		if !strings.Contains(e, "invalid value for header 'Connection'") {
+			t.Fatalf("Unexpected error: %v", e)
+		}
+	case <-time.After(time.Second):
+		t.Fatalf("Should have timed-out")
+	}
+}
+
+func TestWSCloseMsgSendOnConnectionClose(t *testing.T) {
+	o := testWSOptions()
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	wsc, br := testWSCreateClient(t, false, false, o.Websocket.Host, o.Websocket.Port)
+	defer wsc.Close()
+
+	checkClientsCount(t, s, 1)
+	var c *client
+	s.mu.Lock()
+	for _, cli := range s.clients {
+		c = cli
+		break
+	}
+	s.mu.Unlock()
+
+	c.closeConnection(ProtocolViolation)
+	msg := testWSReadFrame(t, br)
+	if len(msg) < 2 {
+		t.Fatalf("Should have 2 bytes to represent the status, got %v", msg)
+	}
+	if sc := int(binary.BigEndian.Uint16(msg[:2])); sc != wsCloseStatusProtocolError {
+		t.Fatalf("Expected status to be %v, got %v", wsCloseStatusProtocolError, sc)
+	}
+	expectedPayload := ProtocolViolation.String()
+	if p := string(msg[2:]); p != expectedPayload {
+		t.Fatalf("Expected payload to be %q, got %q", expectedPayload, p)
+	}
+}
+
+func TestWSAdvertise(t *testing.T) {
+	o := testWSOptions()
+	o.Cluster.Port = 0
+	o.HTTPPort = 0
+	o.Websocket.Advertise = "xxx:host:yyy"
+	s, err := NewServer(o)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
+	}
+	defer s.Shutdown()
+	l := &captureFatalLogger{fatalCh: make(chan string, 1)}
+	s.SetLogger(l, false, false)
+	go s.Start()
+	select {
+	case e := <-l.fatalCh:
+		if !strings.Contains(e, "Unable to get websocket connect URLs") {
+			t.Fatalf("Unexpected error: %q", e)
+		}
+	case <-time.After(time.Second):
+		t.Fatal("Should have failed to start")
+	}
+	s.Shutdown()
+
+	o1 := testWSOptions()
+	o1.Websocket.Advertise = "host1:1234"
+	s1 := RunServer(o1)
+	defer s1.Shutdown()
+
+	wsc, br := testWSCreateClient(t, false, false, o1.Websocket.Host, o1.Websocket.Port)
+	defer wsc.Close()
+
+	o2 := testWSOptions()
+	o2.Websocket.Advertise = "host2:5678"
+	o2.Routes = RoutesFromStr(fmt.Sprintf("nats://%s:%d", o1.Cluster.Host, o1.Cluster.Port))
+	s2 := RunServer(o2)
+	defer s2.Shutdown()
+
+	checkInfo := func(expected []string) {
+		t.Helper()
+		infob := testWSReadFrame(t, br)
+		info := &Info{}
+		json.Unmarshal(infob[5:], info)
+		if n := len(info.ClientConnectURLs); n != len(expected) {
+			t.Fatalf("Unexpected info: %+v", info)
+		}
+		good := 0
+		for _, u := range info.ClientConnectURLs {
+			for _, eu := range expected {
+				if u == eu {
+					good++
+				}
+			}
+		}
+		if good != len(expected) {
+			t.Fatalf("Unexpected connect urls: %q", info.ClientConnectURLs)
+		}
+	}
+	checkInfo([]string{"host1:1234", "host2:5678"})
+
+	// Now shutdown s2 and expect another INFO
+	s2.Shutdown()
+	checkInfo([]string{"host1:1234"})
+
+	// Restart with another advertise and check that it gets updated
+	o2.Websocket.Advertise = "host3:9012"
+	s2 = RunServer(o2)
+	defer s2.Shutdown()
+	checkInfo([]string{"host1:1234", "host3:9012"})
+}
+
+func TestWSFrameOutbound(t *testing.T) {
+	c, _, _ := testWSSetupForRead()
+
+	var bufs net.Buffers
+	bufs = append(bufs, []byte("this "))
+	bufs = append(bufs, []byte("is "))
+	bufs = append(bufs, []byte("a "))
+	bufs = append(bufs, []byte("set "))
+	bufs = append(bufs, []byte("of "))
+	bufs = append(bufs, []byte("buffers"))
+	en := 2
+	for _, b := range bufs {
+		en += len(b)
+	}
+	c.mu.Lock()
+	c.out.nb = bufs
+	res, n := c.collapsePtoNB()
+	c.mu.Unlock()
+	if n != int64(en) {
+		t.Fatalf("Expected size to be %v, got %v", en, n)
+	}
+	if eb := 1 + len(bufs); eb != len(res) {
+		t.Fatalf("Expected %v buffers, got %v", eb, len(res))
+	}
+	var ob []byte
+	for i := 1; i < len(res); i++ {
+		ob = append(ob, res[i]...)
+	}
+	if !bytes.Equal(ob, []byte("this is a set of buffers")) {
+		t.Fatalf("Unexpected outbound: %q", ob)
+	}
+
+	bufs = nil
+	c.out.pb = 0
+	c.ws.fs = 0
+	c.ws.frames = nil
+	c.ws.browser = true
+	bufs = append(bufs, []byte("some smaller "))
+	bufs = append(bufs, []byte("buffers"))
+	bufs = append(bufs, make([]byte, wsFrameSizeForBrowsers+10))
+	bufs = append(bufs, []byte("then some more"))
+	en = 2 + len(bufs[0]) + len(bufs[1])
+	en += 4 + len(bufs[2]) - 10
+	en += 2 + len(bufs[3]) + 10
+	c.mu.Lock()
+	c.out.nb = bufs
+	res, n = c.collapsePtoNB()
+	c.mu.Unlock()
+	if n != int64(en) {
+		t.Fatalf("Expected size to be %v, got %v", en, n)
+	}
+	if len(res) != 8 {
+		t.Fatalf("Unexpected number of outbound buffers: %v", len(res))
+	}
+	if len(res[4]) != wsFrameSizeForBrowsers {
+		t.Fatalf("Big frame should have been limited to %v, got %v", wsFrameSizeForBrowsers, len(res[4]))
+	}
+	if len(res[6]) != 10 {
+		t.Fatalf("Frame 6 should have the partial of 10 bytes, got %v", len(res[6]))
+	}
+
+	bufs = nil
+	c.out.pb = 0
+	c.ws.fs = 0
+	c.ws.frames = nil
+	c.ws.browser = true
+	bufs = append(bufs, []byte("some smaller "))
+	bufs = append(bufs, []byte("buffers"))
+	// Have one of the exact max size
+	bufs = append(bufs, make([]byte, wsFrameSizeForBrowsers))
+	bufs = append(bufs, []byte("then some more"))
+	en = 2 + len(bufs[0]) + len(bufs[1])
+	en += 4 + len(bufs[2])
+	en += 2 + len(bufs[3])
+	c.mu.Lock()
+	c.out.nb = bufs
+	res, n = c.collapsePtoNB()
+	c.mu.Unlock()
+	if n != int64(en) {
+		t.Fatalf("Expected size to be %v, got %v", en, n)
+	}
+	if len(res) != 7 {
+		t.Fatalf("Unexpected number of outbound buffers: %v", len(res))
+	}
+	if len(res[4]) != wsFrameSizeForBrowsers {
+		t.Fatalf("Big frame should have been limited to %v, got %v", wsFrameSizeForBrowsers, len(res[4]))
+	}
+	if string(res[6]) != string(bufs[3]) {
+		t.Fatalf("Frame 6 should be %q, got %q", bufs[3], res[6])
+	}
+
+	bufs = nil
+	c.out.pb = 0
+	c.ws.fs = 0
+	c.ws.frames = nil
+	c.ws.browser = true
+	bufs = append(bufs, []byte("some smaller "))
+	bufs = append(bufs, []byte("buffers"))
+	// Have one of the exact max size, and last in the list
+	bufs = append(bufs, make([]byte, wsFrameSizeForBrowsers))
+	en = 2 + len(bufs[0]) + len(bufs[1])
+	en += 4 + len(bufs[2])
+	c.mu.Lock()
+	c.out.nb = bufs
+	res, n = c.collapsePtoNB()
+	c.mu.Unlock()
+	if n != int64(en) {
+		t.Fatalf("Expected size to be %v, got %v", en, n)
+	}
+	if len(res) != 5 {
+		t.Fatalf("Unexpected number of outbound buffers: %v", len(res))
+	}
+	if len(res[4]) != wsFrameSizeForBrowsers {
+		t.Fatalf("Big frame should have been limited to %v, got %v", wsFrameSizeForBrowsers, len(res[4]))
+	}
+
+	bufs = nil
+	c.out.pb = 0
+	c.ws.fs = 0
+	c.ws.frames = nil
+	c.ws.browser = true
+	bufs = append(bufs, []byte("some smaller buffer"))
+	bufs = append(bufs, make([]byte, wsFrameSizeForBrowsers-5))
+	bufs = append(bufs, []byte("then some more"))
+	en = 2 + len(bufs[0])
+	en += 4 + len(bufs[1])
+	en += 2 + len(bufs[2])
+	c.mu.Lock()
+	c.out.nb = bufs
+	res, n = c.collapsePtoNB()
+	c.mu.Unlock()
+	if n != int64(en) {
+		t.Fatalf("Expected size to be %v, got %v", en, n)
+	}
+	if len(res) != 6 {
+		t.Fatalf("Unexpected number of outbound buffers: %v", len(res))
+	}
+	if len(res[3]) != wsFrameSizeForBrowsers-5 {
+		t.Fatalf("Big frame should have been limited to %v, got %v", wsFrameSizeForBrowsers, len(res[4]))
+	}
+	if string(res[5]) != string(bufs[2]) {
+		t.Fatalf("Frame 6 should be %q, got %q", bufs[2], res[5])
+	}
+}
+
+func TestWSWebrowserClient(t *testing.T) {
+	o := testWSOptions()
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	wsc, br := testWSCreateClient(t, false, true, o.Websocket.Host, o.Websocket.Port)
+	defer wsc.Close()
+
+	checkClientsCount(t, s, 1)
+	var c *client
+	s.mu.Lock()
+	for _, cli := range s.clients {
+		c = cli
+		break
+	}
+	s.mu.Unlock()
+
+	proto := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("SUB foo 1\r\nPING\r\n"))
+	wsc.Write(proto)
+	if res := testWSReadFrame(t, br); !bytes.Equal(res, []byte(pongProto)) {
+		t.Fatalf("Expected PONG back")
+	}
+
+	c.mu.Lock()
+	ok := c.ws != nil && c.ws.browser == true
+	c.mu.Unlock()
+	if !ok {
+		t.Fatalf("Client is not marked as webrowser client")
+	}
+
+	nc := natsConnect(t, s.ClientURL())
+	defer nc.Close()
+
+	// Send a big message and check that it is received in smaller frames
+	psize := 204813
+	nc.Publish("foo", make([]byte, psize))
+	nc.Flush()
+
+	rsize := psize + len(fmt.Sprintf("MSG foo %d\r\n\r\n", psize))
+	nframes := 0
+	for total := 0; total < rsize; nframes++ {
+		res := testWSReadFrame(t, br)
+		total += len(res)
+	}
+	if expected := psize / wsFrameSizeForBrowsers; expected > nframes {
+		t.Fatalf("Expected %v frames, got %v", expected, nframes)
+	}
+}
+
+type testWSWrappedConn struct {
+	net.Conn
+	mu      sync.RWMutex
+	buf     *bytes.Buffer
+	partial bool
+}
+
+func (wc *testWSWrappedConn) Write(p []byte) (int, error) {
+	wc.mu.Lock()
+	defer wc.mu.Unlock()
+	var err error
+	n := len(p)
+	if wc.partial && n > 10 {
+		n = 10
+		err = io.ErrShortWrite
+	}
+	p = p[:n]
+	wc.buf.Write(p)
+	wc.Conn.Write(p)
+	return n, err
+}
+
+func TestWSCompressionBasic(t *testing.T) {
+	payload := "This is the content of a message that will be compresseddddddddddddddddddddd."
+	msgProto := fmt.Sprintf("MSG foo 1 %d\r\n%s\r\n", len(payload), payload)
+
+	cbuf := &bytes.Buffer{}
+	compressor, _ := flate.NewWriter(cbuf, flate.BestSpeed)
+	compressor.Write([]byte(msgProto))
+	compressor.Close()
+	compressed := cbuf.Bytes()
+	// The last 4 bytes are dropped
+	compressed = compressed[:len(compressed)-4]
+
+	o := testWSOptions()
+	o.Websocket.Compression = true
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	c, br := testWSCreateClient(t, true, false, o.Websocket.Host, o.Websocket.Port)
+	defer c.Close()
+
+	proto := testWSCreateClientMsg(wsBinaryMessage, 1, true, true, []byte("SUB foo 1\r\nPING\r\n"))
+	c.Write(proto)
+	l := testWSReadFrame(t, br)
+	if !bytes.Equal(l, []byte(pongProto)) {
+		t.Fatalf("Expected PONG, got %q", l)
+	}
+
+	var wc *testWSWrappedConn
+	s.mu.Lock()
+	for _, c := range s.clients {
+		c.mu.Lock()
+		wc = &testWSWrappedConn{Conn: c.nc, buf: &bytes.Buffer{}}
+		c.nc = wc
+		c.mu.Unlock()
+	}
+	s.mu.Unlock()
+
+	nc := natsConnect(t, s.ClientURL())
+	defer nc.Close()
+	natsPub(t, nc, "foo", []byte(payload))
+
+	res := &bytes.Buffer{}
+	for total := 0; total < len(msgProto); {
+		l := testWSReadFrame(t, br)
+		n, _ := res.Write(l)
+		total += n
+	}
+	if !bytes.Equal([]byte(msgProto), res.Bytes()) {
+		t.Fatalf("Unexpected result: %q", res)
+	}
+
+	// Now check the wrapped connection buffer to check that data was actually compressed.
+	wc.mu.RLock()
+	res = wc.buf
+	wc.mu.RUnlock()
+	if bytes.Contains(res.Bytes(), []byte(payload)) {
+		t.Fatalf("Looks like frame was not compressed: %q", res.Bytes())
+	}
+	header := res.Bytes()[:2]
+	body := res.Bytes()[2:]
+	expectedB0 := byte(wsBinaryMessage) | wsFinalBit | wsRsv1Bit
+	expectedPS := len(compressed)
+	expectedB1 := byte(expectedPS)
+
+	if b := header[0]; b != expectedB0 {
+		t.Fatalf("Expected first byte to be %v, got %v", expectedB0, b)
+	}
+	if b := header[1]; b != expectedB1 {
+		t.Fatalf("Expected second byte to be %v, got %v", expectedB1, b)
+	}
+	if len(body) != expectedPS {
+		t.Fatalf("Expected payload length to be %v, got %v", expectedPS, len(body))
+	}
+	if !bytes.Equal(body, compressed) {
+		t.Fatalf("Unexpected compress body: %q", body)
+	}
+}
+
+func TestWSCompressionWithPartialWrite(t *testing.T) {
+	payload := "This is the content of a message that will be compresseddddddddddddddddddddd."
+	msgProto := fmt.Sprintf("MSG foo 1 %d\r\n%s\r\n", len(payload), payload)
+
+	o := testWSOptions()
+	o.Websocket.Compression = true
+	s := RunServer(o)
+	defer s.Shutdown()
+
+	c, br := testWSCreateClient(t, true, false, o.Websocket.Host, o.Websocket.Port)
+	defer c.Close()
+
+	proto := testWSCreateClientMsg(wsBinaryMessage, 1, true, true, []byte("SUB foo 1\r\nPING\r\n"))
+	c.Write(proto)
+	l := testWSReadFrame(t, br)
+	if !bytes.Equal(l, []byte(pongProto)) {
+		t.Fatalf("Expected PONG, got %q", l)
+	}
+
+	pingPayload := []byte("my ping")
+	pingFromWSClient := testWSCreateClientMsg(wsPingMessage, 1, true, false, pingPayload)
+
+	var wc *testWSWrappedConn
+	var ws *client
+	s.mu.Lock()
+	for _, c := range s.clients {
+		ws = c
+		c.mu.Lock()
+		wc = &testWSWrappedConn{
+			Conn: c.nc,
+			buf:  &bytes.Buffer{},
+		}
+		c.nc = wc
+		c.mu.Unlock()
+		break
+	}
+	s.mu.Unlock()
+
+	wc.mu.Lock()
+	wc.partial = true
+	wc.mu.Unlock()
+
+	nc := natsConnect(t, s.ClientURL())
+	defer nc.Close()
+
+	expected := &bytes.Buffer{}
+	for i := 0; i < 10; i++ {
+		if i > 0 {
+			time.Sleep(10 * time.Millisecond)
+		}
+		expected.Write([]byte(msgProto))
+		natsPub(t, nc, "foo", []byte(payload))
+		if i == 1 {
+			c.Write(pingFromWSClient)
+		}
+	}
+
+	var gotPingResponse bool
+	res := &bytes.Buffer{}
+	for total := 0; total < 10*len(msgProto); {
+		l := testWSReadFrame(t, br)
+		if bytes.Equal(l, pingPayload) {
+			gotPingResponse = true
+		} else {
+			n, _ := res.Write(l)
+			total += n
+		}
+	}
+	if !bytes.Equal(expected.Bytes(), res.Bytes()) {
+		t.Fatalf("Unexpected result: %q", res)
+	}
+	if !gotPingResponse {
+		t.Fatal("Did not get the ping response")
+	}
+
+	ws.mu.Lock()
+	pb := ws.out.pb
+	wf := ws.ws.frames
+	fs := ws.ws.fs
+	ws.mu.Unlock()
+	if pb != 0 {
+		t.Fatalf("Expected pb to be 0, got %v", pb)
+	}
+	if len(wf) != 0 {
+		t.Fatalf("Should not be any frames left to send, got %v", wf)
+	}
+	if fs != 0 {
+		t.Fatalf("Frame size should be 0, got %v", fs)
+	}
+}
+
+func TestWSCompressionFrameSizeLimit(t *testing.T) {
+	opts := testWSOptions()
+	opts.MaxPending = MAX_PENDING_SIZE
+	s := &Server{opts: opts}
+	c := &client{srv: s, ws: &websocket{compress: true, browser: true}}
+	c.initClient()
+
+	// uncompressedPayload := []byte("abcdefghijklmnopqrstuvwxyz")
+	uncompressedPayload := make([]byte, 2*wsFrameSizeForBrowsers)
+	for i := 0; i < len(uncompressedPayload); i++ {
+		uncompressedPayload[i] = byte(rand.Intn(256))
+	}
+
+	c.mu.Lock()
+	c.out.nb = append(net.Buffers(nil), uncompressedPayload)
+	nb, _ := c.collapsePtoNB()
+	c.mu.Unlock()
+
+	bb := &bytes.Buffer{}
+	for i, b := range nb {
+		// frame header buffer are always very small. The payload should not be more
+		// than 10 bytes since that is what we passed as the limit.
+		if len(b) > wsFrameSizeForBrowsers {
+			t.Fatalf("Frame size too big: %v (%q)", len(b), b)
+		}
+		// Check frame headers for the proper formatting.
+		if i%2 == 1 {
+			bb.Write(b)
+		}
+	}
+	buf := bb.Bytes()
+	buf = append(buf, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff)
+	dbr := bytes.NewBuffer(buf)
+	d := flate.NewReader(dbr)
+	uncompressed, err := ioutil.ReadAll(d)
+	if err != nil {
+		t.Fatalf("Error reading frame: %v", err)
+	}
+	if !bytes.Equal(uncompressed, uncompressedPayload) {
+		t.Fatalf("Unexpected uncomressed data: %q", uncompressed)
+	}
+}
+
+// ==================================================================
+// = Benchmark tests
+// ==================================================================
+
+const testWSBenchSubject = "a"
+
+var ch = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@$#%^&*()")
+
+func sizedString(sz int) string {
+	b := make([]byte, sz)
+	for i := range b {
+		b[i] = ch[rand.Intn(len(ch))]
+	}
+	return string(b)
+}
+
+func sizedStringForCompression(sz int) string {
+	b := make([]byte, sz)
+	c := byte(0)
+	s := 0
+	for i := range b {
+		if s%20 == 0 {
+			c = ch[rand.Intn(len(ch))]
+		}
+		b[i] = c
+	}
+	return string(b)
+}
+
+func testWSFlushConn(b *testing.B, compress bool, c net.Conn, br *bufio.Reader) {
+	buf := testWSCreateClientMsg(wsBinaryMessage, 1, true, compress, []byte(pingProto))
+	c.Write(buf)
+	c.SetReadDeadline(time.Now().Add(5 * time.Second))
+	res := testWSReadFrame(b, br)
+	c.SetReadDeadline(time.Time{})
+	if !bytes.HasPrefix(res, []byte(pongProto)) {
+		b.Fatalf("Failed read of PONG: %s\n", res)
+	}
+}
+
+func wsBenchPub(b *testing.B, numPubs int, compress bool, payload string) {
+	b.StopTimer()
+	opts := testWSOptions()
+	opts.Websocket.Compression = compress
+	s := RunServer(opts)
+	defer s.Shutdown()
+
+	n := b.N
+	extra := 0
+	pubProto := []byte(fmt.Sprintf("PUB %s %d\r\n%s\r\n", testWSBenchSubject, len(payload), payload))
+	singleOpBuf := testWSCreateClientMsg(wsBinaryMessage, 1, true, compress, pubProto)
+
+	// Simulate client that would buffer messages before framing/sending.
+	// Figure out how many we can fit in one frame based on b.N and length of pubProto
+	const bufSize = 32768
+	tmpa := [bufSize]byte{}
+	tmp := tmpa[:0]
+	pb := 0
+	for i := 0; i < b.N; i++ {
+		tmp = append(tmp, pubProto...)
+		pb++
+		if len(tmp) >= bufSize {
+			break
+		}
+	}
+	sendBuf := testWSCreateClientMsg(wsBinaryMessage, 1, true, compress, tmp)
+	n = b.N / pb
+	extra = b.N - (n * pb)
+
+	wg := sync.WaitGroup{}
+	wg.Add(numPubs)
+
+	type pub struct {
+		c  net.Conn
+		br *bufio.Reader
+		bw *bufio.Writer
+	}
+	var pubs []pub
+	for i := 0; i < numPubs; i++ {
+		wsc, br := testWSCreateClient(b, compress, false, opts.Websocket.Host, opts.Websocket.Port)
+		defer wsc.Close()
+		bw := bufio.NewWriterSize(wsc, bufSize)
+		pubs = append(pubs, pub{wsc, br, bw})
+	}
+
+	// Average the amount of bytes sent by iteration
+	avg := len(sendBuf) / pb
+	if extra > 0 {
+		avg += len(singleOpBuf)
+		avg /= 2
+	}
+	b.SetBytes(int64(numPubs * avg))
+	b.StartTimer()
+
+	for i := 0; i < numPubs; i++ {
+		p := pubs[i]
+		go func(p pub) {
+			defer wg.Done()
+			for i := 0; i < n; i++ {
+				p.bw.Write(sendBuf)
+			}
+			for i := 0; i < extra; i++ {
+				p.bw.Write(singleOpBuf)
+			}
+			p.bw.Flush()
+			testWSFlushConn(b, compress, p.c, p.br)
+		}(p)
+	}
+	wg.Wait()
+	b.StopTimer()
+}
+
+func Benchmark_WS_Pubx1_CN_____0b(b *testing.B) {
+	wsBenchPub(b, 1, false, "")
+}
+
+func Benchmark_WS_Pubx1_CY_____0b(b *testing.B) {
+	wsBenchPub(b, 1, true, "")
+}
+
+func Benchmark_WS_Pubx1_CN___128b(b *testing.B) {
+	s := sizedString(128)
+	wsBenchPub(b, 1, false, s)
+}
+
+func Benchmark_WS_Pubx1_CY___128b(b *testing.B) {
+	s := sizedStringForCompression(128)
+	wsBenchPub(b, 1, true, s)
+}
+
+func Benchmark_WS_Pubx1_CN__1024b(b *testing.B) {
+	s := sizedString(1024)
+	wsBenchPub(b, 1, false, s)
+}
+
+func Benchmark_WS_Pubx1_CY__1024b(b *testing.B) {
+	s := sizedStringForCompression(1024)
+	wsBenchPub(b, 1, true, s)
+}
+
+func Benchmark_WS_Pubx1_CN__4096b(b *testing.B) {
+	s := sizedString(4 * 1024)
+	wsBenchPub(b, 1, false, s)
+}
+
+func Benchmark_WS_Pubx1_CY__4096b(b *testing.B) {
+	s := sizedStringForCompression(4 * 1024)
+	wsBenchPub(b, 1, true, s)
+}
+
+func Benchmark_WS_Pubx1_CN__8192b(b *testing.B) {
+	s := sizedString(8 * 1024)
+	wsBenchPub(b, 1, false, s)
+}
+
+func Benchmark_WS_Pubx1_CY__8192b(b *testing.B) {
+	s := sizedStringForCompression(8 * 1024)
+	wsBenchPub(b, 1, true, s)
+}
+
+func Benchmark_WS_Pubx1_CN_32768b(b *testing.B) {
+	s := sizedString(32 * 1024)
+	wsBenchPub(b, 1, false, s)
+}
+
+func Benchmark_WS_Pubx1_CY_32768b(b *testing.B) {
+	s := sizedStringForCompression(32 * 1024)
+	wsBenchPub(b, 1, true, s)
+}
+
+func Benchmark_WS_Pubx5_CN_____0b(b *testing.B) {
+	wsBenchPub(b, 5, false, "")
+}
+
+func Benchmark_WS_Pubx5_CY_____0b(b *testing.B) {
+	wsBenchPub(b, 5, true, "")
+}
+
+func Benchmark_WS_Pubx5_CN___128b(b *testing.B) {
+	s := sizedString(128)
+	wsBenchPub(b, 5, false, s)
+}
+
+func Benchmark_WS_Pubx5_CY___128b(b *testing.B) {
+	s := sizedStringForCompression(128)
+	wsBenchPub(b, 5, true, s)
+}
+
+func Benchmark_WS_Pubx5_CN__1024b(b *testing.B) {
+	s := sizedString(1024)
+	wsBenchPub(b, 5, false, s)
+}
+
+func Benchmark_WS_Pubx5_CY__1024b(b *testing.B) {
+	s := sizedStringForCompression(1024)
+	wsBenchPub(b, 5, true, s)
+}
+
+func Benchmark_WS_Pubx5_CN__4096b(b *testing.B) {
+	s := sizedString(4 * 1024)
+	wsBenchPub(b, 5, false, s)
+}
+
+func Benchmark_WS_Pubx5_CY__4096b(b *testing.B) {
+	s := sizedStringForCompression(4 * 1024)
+	wsBenchPub(b, 5, true, s)
+}
+
+func Benchmark_WS_Pubx5_CN__8192b(b *testing.B) {
+	s := sizedString(8 * 1024)
+	wsBenchPub(b, 5, false, s)
+}
+
+func Benchmark_WS_Pubx5_CY__8192b(b *testing.B) {
+	s := sizedStringForCompression(8 * 1024)
+	wsBenchPub(b, 5, true, s)
+}
+
+func Benchmark_WS_Pubx5_CN_32768b(b *testing.B) {
+	s := sizedString(32 * 1024)
+	wsBenchPub(b, 5, false, s)
+}
+
+func Benchmark_WS_Pubx5_CY_32768b(b *testing.B) {
+	s := sizedStringForCompression(32 * 1024)
+	wsBenchPub(b, 5, true, s)
+}
+
+func wsBenchSub(b *testing.B, numSubs int, compress bool, payload string) {
+	b.StopTimer()
+	opts := testWSOptions()
+	opts.Websocket.Compression = compress
+	s := RunServer(opts)
+	defer s.Shutdown()
+
+	var subs []*bufio.Reader
+	for i := 0; i < numSubs; i++ {
+		wsc, br := testWSCreateClient(b, compress, false, opts.Websocket.Host, opts.Websocket.Port)
+		defer wsc.Close()
+		subProto := testWSCreateClientMsg(wsBinaryMessage, 1, true, compress,
+			[]byte(fmt.Sprintf("SUB %s 1\r\nPING\r\n", testWSBenchSubject)))
+		wsc.Write(subProto)
+		// Waiting for PONG
+		testWSReadFrame(b, br)
+		subs = append(subs, br)
+	}
+
+	wg := sync.WaitGroup{}
+	wg.Add(numSubs)
+
+	// Use regular NATS client to publish messages
+	nc := natsConnect(b, s.ClientURL())
+	defer nc.Close()
+
+	b.StartTimer()
+
+	for i := 0; i < numSubs; i++ {
+		br := subs[i]
+		go func(br *bufio.Reader) {
+			defer wg.Done()
+			for count := 0; count < b.N; {
+				msgs := testWSReadFrame(b, br)
+				count += bytes.Count(msgs, []byte("MSG "))
+			}
+		}(br)
+	}
+	for i := 0; i < b.N; i++ {
+		natsPub(b, nc, testWSBenchSubject, []byte(payload))
+	}
+	wg.Wait()
+	b.StopTimer()
+}
+
+func Benchmark_WS_Subx1_CN_____0b(b *testing.B) {
+	wsBenchSub(b, 1, false, "")
+}
+
+func Benchmark_WS_Subx1_CY_____0b(b *testing.B) {
+	wsBenchSub(b, 1, true, "")
+}
+
+func Benchmark_WS_Subx1_CN___128b(b *testing.B) {
+	s := sizedString(128)
+	wsBenchSub(b, 1, false, s)
+}
+
+func Benchmark_WS_Subx1_CY___128b(b *testing.B) {
+	s := sizedStringForCompression(128)
+	wsBenchSub(b, 1, true, s)
+}
+
+func Benchmark_WS_Subx1_CN__1024b(b *testing.B) {
+	s := sizedString(1024)
+	wsBenchSub(b, 1, false, s)
+}
+
+func Benchmark_WS_Subx1_CY__1024b(b *testing.B) {
+	s := sizedStringForCompression(1024)
+	wsBenchSub(b, 1, true, s)
+}
+
+func Benchmark_WS_Subx1_CN__4096b(b *testing.B) {
+	s := sizedString(4096)
+	wsBenchSub(b, 1, false, s)
+}
+
+func Benchmark_WS_Subx1_CY__4096b(b *testing.B) {
+	s := sizedStringForCompression(4096)
+	wsBenchSub(b, 1, true, s)
+}
+
+func Benchmark_WS_Subx1_CN__8192b(b *testing.B) {
+	s := sizedString(8192)
+	wsBenchSub(b, 1, false, s)
+}
+
+func Benchmark_WS_Subx1_CY__8192b(b *testing.B) {
+	s := sizedStringForCompression(8192)
+	wsBenchSub(b, 1, true, s)
+}
+
+func Benchmark_WS_Subx1_CN_32768b(b *testing.B) {
+	s := sizedString(32768)
+	wsBenchSub(b, 1, false, s)
+}
+
+func Benchmark_WS_Subx1_CY_32768b(b *testing.B) {
+	s := sizedStringForCompression(32768)
+	wsBenchSub(b, 1, true, s)
+}
+
+func Benchmark_WS_Subx5_CN_____0b(b *testing.B) {
+	wsBenchSub(b, 5, false, "")
+}
+
+func Benchmark_WS_Subx5_CY_____0b(b *testing.B) {
+	wsBenchSub(b, 5, true, "")
+}
+
+func Benchmark_WS_Subx5_CN___128b(b *testing.B) {
+	s := sizedString(128)
+	wsBenchSub(b, 5, false, s)
+}
+
+func Benchmark_WS_Subx5_CY___128b(b *testing.B) {
+	s := sizedStringForCompression(128)
+	wsBenchSub(b, 5, true, s)
+}
+
+func Benchmark_WS_Subx5_CN__1024b(b *testing.B) {
+	s := sizedString(1024)
+	wsBenchSub(b, 5, false, s)
+}
+
+func Benchmark_WS_Subx5_CY__1024b(b *testing.B) {
+	s := sizedStringForCompression(1024)
+	wsBenchSub(b, 5, true, s)
+}
+
+func Benchmark_WS_Subx5_CN__4096b(b *testing.B) {
+	s := sizedString(4096)
+	wsBenchSub(b, 5, false, s)
+}
+
+func Benchmark_WS_Subx5_CY__4096b(b *testing.B) {
+	s := sizedStringForCompression(4096)
+	wsBenchSub(b, 5, true, s)
+}
+
+func Benchmark_WS_Subx5_CN__8192b(b *testing.B) {
+	s := sizedString(8192)
+	wsBenchSub(b, 5, false, s)
+}
+
+func Benchmark_WS_Subx5_CY__8192b(b *testing.B) {
+	s := sizedStringForCompression(8192)
+	wsBenchSub(b, 5, true, s)
+}
+
+func Benchmark_WS_Subx5_CN_32768b(b *testing.B) {
+	s := sizedString(32768)
+	wsBenchSub(b, 5, false, s)
+}
+
+func Benchmark_WS_Subx5_CY_32768b(b *testing.B) {
+	s := sizedStringForCompression(32768)
+	wsBenchSub(b, 5, true, s)
+}
diff --git a/test/ports_test.go b/test/ports_test.go
index f120e844..1bdeac23 100644
--- a/test/ports_test.go
+++ b/test/ports_test.go
@@ -54,6 +54,12 @@ func TestPortsFile(t *testing.T) {
 	opts.HTTPPort = -1
 	opts.ProfPort = -1
 	opts.Cluster.Port = -1
+	opts.Websocket.Port = -1
+	tc := &server.TLSConfigOpts{
+		CertFile: "./configs/certs/server-cert.pem",
+		KeyFile:  "./configs/certs/server-key.pem",
+	}
+	opts.Websocket.TLSConfig, _ = server.GenTLSConfig(tc)
 
 	s := RunServer(&opts)
 	// this for test cleanup in case we fail - will be ignored if server already shutdown
@@ -100,6 +106,10 @@ func TestPortsFile(t *testing.T) {
 		t.Fatal("Expected at least one profile listen url")
 	}
 
+	if len(readPorts.WebSocket) == 0 || !strings.HasPrefix(readPorts.WebSocket[0], "wss://") {
+		t.Fatal("Expected at least one ws listen url")
+	}
+
 	// testing cleanup
 	s.Shutdown()
 	// if we called shutdown, the cleanup code should have kicked