terminal.go (132 lines of code) (raw):
package terminal
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
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
)
// 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 {
fail500(w, r, err)
log.WithError(err).Print("Terminal: connecting to server failed")
return
}
defer server.UnderlyingConn().Close()
serverAddr := server.UnderlyingConn().RemoteAddr().String()
client, err := upgradeClient(w, r)
if err != nil {
log.WithError(err).Print("Terminal: upgrading client to websocket failed")
return
}
// Regularly send ping messages to the browser to keep the websocket from
// being timed out by intervening proxies.
go pingLoop(client)
defer client.UnderlyingConn().Close()
clientAddr := getClientAddr(r) // We can't know the port with confidence
logEntry := log.WithFields(log.Fields{
"clientAddr": clientAddr,
"serverAddr": serverAddr,
})
logEntry.Print("Terminal: started proxying")
defer logEntry.Print("Terminal: finished proxying")
if err := proxy.Serve(server, client, serverAddr, clientAddr); err != nil {
logEntry.WithError(err).Print("Terminal: error proxying")
}
}
// 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 {
log.WithError(err).Print("Terminal: upgrading client to websocket failed")
return
}
client := NewIOWrapper(clientConn)
// Regularly send ping messages to the browser to keep the websocket from
// being timed out by intervening proxies.
go pingLoop(clientConn)
defer clientConn.UnderlyingConn().Close()
clientAddr := getClientAddr(r) // We can't know the port with confidence
serverAddr := "shell"
logEntry := log.WithFields(log.Fields{
"clientAddr": clientAddr,
"serverAddr": serverAddr,
})
logEntry.Print("Terminal: started proxying")
defer logEntry.Print("Terminal: finished proxying")
if err := proxy.Serve(fd, client, serverAddr, clientAddr); err != nil {
logEntry.WithError(err).Print("Terminal: error proxying")
}
}
// In the future, we might want to look at X-Client-Ip or X-Forwarded-For
func getClientAddr(r *http.Request) string {
return r.RemoteAddr
}
func upgradeClient(w http.ResponseWriter, r *http.Request) (Connection, error) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}
return Wrap(conn, conn.Subprotocol()), nil
}
func pingLoop(conn Connection) {
for {
time.Sleep(BrowserPingInterval)
deadline := time.Now().Add(5 * time.Second)
if err := conn.WriteControl(websocket.PingMessage, nil, deadline); err != nil {
// Either the connection was already closed so no further pings are
// needed, or this connection is now dead and no further pings can
// be sent.
break
}
}
}
func connectToServer(terminal *TerminalSettings, r *http.Request) (Connection, error) {
terminal = terminal.Clone()
setForwardedFor(&terminal.Header, r)
conn, _, err := terminal.Dial()
if err != nil {
return nil, err
}
return Wrap(conn, conn.Subprotocol()), nil
}
func CloseAfterMaxTime(proxy Proxy, maxSessionTime int) {
if maxSessionTime == 0 {
return
}
<-time.After(time.Duration(maxSessionTime) * time.Second)
stopCh := proxy.GetStopCh()
stopCh <- errors.New(
fmt.Sprintf(
"Connection closed: session time greater than maximum time allowed - %v seconds",
maxSessionTime,
),
)
}