[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