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