Deep dive into WebSocket opening handshake protocol with Go
Tags: webdev, websocket, go
Published: 2021-12-11 03:13:05

In this article, I will explain what exactly WebSocket protocol does when handshaking. What a WebSocket server should do is explained in [Learn WebSocket handshake protocol with gorilla/websocket server](https://dev.to/hgsgtk/learn-websocket-handshake-protocol-with-gorillawebsocket-server-10k9). 

This article focuses **what a WebSocket client does**, and explains it with Go codes. 

## WebSocket

WebSocket is a mechanism for low-cost, [full-duplex](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#FULL-DUPLEX) communication on Web, which protocol was standardized as [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).

The following diagram, quoted by [Wikipedia](https://en.wikipedia.org/wiki/WebSocket), describe a communication using WebSocket between client and server.

![A diagram describing a connection using WebSocket](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7qiia95sgoly2rya998.png)

Now, we will focus on the first step "Handshake (HTTP Upgrade)",

## WebSocket client

Next, let's learn the WebSocket protocol with client implementation. The sample code is available on a [GitHub repository](https://github.com/hgsgtk/go-snippets/blob/46f2daa2686b381f413a7ae2d4f9764a7d7b685f/goproxy/wsproxy/client/main.go).

```go
package main

import (
	"log"

	"github.com/gorilla/websocket"
)

func main() {
	ctx := context.Background()

	// (omit)... signal handling
	endpointUrl := "ws://localhost:12345"

	dialer := websocket.Dialer{}
	c, _, err := dialer.DialContext(ctx, endpointUrl, nil)
	if err != nil {
		log.Panicf("Dial failed: %#v\n", err)
	}
	defer c.Close()

	// Codes for bidirectional messages (listening to read messages for server)
	done := make(chan struct{})
	go func() {
		defer c.Close()
		defer close(done)

		for {
			_, message, err := c.ReadMessage()
			if err != nil {
				log.Printf("read message: %#v\n", err)
				return
			}
			log.Printf("recv: %s\n", message)
		}
	}()

	// (omit)... periodic message sending and shutdown handling
}
```

## URI schemes "ws" and "wss"

Specify the endpoint of the request to the WebSocket server like this.

```go
endpointUrl := "ws://localhost:12345"
```

The WebSocket protocol described in [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455#section-11.1) registered two URI schemes.

- ws: WebSocket Protocol
    - A `ws` URI identifies a WebSocket server and resource name
- wss: WebSocket Protocol over TLS
    - A `wss` URI identifies a WebSocket server and resource name and indicates that traffic over that connection is to be protected via TLS.

Some security considerations about WebSocket are described in [RFC 6455 10.6. Connection Confidentiality and Integrity](https://datatracker.ietf.org/doc/html/rfc6455#section-10.6).

> 
Connection confidentiality and integrity is provided by running the WebSocket Protocol over TLS (wss URIs). WebSocket implementations MUST support TLS and SHOULD employ it when communicating with their peers.

In the production use case, we need to use `wss` to ensure connection confidentiality and integrity, but this  sample code is only intended to run locally, so I use `ws` scheme.

## Subprotocols

The next line is an initialization of [Dialer](https://pkg.go.dev/github.com/gorilla/websocket#Dialer) struct. A `Dialer` contains options for connection to WebSocket server.

```go
dialer := websocket.Dialer{}
```

Here is the definition of [Dialer struct](https://github.com/gorilla/websocket/blob/v1.4.2/client.go#L51).

```go
type Dialer struct {
	NetDial func(network, addr string) (net.Conn, error)
	NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
	Proxy func(*http.Request) (*url.URL, error)
	TLSClientConfig *tls.Config
	HandshakeTimeout time.Duration
	ReadBufferSize, WriteBufferSize int
	WriteBufferPool BufferPool
	Subprotocols []string
	EnableCompression bool
	Jar http.CookieJar
}
```

An important field to understand the WebSocket protocol is `Subprotocols`.

[Subprotocols](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols) is like a custom XML schema or doctype declaration. For example, if you're using a subprotocol `json`, all data is passed as JSON.

        Sec-WebSocket-Protocol: soap, wamp


By specifying subprotocols with a client, you can tell the server which protocol you want to use. For instance:

```go
dialer := websocket.Dialer{
	Subprotocols: []string{"json"},
}
c, _, err := dialer.DialContext(ctx, endpointUrl, nil)
if err != nil {
	log.Panicf("Dial failed: %#v\n", err)
}
fmt.Printf("negotiated protocol: %q\n", c.Subprotocol())
// Output: ""
```

[Conn.Subprotocol](https://pkg.go.dev/github.com/gorilla/websocket#Conn.Subprotocol) function return the negotiated protocol for the connection. In the above example, the WebSocket server does not support `json` subprotocol, so the negotiated result is "no subprotocol used".

A WebSocket serve specifies its supported protocols, and when the server does not support any subprotocols, the client's demands will not be met.

## Opening handshake

[Dialer.DialContext](https://pkg.go.dev/github.com/gorilla/websocket#Dialer.DialContext) creates a new client connection to open handshake.

```go
dialer := websocket.Dialer{}
c, _, err := dialer.DialContext(ctx, endpointUrl, nil)
if err != nil {
	log.Panicf("Dial failed: %#v\n", err)
}
defer c.Close()
```

You can understand the specification of WebSocket handshake by reading codes inside [Dialer.DialContext](https://pkg.go.dev/github.com/gorilla/websocket#Dialer.DialContext).

At first, we need to decide the URI scheme. In [gorilla/websocket](https://github.com/gorilla/websocket/blob/v1.4.2/client.go#L164), the URI scheme is determined as follows:

```go
switch u.Scheme {
case "ws":
	u.Scheme = "http"
case "wss":
	u.Scheme = "https"
default:
	return nil, nil, errMalformedURL
}
```

- ws -> http
- wss -> https

In the case of `wss`, specify https as the URI scheme for  opening handshake because communication should be over TLS.

Second, we send HTTP GET request by codes in [gorilla/websocket](https://github.com/gorilla/websocket/blob/v1.4.2/client.go#L178).

```go
req := &http.Request{
	Method:     "GET",
	URL:        u,
	Proto:      "HTTP/1.1",
	ProtoMajor: 1,
	ProtoMinor: 1,
	Header:     make(http.Header),
	Host:       u.Host,
}
// (omit)...
req.Header["Upgrade"] = []string{"websocket"}
req.Header["Connection"] = []string{"Upgrade"}
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
req.Header["Sec-WebSocket-Version"] = []string{"13"}
if len(d.Subprotocols) > 0 {
	req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
}
```

The specification of handshake is as follow:

- The method of the request MUST be GET as described on [RFC 6455 - page 16](https://datatracker.ietf.org/doc/html/rfc6455#page-16)
- The request uses HTTP/1.1.
- The request MUST contain a [Upgrade](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade) header field whose value MUST include the `websocket` keyword.
- The request MUST contain a [Connection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection) header field whose value MUST include the `upgrade` token. 
- `Sec-Websocket-Version` is the WebSocket protocol version which is supported by server. Available versions are listed in [IANA WebSocket Version Number Registry](https://www.iana.org/assignments/websocket/websocket.xml#version-number). RFC 6415 is registered as Version 13, so basically, only 13 should be supported.

Finally, verify that the handshake would be completed correctly by validating the response ([here](https://github.com/gorilla/websocket/blob/v1.4.2/client.go#L350)).

```go
if resp.StatusCode != 101 ||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
	buf := make([]byte, 1024)
	n, _ := io.ReadFull(resp.Body, buf)
	resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
	return nil, resp, ErrBadHandshake
}
```

The handshake response of WebSocket server MUST be as follows:

- Return HTTP status code 101 (Switching Protocols)
	- The HTTP [101 Switching Protocols](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101) response code indicates the protocol the server is switching to as requested by a client.
- The value in the header `Sec-Websocket-Accept` should be computed by the value in `Sec-Websocket-Key`. See more detail on [How decided a value set in Sec-WebSocket-Key/Accept header](https://dev.to/hgsgtk/how-decided-a-value-set-in-sec-websocket-keyaccept-header-l79).

## Conclusion

This article explains WebSocket handshake protocol using gorilla/websocket client implementation.

Additional articles will be published on the  WebSocket protocol from the server perspective and data frame processing when exchanging messages and etc.

- [Learn WebSocket handshake protocol with gorilla/websocket server](https://dev.to/hgsgtk/learn-websocket-handshake-protocol-with-gorillawebsocket-server-10k9)