[ARVADOS] created: 2.1.0-776-gb691255ba
Git user
git at public.arvados.org
Tue May 11 14:59:50 UTC 2021
at b691255ba6a9ce8d57a78911ca31ed83f41cfe35 (commit)
commit b691255ba6a9ce8d57a78911ca31ed83f41cfe35
Author: Tom Clegg <tom at curii.com>
Date: Tue May 11 10:59:18 2021 -0400
17209: Forward http requests to container indicated in vhost.
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>
diff --git a/cmd/arvados-client/container_gateway_test.go b/cmd/arvados-client/container_gateway_test.go
index 89e926f59..9be5c22cc 100644
--- a/cmd/arvados-client/container_gateway_test.go
+++ b/cmd/arvados-client/container_gateway_test.go
@@ -9,6 +9,7 @@ import (
"context"
"crypto/hmac"
"crypto/sha256"
+ "crypto/tls"
"fmt"
"io/ioutil"
"net"
@@ -25,6 +26,8 @@ import (
"git.arvados.org/arvados.git/lib/crunchrun"
"git.arvados.org/arvados.git/sdk/go/arvados"
"git.arvados.org/arvados.git/sdk/go/arvadostest"
+ "git.arvados.org/arvados.git/sdk/go/auth"
+ "git.arvados.org/arvados.git/sdk/go/ctxlog"
"git.arvados.org/arvados.git/sdk/go/httpserver"
check "gopkg.in/check.v1"
)
@@ -55,6 +58,7 @@ func (s *ClientSuite) TestShellGateway(c *check.C) {
ContainerUUID: uuid,
Address: "0.0.0.0:0",
AuthSecret: authSecret,
+ Log: ctxlog.TestLogger(c),
// Just forward connections to localhost instead of a
// container, so we can test without running a
// container.
@@ -171,4 +175,23 @@ func (s *ClientSuite) TestShellGateway(c *check.C) {
}()
}
wg.Wait()
+
+ client := &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ },
+ }
+ _, port, _ := net.SplitHostPort(httpTarget.Addr)
+ req, err := http.NewRequestWithContext(ctx, "GET", "https://"+os.Getenv("ARVADOS_API_HOST")+"/foo", nil)
+ c.Assert(err, check.IsNil)
+ req.AddCookie(&http.Cookie{Name: "arvados_api_token", Value: auth.EncodeTokenCookie([]byte(arvadostest.ActiveTokenV2))})
+ req.Host = uuid + "-" + port + ".example.com"
+ resp, err := client.Do(req)
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+ body, err := ioutil.ReadAll(resp.Body)
+ c.Check(err, check.IsNil)
+ c.Check(string(body), check.Equals, "bar baz\n")
}
diff --git a/lib/controller/federation/conn.go b/lib/controller/federation/conn.go
index 6029056b2..66f7546e7 100644
--- a/lib/controller/federation/conn.go
+++ b/lib/controller/federation/conn.go
@@ -339,6 +339,10 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
return conn.chooseBackend(options.UUID).ContainerUnlock(ctx, options)
}
+func (conn *Conn) ContainerHTTP(ctx context.Context, options arvados.ContainerHTTPOptions) (sshconn arvados.ContainerHTTPResponse, err error) {
+ return conn.chooseBackend(options.UUID).ContainerHTTP(ctx, options)
+}
+
func (conn *Conn) ContainerSSH(ctx context.Context, options arvados.ContainerSSHOptions) (arvados.ContainerSSHConnection, error) {
return conn.chooseBackend(options.UUID).ContainerSSH(ctx, options)
}
diff --git a/lib/controller/handler.go b/lib/controller/handler.go
index a35d00301..817409d6e 100644
--- a/lib/controller/handler.go
+++ b/lib/controller/handler.go
@@ -21,6 +21,7 @@ import (
"git.arvados.org/arvados.git/lib/controller/router"
"git.arvados.org/arvados.git/lib/ctrlctx"
"git.arvados.org/arvados.git/sdk/go/arvados"
+ "git.arvados.org/arvados.git/sdk/go/arvadosclient"
"git.arvados.org/arvados.git/sdk/go/ctxlog"
"git.arvados.org/arvados.git/sdk/go/health"
"git.arvados.org/arvados.git/sdk/go/httpserver"
@@ -35,6 +36,7 @@ type Handler struct {
setupOnce sync.Once
handlerStack http.Handler
+ router http.Handler
proxy *proxy
secureClient *http.Client
insecureClient *http.Client
@@ -63,7 +65,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req = req.WithContext(ctx)
defer cancel()
}
-
+ if len(req.Host) > 27 && arvadosclient.UUIDMatch(req.Host[:27]) && req.Host[27] == '-' {
+ // Bypass the servemux ("proxy everything to RailsAPI
+ // except some specific paths") routing for
+ // "{ctr-uuid}-{port}.example.com" vhosts: all
+ // requests should be routed through the container
+ // gateway.
+ h.router.ServeHTTP(w, req)
+ return
+ }
h.handlerStack.ServeHTTP(w, req)
}
@@ -92,23 +102,23 @@ func (h *Handler) setup() {
})
oidcAuthorizer := localdb.OIDCAccessTokenAuthorizer(h.Cluster, h.db)
- rtr := router.New(federation.New(h.Cluster), router.Config{
+ h.router = router.New(federation.New(h.Cluster), router.Config{
MaxRequestSize: h.Cluster.API.MaxRequestSize,
WrapCalls: api.ComposeWrappers(ctrlctx.WrapCallsInTransactions(h.db), oidcAuthorizer.WrapCalls),
})
- mux.Handle("/arvados/v1/config", rtr)
- mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, rtr) // must come before .../users/
- mux.Handle("/arvados/v1/collections", rtr)
- mux.Handle("/arvados/v1/collections/", rtr)
- mux.Handle("/arvados/v1/users", rtr)
- mux.Handle("/arvados/v1/users/", rtr)
- mux.Handle("/arvados/v1/connect/", rtr)
- mux.Handle("/arvados/v1/container_requests", rtr)
- mux.Handle("/arvados/v1/container_requests/", rtr)
- mux.Handle("/arvados/v1/groups", rtr)
- mux.Handle("/arvados/v1/groups/", rtr)
- mux.Handle("/login", rtr)
- mux.Handle("/logout", rtr)
+ mux.Handle("/arvados/v1/config", h.router)
+ mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, h.router) // must come before .../users/
+ mux.Handle("/arvados/v1/collections", h.router)
+ mux.Handle("/arvados/v1/collections/", h.router)
+ mux.Handle("/arvados/v1/users", h.router)
+ mux.Handle("/arvados/v1/users/", h.router)
+ mux.Handle("/arvados/v1/connect/", h.router)
+ mux.Handle("/arvados/v1/container_requests", h.router)
+ mux.Handle("/arvados/v1/container_requests/", h.router)
+ mux.Handle("/arvados/v1/groups", h.router)
+ mux.Handle("/arvados/v1/groups/", h.router)
+ mux.Handle("/login", h.router)
+ mux.Handle("/logout", h.router)
hs := http.NotFoundHandler()
hs = prepend(hs, h.proxyRailsAPI)
diff --git a/lib/controller/localdb/container_gateway.go b/lib/controller/localdb/container_gateway.go
index 3b40eccaf..6e643253a 100644
--- a/lib/controller/localdb/container_gateway.go
+++ b/lib/controller/localdb/container_gateway.go
@@ -6,6 +6,7 @@ package localdb
import (
"bufio"
+ "bytes"
"context"
"crypto/hmac"
"crypto/sha256"
@@ -13,8 +14,11 @@ import (
"crypto/x509"
"errors"
"fmt"
+ "io"
+ "net"
"net/http"
"net/url"
+ "strconv"
"strings"
"git.arvados.org/arvados.git/sdk/go/arvados"
@@ -23,56 +27,51 @@ import (
"git.arvados.org/arvados.git/sdk/go/httpserver"
)
-// ContainerSSH returns a connection to the SSH server in the
-// appropriate crunch-run process on the worker node where the
-// specified container is running.
-//
-// If the returned error is nil, the caller is responsible for closing
-// sshconn.Conn.
-func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
+type gwConn struct {
+ net.Conn
+ container arvados.Container
+ requestAuth string
+ respondAuth string
+}
+
+func (conn *Conn) connectContainerGateway(ctx context.Context, uuid string) (*gwConn, error) {
user, err := conn.railsProxy.UserGetCurrent(ctx, arvados.GetOptions{})
if err != nil {
- return
+ return nil, err
}
- ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: opts.UUID})
+ ctr, err := conn.railsProxy.ContainerGet(ctx, arvados.GetOptions{UUID: uuid})
if err != nil {
- return
+ return nil, err
}
ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
if !user.IsAdmin || !conn.cluster.Containers.ShellAccess.Admin {
if !conn.cluster.Containers.ShellAccess.User {
- err = httpserver.ErrorWithStatus(errors.New("shell access is disabled in config"), http.StatusServiceUnavailable)
- return
+ return nil, httpserver.ErrorWithStatus(errors.New("shell access is disabled in config"), http.StatusServiceUnavailable)
}
var crs arvados.ContainerRequestList
- crs, err = conn.railsProxy.ContainerRequestList(ctxRoot, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"container_uuid", "=", opts.UUID}}})
+ crs, err = conn.railsProxy.ContainerRequestList(ctxRoot, arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"container_uuid", "=", uuid}}})
if err != nil {
- return
+ return nil, err
}
for _, cr := range crs.Items {
if cr.ModifiedByUserUUID != user.UUID {
- err = httpserver.ErrorWithStatus(errors.New("permission denied: container is associated with requests submitted by other users"), http.StatusForbidden)
- return
+ return nil, httpserver.ErrorWithStatus(errors.New("permission denied: container is associated with requests submitted by other users"), http.StatusForbidden)
}
}
if crs.ItemsAvailable != len(crs.Items) {
- err = httpserver.ErrorWithStatus(errors.New("incomplete response while checking permission"), http.StatusInternalServerError)
- return
+ return nil, httpserver.ErrorWithStatus(errors.New("incomplete response while checking permission"), http.StatusInternalServerError)
}
}
switch ctr.State {
case arvados.ContainerStateQueued, arvados.ContainerStateLocked:
- err = httpserver.ErrorWithStatus(fmt.Errorf("container is not running yet (state is %q)", ctr.State), http.StatusServiceUnavailable)
- return
+ return nil, httpserver.ErrorWithStatus(fmt.Errorf("container is not running yet (state is %q)", ctr.State), http.StatusServiceUnavailable)
case arvados.ContainerStateRunning:
if ctr.GatewayAddress == "" {
- err = httpserver.ErrorWithStatus(errors.New("container is running but gateway is not available -- installation problem or feature not supported"), http.StatusServiceUnavailable)
- return
+ return nil, httpserver.ErrorWithStatus(errors.New("container is running but gateway is not available -- installation problem or feature not supported"), http.StatusServiceUnavailable)
}
default:
- err = httpserver.ErrorWithStatus(fmt.Errorf("container has ended (state is %q)", ctr.State), http.StatusGone)
- return
+ return nil, httpserver.ErrorWithStatus(fmt.Errorf("container has ended (state is %q)", ctr.State), http.StatusGone)
}
// crunch-run uses a self-signed / unverifiable TLS
// certificate, so we use the following scheme to ensure we're
@@ -100,7 +99,7 @@ func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOpt
return errors.New("no certificate received, cannot compute authorization header")
}
h := hmac.New(sha256.New, []byte(conn.cluster.SystemRootToken))
- fmt.Fprint(h, opts.UUID)
+ fmt.Fprint(h, uuid)
authKey := fmt.Sprintf("%x", h.Sum(nil))
h = hmac.New(sha256.New, []byte(authKey))
h.Write(rawCerts[0])
@@ -112,26 +111,45 @@ func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOpt
},
})
if err != nil {
- err = httpserver.ErrorWithStatus(err, http.StatusBadGateway)
- return
+ netconn.Close()
+ return nil, httpserver.ErrorWithStatus(err, http.StatusBadGateway)
}
if respondAuth == "" {
- err = httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
+ netconn.Close()
+ return nil, httpserver.ErrorWithStatus(errors.New("BUG: no respondAuth"), http.StatusInternalServerError)
+ }
+ return &gwConn{
+ Conn: netconn,
+ container: ctr,
+ requestAuth: requestAuth,
+ respondAuth: respondAuth,
+ }, nil
+}
+
+// ContainerSSH returns a connection to the SSH server in the
+// appropriate crunch-run process on the worker node where the
+// specified container is running.
+//
+// If the returned error is nil, the caller is responsible for closing
+// sshconn.Conn.
+func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOptions) (sshconn arvados.ContainerSSHConnection, err error) {
+ gwconn, err := conn.connectContainerGateway(ctx, opts.UUID)
+ if err != nil {
return
}
- bufr := bufio.NewReader(netconn)
- bufw := bufio.NewWriter(netconn)
+ bufr := bufio.NewReader(gwconn)
+ bufw := bufio.NewWriter(gwconn)
u := url.URL{
Scheme: "http",
- Host: ctr.GatewayAddress,
+ Host: gwconn.container.GatewayAddress,
Path: "/ssh",
}
bufw.WriteString("GET " + u.String() + " HTTP/1.1\r\n")
bufw.WriteString("Host: " + u.Host + "\r\n")
bufw.WriteString("Upgrade: ssh\r\n")
bufw.WriteString("X-Arvados-Target-Uuid: " + opts.UUID + "\r\n")
- bufw.WriteString("X-Arvados-Authorization: " + requestAuth + "\r\n")
+ bufw.WriteString("X-Arvados-Authorization: " + gwconn.requestAuth + "\r\n")
bufw.WriteString("X-Arvados-Detach-Keys: " + opts.DetachKeys + "\r\n")
bufw.WriteString("X-Arvados-Login-Username: " + opts.LoginUsername + "\r\n")
bufw.WriteString("\r\n")
@@ -139,22 +157,23 @@ func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOpt
resp, err := http.ReadResponse(bufr, &http.Request{Method: "GET"})
if err != nil {
err = httpserver.ErrorWithStatus(fmt.Errorf("error reading http response from gateway: %w", err), http.StatusBadGateway)
- netconn.Close()
+ gwconn.Close()
return
}
- if resp.Header.Get("X-Arvados-Authorization-Response") != respondAuth {
+ if resp.Header.Get("X-Arvados-Authorization-Response") != gwconn.respondAuth {
err = httpserver.ErrorWithStatus(errors.New("bad X-Arvados-Authorization-Response header"), http.StatusBadGateway)
- netconn.Close()
+ gwconn.Close()
return
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "ssh" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
err = httpserver.ErrorWithStatus(errors.New("bad upgrade"), http.StatusBadGateway)
- netconn.Close()
+ gwconn.Close()
return
}
- if !ctr.InteractiveSessionStarted {
+ if !gwconn.container.InteractiveSessionStarted {
+ ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{conn.cluster.SystemRootToken}})
_, err = conn.railsProxy.ContainerUpdate(ctxRoot, arvados.UpdateOptions{
UUID: opts.UUID,
Attrs: map[string]interface{}{
@@ -162,13 +181,80 @@ func (conn *Conn) ContainerSSH(ctx context.Context, opts arvados.ContainerSSHOpt
},
})
if err != nil {
- netconn.Close()
+ gwconn.Close()
return
}
}
- sshconn.Conn = netconn
+ sshconn.Conn = gwconn
sshconn.Bufrw = &bufio.ReadWriter{Reader: bufr, Writer: bufw}
sshconn.Logger = ctxlog.FromContext(ctx)
return
}
+
+func (conn *Conn) ContainerHTTP(ctx context.Context, opts arvados.ContainerHTTPOptions) (resp arvados.ContainerHTTPResponse, err error) {
+ query := opts.Request.URL.Query()
+ token := query.Get("arvados_api_token")
+ if token != "" {
+ redir := *opts.Request.URL
+ delete(query, "arvados_api_token")
+ redir.RawQuery = query.Encode()
+ return arvados.ContainerHTTPResponse{
+ Response: http.Response{
+ StatusCode: http.StatusSeeOther,
+ Body: io.NopCloser(bytes.NewBufferString("")),
+ Header: http.Header{
+ "Cookie": {auth.EncodeTokenCookie([]byte(token))},
+ "Location": {redir.String()},
+ },
+ },
+ }, nil
+ }
+ url := *opts.Request.URL
+ url.Scheme = "http"
+ url.Host = "localhost"
+ req, err := http.NewRequestWithContext(ctx, opts.Request.Method, url.String(), opts.Request.Body)
+ if err != nil {
+ return
+ }
+ req.Host = opts.Request.Host
+ req.Header = opts.Request.Header
+
+ cookies := req.Cookies()
+ req.Header.Del("Cookie")
+ for _, cookie := range cookies {
+ if cookie.Name == "arvados_api_token" {
+ token, err := auth.DecodeTokenCookie(cookie.Value)
+ if err != nil {
+ return arvados.ContainerHTTPResponse{}, err
+ }
+ ctx = auth.NewContext(ctx, auth.NewCredentials(string(token)))
+ } else {
+ req.AddCookie(cookie)
+ }
+ }
+ gwconn, err := conn.connectContainerGateway(ctx, opts.UUID)
+ if err != nil {
+ return
+ }
+ req.Header.Set("X-Arvados-Target-Uuid", opts.UUID)
+ req.Header.Set("X-Arvados-Target-Port", strconv.Itoa(opts.Port))
+ req.Header.Set("X-Arvados-Authorization", gwconn.requestAuth)
+ req.Header.Add("Via", "HTTP/1.1 arvados-controller")
+
+ client := http.Client{
+ CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return gwconn.Conn, nil
+ },
+ },
+ }
+ httpResp, err := client.Do(req)
+ if err != nil {
+ return arvados.ContainerHTTPResponse{}, err
+ }
+ return arvados.ContainerHTTPResponse{
+ Response: *httpResp,
+ }, nil
+}
diff --git a/lib/controller/router/router.go b/lib/controller/router/router.go
index 5ceabbfb1..fe6a2d6e2 100644
--- a/lib/controller/router/router.go
+++ b/lib/controller/router/router.go
@@ -7,12 +7,14 @@ package router
import (
"context"
"fmt"
+ "io"
"math"
"net/http"
"strings"
"git.arvados.org/arvados.git/lib/controller/api"
"git.arvados.org/arvados.git/sdk/go/arvados"
+ "git.arvados.org/arvados.git/sdk/go/arvadosclient"
"git.arvados.org/arvados.git/sdk/go/auth"
"git.arvados.org/arvados.git/sdk/go/ctxlog"
"git.arvados.org/arvados.git/sdk/go/httpserver"
@@ -522,6 +524,16 @@ func (rtr *router) addRoute(endpoint arvados.APIEndpoint, defaultOpts func() int
}
func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if len(r.Host) > 28 && arvadosclient.UUIDMatch(r.Host[:27]) && r.Host[27] == '-' {
+ var port int
+ fmt.Sscanf(r.Host[28:], "%d", &port)
+ if port < 1 {
+ rtr.sendError(w, httpError(http.StatusBadRequest, fmt.Errorf("cannot parse port number from vhost %q", r.Host)))
+ return
+ }
+ rtr.serveContainerHTTP(r.Host[:27], port, w, r)
+ return
+ }
switch strings.SplitN(strings.TrimLeft(r.URL.Path, "/"), "/", 2)[0] {
case "login", "logout", "auth":
default:
@@ -565,3 +577,21 @@ func (rtr *router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
rtr.mux.ServeHTTP(w, r)
}
+
+func (rtr *router) serveContainerHTTP(uuid string, port int, w http.ResponseWriter, req *http.Request) {
+ resp, err := rtr.backend.ContainerHTTP(req.Context(), arvados.ContainerHTTPOptions{
+ UUID: uuid,
+ Port: port,
+ Request: req,
+ })
+ if err != nil {
+ rtr.sendError(w, err)
+ return
+ }
+ defer resp.Response.Body.Close()
+ for k, v := range resp.Response.Header {
+ w.Header()[k] = v
+ }
+ w.WriteHeader(resp.Response.StatusCode)
+ io.Copy(w, resp.Response.Body)
+}
diff --git a/lib/controller/rpc/conn.go b/lib/controller/rpc/conn.go
index 940f2184b..bee7c5525 100644
--- a/lib/controller/rpc/conn.go
+++ b/lib/controller/rpc/conn.go
@@ -321,6 +321,10 @@ func (conn *Conn) ContainerUnlock(ctx context.Context, options arvados.GetOption
return resp, err
}
+func (conn *Conn) ContainerHTTP(ctx context.Context, options arvados.ContainerHTTPOptions) (sshconn arvados.ContainerHTTPResponse, err error) {
+ return arvados.ContainerHTTPResponse{}, errors.New("not implemented")
+}
+
// ContainerSSH returns a connection to the out-of-band SSH server for
// a running container. If the returned error is nil, the caller is
// responsible for closing sshconn.Conn.
diff --git a/lib/crunchrun/container_gateway.go b/lib/crunchrun/container_gateway.go
index 2ec24bac7..9bb7f5722 100644
--- a/lib/crunchrun/container_gateway.go
+++ b/lib/crunchrun/container_gateway.go
@@ -106,7 +106,7 @@ func (gw *Gateway) Start() error {
srv := &httpserver.Server{
Server: http.Server{
- Handler: http.HandlerFunc(gw.handleSSH),
+ Handler: http.HandlerFunc(gw.serveHTTP),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
@@ -131,6 +131,75 @@ func (gw *Gateway) Start() error {
return nil
}
+func (gw *Gateway) serveHTTP(w http.ResponseWriter, req *http.Request) {
+ if want := req.Header.Get("X-Arvados-Target-Uuid"); want != gw.ContainerUUID {
+ http.Error(w, fmt.Sprintf("misdirected request: meant for %q but received by crunch-run %q", want, gw.ContainerUUID), http.StatusBadGateway)
+ return
+ }
+ if req.Header.Get("X-Arvados-Authorization") != gw.requestAuth {
+ http.Error(w, "bad X-Arvados-Authorization header", http.StatusUnauthorized)
+ return
+ }
+ switch {
+ case req.Method == "GET" && req.Header.Get("Upgrade") == "ssh":
+ // SSH tunnel from
+ // (*lib/controller/localdb.Conn)ContainerSSH()
+ gw.handleSSH(w, req)
+ case req.Header.Get("X-Arvados-Target-Port") != "":
+ // HTTP forwarded through
+ // (*lib/controller/localdb.Conn)ContainerHTTP()
+ gw.handleForwardedHTTP(w, req)
+ default:
+ http.Error(w, "path not found", http.StatusNotFound)
+ }
+}
+
+func (gw *Gateway) handleForwardedHTTP(w http.ResponseWriter, reqIn *http.Request) {
+ port := reqIn.Header.Get("X-Arvados-Target-Port")
+ var host string
+ var err error
+ if gw.ContainerIPAddress != nil {
+ host, err = gw.ContainerIPAddress()
+ if err != nil {
+ http.Error(w, "container has no IP address: "+err.Error(), http.StatusServiceUnavailable)
+ return
+ }
+ }
+ if host == "" {
+ http.Error(w, "container has no IP address", http.StatusServiceUnavailable)
+ return
+ }
+ client := http.Client{
+ CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
+ // Transport: &http.Transport{
+ // DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ // return (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port))
+ // },
+ // },
+ }
+ url := *reqIn.URL
+ url.Scheme = "http"
+ url.Host = net.JoinHostPort(host, port)
+ req, err := http.NewRequestWithContext(reqIn.Context(), reqIn.Method, url.String(), reqIn.Body)
+ req.Host = reqIn.Host
+ req.Header = reqIn.Header
+ req.Header.Del("X-Arvados-Target-Uuid")
+ req.Header.Del("X-Arvados-Target-Port")
+ req.Header.Del("X-Arvados-Authorization")
+ req.Header.Add("Via", "HTTP/1.1 arvados-crunch-run")
+ resp, err := client.Do(req)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadGateway)
+ return
+ }
+ defer resp.Body.Close()
+ for k, v := range resp.Header {
+ w.Header()[k] = v
+ }
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
+}
+
// handleSSH connects to an SSH server that allows the caller to run
// interactive commands as root (or any other desired user) inside the
// container. The tunnel itself can only be created by an
@@ -153,21 +222,6 @@ func (gw *Gateway) Start() error {
// X-Arvados-Login-Username: argument to "docker exec --user": account
// used to run command(s) inside the container.
func (gw *Gateway) handleSSH(w http.ResponseWriter, req *http.Request) {
- // In future we'll handle browser traffic too, but for now the
- // only traffic we expect is an SSH tunnel from
- // (*lib/controller/localdb.Conn)ContainerSSH()
- if req.Method != "GET" || req.Header.Get("Upgrade") != "ssh" {
- http.Error(w, "path not found", http.StatusNotFound)
- return
- }
- if want := req.Header.Get("X-Arvados-Target-Uuid"); want != gw.ContainerUUID {
- http.Error(w, fmt.Sprintf("misdirected request: meant for %q but received by crunch-run %q", want, gw.ContainerUUID), http.StatusBadGateway)
- return
- }
- if req.Header.Get("X-Arvados-Authorization") != gw.requestAuth {
- http.Error(w, "bad X-Arvados-Authorization header", http.StatusUnauthorized)
- return
- }
detachKeys := req.Header.Get("X-Arvados-Detach-Keys")
username := req.Header.Get("X-Arvados-Login-Username")
if username == "" {
diff --git a/sdk/go/arvados/api.go b/sdk/go/arvados/api.go
index bfae393f8..2aaab19ea 100644
--- a/sdk/go/arvados/api.go
+++ b/sdk/go/arvados/api.go
@@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"net"
+ "net/http"
"github.com/sirupsen/logrus"
)
@@ -80,6 +81,16 @@ var (
EndpointAPIClientAuthorizationCurrent = APIEndpoint{"GET", "arvados/v1/api_client_authorizations/current", ""}
)
+type ContainerHTTPOptions struct {
+ UUID string `json:"uuid"`
+ Port int `json:"port"`
+ Request *http.Request
+}
+
+type ContainerHTTPResponse struct {
+ Response http.Response
+}
+
type ContainerSSHOptions struct {
UUID string `json:"uuid"`
DetachKeys string `json:"detach_keys"`
@@ -224,6 +235,7 @@ type API interface {
ContainerDelete(ctx context.Context, options DeleteOptions) (Container, error)
ContainerLock(ctx context.Context, options GetOptions) (Container, error)
ContainerUnlock(ctx context.Context, options GetOptions) (Container, error)
+ ContainerHTTP(ctx context.Context, options ContainerHTTPOptions) (ContainerHTTPResponse, error)
ContainerSSH(ctx context.Context, options ContainerSSHOptions) (ContainerSSHConnection, error)
ContainerRequestCreate(ctx context.Context, options CreateOptions) (ContainerRequest, error)
ContainerRequestUpdate(ctx context.Context, options UpdateOptions) (ContainerRequest, error)
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list