13111: Follow attachment-only policy at /by_id/ and /users/ paths.
[arvados.git] / services / keep-web / handler_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "fmt"
10         "html"
11         "io/ioutil"
12         "net/http"
13         "net/http/httptest"
14         "net/url"
15         "path/filepath"
16         "regexp"
17         "strings"
18
19         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
20         "git.curoverse.com/arvados.git/sdk/go/auth"
21         check "gopkg.in/check.v1"
22 )
23
24 var _ = check.Suite(&UnitSuite{})
25
26 type UnitSuite struct{}
27
28 func (s *UnitSuite) TestCORSPreflight(c *check.C) {
29         h := handler{Config: DefaultConfig()}
30         u, _ := url.Parse("http://keep-web.example/c=" + arvadostest.FooCollection + "/foo")
31         req := &http.Request{
32                 Method:     "OPTIONS",
33                 Host:       u.Host,
34                 URL:        u,
35                 RequestURI: u.RequestURI(),
36                 Header: http.Header{
37                         "Origin":                        {"https://workbench.example"},
38                         "Access-Control-Request-Method": {"POST"},
39                 },
40         }
41
42         // Check preflight for an allowed request
43         resp := httptest.NewRecorder()
44         h.ServeHTTP(resp, req)
45         c.Check(resp.Code, check.Equals, http.StatusOK)
46         c.Check(resp.Body.String(), check.Equals, "")
47         c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
48         c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "COPY, DELETE, GET, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PUT, RMCOL")
49         c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Authorization, Content-Type, Range")
50
51         // Check preflight for a disallowed request
52         resp = httptest.NewRecorder()
53         req.Header.Set("Access-Control-Request-Method", "MAKE-COFFEE")
54         h.ServeHTTP(resp, req)
55         c.Check(resp.Body.String(), check.Equals, "")
56         c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
57 }
58
59 func (s *UnitSuite) TestInvalidUUID(c *check.C) {
60         bogusID := strings.Replace(arvadostest.FooPdh, "+", "-", 1) + "-"
61         token := arvadostest.ActiveToken
62         for _, trial := range []string{
63                 "http://keep-web/c=" + bogusID + "/foo",
64                 "http://keep-web/c=" + bogusID + "/t=" + token + "/foo",
65                 "http://keep-web/collections/download/" + bogusID + "/" + token + "/foo",
66                 "http://keep-web/collections/" + bogusID + "/foo",
67                 "http://" + bogusID + ".keep-web/" + bogusID + "/foo",
68                 "http://" + bogusID + ".keep-web/t=" + token + "/" + bogusID + "/foo",
69         } {
70                 c.Log(trial)
71                 u, err := url.Parse(trial)
72                 c.Assert(err, check.IsNil)
73                 req := &http.Request{
74                         Method:     "GET",
75                         Host:       u.Host,
76                         URL:        u,
77                         RequestURI: u.RequestURI(),
78                 }
79                 resp := httptest.NewRecorder()
80                 cfg := DefaultConfig()
81                 cfg.AnonymousTokens = []string{arvadostest.AnonymousToken}
82                 h := handler{Config: cfg}
83                 h.ServeHTTP(resp, req)
84                 c.Check(resp.Code, check.Equals, http.StatusNotFound)
85         }
86 }
87
88 func mustParseURL(s string) *url.URL {
89         r, err := url.Parse(s)
90         if err != nil {
91                 panic("parse URL: " + s)
92         }
93         return r
94 }
95
96 func (s *IntegrationSuite) TestVhost404(c *check.C) {
97         for _, testURL := range []string{
98                 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
99                 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
100         } {
101                 resp := httptest.NewRecorder()
102                 u := mustParseURL(testURL)
103                 req := &http.Request{
104                         Method:     "GET",
105                         URL:        u,
106                         RequestURI: u.RequestURI(),
107                 }
108                 s.testServer.Handler.ServeHTTP(resp, req)
109                 c.Check(resp.Code, check.Equals, http.StatusNotFound)
110                 c.Check(resp.Body.String(), check.Equals, "")
111         }
112 }
113
114 // An authorizer modifies an HTTP request to make use of the given
115 // token -- by adding it to a header, cookie, query param, or whatever
116 // -- and returns the HTTP status code we should expect from keep-web if
117 // the token is invalid.
118 type authorizer func(*http.Request, string) int
119
120 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
121         s.doVhostRequests(c, authzViaAuthzHeader)
122 }
123 func authzViaAuthzHeader(r *http.Request, tok string) int {
124         r.Header.Add("Authorization", "OAuth2 "+tok)
125         return http.StatusUnauthorized
126 }
127
128 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
129         s.doVhostRequests(c, authzViaCookieValue)
130 }
131 func authzViaCookieValue(r *http.Request, tok string) int {
132         r.AddCookie(&http.Cookie{
133                 Name:  "arvados_api_token",
134                 Value: auth.EncodeTokenCookie([]byte(tok)),
135         })
136         return http.StatusUnauthorized
137 }
138
139 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
140         s.doVhostRequests(c, authzViaPath)
141 }
142 func authzViaPath(r *http.Request, tok string) int {
143         r.URL.Path = "/t=" + tok + r.URL.Path
144         return http.StatusNotFound
145 }
146
147 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
148         s.doVhostRequests(c, authzViaQueryString)
149 }
150 func authzViaQueryString(r *http.Request, tok string) int {
151         r.URL.RawQuery = "api_token=" + tok
152         return http.StatusUnauthorized
153 }
154
155 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
156         s.doVhostRequests(c, authzViaPOST)
157 }
158 func authzViaPOST(r *http.Request, tok string) int {
159         r.Method = "POST"
160         r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
161         r.Body = ioutil.NopCloser(strings.NewReader(
162                 url.Values{"api_token": {tok}}.Encode()))
163         return http.StatusUnauthorized
164 }
165
166 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
167         s.doVhostRequests(c, authzViaPOST)
168 }
169 func authzViaXHRPOST(r *http.Request, tok string) int {
170         r.Method = "POST"
171         r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
172         r.Header.Add("Origin", "https://origin.example")
173         r.Body = ioutil.NopCloser(strings.NewReader(
174                 url.Values{
175                         "api_token":   {tok},
176                         "disposition": {"attachment"},
177                 }.Encode()))
178         return http.StatusUnauthorized
179 }
180
181 // Try some combinations of {url, token} using the given authorization
182 // mechanism, and verify the result is correct.
183 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
184         for _, hostPath := range []string{
185                 arvadostest.FooCollection + ".example.com/foo",
186                 arvadostest.FooCollection + "--collections.example.com/foo",
187                 arvadostest.FooCollection + "--collections.example.com/_/foo",
188                 arvadostest.FooPdh + ".example.com/foo",
189                 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
190                 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
191         } {
192                 c.Log("doRequests: ", hostPath)
193                 s.doVhostRequestsWithHostPath(c, authz, hostPath)
194         }
195 }
196
197 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
198         for _, tok := range []string{
199                 arvadostest.ActiveToken,
200                 arvadostest.ActiveToken[:15],
201                 arvadostest.SpectatorToken,
202                 "bogus",
203                 "",
204         } {
205                 u := mustParseURL("http://" + hostPath)
206                 req := &http.Request{
207                         Method:     "GET",
208                         Host:       u.Host,
209                         URL:        u,
210                         RequestURI: u.RequestURI(),
211                         Header:     http.Header{},
212                 }
213                 failCode := authz(req, tok)
214                 req, resp := s.doReq(req)
215                 code, body := resp.Code, resp.Body.String()
216
217                 // If the initial request had a (non-empty) token
218                 // showing in the query string, we should have been
219                 // redirected in order to hide it in a cookie.
220                 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
221
222                 if tok == arvadostest.ActiveToken {
223                         c.Check(code, check.Equals, http.StatusOK)
224                         c.Check(body, check.Equals, "foo")
225
226                 } else {
227                         c.Check(code >= 400, check.Equals, true)
228                         c.Check(code < 500, check.Equals, true)
229                         if tok == arvadostest.SpectatorToken {
230                                 // Valid token never offers to retry
231                                 // with different credentials.
232                                 c.Check(code, check.Equals, http.StatusNotFound)
233                         } else {
234                                 // Invalid token can ask to retry
235                                 // depending on the authz method.
236                                 c.Check(code, check.Equals, failCode)
237                         }
238                         c.Check(body, check.Equals, "")
239                 }
240         }
241 }
242
243 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
244         resp := httptest.NewRecorder()
245         s.testServer.Handler.ServeHTTP(resp, req)
246         if resp.Code != http.StatusSeeOther {
247                 return req, resp
248         }
249         cookies := (&http.Response{Header: resp.Header()}).Cookies()
250         u, _ := req.URL.Parse(resp.Header().Get("Location"))
251         req = &http.Request{
252                 Method:     "GET",
253                 Host:       u.Host,
254                 URL:        u,
255                 RequestURI: u.RequestURI(),
256                 Header:     http.Header{},
257         }
258         for _, c := range cookies {
259                 req.AddCookie(c)
260         }
261         return s.doReq(req)
262 }
263
264 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
265         s.testVhostRedirectTokenToCookie(c, "GET",
266                 arvadostest.FooCollection+".example.com/foo",
267                 "?api_token="+arvadostest.ActiveToken,
268                 "",
269                 "",
270                 http.StatusOK,
271                 "foo",
272         )
273 }
274
275 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
276         s.testVhostRedirectTokenToCookie(c, "GET",
277                 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
278                 "",
279                 "",
280                 "",
281                 http.StatusOK,
282                 "foo",
283         )
284 }
285
286 // Bad token in URL is 404 Not Found because it doesn't make sense to
287 // retry the same URL with different authorization.
288 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
289         s.testVhostRedirectTokenToCookie(c, "GET",
290                 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
291                 "",
292                 "",
293                 "",
294                 http.StatusNotFound,
295                 "",
296         )
297 }
298
299 // Bad token in a cookie (even if it got there via our own
300 // query-string-to-cookie redirect) is, in principle, retryable at the
301 // same URL so it's 401 Unauthorized.
302 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
303         s.testVhostRedirectTokenToCookie(c, "GET",
304                 arvadostest.FooCollection+".example.com/foo",
305                 "?api_token=thisisabogustoken",
306                 "",
307                 "",
308                 http.StatusUnauthorized,
309                 "",
310         )
311 }
312
313 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
314         s.testVhostRedirectTokenToCookie(c, "GET",
315                 "example.com/c="+arvadostest.FooCollection+"/foo",
316                 "?api_token="+arvadostest.ActiveToken,
317                 "",
318                 "",
319                 http.StatusBadRequest,
320                 "",
321         )
322 }
323
324 // If client requests an attachment by putting ?disposition=attachment
325 // in the query string, and gets redirected, the redirect target
326 // should respond with an attachment.
327 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
328         resp := s.testVhostRedirectTokenToCookie(c, "GET",
329                 arvadostest.FooCollection+".example.com/foo",
330                 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
331                 "",
332                 "",
333                 http.StatusOK,
334                 "foo",
335         )
336         c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
337 }
338
339 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
340         s.testServer.Config.AttachmentOnlyHost = "download.example.com"
341         resp := s.testVhostRedirectTokenToCookie(c, "GET",
342                 "download.example.com/by_id/"+arvadostest.FooCollection+"/foo",
343                 "?api_token="+arvadostest.ActiveToken,
344                 "",
345                 "",
346                 http.StatusOK,
347                 "foo",
348         )
349         c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
350 }
351
352 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
353         s.testServer.Config.TrustAllContent = true
354         s.testVhostRedirectTokenToCookie(c, "GET",
355                 "example.com/c="+arvadostest.FooCollection+"/foo",
356                 "?api_token="+arvadostest.ActiveToken,
357                 "",
358                 "",
359                 http.StatusOK,
360                 "foo",
361         )
362 }
363
364 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
365         s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
366
367         s.testVhostRedirectTokenToCookie(c, "GET",
368                 "example.com/c="+arvadostest.FooCollection+"/foo",
369                 "?api_token="+arvadostest.ActiveToken,
370                 "",
371                 "",
372                 http.StatusBadRequest,
373                 "",
374         )
375
376         resp := s.testVhostRedirectTokenToCookie(c, "GET",
377                 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
378                 "?api_token="+arvadostest.ActiveToken,
379                 "",
380                 "",
381                 http.StatusOK,
382                 "foo",
383         )
384         c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
385 }
386
387 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
388         s.testVhostRedirectTokenToCookie(c, "POST",
389                 arvadostest.FooCollection+".example.com/foo",
390                 "",
391                 "application/x-www-form-urlencoded",
392                 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
393                 http.StatusOK,
394                 "foo",
395         )
396 }
397
398 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
399         s.testVhostRedirectTokenToCookie(c, "POST",
400                 arvadostest.FooCollection+".example.com/foo",
401                 "",
402                 "application/x-www-form-urlencoded",
403                 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
404                 http.StatusNotFound,
405                 "",
406         )
407 }
408
409 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
410         s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
411         s.testVhostRedirectTokenToCookie(c, "GET",
412                 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
413                 "",
414                 "",
415                 "",
416                 http.StatusOK,
417                 "Hello world\n",
418         )
419 }
420
421 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
422         s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
423         s.testVhostRedirectTokenToCookie(c, "GET",
424                 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
425                 "",
426                 "",
427                 "",
428                 http.StatusNotFound,
429                 "",
430         )
431 }
432
433 // XHRs can't follow redirect-with-cookie so they rely on method=POST
434 // and disposition=attachment (telling us it's acceptable to respond
435 // with content instead of a redirect) and an Origin header that gets
436 // added automatically by the browser (telling us it's desirable to do
437 // so).
438 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
439         u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
440         req := &http.Request{
441                 Method:     "POST",
442                 Host:       u.Host,
443                 URL:        u,
444                 RequestURI: u.RequestURI(),
445                 Header: http.Header{
446                         "Origin":       {"https://origin.example"},
447                         "Content-Type": {"application/x-www-form-urlencoded"},
448                 },
449                 Body: ioutil.NopCloser(strings.NewReader(url.Values{
450                         "api_token":   {arvadostest.ActiveToken},
451                         "disposition": {"attachment"},
452                 }.Encode())),
453         }
454         resp := httptest.NewRecorder()
455         s.testServer.Handler.ServeHTTP(resp, req)
456         c.Check(resp.Code, check.Equals, http.StatusOK)
457         c.Check(resp.Body.String(), check.Equals, "foo")
458         c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
459 }
460
461 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
462         u, _ := url.Parse(`http://` + hostPath + queryString)
463         req := &http.Request{
464                 Method:     method,
465                 Host:       u.Host,
466                 URL:        u,
467                 RequestURI: u.RequestURI(),
468                 Header:     http.Header{"Content-Type": {contentType}},
469                 Body:       ioutil.NopCloser(strings.NewReader(reqBody)),
470         }
471
472         resp := httptest.NewRecorder()
473         defer func() {
474                 c.Check(resp.Code, check.Equals, expectStatus)
475                 c.Check(resp.Body.String(), check.Equals, expectRespBody)
476         }()
477
478         s.testServer.Handler.ServeHTTP(resp, req)
479         if resp.Code != http.StatusSeeOther {
480                 return resp
481         }
482         c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
483         cookies := (&http.Response{Header: resp.Header()}).Cookies()
484
485         u, _ = u.Parse(resp.Header().Get("Location"))
486         req = &http.Request{
487                 Method:     "GET",
488                 Host:       u.Host,
489                 URL:        u,
490                 RequestURI: u.RequestURI(),
491                 Header:     http.Header{},
492         }
493         for _, c := range cookies {
494                 req.AddCookie(c)
495         }
496
497         resp = httptest.NewRecorder()
498         s.testServer.Handler.ServeHTTP(resp, req)
499         c.Check(resp.Header().Get("Location"), check.Equals, "")
500         return resp
501 }
502
503 func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
504         s.testServer.Config.AttachmentOnlyHost = "download.example.com"
505         authHeader := http.Header{
506                 "Authorization": {"OAuth2 " + arvadostest.ActiveToken},
507         }
508         for _, trial := range []struct {
509                 uri      string
510                 header   http.Header
511                 expect   []string
512                 redirect string
513                 cutDirs  int
514         }{
515                 {
516                         uri:     strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/",
517                         header:  authHeader,
518                         expect:  []string{"dir1/foo", "dir1/bar"},
519                         cutDirs: 0,
520                 },
521                 {
522                         uri:     strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/dir1/",
523                         header:  authHeader,
524                         expect:  []string{"foo", "bar"},
525                         cutDirs: 1,
526                 },
527                 {
528                         uri:     "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
529                         header:  authHeader,
530                         expect:  []string{"dir1/foo", "dir1/bar"},
531                         cutDirs: 2,
532                 },
533                 {
534                         uri:     "download.example.com/users/active/foo_file_in_dir/",
535                         header:  authHeader,
536                         expect:  []string{"dir1/"},
537                         cutDirs: 3,
538                 },
539                 {
540                         uri:     "download.example.com/users/active/foo_file_in_dir/dir1/",
541                         header:  authHeader,
542                         expect:  []string{"bar"},
543                         cutDirs: 4,
544                 },
545                 {
546                         uri:     "download.example.com/",
547                         header:  authHeader,
548                         expect:  []string{"users/"},
549                         cutDirs: 0,
550                 },
551                 {
552                         uri:      "download.example.com/users",
553                         header:   authHeader,
554                         redirect: "/users/",
555                         expect:   []string{"active/"},
556                         cutDirs:  1,
557                 },
558                 {
559                         uri:     "download.example.com/users/",
560                         header:  authHeader,
561                         expect:  []string{"active/"},
562                         cutDirs: 1,
563                 },
564                 {
565                         uri:      "download.example.com/users/active",
566                         header:   authHeader,
567                         redirect: "/users/active/",
568                         expect:   []string{"foo_file_in_dir/"},
569                         cutDirs:  2,
570                 },
571                 {
572                         uri:     "download.example.com/users/active/",
573                         header:  authHeader,
574                         expect:  []string{"foo_file_in_dir/"},
575                         cutDirs: 2,
576                 },
577                 {
578                         uri:     "collections.example.com/collections/download/" + arvadostest.FooAndBarFilesInDirUUID + "/" + arvadostest.ActiveToken + "/",
579                         header:  nil,
580                         expect:  []string{"dir1/foo", "dir1/bar"},
581                         cutDirs: 4,
582                 },
583                 {
584                         uri:     "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/",
585                         header:  nil,
586                         expect:  []string{"dir1/foo", "dir1/bar"},
587                         cutDirs: 2,
588                 },
589                 {
590                         uri:     "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken,
591                         header:  nil,
592                         expect:  []string{"dir1/foo", "dir1/bar"},
593                         cutDirs: 2,
594                 },
595                 {
596                         uri:     "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID,
597                         header:  authHeader,
598                         expect:  []string{"dir1/foo", "dir1/bar"},
599                         cutDirs: 1,
600                 },
601                 {
602                         uri:      "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1",
603                         header:   authHeader,
604                         redirect: "/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1/",
605                         expect:   []string{"foo", "bar"},
606                         cutDirs:  2,
607                 },
608                 {
609                         uri:     "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/_/dir1/",
610                         header:  authHeader,
611                         expect:  []string{"foo", "bar"},
612                         cutDirs: 3,
613                 },
614                 {
615                         uri:      arvadostest.FooAndBarFilesInDirUUID + ".example.com/dir1?api_token=" + arvadostest.ActiveToken,
616                         header:   authHeader,
617                         redirect: "/dir1/",
618                         expect:   []string{"foo", "bar"},
619                         cutDirs:  1,
620                 },
621                 {
622                         uri:    "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/theperthcountyconspiracydoesnotexist/",
623                         header: authHeader,
624                         expect: nil,
625                 },
626         } {
627                 c.Logf("HTML: %q => %q", trial.uri, trial.expect)
628                 resp := httptest.NewRecorder()
629                 u := mustParseURL("//" + trial.uri)
630                 req := &http.Request{
631                         Method:     "GET",
632                         Host:       u.Host,
633                         URL:        u,
634                         RequestURI: u.RequestURI(),
635                         Header:     trial.header,
636                 }
637                 s.testServer.Handler.ServeHTTP(resp, req)
638                 var cookies []*http.Cookie
639                 for resp.Code == http.StatusSeeOther {
640                         u, _ := req.URL.Parse(resp.Header().Get("Location"))
641                         req = &http.Request{
642                                 Method:     "GET",
643                                 Host:       u.Host,
644                                 URL:        u,
645                                 RequestURI: u.RequestURI(),
646                                 Header:     trial.header,
647                         }
648                         cookies = append(cookies, (&http.Response{Header: resp.Header()}).Cookies()...)
649                         for _, c := range cookies {
650                                 req.AddCookie(c)
651                         }
652                         resp = httptest.NewRecorder()
653                         s.testServer.Handler.ServeHTTP(resp, req)
654                 }
655                 if trial.redirect != "" {
656                         c.Check(req.URL.Path, check.Equals, trial.redirect)
657                 }
658                 if trial.expect == nil {
659                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
660                 } else {
661                         c.Check(resp.Code, check.Equals, http.StatusOK)
662                         for _, e := range trial.expect {
663                                 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="`+e+`".*`)
664                         }
665                         c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`)
666                 }
667
668                 c.Logf("WebDAV: %q => %q", trial.uri, trial.expect)
669                 req = &http.Request{
670                         Method:     "OPTIONS",
671                         Host:       u.Host,
672                         URL:        u,
673                         RequestURI: u.RequestURI(),
674                         Header:     trial.header,
675                         Body:       ioutil.NopCloser(&bytes.Buffer{}),
676                 }
677                 resp = httptest.NewRecorder()
678                 s.testServer.Handler.ServeHTTP(resp, req)
679                 if trial.expect == nil {
680                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
681                 } else {
682                         c.Check(resp.Code, check.Equals, http.StatusOK)
683                 }
684
685                 req = &http.Request{
686                         Method:     "PROPFIND",
687                         Host:       u.Host,
688                         URL:        u,
689                         RequestURI: u.RequestURI(),
690                         Header:     trial.header,
691                         Body:       ioutil.NopCloser(&bytes.Buffer{}),
692                 }
693                 resp = httptest.NewRecorder()
694                 s.testServer.Handler.ServeHTTP(resp, req)
695                 if trial.expect == nil {
696                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
697                 } else {
698                         c.Check(resp.Code, check.Equals, http.StatusMultiStatus)
699                         for _, e := range trial.expect {
700                                 c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+filepath.Join(u.Path, e)+`</D:href>.*`)
701                         }
702                 }
703         }
704 }
705
706 func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) {
707         s.testServer.Config.ManagementToken = arvadostest.ManagementToken
708         authHeader := http.Header{
709                 "Authorization": {"Bearer " + arvadostest.ManagementToken},
710         }
711
712         resp := httptest.NewRecorder()
713         u := mustParseURL("http://download.example.com/_health/ping")
714         req := &http.Request{
715                 Method:     "GET",
716                 Host:       u.Host,
717                 URL:        u,
718                 RequestURI: u.RequestURI(),
719                 Header:     authHeader,
720         }
721         s.testServer.Handler.ServeHTTP(resp, req)
722
723         c.Check(resp.Code, check.Equals, http.StatusOK)
724         c.Check(resp.Body.String(), check.Matches, `{"health":"OK"}\n`)
725 }