1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
31 "git.arvados.org/arvados.git/lib/controller/router"
32 "git.arvados.org/arvados.git/lib/controller/rpc"
33 "git.arvados.org/arvados.git/lib/crunchrun"
34 "git.arvados.org/arvados.git/lib/ctrlctx"
35 "git.arvados.org/arvados.git/sdk/go/arvados"
36 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
37 "git.arvados.org/arvados.git/sdk/go/arvadostest"
38 "git.arvados.org/arvados.git/sdk/go/auth"
39 "git.arvados.org/arvados.git/sdk/go/ctxlog"
40 "git.arvados.org/arvados.git/sdk/go/httpserver"
41 "git.arvados.org/arvados.git/sdk/go/keepclient"
42 "golang.org/x/crypto/ssh"
43 check "gopkg.in/check.v1"
46 var _ = check.Suite(&ContainerGatewaySuite{})
48 type ContainerGatewaySuite struct {
50 containerServices []*httpserver.Server
51 reqCreateOptions arvados.CreateOptions
56 assignedExtPort atomic.Int32
60 testDynamicPortMin = 10000
61 testDynamicPortMax = 20000
64 func (s *ContainerGatewaySuite) SetUpSuite(c *check.C) {
65 s.localdbSuite.SetUpSuite(c)
67 // Set up 10 http servers to play the role of services running
68 // inside a container. (crunchrun.GatewayTargetStub will allow
69 // our crunchrun.Gateway to connect to them directly on
70 // localhost, rather than actually running them inside a
72 for i := 0; i < 10; i++ {
73 srv := &httpserver.Server{
76 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77 w.WriteHeader(http.StatusOK)
78 body := fmt.Sprintf("handled %s %s with Host %s", r.Method, r.URL.String(), r.Host)
85 s.containerServices = append(s.containerServices, srv)
88 // s.containerServices[0] will be unlisted
89 // s.containerServices[1] will be listed with access=public
90 // s.containerServices[2,...] will be listed with access=private
91 publishedPorts := make(map[string]arvados.RequestPublishedPort)
92 for i, srv := range s.containerServices {
93 access := arvados.PublishedPortAccessPrivate
94 _, port, _ := net.SplitHostPort(srv.Addr)
96 access = arvados.PublishedPortAccessPublic
99 publishedPorts[port] = arvados.RequestPublishedPort{
101 Label: "port " + port,
106 s.reqCreateOptions = arvados.CreateOptions{
107 Attrs: map[string]interface{}{
108 "command": []string{"echo", time.Now().Format(time.RFC3339Nano)},
109 "container_count_max": 1,
110 "container_image": "arvados/apitestfixture:latest",
112 "environment": map[string]string{},
113 "output_path": "/out",
115 "state": arvados.ContainerRequestStateCommitted,
116 "mounts": map[string]interface{}{
117 "/out": map[string]interface{}{
122 "runtime_constraints": map[string]interface{}{
126 "published_ports": publishedPorts}}
129 func (s *ContainerGatewaySuite) TearDownSuite(c *check.C) {
130 for _, srv := range s.containerServices {
133 s.containerServices = nil
134 s.localdbSuite.TearDownSuite(c)
137 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
138 s.localdbSuite.SetUpTest(c)
140 s.localdb.cluster.Services.ContainerWebServices.ExternalURL.Host = "*.containers.example.com"
141 s.localdb.cluster.Services.ContainerWebServices.ExternalPortMin = 0
142 s.localdb.cluster.Services.ContainerWebServices.ExternalPortMax = 0
144 cr, err := s.localdb.ContainerRequestCreate(s.userctx, s.reqCreateOptions)
145 c.Assert(err, check.IsNil)
147 s.ctrUUID = cr.ContainerUUID
149 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
150 fmt.Fprint(h, s.ctrUUID)
151 authKey := fmt.Sprintf("%x", h.Sum(nil))
153 rtr := router.New(s.localdb, router.Config{
154 ContainerWebServices: &s.localdb.cluster.Services.ContainerWebServices,
156 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
158 // the test setup doesn't use lib/service so
159 // service.URLFromContext() returns nothing -- instead, this
160 // is how we advertise our internal URL and enable
161 // proxy-to-other-controller mode,
162 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: s.srv.Listener.Addr().String()}
163 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
164 ac := &arvados.Client{
165 APIHost: s.srv.Listener.Addr().String(),
166 AuthToken: arvadostest.SystemRootToken,
169 s.gw = &crunchrun.Gateway{
170 ContainerUUID: s.ctrUUID,
172 Address: "localhost:0",
173 Log: ctxlog.TestLogger(c),
174 Target: crunchrun.GatewayTargetStub{},
177 c.Assert(s.gw.Start(), check.IsNil)
179 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
180 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
182 Attrs: map[string]interface{}{
183 "state": arvados.ContainerStateLocked}})
184 c.Assert(err, check.IsNil)
185 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
187 Attrs: map[string]interface{}{
188 "state": arvados.ContainerStateRunning,
189 "gateway_address": s.gw.Address}})
190 c.Assert(err, check.IsNil)
192 s.cluster.Containers.ShellAccess.Admin = true
193 s.cluster.Containers.ShellAccess.User = true
194 _, err = s.db.Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
195 c.Check(err, check.IsNil)
197 s.assignedExtPort.Store(testDynamicPortMin)
200 func (s *ContainerGatewaySuite) TearDownTest(c *check.C) {
201 forceProxyForTest = false
203 _, err := s.localdb.ContainerRequestDelete(s.userctx, arvados.DeleteOptions{UUID: s.reqUUID})
204 c.Check(err, check.IsNil)
210 _, err := s.db.Exec(`delete from container_ports where external_port >= $1 and external_port <= $2`, testDynamicPortMin, testDynamicPortMax)
211 c.Check(err, check.IsNil)
212 s.localdbSuite.TearDownTest(c)
215 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
216 for _, trial := range []struct {
222 {true, true, arvadostest.ActiveTokenV2, 0},
223 {true, false, arvadostest.ActiveTokenV2, 503},
224 {false, true, arvadostest.ActiveTokenV2, 0},
225 {false, false, arvadostest.ActiveTokenV2, 503},
226 {true, true, arvadostest.AdminToken, 0},
227 {true, false, arvadostest.AdminToken, 0},
228 {false, true, arvadostest.AdminToken, 403},
229 {false, false, arvadostest.AdminToken, 503},
231 c.Logf("trial %#v", trial)
232 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
233 s.cluster.Containers.ShellAccess.User = trial.configUser
234 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, trial.sendToken)
235 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
236 if trial.errorCode == 0 {
237 if !c.Check(err, check.IsNil) {
240 if !c.Check(sshconn.Conn, check.NotNil) {
245 c.Check(err, check.NotNil)
246 err, ok := err.(interface{ HTTPStatus() int })
247 if c.Check(ok, check.Equals, true) {
248 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
254 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
255 // Set up servers on a few TCP ports
257 for i := 0; i < 3; i++ {
258 ln, err := net.Listen("tcp", ":0")
259 c.Assert(err, check.IsNil)
261 addrs = append(addrs, ln.Addr().String())
264 conn, err := ln.Accept()
269 fmt.Fscanf(conn, "%s\n", &gotAddr)
270 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
271 if gotAddr == ln.Addr().String() {
272 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
279 c.Logf("connecting to %s", s.gw.Address)
280 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
281 c.Assert(err, check.IsNil)
282 c.Assert(sshconn.Conn, check.NotNil)
283 defer sshconn.Conn.Close()
284 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
285 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
287 c.Assert(err, check.IsNil)
288 client := ssh.NewClient(conn, chans, reqs)
289 for _, expectAddr := range addrs {
290 _, port, err := net.SplitHostPort(expectAddr)
291 c.Assert(err, check.IsNil)
293 c.Logf("trying foo:%s", port)
295 conn, err := client.Dial("tcp", "foo:"+port)
296 c.Assert(err, check.IsNil)
297 conn.SetDeadline(time.Now().Add(time.Second))
298 buf, err := ioutil.ReadAll(conn)
299 c.Check(err, check.IsNil)
300 c.Check(string(buf), check.Equals, "")
303 c.Logf("trying localhost:%s", port)
305 conn, err := client.Dial("tcp", "localhost:"+port)
306 c.Assert(err, check.IsNil)
307 conn.SetDeadline(time.Now().Add(time.Second))
308 conn.Write([]byte(expectAddr + "\n"))
310 fmt.Fscanf(conn, "%s\n", &gotAddr)
311 c.Check(gotAddr, check.Equals, expectAddr)
316 // Connect to crunch-run container gateway directly, using container
318 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Direct(c *check.C) {
319 s.testContainerHTTPProxy(c, s.ctrUUID, s.vhostAndTargetForWildcard)
322 // Connect to crunch-run container gateway directly, using container
324 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Direct_ContainerRequestUUID(c *check.C) {
325 s.testContainerHTTPProxy(c, s.reqUUID, s.vhostAndTargetForWildcard)
328 // Connect through a tunnel terminated at this controller process.
329 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Tunnel(c *check.C) {
330 s.gw = s.setupGatewayWithTunnel(c)
331 s.testContainerHTTPProxy(c, s.ctrUUID, s.vhostAndTargetForWildcard)
334 // Connect through a tunnel terminated at a different controller
336 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ProxyTunnel(c *check.C) {
337 forceProxyForTest = true
338 s.gw = s.setupGatewayWithTunnel(c)
339 s.testContainerHTTPProxy(c, s.ctrUUID, s.vhostAndTargetForWildcard)
342 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_DynamicPort(c *check.C) {
343 s.testContainerHTTPProxy(c, s.ctrUUID, s.vhostAndTargetForDynamicPort)
346 func (s *ContainerGatewaySuite) testContainerHTTPProxy(c *check.C, targetUUID string, vhostAndTargetFunc func(*check.C, string, string) (string, string)) {
347 testMethods := []string{"GET", "POST", "PATCH", "OPTIONS", "DELETE"}
349 var wg sync.WaitGroup
350 for idx, srv := range s.containerServices {
355 method := testMethods[idx%len(testMethods)]
356 _, port, err := net.SplitHostPort(srv.Addr)
357 c.Assert(err, check.IsNil, check.Commentf("%s", srv.Addr))
358 vhost, target := vhostAndTargetFunc(c, targetUUID, port)
359 comment := check.Commentf("srv.Addr %s => proxy vhost %s, target %s", srv.Addr, vhost, target)
360 c.Logf("%s", comment.CheckCommentString())
361 req, err := http.NewRequest(method, "https://"+vhost+"/via-"+s.gw.Address, nil)
362 c.Assert(err, check.IsNil)
363 // Token is already passed to
364 // ContainerHTTPProxy() call in s.userctx, but
365 // we also need to add an auth cookie to the
366 // http request: if the request gets passed
367 // through http (see forceProxyForTest), the
368 // target router will start with a fresh
369 // context and load tokens from the request.
370 req.AddCookie(&http.Cookie{
371 Name: "arvados_api_token",
372 Value: auth.EncodeTokenCookie([]byte(arvadostest.ActiveTokenV2)),
374 handler, err := s.localdb.ContainerHTTPProxy(s.userctx, arvados.ContainerHTTPProxyOptions{
378 c.Assert(err, check.IsNil, comment)
379 rw := httptest.NewRecorder()
380 handler.ServeHTTP(rw, req)
382 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
383 if cookie := getCookie(resp, "arvados_container_uuid"); c.Check(cookie, check.NotNil) {
384 c.Check(cookie.Value, check.Equals, s.ctrUUID)
386 body, err := io.ReadAll(resp.Body)
387 c.Assert(err, check.IsNil)
388 c.Check(string(body), check.Matches, `handled `+method+` /via-.* with Host \Q`+vhost+`\E`)
394 // Return the virtualhost (in the http request) and opts.Target that
395 // lib/controller/router.Router will pass to ContainerHTTPProxy() when
396 // Services.ContainerWebServices.ExternalURL is a wildcard like
397 // "*.containers.example.com".
398 func (s *ContainerGatewaySuite) vhostAndTargetForWildcard(c *check.C, targetUUID, targetPort string) (string, string) {
399 return targetUUID + "-" + targetPort + ".containers.example.com", fmt.Sprintf("%s-%s", targetUUID, targetPort)
402 // Return the virtualhost (in the http request) and opts.Target that
403 // lib/controller/router.Router will pass to ContainerHTTPProxy() when
404 // Services.ContainerWebServices.ExternalPortMin and
405 // Services.ContainerWebServices.ExternalPortMax are positive, and
406 // Services.ContainerWebServices.ExternalURL is not a wildcard.
407 func (s *ContainerGatewaySuite) vhostAndTargetForDynamicPort(c *check.C, targetUUID, targetPort string) (string, string) {
408 exthost := "containers.example.com"
409 s.localdb.cluster.Services.ContainerWebServices.ExternalURL.Host = exthost
410 s.localdb.cluster.Services.ContainerWebServices.ExternalPortMin = testDynamicPortMin
411 s.localdb.cluster.Services.ContainerWebServices.ExternalPortMax = testDynamicPortMax
412 assignedPort := s.assignedExtPort.Add(1)
413 _, err := s.db.Exec(`insert into container_ports (external_port, container_uuid, container_port) values ($1, $2, $3)`,
414 assignedPort, targetUUID, targetPort)
415 c.Assert(err, check.IsNil)
416 return fmt.Sprintf("%s:%d", exthost, assignedPort), fmt.Sprintf(":%d", assignedPort)
419 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_NoToken_Unlisted(c *check.C) {
420 s.testContainerHTTPProxyError(c, 0, "", s.vhostAndTargetForWildcard, http.StatusUnauthorized)
423 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_NoToken_Private(c *check.C) {
424 s.testContainerHTTPProxyError(c, 2, "", s.vhostAndTargetForWildcard, http.StatusUnauthorized)
427 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_InvalidToken(c *check.C) {
428 s.testContainerHTTPProxyError(c, 0, arvadostest.ActiveTokenV2+"bogus", s.vhostAndTargetForWildcard, http.StatusUnauthorized)
431 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_AnonymousToken_Unlisted(c *check.C) {
432 s.testContainerHTTPProxyError(c, 0, arvadostest.AnonymousToken, s.vhostAndTargetForWildcard, http.StatusNotFound)
435 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_AnonymousToken_Private(c *check.C) {
436 s.testContainerHTTPProxyError(c, 2, arvadostest.AnonymousToken, s.vhostAndTargetForWildcard, http.StatusNotFound)
439 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_CRsDifferentUsers(c *check.C) {
440 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
441 cr, err := s.localdb.ContainerRequestCreate(rootctx, s.reqCreateOptions)
442 defer s.localdb.ContainerRequestDelete(rootctx, arvados.DeleteOptions{UUID: cr.UUID})
443 c.Assert(err, check.IsNil)
444 c.Assert(cr.ContainerUUID, check.Equals, s.ctrUUID)
445 s.testContainerHTTPProxyError(c, 0, arvadostest.ActiveTokenV2, s.vhostAndTargetForWildcard, http.StatusForbidden)
448 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_ContainerNotReadable(c *check.C) {
449 s.testContainerHTTPProxyError(c, 0, arvadostest.SpectatorToken, s.vhostAndTargetForWildcard, http.StatusNotFound)
452 func (s *ContainerGatewaySuite) TestContainerHTTPProxyError_DynamicPort(c *check.C) {
453 s.testContainerHTTPProxyError(c, 0, arvadostest.SpectatorToken, s.vhostAndTargetForDynamicPort, http.StatusNotFound)
456 func (s *ContainerGatewaySuite) testContainerHTTPProxyError(c *check.C, svcIdx int, token string, vhostAndTargetFunc func(*check.C, string, string) (string, string), expectCode int) {
457 _, svcPort, err := net.SplitHostPort(s.containerServices[svcIdx].Addr)
458 c.Assert(err, check.IsNil)
459 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, token)
460 vhost, target := vhostAndTargetFunc(c, s.ctrUUID, svcPort)
461 req, err := http.NewRequest("GET", "https://"+vhost+"/via-"+s.gw.Address, nil)
462 c.Assert(err, check.IsNil)
463 _, err = s.localdb.ContainerHTTPProxy(ctx, arvados.ContainerHTTPProxyOptions{
467 c.Check(err, check.NotNil)
468 var se httpserver.HTTPStatusError
469 c.Assert(errors.As(err, &se), check.Equals, true)
470 c.Check(se.HTTPStatus(), check.Equals, expectCode)
473 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_CookieAuth(c *check.C) {
474 s.testContainerHTTPProxyUsingCurl(c, 0, arvadostest.ActiveTokenV2, "GET", "/foobar")
477 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_CookieAuth_POST(c *check.C) {
478 s.testContainerHTTPProxyUsingCurl(c, 0, arvadostest.ActiveTokenV2, "POST", "/foobar")
481 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_QueryAuth(c *check.C) {
482 s.testContainerHTTPProxyUsingCurl(c, 0, "", "GET", "/foobar?arvados_api_token="+arvadostest.ActiveTokenV2)
485 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_QueryAuth_Tunnel(c *check.C) {
486 s.gw = s.setupGatewayWithTunnel(c)
487 s.testContainerHTTPProxyUsingCurl(c, 0, "", "GET", "/foobar?arvados_api_token="+arvadostest.ActiveTokenV2)
490 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_QueryAuth_ProxyTunnel(c *check.C) {
491 forceProxyForTest = true
492 s.gw = s.setupGatewayWithTunnel(c)
493 s.testContainerHTTPProxyUsingCurl(c, 0, "", "GET", "/foobar?arvados_api_token="+arvadostest.ActiveTokenV2)
496 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_Anonymous(c *check.C) {
497 s.testContainerHTTPProxyUsingCurl(c, 1, "", "GET", "/foobar")
500 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_Anonymous_OPTIONS(c *check.C) {
501 s.testContainerHTTPProxyUsingCurl(c, 1, "", "OPTIONS", "/foobar")
504 // Check other query parameters are preserved in the
505 // redirect-with-cookie.
507 // Note the original request has "?baz&baz&..." and this changes to
508 // "?baz=&baz=&..." in the redirect location. We trust the target
509 // service won't be sensitive to this difference.
510 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_QueryAuth_PreserveQuery(c *check.C) {
511 body := s.testContainerHTTPProxyUsingCurl(c, 0, "", "GET", "/foobar?baz&baz&arvados_api_token="+arvadostest.ActiveTokenV2+"&waz=quux")
512 c.Check(body, check.Matches, `handled GET /foobar\?baz=&baz=&waz=quux with Host `+s.ctrUUID+`.*`)
515 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_Curl_Patch(c *check.C) {
516 body := s.testContainerHTTPProxyUsingCurl(c, 0, arvadostest.ActiveTokenV2, "PATCH", "/foobar")
517 c.Check(body, check.Matches, `handled PATCH /foobar with Host `+s.ctrUUID+`.*`)
520 // Note there is no particular reason this test needs to use curl. It
521 // would make sense to rewrite it to use stdlib instead, as we did
522 // with other tests in commit
523 // 16f957acf378cd3384d4b9c6ce844fe3cefa600b.
524 func (s *ContainerGatewaySuite) testContainerHTTPProxyUsingCurl(c *check.C, svcIdx int, cookietoken, method, path string) string {
525 _, svcPort, err := net.SplitHostPort(s.containerServices[svcIdx].Addr)
526 c.Assert(err, check.IsNil)
528 vhost, err := url.Parse(s.srv.URL)
529 c.Assert(err, check.IsNil)
530 controllerHost := vhost.Host
531 vhost.Host = s.ctrUUID + "-" + svcPort + ".containers.example.com"
532 target, err := vhost.Parse(path)
533 c.Assert(err, check.IsNil)
536 cmd := exec.Command("curl")
537 if cookietoken != "" {
538 cmd.Args = append(cmd.Args, "--cookie", "arvados_api_token="+string(auth.EncodeTokenCookie([]byte(cookietoken))))
540 cmd.Args = append(cmd.Args, "--cookie-jar", filepath.Join(tempdir, "cookies.txt"))
543 cmd.Args = append(cmd.Args, "--request", method)
545 cmd.Args = append(cmd.Args, "--silent", "--insecure", "--location", "--connect-to", vhost.Hostname()+":443:"+controllerHost, target.String())
547 stdout, err := cmd.StdoutPipe()
548 c.Assert(err, check.Equals, nil)
549 cmd.Stderr = cmd.Stdout
550 c.Logf("cmd: %v", cmd.Args)
554 _, err = io.Copy(&buf, stdout)
555 c.Check(err, check.Equals, nil)
557 c.Check(err, check.Equals, nil)
558 c.Check(buf.String(), check.Matches, `handled `+method+` /.*`)
562 // See testContainerHTTPProxy_ReusedPort_FollowRedirs(). These
563 // integration tests check the redirect-with-cookie behavior when a
564 // request arrives on a dynamically-assigned port and it has cookies
565 // indicating that the client has previously accessed a different
566 // container's web services on this same port, i.e., it is susceptible
567 // to leaking cache/cookie/localstorage data from the previous
568 // container's service to the current container's service.
569 type testReusedPortFollowRedirs struct {
576 // Reject non-GET requests. In principle we could 303 them, but in
577 // the most obvious case (an AJAX request initiated by the previous
578 // container's web application), delivering the request to the new
579 // container would surely not be the intended behavior.
580 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_FollowRedirs_RejectPOST(c *check.C) {
581 code, body, redirs := s.testContainerHTTPProxy_ReusedPort_FollowRedirs(c, testReusedPortFollowRedirs{
583 cookietoken: arvadostest.ActiveTokenV2,
585 c.Check(code, check.Equals, http.StatusGone)
586 c.Check(body, check.Equals, "")
587 c.Check(redirs, check.HasLen, 0)
590 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_FollowRedirs_WithoutToken_ClearApplicationCookie(c *check.C) {
591 code, body, redirs := s.testContainerHTTPProxy_ReusedPort_FollowRedirs(c, testReusedPortFollowRedirs{
594 cookietoken: arvadostest.ActiveTokenV2,
596 c.Check(code, check.Equals, http.StatusOK)
597 c.Check(body, check.Matches, `handled GET /foobar with Host containers\.example\.com:\d+`)
598 c.Check(redirs, check.HasLen, 1)
601 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_FollowRedirs_WithToken_ClearApplicationCookie(c *check.C) {
602 code, body, redirs := s.testContainerHTTPProxy_ReusedPort_FollowRedirs(c, testReusedPortFollowRedirs{
604 querytoken: arvadostest.ActiveTokenV2,
606 c.Check(code, check.Equals, http.StatusOK)
607 c.Check(body, check.Matches, `handled GET /foobar with Host containers\.example\.com:\d+`)
608 if c.Check(redirs, check.HasLen, 1) {
609 c.Check(redirs[0], check.Matches, `https://containers\.example\.com:\d+/foobar`)
613 func (s *ContainerGatewaySuite) testContainerHTTPProxy_ReusedPort_FollowRedirs(c *check.C, t testReusedPortFollowRedirs) (responseCode int, responseBody string, redirectsFollowed []string) {
614 _, svcPort, err := net.SplitHostPort(s.containerServices[t.svcIdx].Addr)
615 c.Assert(err, check.IsNil)
617 srvurl, err := url.Parse(s.srv.URL)
618 c.Assert(err, check.IsNil)
619 controllerHost := srvurl.Host
621 vhost, _ := s.vhostAndTargetForDynamicPort(c, s.ctrUUID, svcPort)
627 if t.querytoken != "" {
628 requrl.RawQuery = "arvados_api_token=" + t.querytoken
631 cookies := []*http.Cookie{
632 &http.Cookie{Name: "arvados_container_uuid", Value: arvadostest.CompletedContainerUUID},
633 &http.Cookie{Name: "stale_cookie", Value: "abcdefghij"},
635 if t.cookietoken != "" {
636 cookies = append(cookies, &http.Cookie{Name: "arvados_api_token", Value: string(auth.EncodeTokenCookie([]byte(t.cookietoken)))})
638 jar, err := cookiejar.New(nil)
639 c.Assert(err, check.IsNil)
641 client := &http.Client{
643 CheckRedirect: func(req *http.Request, via []*http.Request) error {
644 redirectsFollowed = append(redirectsFollowed, req.URL.String())
647 Transport: &http.Transport{
648 DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
649 return tls.Dial(network, controllerHost, &tls.Config{
650 InsecureSkipVerify: true,
653 TLSClientConfig: &tls.Config{
654 InsecureSkipVerify: true}}}
655 client.Jar.SetCookies(&url.URL{Scheme: "https", Host: "containers.example.com"}, cookies)
656 req, err := http.NewRequest(t.method, requrl.String(), nil)
657 c.Assert(err, check.IsNil)
658 resp, err := client.Do(req)
659 c.Assert(err, check.IsNil)
660 responseCode = resp.StatusCode
661 body, err := ioutil.ReadAll(resp.Body)
662 c.Assert(err, check.IsNil)
663 responseBody = string(body)
664 if responseCode < 400 {
665 for _, cookie := range client.Jar.Cookies(&url.URL{Scheme: "https", Host: "containers.example.com"}) {
666 c.Check(cookie.Name, check.Not(check.Equals), "stale_cookie")
667 if cookie.Name == "arvados_container_uuid" {
668 c.Check(cookie.Value, check.Not(check.Equals), arvadostest.CompletedContainerUUID)
675 // Unit tests for clear-cookies-and-redirect behavior when the client
676 // still has active cookies (and possibly client-side cache) from a
677 // different container that used to be served on the same
678 // dynamically-assigned port.
679 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_QueryToken(c *check.C) {
680 s.testContainerHTTPProxy_ReusedPort(c, arvadostest.ActiveTokenV2, "")
682 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_CookieToken(c *check.C) {
683 s.testContainerHTTPProxy_ReusedPort(c, "", arvadostest.ActiveTokenV2)
685 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_ReusedPort_NoToken(c *check.C) {
686 s.testContainerHTTPProxy_ReusedPort(c, "", "")
688 func (s *ContainerGatewaySuite) testContainerHTTPProxy_ReusedPort(c *check.C, querytoken, cookietoken string) {
689 srv := s.containerServices[0]
691 _, port, err := net.SplitHostPort(srv.Addr)
692 c.Assert(err, check.IsNil, check.Commentf("%s", srv.Addr))
693 vhost, target := s.vhostAndTargetForDynamicPort(c, s.ctrUUID, port)
695 var tokenCookie *http.Cookie
696 if cookietoken != "" {
697 tokenCookie = &http.Cookie{
698 Name: "arvados_api_token",
699 Value: string(auth.EncodeTokenCookie([]byte(cookietoken))),
703 initialURL := "https://" + vhost + "/via-" + s.gw.Address + "/preserve-path?preserve-param=preserve-value"
704 if querytoken != "" {
705 initialURL += "&arvados_api_token=" + querytoken
707 req, err := http.NewRequest(method, initialURL, nil)
708 c.Assert(err, check.IsNil)
709 req.Header.Add("Cookie", "arvados_container_uuid=zzzzz-dz642-compltcontainer")
710 req.Header.Add("Cookie", "stale_cookie=abcdefghij")
711 if tokenCookie != nil {
712 req.Header.Add("Cookie", tokenCookie.String())
714 handler, err := s.localdb.ContainerHTTPProxy(s.userctx, arvados.ContainerHTTPProxyOptions{
718 c.Assert(err, check.IsNil)
719 rw := httptest.NewRecorder()
720 handler.ServeHTTP(rw, req)
722 c.Check(resp.StatusCode, check.Equals, http.StatusSeeOther)
723 c.Logf("Received Location: %s", resp.Header.Get("Location"))
724 c.Logf("Received cookies: %v", resp.Cookies())
725 newTokenCookie := getCookie(resp, "arvados_api_token")
726 if querytoken != "" {
727 if c.Check(newTokenCookie, check.NotNil) {
728 c.Check(newTokenCookie.Expires.IsZero(), check.Equals, true)
731 if newTokenCookie != nil {
732 tokenCookie = newTokenCookie
734 if staleCookie := getCookie(resp, "stale_cookie"); c.Check(staleCookie, check.NotNil) {
735 c.Check(staleCookie.Expires.Before(time.Now()), check.Equals, true)
736 c.Check(staleCookie.Value, check.Equals, "")
738 if ctrCookie := getCookie(resp, "arvados_container_uuid"); c.Check(ctrCookie, check.NotNil) {
739 c.Check(ctrCookie.Expires.Before(time.Now()), check.Equals, true)
740 c.Check(ctrCookie.Value, check.Equals, "")
742 c.Check(resp.Header.Get("Clear-Site-Data"), check.Equals, `"cache", "storage"`)
744 req, err = http.NewRequest(method, resp.Header.Get("Location"), nil)
745 c.Assert(err, check.IsNil)
746 req.Header.Add("Cookie", tokenCookie.String())
747 handler, err = s.localdb.ContainerHTTPProxy(s.userctx, arvados.ContainerHTTPProxyOptions{
751 c.Assert(err, check.IsNil)
752 rw = httptest.NewRecorder()
753 handler.ServeHTTP(rw, req)
755 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
756 if ctrCookie := getCookie(resp, "arvados_container_uuid"); c.Check(ctrCookie, check.NotNil) {
757 c.Check(ctrCookie.Expires.IsZero(), check.Equals, true)
758 c.Check(ctrCookie.Value, check.Equals, s.ctrUUID)
760 body, err := ioutil.ReadAll(resp.Body)
761 c.Check(err, check.IsNil)
762 c.Check(string(body), check.Matches, `handled GET /via-localhost:\d+/preserve-path\?preserve-param=preserve-value with Host containers.example.com:\d+`)
765 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_PublishedPortByName_ProxyTunnel(c *check.C) {
766 forceProxyForTest = true
767 s.gw = s.setupGatewayWithTunnel(c)
768 s.testContainerHTTPProxy_PublishedPortByName(c)
771 func (s *ContainerGatewaySuite) TestContainerHTTPProxy_PublishedPortByName(c *check.C) {
772 s.testContainerHTTPProxy_PublishedPortByName(c)
775 func (s *ContainerGatewaySuite) testContainerHTTPProxy_PublishedPortByName(c *check.C) {
776 srv := s.containerServices[1]
777 _, port, _ := net.SplitHostPort(srv.Addr)
778 portnum, err := strconv.Atoi(port)
779 c.Assert(err, check.IsNil)
780 namelink, err := s.localdb.LinkCreate(s.userctx, arvados.CreateOptions{
781 Attrs: map[string]interface{}{
782 "link_class": "published_port",
783 "name": "warthogfacedbuffoon",
784 "head_uuid": s.reqUUID,
785 "properties": map[string]interface{}{
787 c.Assert(err, check.IsNil)
788 defer s.localdb.LinkDelete(s.userctx, arvados.DeleteOptions{UUID: namelink.UUID})
790 vhost := namelink.Name + ".containers.example.com"
791 req, err := http.NewRequest("METHOD", "https://"+vhost+"/path", nil)
792 c.Assert(err, check.IsNil)
793 // Token is already passed to ContainerHTTPProxy() call in
794 // s.userctx, but we also need to add an auth cookie to the
795 // http request: if the request gets passed through http (see
796 // forceProxyForTest), the target router will start with a
797 // fresh context and load tokens from the request.
798 req.AddCookie(&http.Cookie{
799 Name: "arvados_api_token",
800 Value: auth.EncodeTokenCookie([]byte(arvadostest.ActiveTokenV2)),
802 handler, err := s.localdb.ContainerHTTPProxy(s.userctx, arvados.ContainerHTTPProxyOptions{
803 Target: namelink.Name,
806 c.Assert(err, check.IsNil)
807 rw := httptest.NewRecorder()
808 handler.ServeHTTP(rw, req)
810 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
811 body, err := io.ReadAll(resp.Body)
812 c.Assert(err, check.IsNil)
813 c.Check(string(body), check.Matches, `handled METHOD /path with Host \Q`+vhost+`\E`)
816 func (s *ContainerGatewaySuite) setupLogCollection(c *check.C) {
817 files := map[string]string{
818 "stderr.txt": "hello world\n",
819 "a/b/c/d.html": "<html></html>\n",
821 client := arvados.NewClientFromEnv()
822 ac, err := arvadosclient.New(client)
823 c.Assert(err, check.IsNil)
824 kc, err := keepclient.MakeKeepClient(ac)
825 c.Assert(err, check.IsNil)
826 cfs, err := (&arvados.Collection{}).FileSystem(client, kc)
827 c.Assert(err, check.IsNil)
828 for name, content := range files {
829 for i, ch := range name {
831 err := cfs.Mkdir("/"+name[:i], 0777)
832 c.Assert(err, check.IsNil)
835 f, err := cfs.OpenFile("/"+name, os.O_CREATE|os.O_WRONLY, 0777)
836 c.Assert(err, check.IsNil)
837 f.Write([]byte(content))
839 c.Assert(err, check.IsNil)
842 s.gw.LogCollection = cfs
845 func (s *ContainerGatewaySuite) saveLogAndCloseGateway(c *check.C) {
846 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
847 txt, err := s.gw.LogCollection.MarshalManifest(".")
848 c.Assert(err, check.IsNil)
849 coll, err := s.localdb.CollectionCreate(rootctx, arvados.CreateOptions{
850 Attrs: map[string]interface{}{
851 "manifest_text": txt,
853 c.Assert(err, check.IsNil)
854 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
856 Attrs: map[string]interface{}{
857 "state": arvados.ContainerStateComplete,
859 "log": coll.PortableDataHash,
861 c.Assert(err, check.IsNil)
862 updatedReq, err := s.localdb.ContainerRequestGet(rootctx, arvados.GetOptions{UUID: s.reqUUID})
863 c.Assert(err, check.IsNil)
864 c.Logf("container request log UUID is %s", updatedReq.LogUUID)
865 crLog, err := s.localdb.CollectionGet(rootctx, arvados.GetOptions{UUID: updatedReq.LogUUID, Select: []string{"manifest_text"}})
866 c.Assert(err, check.IsNil)
867 c.Logf("collection log manifest:\n%s", crLog.ManifestText)
868 // Ensure localdb can't circumvent the keep-web proxy test by
869 // getting content from the container gateway.
870 s.gw.LogCollection = nil
873 func (s *ContainerGatewaySuite) TestContainerRequestLogViaTunnel(c *check.C) {
874 forceProxyForTest = true
875 s.gw = s.setupGatewayWithTunnel(c)
876 s.setupLogCollection(c)
878 for _, broken := range []bool{false, true} {
879 c.Logf("broken=%v", broken)
882 delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
885 r, err := http.NewRequestWithContext(s.userctx, "GET", "https://controller.example/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID+"/stderr.txt", nil)
886 c.Assert(err, check.IsNil)
887 r.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
888 handler, err := s.localdb.ContainerRequestLog(s.userctx, arvados.ContainerLogOptions{
890 WebDAVOptions: arvados.WebDAVOptions{
893 Path: "/" + s.ctrUUID + "/stderr.txt",
897 c.Check(err, check.ErrorMatches, `.*tunnel endpoint is invalid.*`)
900 c.Check(err, check.IsNil)
901 c.Assert(handler, check.NotNil)
902 rec := httptest.NewRecorder()
903 handler.ServeHTTP(rec, r)
905 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
906 buf, err := ioutil.ReadAll(resp.Body)
907 c.Check(err, check.IsNil)
908 c.Check(string(buf), check.Equals, "hello world\n")
912 func (s *ContainerGatewaySuite) TestContainerRequestLogViaGateway(c *check.C) {
913 s.setupLogCollection(c)
914 s.testContainerRequestLog(c)
917 func (s *ContainerGatewaySuite) TestContainerRequestLogViaKeepWeb(c *check.C) {
918 s.setupLogCollection(c)
919 s.saveLogAndCloseGateway(c)
920 s.testContainerRequestLog(c)
923 func (s *ContainerGatewaySuite) testContainerRequestLog(c *check.C) {
924 for _, trial := range []struct {
931 expectHeader http.Header
935 path: s.ctrUUID + "/stderr.txt",
936 expectStatus: http.StatusOK,
937 expectBodyRe: "hello world\n",
938 expectHeader: http.Header{
939 "Content-Type": {"text/plain; charset=utf-8"},
944 path: s.ctrUUID + "/stderr.txt",
946 "Range": {"bytes=-6"},
948 expectStatus: http.StatusPartialContent,
949 expectBodyRe: "world\n",
950 expectHeader: http.Header{
951 "Content-Type": {"text/plain; charset=utf-8"},
952 "Content-Range": {"bytes 6-11/12"},
957 path: s.ctrUUID + "/stderr.txt",
958 expectStatus: http.StatusOK,
960 expectHeader: http.Header{
962 "Allow": {"OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"},
967 path: s.ctrUUID + "/stderr.txt",
968 unauthenticated: true,
970 "Access-Control-Request-Method": {"POST"},
972 expectStatus: http.StatusOK,
974 expectHeader: http.Header{
975 "Access-Control-Allow-Headers": {"Authorization, Content-Type, Range, Depth, Destination, If, Lock-Token, Overwrite, Timeout, Cache-Control"},
976 "Access-Control-Allow-Methods": {"COPY, DELETE, GET, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, RMCOL, UNLOCK"},
977 "Access-Control-Allow-Origin": {"*"},
978 "Access-Control-Max-Age": {"86400"},
983 path: s.ctrUUID + "/",
984 expectStatus: http.StatusMultiStatus,
985 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*>\n?`,
986 expectHeader: http.Header{
987 "Content-Type": {"text/xml; charset=utf-8"},
993 expectStatus: http.StatusMultiStatus,
994 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*>\n?`,
995 expectHeader: http.Header{
996 "Content-Type": {"text/xml; charset=utf-8"},
1001 path: s.ctrUUID + "/a/b/c/",
1002 expectStatus: http.StatusMultiStatus,
1003 expectBodyRe: `.*\Q<D:displayname>d.html</D:displayname>\E.*>\n?`,
1004 expectHeader: http.Header{
1005 "Content-Type": {"text/xml; charset=utf-8"},
1010 path: s.ctrUUID + "/a/b/c/d.html",
1011 expectStatus: http.StatusOK,
1012 expectBodyRe: "<html></html>\n",
1013 expectHeader: http.Header{
1014 "Content-Type": {"text/html; charset=utf-8"},
1018 c.Logf("trial %#v", trial)
1020 if trial.unauthenticated {
1021 ctx = auth.NewContext(context.Background(), auth.CredentialsFromRequest(&http.Request{URL: &url.URL{}, Header: http.Header{}}))
1023 r, err := http.NewRequestWithContext(ctx, trial.method, "https://controller.example/arvados/v1/container_requests/"+s.reqUUID+"/log/"+trial.path, nil)
1024 c.Assert(err, check.IsNil)
1025 for k := range trial.header {
1026 r.Header.Set(k, trial.header.Get(k))
1028 handler, err := s.localdb.ContainerRequestLog(ctx, arvados.ContainerLogOptions{
1030 WebDAVOptions: arvados.WebDAVOptions{
1031 Method: trial.method,
1033 Path: "/" + trial.path,
1036 c.Assert(err, check.IsNil)
1037 c.Assert(handler, check.NotNil)
1038 rec := httptest.NewRecorder()
1039 handler.ServeHTTP(rec, r)
1040 resp := rec.Result()
1041 c.Check(resp.StatusCode, check.Equals, trial.expectStatus)
1042 for k := range trial.expectHeader {
1043 c.Check(resp.Header[k], check.DeepEquals, trial.expectHeader[k])
1045 buf, err := ioutil.ReadAll(resp.Body)
1046 c.Check(err, check.IsNil)
1047 c.Check(string(buf), check.Matches, trial.expectBodyRe)
1051 func (s *ContainerGatewaySuite) TestContainerRequestLogViaCadaver(c *check.C) {
1052 s.setupLogCollection(c)
1054 out := s.runCadaver(c, arvadostest.ActiveToken, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "ls")
1055 c.Check(out, check.Matches, `(?ms).*stderr\.txt\s+12\s.*`)
1056 c.Check(out, check.Matches, `(?ms).*a\s+0\s.*`)
1058 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "get stderr.txt")
1059 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
1061 s.saveLogAndCloseGateway(c)
1063 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "get stderr.txt")
1064 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
1067 func (s *ContainerGatewaySuite) runCadaver(c *check.C, password, path, stdin string) string {
1068 // Replace s.srv with an HTTP server, otherwise cadaver will
1069 // just fail on TLS cert verification.
1071 rtr := router.New(s.localdb, router.Config{})
1072 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
1075 tempdir, err := ioutil.TempDir("", "localdb-test-")
1076 c.Assert(err, check.IsNil)
1077 defer os.RemoveAll(tempdir)
1079 cmd := exec.Command("cadaver", s.srv.URL+path)
1081 cmd.Env = append(os.Environ(), "HOME="+tempdir)
1082 f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
1083 c.Assert(err, check.IsNil)
1084 _, err = fmt.Fprintf(f, "default login none password %s\n", password)
1085 c.Assert(err, check.IsNil)
1086 c.Assert(f.Close(), check.IsNil)
1088 cmd.Stdin = bytes.NewBufferString(stdin)
1090 stdout, err := cmd.StdoutPipe()
1091 c.Assert(err, check.Equals, nil)
1092 cmd.Stderr = cmd.Stdout
1093 c.Logf("cmd: %v", cmd.Args)
1096 var buf bytes.Buffer
1097 _, err = io.Copy(&buf, stdout)
1098 c.Check(err, check.Equals, nil)
1100 c.Check(err, check.Equals, nil)
1104 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
1105 c.Logf("connecting to %s", s.gw.Address)
1106 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
1107 c.Assert(err, check.IsNil)
1108 c.Assert(sshconn.Conn, check.NotNil)
1109 defer sshconn.Conn.Close()
1111 done := make(chan struct{})
1115 // Receive text banner
1116 buf := make([]byte, 12)
1117 _, err := io.ReadFull(sshconn.Conn, buf)
1118 c.Check(err, check.IsNil)
1119 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
1122 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
1123 c.Check(err, check.IsNil)
1126 _, err = io.ReadFull(sshconn.Conn, buf[:4])
1127 c.Check(err, check.IsNil)
1129 // If we can get this far into an SSH handshake...
1130 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
1134 case <-time.After(time.Second):
1137 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
1138 c.Check(err, check.IsNil)
1139 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
1142 func (s *ContainerGatewaySuite) TestConnectFail_NoToken(c *check.C) {
1143 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, "")
1144 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
1145 c.Check(err, check.ErrorMatches, `.* 401 .*`)
1148 func (s *ContainerGatewaySuite) TestConnectFail_AnonymousToken(c *check.C) {
1149 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AnonymousToken)
1150 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
1151 c.Check(err, check.ErrorMatches, `.* 404 .*`)
1154 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
1156 conn, err := s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
1159 c.Check(err, check.ErrorMatches, `authentication error`)
1160 c.Check(conn.Conn, check.IsNil)
1163 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
1165 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1167 c.Check(err, check.ErrorMatches, `authentication error`)
1168 c.Check(conn.Conn, check.IsNil)
1171 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
1173 AuthSecret: s.gw.AuthSecret,
1175 c.Check(err, check.IsNil)
1176 c.Check(conn.Conn, check.NotNil)
1179 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
1180 forceProxyForTest = true
1181 s.testConnectThroughTunnel(c, "")
1184 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
1185 forceProxyForTest = true
1186 delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
1187 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
1190 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
1191 s.testConnectThroughTunnel(c, "")
1194 func (s *ContainerGatewaySuite) setupGatewayWithTunnel(c *check.C) *crunchrun.Gateway {
1195 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
1196 // Until the tunnel starts up, set gateway_address to a value
1197 // that can't work. We want to ensure the only way we can
1198 // reach the gateway is through the tunnel.
1199 tungw := &crunchrun.Gateway{
1200 ContainerUUID: s.ctrUUID,
1201 AuthSecret: s.gw.AuthSecret,
1202 Log: ctxlog.TestLogger(c),
1203 Target: crunchrun.GatewayTargetStub{},
1204 ArvadosClient: s.gw.ArvadosClient,
1205 UpdateTunnelURL: func(url string) {
1206 c.Logf("UpdateTunnelURL(%q)", url)
1207 gwaddr := "tunnel " + url
1208 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
1210 Attrs: map[string]interface{}{
1211 "gateway_address": gwaddr}})
1214 c.Assert(tungw.Start(), check.IsNil)
1216 // We didn't supply an external hostname in the Address field,
1217 // so Start() should assign a local address.
1218 host, _, err := net.SplitHostPort(tungw.Address)
1219 c.Assert(err, check.IsNil)
1220 c.Check(host, check.Equals, "127.0.0.1")
1222 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
1224 Attrs: map[string]interface{}{
1225 "state": arvados.ContainerStateRunning,
1227 c.Assert(err, check.IsNil)
1229 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
1230 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
1231 c.Assert(err, check.IsNil)
1232 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
1233 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
1234 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
1241 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
1242 s.setupGatewayWithTunnel(c)
1243 c.Log("connecting to gateway through tunnel")
1244 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
1245 sshconn, err := arpc.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
1246 if expectErrorMatch != "" {
1247 c.Check(err, check.ErrorMatches, expectErrorMatch)
1250 c.Assert(err, check.IsNil)
1251 c.Assert(sshconn.Conn, check.NotNil)
1252 defer sshconn.Conn.Close()
1254 done := make(chan struct{})
1258 // Receive text banner
1259 buf := make([]byte, 12)
1260 _, err := io.ReadFull(sshconn.Conn, buf)
1261 c.Check(err, check.IsNil)
1262 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
1265 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
1266 c.Check(err, check.IsNil)
1269 _, err = io.ReadFull(sshconn.Conn, buf[:4])
1270 c.Check(err, check.IsNil)
1272 // If we can get this far into an SSH handshake...
1273 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
1277 case <-time.After(time.Second):
1280 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
1281 c.Check(err, check.IsNil)
1282 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
1285 func getCookie(resp *http.Response, name string) *http.Cookie {
1286 for _, cookie := range resp.Cookies() {
1287 if cookie.Name == name {