17598: Handle comparison URLs with :80 or :443
[arvados.git] / services / keep-web / handler_test.go
index f6f3de8877fa14abca9fc479216f13d4cb85a308..4560adc68c0c89a1561a165dfb078cc3f88174a3 100644 (file)
@@ -6,6 +6,7 @@ package main
 
 import (
        "bytes"
+       "context"
        "fmt"
        "html"
        "io/ioutil"
@@ -16,6 +17,7 @@ import (
        "path/filepath"
        "regexp"
        "strings"
+       "time"
 
        "git.arvados.org/arvados.git/lib/config"
        "git.arvados.org/arvados.git/sdk/go/arvados"
@@ -24,6 +26,7 @@ import (
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/keepclient"
+       "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
 )
 
@@ -72,6 +75,64 @@ func (s *UnitSuite) TestCORSPreflight(c *check.C) {
        c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
 }
 
+func (s *UnitSuite) TestEmptyResponse(c *check.C) {
+       for _, trial := range []struct {
+               dataExists    bool
+               sendIMSHeader bool
+               expectStatus  int
+               logRegexp     string
+       }{
+               // If we return no content due to a Keep read error,
+               // we should emit a log message.
+               {false, false, http.StatusOK, `(?ms).*only wrote 0 bytes.*`},
+
+               // If we return no content because the client sent an
+               // If-Modified-Since header, our response should be
+               // 304, and we should not emit a log message.
+               {true, true, http.StatusNotModified, ``},
+       } {
+               c.Logf("trial: %+v", trial)
+               arvadostest.StartKeep(2, true)
+               if trial.dataExists {
+                       arv, err := arvadosclient.MakeArvadosClient()
+                       c.Assert(err, check.IsNil)
+                       arv.ApiToken = arvadostest.ActiveToken
+                       kc, err := keepclient.MakeKeepClient(arv)
+                       c.Assert(err, check.IsNil)
+                       _, _, err = kc.PutB([]byte("foo"))
+                       c.Assert(err, check.IsNil)
+               }
+
+               h := handler{Config: newConfig(s.Config)}
+               u := mustParseURL("http://" + arvadostest.FooCollection + ".keep-web.example/foo")
+               req := &http.Request{
+                       Method:     "GET",
+                       Host:       u.Host,
+                       URL:        u,
+                       RequestURI: u.RequestURI(),
+                       Header: http.Header{
+                               "Authorization": {"Bearer " + arvadostest.ActiveToken},
+                       },
+               }
+               if trial.sendIMSHeader {
+                       req.Header.Set("If-Modified-Since", strings.Replace(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT", -1))
+               }
+
+               var logbuf bytes.Buffer
+               logger := logrus.New()
+               logger.Out = &logbuf
+               req = req.WithContext(ctxlog.Context(context.Background(), logger))
+
+               resp := httptest.NewRecorder()
+               h.ServeHTTP(resp, req)
+               c.Check(resp.Code, check.Equals, trial.expectStatus)
+               c.Check(resp.Body.String(), check.Equals, "")
+
+               c.Log(logbuf.String())
+               c.Check(logbuf.String(), check.Matches, trial.logRegexp)
+       }
+}
+
 func (s *UnitSuite) TestInvalidUUID(c *check.C) {
        bogusID := strings.Replace(arvadostest.FooCollectionPDH, "+", "-", 1) + "-"
        token := arvadostest.ActiveToken
@@ -122,7 +183,7 @@ func (s *IntegrationSuite) TestVhost404(c *check.C) {
                }
                s.testServer.Handler.ServeHTTP(resp, req)
                c.Check(resp.Code, check.Equals, http.StatusNotFound)
-               c.Check(resp.Body.String(), check.Equals, "")
+               c.Check(resp.Body.String(), check.Equals, notFoundMessage+"\n")
        }
 }
 
@@ -132,11 +193,18 @@ func (s *IntegrationSuite) TestVhost404(c *check.C) {
 // the token is invalid.
 type authorizer func(*http.Request, string) int
 
-func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
-       s.doVhostRequests(c, authzViaAuthzHeader)
+func (s *IntegrationSuite) TestVhostViaAuthzHeaderOAuth2(c *check.C) {
+       s.doVhostRequests(c, authzViaAuthzHeaderOAuth2)
 }
-func authzViaAuthzHeader(r *http.Request, tok string) int {
-       r.Header.Add("Authorization", "OAuth2 "+tok)
+func authzViaAuthzHeaderOAuth2(r *http.Request, tok string) int {
+       r.Header.Add("Authorization", "Bearer "+tok)
+       return http.StatusUnauthorized
+}
+func (s *IntegrationSuite) TestVhostViaAuthzHeaderBearer(c *check.C) {
+       s.doVhostRequests(c, authzViaAuthzHeaderBearer)
+}
+func authzViaAuthzHeaderBearer(r *http.Request, tok string) int {
+       r.Header.Add("Authorization", "Bearer "+tok)
        return http.StatusUnauthorized
 }
 
@@ -237,7 +305,6 @@ func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authori
                if tok == arvadostest.ActiveToken {
                        c.Check(code, check.Equals, http.StatusOK)
                        c.Check(body, check.Equals, "foo")
-
                } else {
                        c.Check(code >= 400, check.Equals, true)
                        c.Check(code < 500, check.Equals, true)
@@ -250,7 +317,33 @@ func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authori
                                // depending on the authz method.
                                c.Check(code, check.Equals, failCode)
                        }
-                       c.Check(body, check.Equals, "")
+                       if code == 404 {
+                               c.Check(body, check.Equals, notFoundMessage+"\n")
+                       } else {
+                               c.Check(body, check.Equals, unauthorizedMessage+"\n")
+                       }
+               }
+       }
+}
+
+func (s *IntegrationSuite) TestVhostPortMatch(c *check.C) {
+       for _, port := range []string{"80", "443", "8000"} {
+               s.testServer.Config.cluster.Services.WebDAVDownload.ExternalURL.Host = fmt.Sprintf("download.example.com:%v", port)
+               u := mustParseURL(fmt.Sprintf("http://download.example.com/by_id/%v/foo", arvadostest.FooCollection))
+               req := &http.Request{
+                       Method:     "GET",
+                       Host:       u.Host,
+                       URL:        u,
+                       RequestURI: u.RequestURI(),
+                       Header:     http.Header{"Authorization": []string{"Bearer " + arvadostest.ActiveToken}},
+               }
+               req, resp := s.doReq(req)
+               code, _ := resp.Code, resp.Body.String()
+
+               if port == "8000" {
+                       c.Check(code, check.Equals, 401)
+               } else {
+                       c.Check(code, check.Equals, 200)
                }
        }
 }
@@ -307,7 +400,7 @@ func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
                "",
                "",
                http.StatusNotFound,
-               "",
+               notFoundMessage+"\n",
        )
 }
 
