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