202 lines
8.2 KiB
Markdown
202 lines
8.2 KiB
Markdown
|
---
|
||
|
stage: Create
|
||
|
group: Source Code
|
||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||
|
---
|
||
|
|
||
|
# Websocket channel support for Workhorse
|
||
|
|
||
|
In some cases, GitLab can provide the following through a WebSocket:
|
||
|
|
||
|
- In-browser terminal access to an environment: a running server or container,
|
||
|
onto which a project has been deployed.
|
||
|
- Access to services running in CI.
|
||
|
|
||
|
Workhorse manages the WebSocket upgrade and long-lived connection to the websocket
|
||
|
connection, which frees up GitLab to process other requests. This document outlines
|
||
|
the architecture of these connections.
|
||
|
|
||
|
## Introduction to WebSockets
|
||
|
|
||
|
Websockets are an "upgraded" `HTTP/1.1` request. They permit bidirectional
|
||
|
communication between a client and a server. **Websockets are not HTTP**.
|
||
|
Clients can send messages (known as frames) to the server at any time, and
|
||
|
vice versa. Client messages are not necessarily requests, and server messages are
|
||
|
not necessarily responses. WebSocket URLs have schemes like `ws://` (unencrypted) or
|
||
|
`wss://` (TLS-secured).
|
||
|
|
||
|
When requesting an upgrade to WebSocket, the browser sends a `HTTP/1.1`
|
||
|
request like this:
|
||
|
|
||
|
```plaintext
|
||
|
GET /path.ws HTTP/1.1
|
||
|
Connection: upgrade
|
||
|
Upgrade: websocket
|
||
|
Sec-WebSocket-Protocol: terminal.gitlab.com
|
||
|
# More headers, including security measures
|
||
|
```
|
||
|
|
||
|
At this point, the connection is still HTTP, so this is a request.
|
||
|
The server can send a normal HTTP response, such as `404 Not Found` or
|
||
|
`500 Internal Server Error`.
|
||
|
|
||
|
If the server decides to permit the upgrade, it sends a HTTP
|
||
|
`101 Switching Protocols` response. From this point, the connection is no longer
|
||
|
HTTP. It is now a WebSocket and frames, not HTTP requests, flow over it. The connection
|
||
|
persists until the client or server closes the connection.
|
||
|
|
||
|
In addition to the sub-protocol, individual websocket frames may
|
||
|
also specify a message type, such as:
|
||
|
|
||
|
- `BinaryMessage`
|
||
|
- `TextMessage`
|
||
|
- `Ping`
|
||
|
- `Pong`
|
||
|
- `Close`
|
||
|
|
||
|
Only binary frames can contain arbitrary data. The frames are expected to be valid
|
||
|
UTF-8 strings, in addition to any sub-protocol expectations.
|
||
|
|
||
|
## Browser to Workhorse
|
||
|
|
||
|
Using the terminal as an example:
|
||
|
|
||
|
1. GitLab serves a JavaScript terminal emulator to the browser on a URL like
|
||
|
`https://gitlab.com/group/project/-/environments/1/terminal`.
|
||
|
1. This URL opens a websocket connection to
|
||
|
`wss://gitlab.com/group/project/-/environments/1/terminal.ws`.
|
||
|
This endpoint exists only in Workhorse, and doesn't exist in GitLab.
|
||
|
1. When receiving the connection, Workhorse first performs a `preauthentication`
|
||
|
request to GitLab to confirm the client is authorized to access the requested terminal:
|
||
|
- If the client has the appropriate permissions and the terminal exists, GitLab
|
||
|
responds with a successful response that includes details of the terminal
|
||
|
the client should be connected to.
|
||
|
- Otherwise, Workhorse returns an appropriate HTTP error response.
|
||
|
1. If GitLab returns valid terminal details to Workhorse, it:
|
||
|
1. Connects to the specified terminal.
|
||
|
1. Upgrades the browser to a WebSocket.
|
||
|
1. Proxies between the two connections for as long as the browser's credentials are valid.
|
||
|
1. Send regular `PingMessage` control frames to the browser, to prevent intervening
|
||
|
proxies from terminating the connection while the browser is present.
|
||
|
|
||
|
The browser must request an upgrade with a specific sub-protocol:
|
||
|
|
||
|
- [`terminal.gitlab.com`](#terminalgitlabcom)
|
||
|
- [`base64.terminal.gitlab.com`](#base64terminalgitlabcom)
|
||
|
|
||
|
### `terminal.gitlab.com`
|
||
|
|
||
|
This sub-protocol considers `TextMessage` frames to be invalid. Control frames,
|
||
|
such as `PingMessage` or `CloseMessage`, have their usual meanings.
|
||
|
|
||
|
- `BinaryMessage` frames sent from the browser to the server are
|
||
|
arbitrary text input.
|
||
|
- `BinaryMessage` frames sent from the server to the browser are
|
||
|
arbitrary text output.
|
||
|
|
||
|
These frames are expected to contain ANSI text control codes
|
||
|
and may be in any encoding.
|
||
|
|
||
|
### `base64.terminal.gitlab.com`
|
||
|
|
||
|
This sub-protocol considers `BinaryMessage` frames to be invalid.
|
||
|
Control frames, such as `PingMessage` or `CloseMessage`, have
|
||
|
their usual meanings.
|
||
|
|
||
|
- `TextMessage` frames sent from the browser to the server are
|
||
|
base64-encoded arbitrary text input. The server must
|
||
|
base64-decode them before inputting them.
|
||
|
- `TextMessage` frames sent from the server to the browser are
|
||
|
base64-encoded arbitrary text output. The browser must
|
||
|
base64-decode them before outputting them.
|
||
|
|
||
|
In their base64-encoded form, these frames are expected to
|
||
|
contain ANSI terminal control codes, and may be in any encoding.
|
||
|
|
||
|
## Workhorse to GitLab
|
||
|
|
||
|
Using the terminal as an example, before upgrading the browser,
|
||
|
Workhorse sends a normal HTTP request to GitLab on a URL like
|
||
|
`https://gitlab.com/group/project/environments/1/terminal.ws/authorize`.
|
||
|
This returns a JSON response containing details of where the
|
||
|
terminal can be found, and how to connect it. In particular,
|
||
|
the following details are returned in case of success:
|
||
|
|
||
|
- WebSocket URL to connect** to, such as `wss://example.com/terminals/1.ws?tty=1`.
|
||
|
- WebSocket sub-protocols to support, such as `["channel.k8s.io"]`.
|
||
|
- Headers to send, such as `Authorization: Token xxyyz`.
|
||
|
- Optional. Certificate authority to verify `wss` connections with.
|
||
|
|
||
|
Workhorse periodically rechecks this endpoint. If it receives an error response,
|
||
|
or the details of the terminal change, it terminates the websocket session.
|
||
|
|
||
|
## Workhorse to the WebSocket server
|
||
|
|
||
|
In GitLab, environments or CI jobs may have a deployment service (like
|
||
|
`KubernetesService`) associated with them. This service knows
|
||
|
where the terminals or the service for an environment may be found, and GitLab
|
||
|
returns these details to Workhorse.
|
||
|
|
||
|
These URLs are also WebSocket URLs. GitLab tells Workhorse which sub-protocols to
|
||
|
speak over the connection, along with any authentication details required by the
|
||
|
remote end.
|
||
|
|
||
|
Before upgrading the browser's connection to a websocket, Workhorse:
|
||
|
|
||
|
1. Opens a HTTP client connection, according to the details given to it by Workhorse.
|
||
|
1. Attempts to upgrade that connection to a websocket.
|
||
|
- If it fails, an error response is sent to the browser.
|
||
|
- If it succeeds, the browser is also upgraded.
|
||
|
|
||
|
Workhorse now has two websocket connections, albeit with differing sub-protocols,
|
||
|
and then:
|
||
|
|
||
|
- Decodes incoming frames from the browser, re-encodes them to the channel's
|
||
|
sub-protocol, and sends them to the channel.
|
||
|
- Decodes incoming frames from the channel, re-encodes them to the browser's
|
||
|
sub-protocol, and sends them to the browser.
|
||
|
|
||
|
When either connection closes or enters an error state, Workhorse detects the error
|
||
|
and closes the other connection, terminating the channel session. If the browser
|
||
|
is the connection that has disconnected, Workhorse sends an ANSI `End of Transmission`
|
||
|
control code (the `0x04` byte) to the channel, encoded according to the appropriate
|
||
|
sub-protocol. To avoid being disconnected, Workhorse replies to any websocket ping
|
||
|
frame sent by the channel.
|
||
|
|
||
|
Workhorse only supports the following sub-protocols:
|
||
|
|
||
|
- [`channel.k8s.io`](#channelk8sio)
|
||
|
- [`base64.channel.k8s.io`](#base64channelk8sio)
|
||
|
|
||
|
Supporting new deployment services requires new sub-protocols to be supported.
|
||
|
|
||
|
### `channel.k8s.io`
|
||
|
|
||
|
Used by Kubernetes, this sub-protocol defines a simple multiplexed channel.
|
||
|
|
||
|
Control frames have their usual meanings. `TextMessage` frames are
|
||
|
invalid. `BinaryMessage` frames represent I/O to a specific file
|
||
|
descriptor.
|
||
|
|
||
|
The first byte of each `BinaryMessage` frame represents the file
|
||
|
descriptor (`fd`) number, as a `uint8`. For example:
|
||
|
|
||
|
- `0x00` corresponds to `fd 0`, `STDIN`.
|
||
|
- `0x01` corresponds to `fd 1`, `STDOUT`.
|
||
|
|
||
|
The remaining bytes represent arbitrary data. For frames received
|
||
|
from the server, they are bytes that have been received from that
|
||
|
`fd`. For frames sent to the server, they are bytes that should be
|
||
|
written to that `fd`.
|
||
|
|
||
|
### `base64.channel.k8s.io`
|
||
|
|
||
|
Also used by Kubernetes, this sub-protocol defines a similar multiplexed
|
||
|
channel to `channel.k8s.io`. The main differences are:
|
||
|
|
||
|
- `TextMessage` frames are valid, rather than `BinaryMessage` frames.
|
||
|
- The first byte of each `TextMessage` frame represents the file
|
||
|
descriptor as a numeric UTF-8 character, so the character `U+0030`,
|
||
|
or "0", is `fd 0`, `STDIN`.
|
||
|
- The remaining bytes represent base64-encoded arbitrary data.
|