@@ -321,7 +414,7 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C)
                "",
                "",
                http.StatusUnauthorized,
-               "",
+               unauthorizedMessage+"\n",
        )
 }
 
@@ -439,7 +532,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C)
                "application/x-www-form-urlencoded",
                url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
                http.StatusNotFound,
-               "",
+               notFoundMessage+"\n",
        )
 }
 
@@ -463,7 +556,7 @@ func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
                "",
                "",
                http.StatusNotFound,
-               "",
+               notFoundMessage+"\n",
        )
 }
 
@@ -579,6 +672,25 @@ func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
        c.Check(resp.Code, check.Equals, http.StatusOK)
        c.Check(resp.Body.String(), check.Equals, "foo")
        c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
+
+       // GET + Origin header is representative of both AJAX GET
+       // requests and inline images via <IMG crossorigin="anonymous"
+       // src="...">.
+       u.RawQuery = "api_token=" + url.QueryEscape(arvadostest.ActiveTokenV2)
+       req = &http.Request{
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header: http.Header{
+                       "Origin": {"https://origin.example"},
+               },
+       }
+       resp = httptest.NewRecorder()
+       s.testServer.Handler.ServeHTTP(resp, req)
+       c.Check(resp.Code, check.Equals, http.StatusOK)
+       c.Check(resp.Body.String(), check.Equals, "foo")
+       c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
 }
 
 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
@@ -660,7 +772,7 @@ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
                {
                        // URLs of this form ignore authHeader, and
                        // FooAndBarFilesInDirUUID isn't public, so
-                       // this returns 404.
+                       // this returns 401.
                        uri:    "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
                        header: authHeader,
                        expect: nil,
@@ -803,7 +915,11 @@ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
                        c.Check(req.URL.Path, check.Equals, trial.redirect, comment)
                }
                if trial.expect == nil {
-                       c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
+                               c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
+                       } else {
+                               c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       }
                } else {
                        c.Check(resp.Code, check.Equals, http.StatusOK, comment)
                        for _, e := range trial.expect {
@@ -824,7 +940,11 @@ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
                resp = httptest.NewRecorder()
                s.testServer.Handler.ServeHTTP(resp, req)
                if trial.expect == nil {
-                       c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
+                               c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
+                       } else {
+                               c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       }
                } else {
                        c.Check(resp.Code, check.Equals, http.StatusOK, comment)
                }
@@ -840,7 +960,11 @@ func (s *IntegrationSuite) testDirectoryListing(c *check.C) {
                resp = httptest.NewRecorder()
                s.testServer.Handler.ServeHTTP(resp, req)
                if trial.expect == nil {
-                       c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       if s.testServer.Config.cluster.Users.AnonymousUserToken == "" {
+                               c.Check(resp.Code, check.Equals, http.StatusUnauthorized, comment)
+                       } else {
+                               c.Check(resp.Code, check.Equals, http.StatusNotFound, comment)
+                       }
                } else {
                        c.Check(resp.Code, check.Equals, http.StatusMultiStatus, comment)
                        for _, e := range trial.expect {