Commit 3b825b62 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'fj-replace-terminal-with-channel' into 'master'

Replace terminal terminology to channel

See merge request gitlab-org/gitlab-workhorse!382
parents 17ec2e0e 57940239
......@@ -29,7 +29,50 @@ type connWithReq struct {
req *http.Request
}
func TestTerminalHappyPath(t *testing.T) {
func TestChannelHappyPath(t *testing.T) {
tests := []struct {
name string
channelPath string
}{
{"environments", envTerminalPath},
{"jobs", jobTerminalPath},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
serverConns, clientURL, close := wireupChannel(test.channelPath, nil, "channel.k8s.io")
defer close()
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
if err != nil {
t.Fatal(err)
}
server := (<-serverConns).conn
defer server.Close()
message := "test message"
// channel.k8s.io: server writes to channel 1, STDOUT
if err := say(server, "\x01"+message); err != nil {
t.Fatal(err)
}
assertReadMessage(t, client, websocket.BinaryMessage, message)
if err := say(client, message); err != nil {
t.Fatal(err)
}
// channel.k8s.io: client writes get put on channel 0, STDIN
assertReadMessage(t, server, websocket.BinaryMessage, "\x00"+message)
// Closing the client should send an EOT signal to the server's STDIN
client.Close()
assertReadMessage(t, server, websocket.BinaryMessage, "\x00\x04")
})
}
}
func TestChannelHappyPathWithTerminalResponse(t *testing.T) {
tests := []struct {
name string
terminalPath string
......@@ -72,8 +115,8 @@ func TestTerminalHappyPath(t *testing.T) {
}
}
func TestTerminalBadTLS(t *testing.T) {
_, clientURL, close := wireupTerminal(envTerminalPath, badCA, "channel.k8s.io")
func TestChannelBadTLS(t *testing.T) {
_, clientURL, close := wireupChannel(envTerminalPath, badCA, "channel.k8s.io")
defer close()
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
......@@ -86,8 +129,8 @@ func TestTerminalBadTLS(t *testing.T) {
}
}
func TestTerminalSessionTimeout(t *testing.T) {
serverConns, clientURL, close := wireupTerminal(envTerminalPath, timeout, "channel.k8s.io")
func TestChannelSessionTimeout(t *testing.T) {
serverConns, clientURL, close := wireupChannel(envTerminalPath, timeout, "channel.k8s.io")
defer close()
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
......@@ -106,10 +149,10 @@ func TestTerminalSessionTimeout(t *testing.T) {
}
}
func TestTerminalProxyForwardsHeadersFromUpstream(t *testing.T) {
func TestChannelProxyForwardsHeadersFromUpstream(t *testing.T) {
hdr := make(http.Header)
hdr.Set("Random-Header", "Value")
serverConns, clientURL, close := wireupTerminal(envTerminalPath, setHeader(hdr), "channel.k8s.io")
serverConns, clientURL, close := wireupChannel(envTerminalPath, setHeader(hdr), "channel.k8s.io")
defer close()
client, _, err := dialWebsocket(clientURL, nil, "terminal.gitlab.com")
......@@ -125,8 +168,8 @@ func TestTerminalProxyForwardsHeadersFromUpstream(t *testing.T) {
}
}
func TestTerminalProxyForwardsXForwardedForFromClient(t *testing.T) {
serverConns, clientURL, close := wireupTerminal(envTerminalPath, nil, "channel.k8s.io")
func TestChannelProxyForwardsXForwardedForFromClient(t *testing.T) {
serverConns, clientURL, close := wireupChannel(envTerminalPath, nil, "channel.k8s.io")
defer close()
hdr := make(http.Header)
......@@ -149,6 +192,22 @@ func TestTerminalProxyForwardsXForwardedForFromClient(t *testing.T) {
}
}
func wireupChannel(channelPath string, modifier func(*api.Response), subprotocols ...string) (chan connWithReq, string, func()) {
serverConns, remote := startWebsocketServer(subprotocols...)
authResponse := channelOkBody(remote, nil, subprotocols...)
if modifier != nil {
modifier(authResponse)
}
upstream := testAuthServer(nil, 200, authResponse)
workhorse := startWorkhorseServer(upstream.URL)
return serverConns, websocketURL(workhorse.URL, channelPath), func() {
workhorse.Close()
upstream.Close()
remote.Close()
}
}
func wireupTerminal(terminalPath string, modifier func(*api.Response), subprotocols ...string) (chan connWithReq, string, func()) {
serverConns, remote := startWebsocketServer(subprotocols...)
authResponse := terminalOkBody(remote, nil, subprotocols...)
......@@ -183,6 +242,25 @@ func startWebsocketServer(subprotocols ...string) (chan connWithReq, *httptest.S
return connCh, server
}
func channelOkBody(remote *httptest.Server, header http.Header, subprotocols ...string) *api.Response {
out := &api.Response{
Channel: &api.ChannelSettings{
Url: websocketURL(remote.URL),
Header: header,
Subprotocols: subprotocols,
MaxSessionTime: 0,
},
}
if len(remote.TLS.Certificates) > 0 {
data := bytes.NewBuffer(nil)
pem.Encode(data, &pem.Block{Type: "CERTIFICATE", Bytes: remote.TLS.Certificates[0].Certificate[0]})
out.Channel.CAPem = data.String()
}
return out
}
func terminalOkBody(remote *httptest.Server, header http.Header, subprotocols ...string) *api.Response {
out := &api.Response{
Terminal: &api.TerminalSettings{
......@@ -203,16 +281,16 @@ func terminalOkBody(remote *httptest.Server, header http.Header, subprotocols ..
}
func badCA(authResponse *api.Response) {
authResponse.Terminal.CAPem = "Bad CA"
authResponse.Channel.CAPem = "Bad CA"
}
func timeout(authResponse *api.Response) {
authResponse.Terminal.MaxSessionTime = 1
authResponse.Channel.MaxSessionTime = 1
}
func setHeader(hdr http.Header) func(*api.Response) {
return func(authResponse *api.Response) {
authResponse.Terminal.Header = hdr
authResponse.Channel.Header = hdr
}
}
......
# Terminal support
# Websocket channel support
In some cases, GitLab can provide in-browser terminal access to an
environment (which is a running server or container, onto which a
project has been deployed) through a WebSocket. Workhorse manages
the WebSocket upgrade and long-lived connection to the terminal for
the environment, which frees up GitLab to process other requests.
project has been deployed), or even access to services running in CI
through a WebSocket. 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.
......@@ -47,8 +48,9 @@ UTF-8 strings, in addition to any subprotocol expectations.
## Browser to Workhorse
GitLab serves a JavaScript terminal emulator to the browser on
a URL like `https://gitlab.com/group/project/environments/1/terminal`.
Using the terminal as an example, GitLab serves a JavaScript terminal
emulator to the browser on a URL like
`https://gitlab.com/group/project/environments/1/terminal`.
This opens a websocket connection to, e.g.,
`wss://gitlab.com/group/project/environments/1/terminal.ws`,
This endpoint doesn't exist in GitLab - only in Workhorse.
......@@ -80,12 +82,12 @@ Control frames, such as `PingMessage` or `CloseMessage`, have
their usual meanings.
`BinaryMessage` frames sent from the browser to the server are
arbitrary terminal input.
arbitrary text input.
`BinaryMessage` frames sent from the server to the browser are
arbitrary terminal output.
arbitrary text output.
These frames are expected to contain ANSI terminal control codes
These frames are expected to contain ANSI text control codes
and may be in any encoding.
### `base64.terminal.gitlab.com`
......@@ -95,11 +97,11 @@ Control frames, such as `PingMessage` or `CloseMessage`, have
their usual meanings.
`TextMessage` frames sent from the browser to the server are
base64-encoded arbitrary terminal input (so the server must
base64-encoded arbitrary text input (so the server must
base64-decode them before inputting them).
`TextMessage` frames sent from the server to the browser are
base64-encoded arbitrary terminal output (so the browser must
base64-encoded arbitrary text output (so the browser must
base64-decode them before outputting them).
In their base64-encoded form, these frames are expected to
......@@ -107,8 +109,8 @@ contain ANSI terminal control codes, and may be in any encoding.
## Workhorse to GitLab
Before upgrading the browser, Workhorse sends a normal HTTP
request to GitLab on a URL like
Using again 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,
......@@ -123,11 +125,11 @@ Workhorse periodically re-checks this endpoint, and if it gets an
error response, or the details of the terminal change, it will
terminate the websocket session.
## Workhorse to Terminal
## Workhorse to the WebSocket server
In GitLab, environments may have a deployment service (e.g.,
In GitLab, environments or CI jobs may have a deployment service (e.g.,
`KubernetesService`) associated with them. This service knows
where the terminals for an environment may be found, and these
where the terminals or the service for an environment may be found, and these
details are returned to Workhorse by GitLab.
These URLs are *also* WebSocket URLs, and GitLab tells Workhorse
......@@ -143,19 +145,19 @@ also upgraded.
Workhorse now has two websocket connections, albeit with
differing subprotocols. It decodes incoming frames from the
browser, re-encodes them to the terminal's subprotocol, and
sends them to the terminal. Similarly, it decodes incoming
frames from the terminal, re-encodes them to the browser's
browser, re-encodes them to the the channel's subprotocol, and
sends them to the channel. Similarly, it decodes incoming
frames from the channel, re-encodes them to the browser's
subprotocol, 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 terminal session. If the browser is the
terminating the channel session. If the browser is the
connection that has disconnected, Workhorse will send an ANSI
`End of Transmission` control code (the `0x04` byte) to the
terminal, encoded according to the appropriate subprotocol.
channel, encoded according to the appropriate subprotocol.
Workhorse will automatically reply to any websocket ping frame
sent by the terminal, to avoid being disconnected.
sent by the channel, to avoid being disconnected.
Currently, Workhorse only supports the following subprotocols.
Supporting new deployment services will require new subprotocols
......
......@@ -123,7 +123,10 @@ type Response struct {
Archive string `json:"archive"`
// Entry is a filename inside the archive point to file that needs to be extracted
Entry string `json:"entry"`
// Used to communicate terminal session details
// Used to communicate channel session details
Channel *ChannelSettings
// Used to communicate terminal session details (Deprecated)
// Issue to remove this field https://gitlab.com/gitlab-org/gitlab-workhorse/issues/214
Terminal *TerminalSettings
// GitalyServer specifies an address and authentication token for a gitaly server we should connect to.
GitalyServer gitaly.Server
......
package api
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"github.com/gorilla/websocket"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
)
type ChannelSettings struct {
// The channel provider may require use of a particular subprotocol. If so,
// it must be specified here, and Workhorse must have a matching codec.
Subprotocols []string
// The websocket URL to connect to.
Url string
// Any headers (e.g., Authorization) to send with the websocket request
Header http.Header
// The CA roots to validate the remote endpoint with, for wss:// URLs. The
// system-provided CA pool will be used if this is blank. PEM-encoded data.
CAPem string
// The value is specified in seconds. It is converted to time.Duration
// later.
MaxSessionTime int
}
func (t *ChannelSettings) URL() (*url.URL, error) {
return url.Parse(t.Url)
}
func (t *ChannelSettings) Dialer() *websocket.Dialer {
dialer := &websocket.Dialer{
Subprotocols: t.Subprotocols,
}
if len(t.CAPem) > 0 {
pool := x509.NewCertPool()
pool.AppendCertsFromPEM([]byte(t.CAPem))
dialer.TLSClientConfig = &tls.Config{RootCAs: pool}
}
return dialer
}
func (t *ChannelSettings) Clone() *ChannelSettings {
// Doesn't clone the strings, but that's OK as strings are immutable in go
cloned := *t
cloned.Header = helper.HeaderClone(t.Header)
return &cloned
}
func (t *ChannelSettings) Dial() (*websocket.Conn, *http.Response, error) {
return t.Dialer().Dial(t.Url, t.Header)
}
func (t *ChannelSettings) Validate() error {
if t == nil {
return fmt.Errorf("channel details not specified")
}
if len(t.Subprotocols) == 0 {
return fmt.Errorf("no subprotocol specified")
}
parsedURL, err := t.URL()
if err != nil {
return fmt.Errorf("invalid URL")
}
if parsedURL.Scheme != "ws" && parsedURL.Scheme != "wss" {
return fmt.Errorf("invalid websocket scheme: %q", parsedURL.Scheme)
}
return nil
}
func (t *ChannelSettings) IsEqual(other *ChannelSettings) bool {
if t == nil && other == nil {
return true
}
if t == nil || other == nil {
return false
}
if len(t.Subprotocols) != len(other.Subprotocols) {
return false
}
for i, subprotocol := range t.Subprotocols {
if other.Subprotocols[i] != subprotocol {
return false
}
}
if len(t.Header) != len(other.Header) {
return false
}
for header, values := range t.Header {
if len(values) != len(other.Header[header]) {
return false
}
for i, value := range values {
if other.Header[header][i] != value {
return false
}
}
}
return t.Url == other.Url &&
t.CAPem == other.CAPem &&
t.MaxSessionTime == other.MaxSessionTime
}
......@@ -5,47 +5,47 @@ import (
"testing"
)
func terminal(url string, subprotocols ...string) *TerminalSettings {
return &TerminalSettings{
func channel(url string, subprotocols ...string) *ChannelSettings {
return &ChannelSettings{
Url: url,
Subprotocols: subprotocols,
MaxSessionTime: 0,
}
}
func ca(term *TerminalSettings) *TerminalSettings {
term = term.Clone()
term.CAPem = "Valid CA data"
func ca(channel *ChannelSettings) *ChannelSettings {
channel = channel.Clone()
channel.CAPem = "Valid CA data"
return term
return channel
}
func timeout(term *TerminalSettings) *TerminalSettings {
term = term.Clone()
term.MaxSessionTime = 600
func timeout(channel *ChannelSettings) *ChannelSettings {
channel = channel.Clone()
channel.MaxSessionTime = 600
return term
return channel
}
func header(term *TerminalSettings, values ...string) *TerminalSettings {
func header(channel *ChannelSettings, values ...string) *ChannelSettings {
if len(values) == 0 {
values = []string{"Dummy Value"}
}
term = term.Clone()
term.Header = http.Header{
channel = channel.Clone()
channel.Header = http.Header{
"Header": values,
}
return term
return channel
}
func TestClone(t *testing.T) {
a := ca(header(terminal("ws:", "", "")))
a := ca(header(channel("ws:", "", "")))
b := a.Clone()
if a == b {
t.Fatalf("Address of cloned terminal didn't change")
t.Fatalf("Address of cloned channel didn't change")
}
if &a.Subprotocols == &b.Subprotocols {
......@@ -59,40 +59,40 @@ func TestClone(t *testing.T) {
func TestValidate(t *testing.T) {
for i, tc := range []struct {
terminal *TerminalSettings
valid bool
msg string
channel *ChannelSettings
valid bool
msg string
}{
{nil, false, "nil terminal"},
{terminal("", ""), false, "empty URL"},
{terminal("ws:"), false, "empty subprotocols"},
{terminal("ws:", "foo"), true, "any subprotocol"},
{terminal("ws:", "foo", "bar"), true, "multiple subprotocols"},
{terminal("ws:", ""), true, "websocket URL"},
{terminal("wss:", ""), true, "secure websocket URL"},
{terminal("http:", ""), false, "HTTP URL"},
{terminal("https:", ""), false, " HTTPS URL"},
{ca(terminal("ws:", "")), true, "any CA pem"},
{header(terminal("ws:", "")), true, "any headers"},
{ca(header(terminal("ws:", ""))), true, "PEM and headers"},
{nil, false, "nil channel"},
{channel("", ""), false, "empty URL"},
{channel("ws:"), false, "empty subprotocols"},
{channel("ws:", "foo"), true, "any subprotocol"},
{channel("ws:", "foo", "bar"), true, "multiple subprotocols"},
{channel("ws:", ""), true, "websocket URL"},
{channel("wss:", ""), true, "secure websocket URL"},
{channel("http:", ""), false, "HTTP URL"},
{channel("https:", ""), false, " HTTPS URL"},
{ca(channel("ws:", "")), true, "any CA pem"},
{header(channel("ws:", "")), true, "any headers"},
{ca(header(channel("ws:", ""))), true, "PEM and headers"},
} {
if err := tc.terminal.Validate(); (err != nil) == tc.valid {
t.Fatalf("test case %d: "+tc.msg+": valid=%v: %s: %+v", i, tc.valid, err, tc.terminal)
if err := tc.channel.Validate(); (err != nil) == tc.valid {
t.Fatalf("test case %d: "+tc.msg+": valid=%v: %s: %+v", i, tc.valid, err, tc.channel)
}
}
}
func TestDialer(t *testing.T) {
terminal := terminal("ws:", "foo")
dialer := terminal.Dialer()
channel := channel("ws:", "foo")
dialer := channel.Dialer()
if len(dialer.Subprotocols) != len(terminal.Subprotocols) {
t.Fatalf("Subprotocols don't match: %+v vs. %+v", terminal.Subprotocols, dialer.Subprotocols)
if len(dialer.Subprotocols) != len(channel.Subprotocols) {
t.Fatalf("Subprotocols don't match: %+v vs. %+v", channel.Subprotocols, dialer.Subprotocols)
}
for i, subprotocol := range terminal.Subprotocols {
for i, subprotocol := range channel.Subprotocols {
if dialer.Subprotocols[i] != subprotocol {
t.Fatalf("Subprotocols don't match: %+v vs. %+v", terminal.Subprotocols, dialer.Subprotocols)
t.Fatalf("Subprotocols don't match: %+v vs. %+v", channel.Subprotocols, dialer.Subprotocols)
}
}
......@@ -100,8 +100,8 @@ func TestDialer(t *testing.T) {
t.Fatalf("Unexpected TLSClientConfig: %+v", dialer)
}
terminal = ca(terminal)
dialer = terminal.Dialer()
channel = ca(channel)
dialer = channel.Dialer()
if dialer.TLSClientConfig == nil || dialer.TLSClientConfig.RootCAs == nil {
t.Fatalf("Custom CA certificates not recognised!")
......@@ -109,45 +109,45 @@ func TestDialer(t *testing.T) {
}
func TestIsEqual(t *testing.T) {
term := terminal("ws:", "foo")
chann := channel("ws:", "foo")
term_header2 := header(term, "extra")
term_header3 := header(term)
term_header3.Header.Add("Extra", "extra")
chann_header2 := header(chann, "extra")
chann_header3 := header(chann)
chann_header3.Header.Add("Extra", "extra")
term_ca2 := ca(term)
term_ca2.CAPem = "other value"
chann_ca2 := ca(chann)
chann_ca2.CAPem = "other value"
for i, tc := range []struct {
termA *TerminalSettings
termB *TerminalSettings
channelA *ChannelSettings
channelB *ChannelSettings
expected bool
}{
{nil, nil, true},
{term, nil, false},
{nil, term, false},
{term, term, true},
{term.Clone(), term.Clone(), true},
{term, terminal("foo:"), false},
{term, terminal(term.Url), false},
{header(term), header(term), true},
{term_header2, term_header2, true},
{term_header3, term_header3, true},
{header(term), term_header2, false},
{header(term), term_header3, false},
{header(term), term, false},
{term, header(term), false},
{ca(term), ca(term), true},
{ca(term), term, false},
{term, ca(term), false},
{ca(header(term)), ca(header(term)), true},
{term_ca2, ca(term), false},
{term, timeout(term), false},
{chann, nil, false},
{nil, chann, false},
{chann, chann, true},
{chann.Clone(), chann.Clone(), true},
{chann, channel("foo:"), false},
{chann, channel(chann.Url), false},
{header(chann), header(chann), true},
{chann_header2, chann_header2, true},
{chann_header3, chann_header3, true},
{header(chann), chann_header2, false},
{header(chann), chann_header3, false},
{header(chann), chann, false},
{chann, header(chann), false},
{ca(chann), ca(chann), true},
{ca(chann), chann, false},
{chann, ca(chann), false},
{ca(header(chann)), ca(header(chann)), true},
{chann_ca2, ca(chann), false},
{chann, timeout(chann), false},
} {
if actual := tc.termA.IsEqual(tc.termB); tc.expected != actual {
if actual := tc.channelA.IsEqual(tc.channelB); tc.expected != actual {
t.Fatalf(
"test case %d: Comparison:\n-%+v\n+%+v\nexpected=%v: actual=%v",
i, tc.termA, tc.termB, tc.expected, actual,
i, tc.channelA, tc.channelB, tc.expected, actual,
)
}
}
......
......@@ -3,7 +3,6 @@ package api
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
......@@ -13,7 +12,7 @@ import (
)
type TerminalSettings struct {
// The terminal provider may require use of a particular subprotocol. If so,
// The channel provider may require use of a particular subprotocol. If so,
// it must be specified here, and Workhorse must have a matching codec.
Subprotocols []string
......@@ -61,62 +60,11 @@ func (t *TerminalSettings) Dial() (*websocket.Conn, *http.Response, error) {
return t.Dialer().Dial(t.Url, t.Header)
}
func (t *TerminalSettings) Validate() error {
if t == nil {
return fmt.Errorf("terminal details not specified")
func (t *TerminalSettings) Channel() *ChannelSettings {
return &ChannelSettings{
Subprotocols: t.Subprotocols,
Url: t.Url,
CAPem: t.CAPem,
MaxSessionTime: t.MaxSessionTime,
}
if len(t.Subprotocols) == 0 {
return fmt.Errorf("no subprotocol specified")
}
parsedURL, err := t.URL()
if err != nil {
return fmt.Errorf("invalid URL")
}
if parsedURL.Scheme != "ws" && parsedURL.Scheme != "wss" {
return fmt.Errorf("invalid websocket scheme: %q", parsedURL.Scheme)
}
return nil
}
func (t *TerminalSettings) IsEqual(other *TerminalSettings) bool {
if t == nil && other == nil {
return true
}
if t == nil || other == nil {
return false
}
if len(t.Subprotocols) != len(other.Subprotocols) {
return false
}
for i, subprotocol := range t.Subprotocols {
if other.Subprotocols[i] != subprotocol {
return false
}
}
if len(t.Header) != len(other.Header) {
return false
}
for header, values := range t.Header {
if len(values) != len(other.Header[header]) {
return false
}
for i, value := range values {
if other.Header[header][i] != value {
return false
}
}
}
return t.Url == other.Url &&
t.CAPem == other.CAPem &&
t.MaxSessionTime == other.MaxSessionTime
}
package terminal
package channel
import (
"errors"
......@@ -8,13 +8,13 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
)
type AuthCheckerFunc func() *api.TerminalSettings
type AuthCheckerFunc func() *api.ChannelSettings
// Regularly checks that authorization is still valid for a terminal, outputting
// Regularly checks that authorization is still valid for a channel, outputting
// to the stopper when it isn't
type AuthChecker struct {
Checker AuthCheckerFunc
Template *api.TerminalSettings
Template *api.ChannelSettings
StopCh chan error
Done chan struct{}
Count int64
......@@ -22,7 +22,7 @@ type AuthChecker struct {
var ErrAuthChanged = errors.New("connection closed: authentication changed or endpoint unavailable")
func NewAuthChecker(f AuthCheckerFunc, template *api.TerminalSettings, stopCh chan error) *AuthChecker {
func NewAuthChecker(f AuthCheckerFunc, template *api.ChannelSettings, stopCh chan error) *AuthChecker {
return &AuthChecker{
Checker: f,
Template: template,
......@@ -53,7 +53,7 @@ func (c *AuthChecker) Close() error {
// Generates a CheckerFunc from an *api.API + request needing authorization
func authCheckFunc(myAPI *api.API, r *http.Request, suffix string) AuthCheckerFunc {
return func() *api.TerminalSettings {
return func() *api.ChannelSettings {
httpResponse, authResponse, err := myAPI.PreAuthorize(suffix, r)
if err != nil {
return nil
......@@ -64,6 +64,6 @@ func authCheckFunc(myAPI *api.API, r *http.Request, suffix string) AuthCheckerFu
return nil
}
return authResponse.Terminal
return authResponse.Channel
}
}
package terminal
package channel
import (
"testing"
......@@ -7,8 +7,8 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
)
func checkerSeries(values ...*api.TerminalSettings) AuthCheckerFunc {
return func() *api.TerminalSettings {
func checkerSeries(values ...*api.ChannelSettings) AuthCheckerFunc {
return func() *api.ChannelSettings {
if len(values) == 0 {
return nil
}
......@@ -19,7 +19,7 @@ func checkerSeries(values ...*api.TerminalSettings) AuthCheckerFunc {
}
func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
template := &api.TerminalSettings{Url: "ws://example.com"}
template := &api.ChannelSettings{Url: "ws://example.com"}
stopCh := make(chan error)
series := checkerSeries(template, template, template)
ac := NewAuthChecker(series, template, stopCh)
......@@ -35,7 +35,7 @@ func TestAuthCheckerStopsWhenAuthFails(t *testing.T) {
}
func TestAuthCheckerStopsWhenAuthChanges(t *testing.T) {
template := &api.TerminalSettings{Url: "ws://example.com"}
template := &api.ChannelSettings{Url: "ws://example.com"}
changed := template.Clone()
changed.Url = "wss://example.com"
stopCh := make(chan error)
......
package terminal
package channel
import (
"fmt"
......@@ -13,7 +13,7 @@ import (
)
var (
// See doc/terminal.md for documentation of this subprotocol
// See doc/channel.md for documentation of this subprotocol
subprotocols = []string{"terminal.gitlab.com", "base64.terminal.gitlab.com"}
upgrader = &websocket.Upgrader{Subprotocols: subprotocols}
ReauthenticationInterval = 5 * time.Minute
......@@ -22,7 +22,15 @@ var (
func Handler(myAPI *api.API) http.Handler {
return myAPI.PreAuthorizeHandler(func(w http.ResponseWriter, r *http.Request, a *api.Response) {
if err := a.Terminal.Validate(); err != nil {
// Used during the transition from Terminal to Channel
// Once we remove the TerminalSettings object we can remove
// this condition
// https://gitlab.com/gitlab-org/gitlab-workhorse/issues/214
if a.Terminal != nil {
a.Channel = a.Terminal.Channel()
}
if err := a.Channel.Validate(); err != nil {
helper.Fail500(w, r, err)
return
}
......@@ -30,22 +38,22 @@ func Handler(myAPI *api.API) http.Handler {
proxy := NewProxy(2) // two stoppers: auth checker, max time
checker := NewAuthChecker(
authCheckFunc(myAPI, r, "authorize"),
a.Terminal,
a.Channel,
proxy.StopCh,
)
defer checker.Close()
go checker.Loop(ReauthenticationInterval)
go closeAfterMaxTime(proxy, a.Terminal.MaxSessionTime)
go closeAfterMaxTime(proxy, a.Channel.MaxSessionTime)
ProxyTerminal(w, r, a.Terminal, proxy)
ProxyChannel(w, r, a.Channel, proxy)
}, "authorize")
}
func ProxyTerminal(w http.ResponseWriter, r *http.Request, terminal *api.TerminalSettings, proxy *Proxy) {
server, err := connectToServer(terminal, r)
func ProxyChannel(w http.ResponseWriter, r *http.Request, settings *api.ChannelSettings, proxy *Proxy) {
server, err := connectToServer(settings, r)
if err != nil {
helper.Fail500(w, r, err)
log.WithError(r.Context(), err).Print("Terminal: connecting to server failed")
log.WithError(r.Context(), err).Print("Channel: connecting to server failed")
return
}
defer server.UnderlyingConn().Close()
......@@ -53,7 +61,7 @@ func ProxyTerminal(w http.ResponseWriter, r *http.Request, terminal *api.Termina
client, err := upgradeClient(w, r)
if err != nil {
log.WithError(r.Context(), err).Print("Terminal: upgrading client to websocket failed")
log.WithError(r.Context(), err).Print("Channel: upgrading client to websocket failed")
return
}
......@@ -69,12 +77,12 @@ func ProxyTerminal(w http.ResponseWriter, r *http.Request, terminal *api.Termina
"serverAddr": serverAddr,
})
logEntry.Print("Terminal: started proxying")
logEntry.Print("Channel: started proxying")
defer logEntry.Print("Terminal: finished proxying")
defer logEntry.Print("Channel: finished proxying")
if err := proxy.Serve(server, client, serverAddr, clientAddr); err != nil {
logEntry.WithError(err).Print("Terminal: error proxying")
logEntry.WithError(err).Print("Channel: error proxying")
}
}
......@@ -105,12 +113,12 @@ func pingLoop(conn Connection) {
}
}
func connectToServer(terminal *api.TerminalSettings, r *http.Request) (Connection, error) {
terminal = terminal.Clone()
func connectToServer(settings *api.ChannelSettings, r *http.Request) (Connection, error) {
settings = settings.Clone()
helper.SetForwardedFor(&terminal.Header, r)
helper.SetForwardedFor(&settings.Header, r)
conn, _, err := terminal.Dial()
conn, _, err := settings.Dial()
if err != nil {
return nil, err
}
......
package terminal
package channel
import (
"fmt"
......@@ -8,7 +8,7 @@ import (
"github.com/gorilla/websocket"
)
// ANSI "end of terminal" code
// ANSI "end of channel" code
var eot = []byte{0x04}
// An abstraction of gorilla's *websocket.Conn
......@@ -31,7 +31,7 @@ func NewProxy(stoppers int) *Proxy {
}
func (p *Proxy) Serve(upstream, downstream Connection, upstreamAddr, downstreamAddr string) error {
// This signals the upstream terminal to kill the exec'd process
// This signals the upstream channel to kill the exec'd process
defer upstream.WriteMessage(websocket.BinaryMessage, eot)
go p.proxy(upstream, downstream, upstreamAddr, downstreamAddr)
......
package terminal
package channel
import (
"encoding/base64"
......
......@@ -13,6 +13,7 @@ import (
apipkg "gitlab.com/gitlab-org/gitlab-workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/artifacts"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/builds"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/channel"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/git"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/helper"
......@@ -25,7 +26,6 @@ import (
"gitlab.com/gitlab-org/gitlab-workhorse/internal/sendfile"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/sendurl"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/staticpages"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/terminal"
"gitlab.com/gitlab-org/gitlab-workhorse/internal/upload"
)
......@@ -187,8 +187,8 @@ func (u *upstream) configureRoutes() {
route("POST", ciAPIPattern+`v1/builds/[0-9]+/artifacts\z`, contentEncodingHandler(artifacts.UploadArtifacts(api, proxy))),
// Terminal websocket
wsRoute(projectPattern+`environments/[0-9]+/terminal.ws\z`, terminal.Handler(api)),
wsRoute(projectPattern+`-/jobs/[0-9]+/terminal.ws\z`, terminal.Handler(api)),
wsRoute(projectPattern+`environments/[0-9]+/terminal.ws\z`, channel.Handler(api)),
wsRoute(projectPattern+`-/jobs/[0-9]+/terminal.ws\z`, channel.Handler(api)),
// Long poll and limit capacity given to jobs/request and builds/register.json
route("", apiPattern+`v4/jobs/request\z`, ciAPILongPolling),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment