Merge branch 'master' into 7490-datamanager-dont-die-return-error
[arvados.git] / services / keep-web / handler_test.go
index 09c731cb5159c866ba6ce692ff62ebee2c1feb51..94a1cf7c0746a3fd6a883a9f84177f61646da257 100644 (file)
@@ -32,9 +32,11 @@ func (s *IntegrationSuite) TestVhost404(c *check.C) {
                arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
        } {
                resp := httptest.NewRecorder()
+               u := mustParseURL(testURL)
                req := &http.Request{
-                       Method: "GET",
-                       URL:    mustParseURL(testURL),
+                       Method:     "GET",
+                       URL:        u,
+                       RequestURI: u.RequestURI(),
                }
                (&handler{}).ServeHTTP(resp, req)
                c.Check(resp.Code, check.Equals, http.StatusNotFound)
@@ -103,6 +105,7 @@ func doVhostRequests(c *check.C, authz authorizer) {
                arvadostest.FooCollection + "--collections.example.com/_/foo",
                arvadostest.FooPdh + ".example.com/foo",
                strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
+               arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
        } {
                c.Log("doRequests: ", hostPath)
                doVhostRequestsWithHostPath(c, authz, hostPath)
@@ -119,10 +122,11 @@ func doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string)
        } {
                u := mustParseURL("http://" + hostPath)
                req := &http.Request{
-                       Method: "GET",
-                       Host:   u.Host,
-                       URL:    u,
-                       Header: http.Header{},
+                       Method:     "GET",
+                       Host:       u.Host,
+                       URL:        u,
+                       RequestURI: u.RequestURI(),
+                       Header:     http.Header{},
                }
                failCode := authz(req, tok)
                resp := doReq(req)
@@ -156,10 +160,11 @@ func doReq(req *http.Request) *httptest.ResponseRecorder {
        cookies := (&http.Response{Header: resp.Header()}).Cookies()
        u, _ := req.URL.Parse(resp.Header().Get("Location"))
        req = &http.Request{
-               Method: "GET",
-               Host:   u.Host,
-               URL:    u,
-               Header: http.Header{},
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     http.Header{},
        }
        for _, c := range cookies {
                req.AddCookie(c)
@@ -171,9 +176,48 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "GET",
                arvadostest.FooCollection+".example.com/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "text/plain",
+               "",
+               "",
+               http.StatusOK,
+               "foo",
+       )
+}
+
+func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
+               "",
+               "",
                "",
                http.StatusOK,
+               "foo",
+       )
+}
+
+// Bad token in URL is 404 Not Found because it doesn't make sense to
+// retry the same URL with different authorization.
+func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
+               "",
+               "",
+               "",
+               http.StatusNotFound,
+               "",
+       )
+}
+
+// Bad token in a cookie (even if it got there via our own
+// query-string-to-cookie redirect) is, in principle, retryable at the
+// same URL so it's 401 Unauthorized.
+func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               arvadostest.FooCollection+".example.com/foo",
+               "?api_token=thisisabogustoken",
+               "",
+               "",
+               http.StatusUnauthorized,
+               "",
        )
 }
 
@@ -181,12 +225,28 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "text/plain",
+               "",
                "",
                http.StatusBadRequest,
+               "",
        )
 }
 
+// If client requests an attachment by putting ?disposition=attachment
+// in the query string, and gets redirected, the redirect target
+// should respond with an attachment.
+func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
+       resp := s.testVhostRedirectTokenToCookie(c, "GET",
+               arvadostest.FooCollection+".example.com/foo",
+               "?disposition=attachment&api_token="+arvadostest.ActiveToken,
+               "",
+               "",
+               http.StatusOK,
+               "foo",
+       )
+       c.Check(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
+}
+
 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
        defer func(orig bool) {
                trustAllContent = orig
@@ -195,9 +255,10 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "text/plain",
+               "",
                "",
                http.StatusOK,
+               "foo",
        )
 }
 
@@ -210,17 +271,19 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *chec
        s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "text/plain",
+               "",
                "",
                http.StatusBadRequest,
+               "",
        )
 
        resp := s.testVhostRedirectTokenToCookie(c, "GET",
                "example.com:1234/c="+arvadostest.FooCollection+"/foo",
                "?api_token="+arvadostest.ActiveToken,
-               "text/plain",
+               "",
                "",
                http.StatusOK,
+               "foo",
        )
        c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
 }
@@ -232,6 +295,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
                "application/x-www-form-urlencoded",
                url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
                http.StatusOK,
+               "foo",
        )
 }
 
@@ -242,34 +306,107 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C)
                "application/x-www-form-urlencoded",
                url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
                http.StatusNotFound,
+               "",
+       )
+}
+
+func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
+       anonymousTokens = []string{arvadostest.AnonymousToken}
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
+               "",
+               "",
+               "",
+               http.StatusOK,
+               "Hello world\n",
+       )
+}
+
+func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
+       anonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
+               "",
+               "",
+               "",
+               http.StatusNotFound,
+               "",
        )
 }
 
-func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, body string, expectStatus int) *httptest.ResponseRecorder {
+func (s *IntegrationSuite) TestRange(c *check.C) {
+       u, _ := url.Parse("http://example.com/c=" + arvadostest.HelloWorldCollection + "/Hello%20world.txt")
+       req := &http.Request{
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     http.Header{"Range": {"bytes=0-4"}},
+       }
+       resp := httptest.NewRecorder()
+       (&handler{}).ServeHTTP(resp, req)
+       c.Check(resp.Code, check.Equals, http.StatusPartialContent)
+       c.Check(resp.Body.String(), check.Equals, "Hello")
+       c.Check(resp.Header().Get("Content-Length"), check.Equals, "5")
+       c.Check(resp.Header().Get("Content-Range"), check.Equals, "bytes 0-4/12")
+
+       req.Header.Set("Range", "bytes=0-")
+       resp = httptest.NewRecorder()
+       (&handler{}).ServeHTTP(resp, req)
+       // 200 and 206 are both correct:
+       c.Check(resp.Code, check.Equals, http.StatusOK)
+       c.Check(resp.Body.String(), check.Equals, "Hello world\n")
+       c.Check(resp.Header().Get("Content-Length"), check.Equals, "12")
+
+       // Unsupported ranges are ignored
+       for _, hdr := range []string{
+               "bytes=5-5",  // non-zero start byte
+               "bytes=-5",   // last 5 bytes
+               "cubits=0-5", // unsupported unit
+               "bytes=0-340282366920938463463374607431768211456", // 2^128
+       } {
+               req.Header.Set("Range", hdr)
+               resp = httptest.NewRecorder()
+               (&handler{}).ServeHTTP(resp, req)
+               c.Check(resp.Code, check.Equals, http.StatusOK)
+               c.Check(resp.Body.String(), check.Equals, "Hello world\n")
+               c.Check(resp.Header().Get("Content-Length"), check.Equals, "12")
+               c.Check(resp.Header().Get("Content-Range"), check.Equals, "")
+               c.Check(resp.Header().Get("Accept-Ranges"), check.Equals, "bytes")
+       }
+}
+
+func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
        u, _ := url.Parse(`http://` + hostPath + queryString)
        req := &http.Request{
-               Method: method,
-               Host:   u.Host,
-               URL:    u,
-               Header: http.Header{"Content-Type": {contentType}},
-               Body:   ioutil.NopCloser(strings.NewReader(body)),
+               Method:     method,
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     http.Header{"Content-Type": {contentType}},
+               Body:       ioutil.NopCloser(strings.NewReader(reqBody)),
        }
 
        resp := httptest.NewRecorder()
+       defer func() {
+               c.Check(resp.Code, check.Equals, expectStatus)
+               c.Check(resp.Body.String(), check.Equals, expectRespBody)
+       }()
+
        (&handler{}).ServeHTTP(resp, req)
        if resp.Code != http.StatusSeeOther {
-               c.Assert(resp.Code, check.Equals, expectStatus)
                return resp
        }
-       c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`".*`)
+       c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
        cookies := (&http.Response{Header: resp.Header()}).Cookies()
 
        u, _ = u.Parse(resp.Header().Get("Location"))
        req = &http.Request{
-               Method: "GET",
-               Host:   u.Host,
-               URL:    u,
-               Header: http.Header{},
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     http.Header{},
        }
        for _, c := range cookies {
                req.AddCookie(c)
@@ -278,9 +415,5 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
        resp = httptest.NewRecorder()
        (&handler{}).ServeHTTP(resp, req)
        c.Check(resp.Header().Get("Location"), check.Equals, "")
-       c.Check(resp.Code, check.Equals, expectStatus)
-       if expectStatus == http.StatusOK {
-               c.Check(resp.Body.String(), check.Equals, "foo")
-       }
        return resp
 }