12483: Use real modTimes.
[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(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
337 }
338
339 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
340         s.testServer.Config.TrustAllContent = true
341         s.testVhostRedirectTokenToCookie(c, "GET",
342                 "example.com/c="+arvadostest.FooCollection+"/foo",
343                 "?api_token="+arvadostest.ActiveToken,
344                 "",
345                 "",
346                 http.StatusOK,
347                 "foo",
348         )
349 }
350
351 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
352         s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
353
354         s.testVhostRedirectTokenToCookie(c, "GET",
355                 "example.com/c="+arvadostest.FooCollection+"/foo",
356                 "?api_token="+arvadostest.ActiveToken,
357                 "",
358                 "",
359                 http.StatusBadRequest,
360                 "",
361         )
362
363         resp := s.testVhostRedirectTokenToCookie(c, "GET",
364                 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
365                 "?api_token="+arvadostest.ActiveToken,
366                 "",
367                 "",
368                 http.StatusOK,
369                 "foo",
370         )
371         c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
372 }
373
374 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
375         s.testVhostRedirectTokenToCookie(c, "POST",
376                 arvadostest.FooCollection+".example.com/foo",
377                 "",
378                 "application/x-www-form-urlencoded",
379                 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
380                 http.StatusOK,
381                 "foo",
382         )
383 }
384
385 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
386         s.testVhostRedirectTokenToCookie(c, "POST",
387                 arvadostest.FooCollection+".example.com/foo",
388                 "",
389                 "application/x-www-form-urlencoded",
390                 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
391                 http.StatusNotFound,
392                 "",
393         )
394 }
395
396 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
397         s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
398         s.testVhostRedirectTokenToCookie(c, "GET",
399                 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
400                 "",
401                 "",
402                 "",
403                 http.StatusOK,
404                 "Hello world\n",
405         )
406 }
407
408 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
409         s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
410         s.testVhostRedirectTokenToCookie(c, "GET",
411                 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
412                 "",
413                 "",
414                 "",
415                 http.StatusNotFound,
416                 "",
417         )
418 }
419
420 // XHRs can't follow redirect-with-cookie so they rely on method=POST
421 // and disposition=attachment (telling us it's acceptable to respond
422 // with content instead of a redirect) and an Origin header that gets
423 // added automatically by the browser (telling us it's desirable to do
424 // so).
425 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
426         u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
427         req := &http.Request{
428                 Method:     "POST",
429                 Host:       u.Host,
430                 URL:        u,
431                 RequestURI: u.RequestURI(),
432                 Header: http.Header{
433                         "Origin":       {"https://origin.example"},
434                         "Content-Type": {"application/x-www-form-urlencoded"},
435                 },
436                 Body: ioutil.NopCloser(strings.NewReader(url.Values{
437                         "api_token":   {arvadostest.ActiveToken},
438                         "disposition": {"attachment"},
439                 }.Encode())),
440         }
441         resp := httptest.NewRecorder()
442         s.testServer.Handler.ServeHTTP(resp, req)
443         c.Check(resp.Code, check.Equals, http.StatusOK)
444         c.Check(resp.Body.String(), check.Equals, "foo")
445         c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
446 }
447
448 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
449         u, _ := url.Parse(`http://` + hostPath + queryString)
450         req := &http.Request{
451                 Method:     method,
452                 Host:       u.Host,
453                 URL:        u,
454                 RequestURI: u.RequestURI(),
455                 Header:     http.Header{"Content-Type": {contentType}},
456                 Body:       ioutil.NopCloser(strings.NewReader(reqBody)),
457         }
458
459         resp := httptest.NewRecorder()
460         defer func() {
461                 c.Check(resp.Code, check.Equals, expectStatus)
462                 c.Check(resp.Body.String(), check.Equals, expectRespBody)
463         }()
464
465         s.testServer.Handler.ServeHTTP(resp, req)
466         if resp.Code != http.StatusSeeOther {
467                 return resp
468         }
469         c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
470         cookies := (&http.Response{Header: resp.Header()}).Cookies()
471
472         u, _ = u.Parse(resp.Header().Get("Location"))
473         req = &http.Request{
474                 Method:     "GET",
475                 Host:       u.Host,
476                 URL:        u,
477                 RequestURI: u.RequestURI(),
478                 Header:     http.Header{},
479         }
480         for _, c := range cookies {
481                 req.AddCookie(c)
482         }
483
484         resp = httptest.NewRecorder()
485         s.testServer.Handler.ServeHTTP(resp, req)
486         c.Check(resp.Header().Get("Location"), check.Equals, "")
487         return resp
488 }
489
490 func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
491         s.testServer.Config.AttachmentOnlyHost = "download.example.com"
492         authHeader := http.Header{
493                 "Authorization": {"OAuth2 " + arvadostest.ActiveToken},
494         }
495         for _, trial := range []struct {
496                 uri     string
497                 header  http.Header
498                 expect  []string
499                 cutDirs int
500         }{
501                 {
502                         uri:     strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/",
503                         header:  authHeader,
504                         expect:  []string{"dir1/foo", "dir1/bar"},
505                         cutDirs: 0,
506                 },
507                 {
508                         uri:     strings.Replace(arvadostest.FooAndBarFilesInDirPDH, "+", "-", -1) + ".example.com/dir1/",
509                         header:  authHeader,
510                         expect:  []string{"foo", "bar"},
511                         cutDirs: 0,
512                 },
513                 {
514                         uri:     "download.example.com/collections/" + arvadostest.FooAndBarFilesInDirUUID + "/",
515                         header:  authHeader,
516                         expect:  []string{"dir1/foo", "dir1/bar"},
517                         cutDirs: 2,
518                 },
519                 {
520                         uri:     "collections.example.com/collections/download/" + arvadostest.FooAndBarFilesInDirUUID + "/" + arvadostest.ActiveToken + "/",
521                         header:  nil,
522                         expect:  []string{"dir1/foo", "dir1/bar"},
523                         cutDirs: 4,
524                 },
525                 {
526                         uri:     "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken + "/",
527                         header:  nil,
528                         expect:  []string{"dir1/foo", "dir1/bar"},
529                         cutDirs: 2,
530                 },
531                 {
532                         uri:     "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/t=" + arvadostest.ActiveToken,
533                         header:  nil,
534                         expect:  []string{"dir1/foo", "dir1/bar"},
535                         cutDirs: 2,
536                 },
537                 {
538                         uri:     "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID,
539                         header:  authHeader,
540                         expect:  []string{"dir1/foo", "dir1/bar"},
541                         cutDirs: 1,
542                 },
543                 {
544                         uri:     "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/dir1/",
545                         header:  authHeader,
546                         expect:  []string{"foo", "bar"},
547                         cutDirs: 1,
548                 },
549                 {
550                         uri:     "download.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/_/dir1/",
551                         header:  authHeader,
552                         expect:  []string{"foo", "bar"},
553                         cutDirs: 2,
554                 },
555                 {
556                         uri:     arvadostest.FooAndBarFilesInDirUUID + ".example.com/dir1?api_token=" + arvadostest.ActiveToken,
557                         header:  authHeader,
558                         expect:  []string{"foo", "bar"},
559                         cutDirs: 0,
560                 },
561                 {
562                         uri:    "collections.example.com/c=" + arvadostest.FooAndBarFilesInDirUUID + "/theperthcountyconspiracydoesnotexist/",
563                         header: authHeader,
564                         expect: nil,
565                 },
566         } {
567                 c.Logf("HTML: %q => %q", trial.uri, trial.expect)
568                 resp := httptest.NewRecorder()
569                 u := mustParseURL("//" + trial.uri)
570                 req := &http.Request{
571                         Method:     "GET",
572                         Host:       u.Host,
573                         URL:        u,
574                         RequestURI: u.RequestURI(),
575                         Header:     trial.header,
576                 }
577                 s.testServer.Handler.ServeHTTP(resp, req)
578                 var cookies []*http.Cookie
579                 for resp.Code == http.StatusSeeOther {
580                         u, _ := req.URL.Parse(resp.Header().Get("Location"))
581                         req = &http.Request{
582                                 Method:     "GET",
583                                 Host:       u.Host,
584                                 URL:        u,
585                                 RequestURI: u.RequestURI(),
586                                 Header:     trial.header,
587                         }
588                         cookies = append(cookies, (&http.Response{Header: resp.Header()}).Cookies()...)
589                         for _, c := range cookies {
590                                 req.AddCookie(c)
591                         }
592                         resp = httptest.NewRecorder()
593                         s.testServer.Handler.ServeHTTP(resp, req)
594                 }
595                 if trial.expect == nil {
596                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
597                 } else {
598                         c.Check(resp.Code, check.Equals, http.StatusOK)
599                         for _, e := range trial.expect {
600                                 c.Check(resp.Body.String(), check.Matches, `(?ms).*href="`+e+`".*`)
601                         }
602                         c.Check(resp.Body.String(), check.Matches, `(?ms).*--cut-dirs=`+fmt.Sprintf("%d", trial.cutDirs)+` .*`)
603                 }
604
605                 c.Logf("WebDAV: %q => %q", trial.uri, trial.expect)
606                 req = &http.Request{
607                         Method:     "OPTIONS",
608                         Host:       u.Host,
609                         URL:        u,
610                         RequestURI: u.RequestURI(),
611                         Header:     trial.header,
612                         Body:       ioutil.NopCloser(&bytes.Buffer{}),
613                 }
614                 resp = httptest.NewRecorder()
615                 s.testServer.Handler.ServeHTTP(resp, req)
616                 if trial.expect == nil {
617                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
618                 } else {
619                         c.Check(resp.Code, check.Equals, http.StatusOK)
620                 }
621
622                 req = &http.Request{
623                         Method:     "PROPFIND",
624                         Host:       u.Host,
625                         URL:        u,
626                         RequestURI: u.RequestURI(),
627                         Header:     trial.header,
628                         Body:       ioutil.NopCloser(&bytes.Buffer{}),
629                 }
630                 resp = httptest.NewRecorder()
631                 s.testServer.Handler.ServeHTTP(resp, req)
632                 if trial.expect == nil {
633                         c.Check(resp.Code, check.Equals, http.StatusNotFound)
634                 } else {
635                         c.Check(resp.Code, check.Equals, http.StatusMultiStatus)
636                         for _, e := range trial.expect {
637                                 c.Check(resp.Body.String(), check.Matches, `(?ms).*<D:href>`+filepath.Join(u.Path, e)+`</D:href>.*`)
638                         }
639                 }
640         }
641 }
642
643 func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) {
644         s.testServer.Config.ManagementToken = arvadostest.ManagementToken
645         authHeader := http.Header{
646                 "Authorization": {"Bearer " + arvadostest.ManagementToken},
647         }
648
649         resp := httptest.NewRecorder()
650         u := mustParseURL("http://download.example.com/_health/ping")
651         req := &http.Request{
652                 Method:     "GET",
653                 Host:       u.Host,
654                 URL:        u,
655                 RequestURI: u.RequestURI(),
656                 Header:     authHeader,
657         }
658         s.testServer.Handler.ServeHTTP(resp, req)
659
660         c.Check(resp.Code, check.Equals, http.StatusOK)
661         c.Check(resp.Body.String(), check.Matches, `{"health":"OK"}\n`)
662 }