Series introduction
In my previous post I talked about how to start WebSocket server in Go.
Reverse HTTP proxy over WebSocket in Go (Part 1)
Kazuki Higashiguchi ・ Dec 14 '21
In this post, I will be starting to talk about how to establish a WebSocket connection in Go.
- Start a WebSocket server (Part 1)
- Establish a WebSocket connection (Part 2)
- Keep a established connection (Part 3 | Part 4)
- Relay TCP connection from "App" to the peer of WebSocket
- Relay TCP connection in WebSocket data to "internal API"
Reverse HTTP proxy over WebSocket
A reverse HTTP proxy over WebSocket is a type of proxies, which retrieves resources on behalf on a client from servers and uses the WebSocket protocol as a "tunnel" to pass TCP communication from server to client.
I'll use root-gg/wsp as a sample code to explain it. I'll use the forked code to explain because maintenance has stopped and the Go language and libraries version needed to be updated.
Establish a WebSocket connection
In my previous post, I explained the implementation for starting a WebSocket server.
To establish a WebSocket connection, the WebSocket client needs to request a handshake to the server.
When you run this client program, it will start to negotiate the handshake, then finished to establish the connection.
$ go run cmd/wsp_client/main.go -config examples/wsp_client.cfg
2021/12/15 15:10:54 Connecting to ws://127.0.0.1:8080/register
2021/12/15 15:10:54 Connected to ws://127.0.0.1:8080/register
Let's read the Go code. First is the main function (cmd/client/main.go), which is the entry point.
package main
import (
// (omit)
)
func main() {
// (omit)
proxy := client.NewClient(config)
proxy.Start(ctx)
// (omit: shutdown)
}
Initialize a WebSocket client
The first line is:
proxy := client.NewClient(config)
client.NewClient function returns a pointer of client.Client struct which connects to servers using the WebSocket protocol.
type Client struct {
Config *Config
client *http.Client
dialer *websocket.Dialer
pools map[string]*Pool
}
An important fields is dialer
whose type is websocket.Dialer to contains options for connection to WebSocket server. The websocket.Dialer struct is like that:
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
}
There are some points to understand the WebSocket protocol, see the following post more detail.
Deep dive into WebSocket opening handshake protocol with Go
Kazuki Higashiguchi ・ Dec 11 '21
Send a handshake request to a WebSocket server
The next line is:
proxy.Start()
Several functions are called from this code but the important one is the following code.
connection.ws, _, err = connection.pool.client.dialer.DialContext(
ctx,
connection.pool.target,
http.Header{"X-SECRET-KEY": {connection.pool.secretKey}},
)
Dialer.DialContext creates a new client connection to open handshake, then it returns a pointer of websocket.Conn struct which represents a WebSocket connection.
If successful, WebSocket handshake is complete. You can start exchanging messages over the established WebSocket connection.
Greeting messages
Immediately after handshake, we sometimes do greeting to send client meta data (i.e. device ID) or authentication information via WebsSocket messasge.
For this scene, you can receive and send messages by calling the Conn's ReadMessage and WriteMessage.
Data is transmitted using a sequence of frames. This wire format for the data transfer part is described by the ABNF (Augmented BNF for Syntax Specification) in the WebSocket protocol. A overview of the framing is given in the following figure.
In this example, the client sends a message to the server.
greeting := fmt.Sprintf(
"%s_%d",
connection.pool.client.Config.ID,
connection.pool.client.Config.PoolIdleSize,
)
err = connection.ws.WriteMessage(websocket.TextMessage, []byte(greeting))
if err != nil {
log.Println("greeting error :", err)
connection.Close()
return
}
func (c *Conn) WriteMessage(messageType int, data []byte) error
And then, websocket.TextMessage
is one of opcodes which is defined in RFC 6455 - 11.8. WebSocket Opcode Registry.
Opcode | Meaning |
---|---|
0 | Continuation Frame |
1 | Text Frame |
2 | Binary Frame |
8 | Connection Close Frame |
9 | Ping Frame |
10 | Pong Frame |
websocket.TextMessage
means the opcode 1
(Text Frame), which is for exchanging text data over a WebSocket connection.
The server will receive the message sent by the above code (code.
// Handshaking
ws, err := server.upgrader.Upgrade(w, r, nil)
if err != nil {
wsp.ProxyErrorf(w, "HTTP upgrade error : %v", err)
return
}
// Receive the message from the client
_, greeting, err := ws.ReadMessage()
if err != nil {
wsp.ProxyErrorf(w, "Unable to read greeting message : %s", err)
ws.Close()
return
}
ReadMessage is a method to read messages from the peer which interprets data in base framing protocol.
func (c *Conn) ReadMessage() (messageType int, p []byte, err error)
When you want to exchange simple text messages over WebSocket connection, you can do it with just two methods I just introduced in this post.
Conclusion
This post explained how to establish a WebSocket connection.
I will explain the rest points in part 3 and beyond.
- Keep a established connection
Reverse HTTP proxy over WebSocket in Go (Part 3)
Kazuki Higashiguchi ・ Dec 16 '21
- Relay TCP connection from "App" to the peer of WebSocket
- Relay TCP connection in WebSocket data to "internal API"
Top comments (1)
github.com/lil5/http-proxy-logger
Here's a CLI app I made for mitm proxy logger