Commit 5af59b87 authored by Steve Azzopardi's avatar Steve Azzopardi

Add docker support for terminal

Create a new proxy for encoded streams, which is similar to
`FileDescriptorProxy` but instead takeing an `ReadWriteCloser` which is
something more generic.

closes #3
parent 062b4de9
Pipeline #31027578 passed with stage
in 1 minute and 10 seconds
module gitlab.com/gitlab-org/gitlab-terminal
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/websocket v1.4.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.0.6
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 // indirect
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect
golang.org/x/text v0.3.0
gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
package terminal
import (
"fmt"
"io"
)
type StreamProxy struct {
StopCh chan error
}
func NewStreamProxy(stoppers int) *StreamProxy {
return &StreamProxy{
StopCh: make(chan error, stoppers+2), // each proxy() call is a stopper
}
}
func (p *StreamProxy) GetStopCh() chan error {
return p.StopCh
}
func (p *StreamProxy) Serve(client io.ReadWriter, server io.ReadWriter) error {
go p.proxy(client, server)
go p.proxy(server, client)
err := <-p.StopCh
return err
}
func (p *StreamProxy) proxy(to, from io.ReadWriter) {
_, err := io.Copy(to, from)
if err != nil {
p.StopCh <- fmt.Errorf("failed to pipe stream: %v", err)
}
}
package terminal
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type chStringReadWriter struct {
wrieDone chan struct{}
bytes.Buffer
}
func (c *chStringReadWriter) Write(p []byte) (int, error) {
defer func() {
c.wrieDone <- struct{}{}
}()
return c.Buffer.Write(p)
}
func TestServe(t *testing.T) {
encProxy := NewStreamProxy(1)
downstream := bytes.Buffer{}
upstream := chStringReadWriter{
wrieDone: make(chan struct{}),
}
writeString := []byte("data from downstream")
downstream.Write([]byte(writeString))
go func() {
err := encProxy.Serve(&upstream, &downstream)
if err != nil {
t.Fatalf("unexpected error from serve: %v", err)
}
}()
// Wait until the write is done
<-upstream.wrieDone
b := make([]byte, 20)
_, err := upstream.Read(b)
require.NoError(t, err)
assert.Equal(t, writeString, b)
}
func TestServeError(t *testing.T) {
encProxy := NewStreamProxy(1)
downstream := errorReadWriter{}
upstream := bytes.Buffer{}
err := encProxy.Serve(&upstream, &downstream)
assert.Error(t, err)
}
type errorReadWriter struct {
}
func (rw *errorReadWriter) Read(p []byte) (int, error) {
return 0, errors.New("failed to read")
}
func (rw *errorReadWriter) Write(p []byte) (int, error) {
return 0, errors.New("failed to read")
}
package terminal
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
log "github.com/sirupsen/logrus"
"github.com/gorilla/websocket"
"fmt"
"errors"
log "github.com/sirupsen/logrus"
)
var (
// See doc/terminal.md for documentation of this subprotocol
subprotocols = []string{"terminal.gitlab.com", "base64.terminal.gitlab.com"}
upgrader = &websocket.Upgrader{Subprotocols: subprotocols}
BrowserPingInterval = 30 * time.Second
subprotocols = []string{"terminal.gitlab.com", "base64.terminal.gitlab.com"}
upgrader = &websocket.Upgrader{Subprotocols: subprotocols}
BrowserPingInterval = 30 * time.Second
)
// ProxyStream takes the given request, upgrades the connection to a WebSocket
// connection, and also takes a dst ReadWriteCloser where a
// bi-directional stream is set up, were the STDIN of the WebSocket it sent
// dst and the STDOUT/STDERR of dst is written to the WebSocket
// connection. The messages to the WebSocket are encoded into binary text.
func ProxyStream(w http.ResponseWriter, r *http.Request, stream io.ReadWriteCloser, proxy *StreamProxy) {
clientAddr := getClientAddr(r) // We can't know the port with confidence
logger := log.WithFields(log.Fields{
"clientAddr": clientAddr,
"pkg": "terminal",
})
clientConn, err := upgradeClient(w, r)
if err != nil {
logger.WithError(err).Error("failed to upgrade client connection to websocket")
return
}
defer func() {
err := clientConn.UnderlyingConn().Close()
if err != nil {
logger.WithError(err).Error("failed to close client connection")
}
err = stream.Close()
if err != nil {
logger.WithError(err).Error("failed to close stream")
}
}()
client := NewIOWrapper(clientConn)
// Regularly send ping messages to the browser to keep the websocket from
// being timed out by intervening proxies.
go pingLoop(client)
if err := proxy.Serve(client, stream); err != nil {
logger.WithError(err).Error("failed to proxy stream")
}
}
// ProxyWebSocket takes the given request, upgrades the connection to a
// WebSocket connection. The terminal settings are used to connect to the
// dst WebSocket connection where it establishes a bi-directional stream
// between both web sockets.
func ProxyWebSocket(w http.ResponseWriter, r *http.Request, terminal *TerminalSettings, proxy *WebSocketProxy) {
server, err := connectToServer(terminal, r)
if err != nil {
......@@ -55,6 +102,10 @@ func ProxyWebSocket(w http.ResponseWriter, r *http.Request, terminal *TerminalSe
}
}
// ProxyFileDescriptor takes the given request, upgrades the connection to a
// WebSocket connection. A bi-directional stream is opened between the WebSocket
// and FileDescriptor that pipes the STDIN from the WebSocket to the
// FileDescriptor , and STDERR/STDOUT back to the WebSocket.
func ProxyFileDescriptor(w http.ResponseWriter, r *http.Request, fd *os.File, proxy *FileDescriptorProxy) {
clientConn, err := upgradeClient(w, r)
if err != nil {
......
package terminal
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const StreamMessage = "this is a test"
func TestProxyStream(t *testing.T) {
downstream := bufferCloser{}
downstream.Write([]byte(StreamMessage))
srv := streamServer{
downstream: downstream,
}
s := httptest.NewServer(&srv)
defer s.Close()
c, _, err := websocket.DefaultDialer.Dial("ws://"+s.Listener.Addr().String()+"/ws", nil)
require.NoError(t, err)
// Check if writing to websocket works
c.WriteMessage(websocket.BinaryMessage, []byte(StreamMessage))
b := make([]byte, len(StreamMessage))
_, err = downstream.Read(b)
require.NoError(t, err)
assert.Equal(t, []byte(StreamMessage), b)
// Check if reading from websocket works
typ, b, err := c.ReadMessage()
require.NoError(t, err)
assert.Equal(t, typ, websocket.BinaryMessage)
assert.Equal(t, []byte(StreamMessage), b)
}
type streamServer struct {
downstream bufferCloser
}
func (d *streamServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
proxy := NewStreamProxy(1)
ProxyStream(w, r, &d.downstream, proxy)
}
type bufferCloser struct {
bytes.Buffer
}
func (b *bufferCloser) Close() error {
return nil
}
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