17170: Add "arvados-client shell" subcommand and backend support.
[arvados.git] / lib / controller / localdb / container_gateway.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "bufio"
9         "context"
10         "crypto/hmac"
11         "crypto/sha256"
12         "errors"
13         "fmt"
14         "net"
15         "net/http"
16         "net/url"
17         "strings"
18
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20         "git.arvados.org/arvados.git/sdk/go/httpserver"
21 )
22
23 // ContainerSSH returns a connection to the SSH server in the
24 // appropriate crunch-run process on the worker node where the
25 // specified container is running.
26 //
27 // If the returned error is nil, the caller is responsible for closing
28 // sshconn.Conn.
29 func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
30         ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
31         if err != nil {
32                 return
33         }
34         if ctr.GatewayAddress == "" || ctr.State != arvados.ContainerStateRunning {
35                 err = httpserver.ErrorWithStatus(fmt.Errorf("gateway is not available, container is %s", strings.ToLower(string(ctr.State))), http.StatusBadGateway)
36                 return
37         }
38         netconn, err := net.Dial("tcp", ctr.GatewayAddress)
39         if err != nil {
40                 return
41         }
42         bufr := bufio.NewReader(netconn)
43         bufw := bufio.NewWriter(netconn)
44
45         // Note this auth header does not protect from replay/mitm
46         // attacks (TODO: use TLS for that). It only authenticates us
47         // to crunch-run.
48         h := hmac.New(sha256.New, []byte(conn.cluster.SystemRootToken))
49         fmt.Fprint(h, "%s", opts.UUID)
50         auth := fmt.Sprintf("%x", h.Sum(nil))
51
52         u := url.URL{
53                 Scheme: "http",
54                 Host:   ctr.GatewayAddress,
55                 Path:   "/ssh",
56         }
57         bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
58         bufw.WriteString("Host: " + u.Host + "\r\n")
59         bufw.WriteString("Upgrade: ssh\r\n")
60         bufw.WriteString("X-Arvados-Target-Uuid: " + opts.UUID + "\r\n")
61         bufw.WriteString("X-Arvados-Authorization: " + auth + "\r\n")
62         bufw.WriteString("X-Arvados-Detach-Keys: " + opts.DetachKeys + "\r\n")
63         bufw.WriteString("\r\n")
64         bufw.Flush()
65         resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
66         if err != nil {
67                 err = fmt.Errorf("error reading http response from gateway: %w", err)
68                 netconn.Close()
69                 return
70         }
71         if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
72                 strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
73                 err = httpserver.ErrorWithStatus(errors.New("bad upgrade"), http.StatusBadGateway)
74                 netconn.Close()
75                 return
76         }
77         sshconn.Conn = netconn
78         sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
79         return
80 }