DEV Community

Kazuki Higashiguchi
Kazuki Higashiguchi

Posted on • Updated on

Reverse HTTP proxy over WebSocket in Go (Part 2)

Series introduction

In my previous post I talked about how to start WebSocket server in Go.

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.

A network diagram for reverse proxy over WebSocket

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.

GitHub logo hgsgtk / wsp

HTTP tunnel over Websocket

Establish a WebSocket connection

In my previous post, I explained the implementation for starting a WebSocket server.

A diagram describing to establish WebSocket connection from client

To establish a WebSocket connection, the WebSocket client needs to request a handshake to the server.

A diagram describing a connection using WebSocket

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Initialize a WebSocket client

The first line is:

proxy := client.NewClient(config)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

There are some points to understand the WebSocket protocol, see the following post more detail.

Send a handshake request to a WebSocket server

The next line is:

proxy.Start()
Enter fullscreen mode Exit fullscreen mode

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}},
)
Enter fullscreen mode Exit fullscreen mode

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.

A figure of base framing protocol

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
}
Enter fullscreen mode Exit fullscreen mode

Conn.WriteMessage

func (c *Conn) WriteMessage(messageType int, data []byte) error
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
  • Relay TCP connection from "App" to the peer of WebSocket
  • Relay TCP connection in WebSocket data to "internal API"

Discussion (1)

Collapse
lil5 profile image
Lucian I. Last

github.com/lil5/http-proxy-logger

Here's a CLI app I made for mitm proxy logger