1package terminal
2
3import (
4	"errors"
5	"fmt"
6	"io"
7	"net/http"
8	"os"
9	"time"
10
11	"github.com/gorilla/websocket"
12	log "github.com/sirupsen/logrus"
13)
14
15var (
16	// See doc/terminal.md for documentation of this subprotocol
17	subprotocols        = []string{"terminal.gitlab.com", "base64.terminal.gitlab.com"}
18	upgrader            = &websocket.Upgrader{Subprotocols: subprotocols}
19	BrowserPingInterval = 30 * time.Second
20)
21
22// ProxyStream takes the given request, upgrades the connection to a WebSocket
23// connection, and also takes a dst ReadWriteCloser where a
24// bi-directional stream is set up, were the STDIN of the WebSocket it sent
25// dst and the STDOUT/STDERR of dst is written to the WebSocket
26// connection. The messages to the WebSocket are encoded into binary text.
27func ProxyStream(w http.ResponseWriter, r *http.Request, stream io.ReadWriteCloser, proxy *StreamProxy) {
28	clientAddr := getClientAddr(r) // We can't know the port with confidence
29
30	logger := log.WithFields(log.Fields{
31		"clientAddr": clientAddr,
32		"pkg":        "terminal",
33	})
34
35	clientConn, err := upgradeClient(w, r)
36	if err != nil {
37		logger.WithError(err).Error("failed to upgrade client connection to websocket")
38		return
39	}
40
41	defer func() {
42		err := clientConn.UnderlyingConn().Close()
43		if err != nil {
44			logger.WithError(err).Error("failed to close client connection")
45		}
46
47		err = stream.Close()
48		if err != nil {
49			logger.WithError(err).Error("failed to close stream")
50		}
51	}()
52
53	client := NewIOWrapper(clientConn)
54
55	// Regularly send ping messages to the browser to keep the websocket from
56	// being timed out by intervening proxies.
57	go pingLoop(client)
58
59	if err := proxy.Serve(client, stream); err != nil {
60		logger.WithError(err).Error("failed to proxy stream")
61	}
62}
63
64// ProxyWebSocket takes the given request, upgrades the connection to a
65// WebSocket connection. The terminal settings are used to connect to the
66// dst WebSocket connection where it establishes a bi-directional stream
67// between both web sockets.
68func ProxyWebSocket(w http.ResponseWriter, r *http.Request, terminal *TerminalSettings, proxy *WebSocketProxy) {
69	server, err := connectToServer(terminal, r)
70	if err != nil {
71		fail500(w, r, err)
72		log.WithError(err).Print("Terminal: connecting to server failed")
73		return
74	}
75	defer server.UnderlyingConn().Close()
76	serverAddr := server.UnderlyingConn().RemoteAddr().String()
77
78	client, err := upgradeClient(w, r)
79	if err != nil {
80		log.WithError(err).Print("Terminal: upgrading client to websocket failed")
81		return
82	}
83
84	// Regularly send ping messages to the browser to keep the websocket from
85	// being timed out by intervening proxies.
86	go pingLoop(client)
87
88	defer client.UnderlyingConn().Close()
89	clientAddr := getClientAddr(r) // We can't know the port with confidence
90
91	logEntry := log.WithFields(log.Fields{
92		"clientAddr": clientAddr,
93		"serverAddr": serverAddr,
94	})
95
96	logEntry.Print("Terminal: started proxying")
97
98	defer logEntry.Print("Terminal: finished proxying")
99
100	if err := proxy.Serve(server, client, serverAddr, clientAddr); err != nil {
101		logEntry.WithError(err).Print("Terminal: error proxying")
102	}
103}
104
105// ProxyFileDescriptor takes the given request, upgrades the connection to a
106// WebSocket connection. A bi-directional stream is opened between the WebSocket
107// and FileDescriptor that pipes the STDIN from the WebSocket to the
108// FileDescriptor , and STDERR/STDOUT back to the WebSocket.
109func ProxyFileDescriptor(w http.ResponseWriter, r *http.Request, fd *os.File, proxy *FileDescriptorProxy) {
110	clientConn, err := upgradeClient(w, r)
111	if err != nil {
112		log.WithError(err).Print("Terminal: upgrading client to websocket failed")
113		return
114	}
115	client := NewIOWrapper(clientConn)
116
117	// Regularly send ping messages to the browser to keep the websocket from
118	// being timed out by intervening proxies.
119	go pingLoop(clientConn)
120
121	defer clientConn.UnderlyingConn().Close()
122	clientAddr := getClientAddr(r) // We can't know the port with confidence
123
124	serverAddr := "shell"
125	logEntry := log.WithFields(log.Fields{
126		"clientAddr": clientAddr,
127		"serverAddr": serverAddr,
128	})
129
130	logEntry.Print("Terminal: started proxying")
131
132	defer logEntry.Print("Terminal: finished proxying")
133
134	if err := proxy.Serve(fd, client, serverAddr, clientAddr); err != nil {
135		logEntry.WithError(err).Print("Terminal: error proxying")
136	}
137}
138
139// In the future, we might want to look at X-Client-Ip or X-Forwarded-For
140func getClientAddr(r *http.Request) string {
141	return r.RemoteAddr
142}
143
144func upgradeClient(w http.ResponseWriter, r *http.Request) (Connection, error) {
145	conn, err := upgrader.Upgrade(w, r, nil)
146	if err != nil {
147		return nil, err
148	}
149
150	return Wrap(conn, conn.Subprotocol()), nil
151}
152
153func pingLoop(conn Connection) {
154	for {
155		time.Sleep(BrowserPingInterval)
156		deadline := time.Now().Add(5 * time.Second)
157		if err := conn.WriteControl(websocket.PingMessage, nil, deadline); err != nil {
158			// Either the connection was already closed so no further pings are
159			// needed, or this connection is now dead and no further pings can
160			// be sent.
161			break
162		}
163	}
164}
165
166func connectToServer(terminal *TerminalSettings, r *http.Request) (Connection, error) {
167	terminal = terminal.Clone()
168
169	setForwardedFor(&terminal.Header, r)
170
171	conn, _, err := terminal.Dial()
172	if err != nil {
173		return nil, err
174	}
175
176	return Wrap(conn, conn.Subprotocol()), nil
177}
178
179func CloseAfterMaxTime(proxy Proxy, maxSessionTime int) {
180	if maxSessionTime == 0 {
181		return
182	}
183
184	<-time.After(time.Duration(maxSessionTime) * time.Second)
185	stopCh := proxy.GetStopCh()
186	stopCh <- errors.New(
187		fmt.Sprintf(
188			"Connection closed: session time greater than maximum time allowed - %v seconds",
189			maxSessionTime,
190		),
191	)
192}
193