12 "git.curoverse.com/arvados.git/sdk/go/arvadostest"
13 "git.curoverse.com/arvados.git/sdk/go/auth"
14 check "gopkg.in/check.v1"
17 var _ = check.Suite(&UnitSuite{})
19 type UnitSuite struct{}
21 func (s *UnitSuite) TestCORSPreflight(c *check.C) {
22 h := handler{Config: &Config{}}
23 u, _ := url.Parse("http://keep-web.example/c=" + arvadostest.FooCollection + "/foo")
28 RequestURI: u.RequestURI(),
30 "Origin": {"https://workbench.example"},
31 "Access-Control-Request-Method": {"POST"},
35 // Check preflight for an allowed request
36 resp := httptest.NewRecorder()
37 h.ServeHTTP(resp, req)
38 c.Check(resp.Code, check.Equals, http.StatusOK)
39 c.Check(resp.Body.String(), check.Equals, "")
40 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
41 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "GET, POST")
42 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Range")
44 // Check preflight for a disallowed request
45 resp = httptest.NewRecorder()
46 req.Header.Set("Access-Control-Request-Method", "DELETE")
47 h.ServeHTTP(resp, req)
48 c.Check(resp.Body.String(), check.Equals, "")
49 c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
52 func (s *UnitSuite) TestInvalidUUID(c *check.C) {
53 bogusID := strings.Replace(arvadostest.FooPdh, "+", "-", 1) + "-"
54 token := arvadostest.ActiveToken
55 for _, trial := range []string{
56 "http://keep-web/c=" + bogusID + "/foo",
57 "http://keep-web/c=" + bogusID + "/t=" + token + "/foo",
58 "http://keep-web/collections/download/" + bogusID + "/" + token + "/foo",
59 "http://keep-web/collections/" + bogusID + "/foo",
60 "http://" + bogusID + ".keep-web/" + bogusID + "/foo",
61 "http://" + bogusID + ".keep-web/t=" + token + "/" + bogusID + "/foo",
64 u, err := url.Parse(trial)
65 c.Assert(err, check.IsNil)
70 RequestURI: u.RequestURI(),
72 resp := httptest.NewRecorder()
73 h := handler{Config: &Config{
74 AnonymousTokens: []string{arvadostest.AnonymousToken},
76 h.ServeHTTP(resp, req)
77 c.Check(resp.Code, check.Equals, http.StatusNotFound)
81 func mustParseURL(s string) *url.URL {
82 r, err := url.Parse(s)
84 panic("parse URL: " + s)
89 func (s *IntegrationSuite) TestVhost404(c *check.C) {
90 for _, testURL := range []string{
91 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
92 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
94 resp := httptest.NewRecorder()
95 u := mustParseURL(testURL)
99 RequestURI: u.RequestURI(),
101 s.testServer.Handler.ServeHTTP(resp, req)
102 c.Check(resp.Code, check.Equals, http.StatusNotFound)
103 c.Check(resp.Body.String(), check.Equals, "")
107 // An authorizer modifies an HTTP request to make use of the given
108 // token -- by adding it to a header, cookie, query param, or whatever
109 // -- and returns the HTTP status code we should expect from keep-web if
110 // the token is invalid.
111 type authorizer func(*http.Request, string) int
113 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
114 s.doVhostRequests(c, authzViaAuthzHeader)
116 func authzViaAuthzHeader(r *http.Request, tok string) int {
117 r.Header.Add("Authorization", "OAuth2 "+tok)
118 return http.StatusUnauthorized
121 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
122 s.doVhostRequests(c, authzViaCookieValue)
124 func authzViaCookieValue(r *http.Request, tok string) int {
125 r.AddCookie(&http.Cookie{
126 Name: "arvados_api_token",
127 Value: auth.EncodeTokenCookie([]byte(tok)),
129 return http.StatusUnauthorized
132 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
133 s.doVhostRequests(c, authzViaPath)
135 func authzViaPath(r *http.Request, tok string) int {
136 r.URL.Path = "/t=" + tok + r.URL.Path
137 return http.StatusNotFound
140 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
141 s.doVhostRequests(c, authzViaQueryString)
143 func authzViaQueryString(r *http.Request, tok string) int {
144 r.URL.RawQuery = "api_token=" + tok
145 return http.StatusUnauthorized
148 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
149 s.doVhostRequests(c, authzViaPOST)
151 func authzViaPOST(r *http.Request, tok string) int {
153 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
154 r.Body = ioutil.NopCloser(strings.NewReader(
155 url.Values{"api_token": {tok}}.Encode()))
156 return http.StatusUnauthorized
159 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
160 s.doVhostRequests(c, authzViaPOST)
162 func authzViaXHRPOST(r *http.Request, tok string) int {
164 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
165 r.Header.Add("Origin", "https://origin.example")
166 r.Body = ioutil.NopCloser(strings.NewReader(
169 "disposition": {"attachment"},
171 return http.StatusUnauthorized
174 // Try some combinations of {url, token} using the given authorization
175 // mechanism, and verify the result is correct.
176 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
177 for _, hostPath := range []string{
178 arvadostest.FooCollection + ".example.com/foo",
179 arvadostest.FooCollection + "--collections.example.com/foo",
180 arvadostest.FooCollection + "--collections.example.com/_/foo",
181 arvadostest.FooPdh + ".example.com/foo",
182 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
183 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
185 c.Log("doRequests: ", hostPath)
186 s.doVhostRequestsWithHostPath(c, authz, hostPath)
190 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
191 for _, tok := range []string{
192 arvadostest.ActiveToken,
193 arvadostest.ActiveToken[:15],
194 arvadostest.SpectatorToken,
198 u := mustParseURL("http://" + hostPath)
199 req := &http.Request{
203 RequestURI: u.RequestURI(),
204 Header: http.Header{},
206 failCode := authz(req, tok)
207 req, resp := s.doReq(req)
208 code, body := resp.Code, resp.Body.String()
210 // If the initial request had a (non-empty) token
211 // showing in the query string, we should have been
212 // redirected in order to hide it in a cookie.
213 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
215 if tok == arvadostest.ActiveToken {
216 c.Check(code, check.Equals, http.StatusOK)
217 c.Check(body, check.Equals, "foo")
220 c.Check(code >= 400, check.Equals, true)
221 c.Check(code < 500, check.Equals, true)
222 if tok == arvadostest.SpectatorToken {
223 // Valid token never offers to retry
224 // with different credentials.
225 c.Check(code, check.Equals, http.StatusNotFound)
227 // Invalid token can ask to retry
228 // depending on the authz method.
229 c.Check(code, check.Equals, failCode)
231 c.Check(body, check.Equals, "")
236 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
237 resp := httptest.NewRecorder()
238 s.testServer.Handler.ServeHTTP(resp, req)
239 if resp.Code != http.StatusSeeOther {
242 cookies := (&http.Response{Header: resp.Header()}).Cookies()
243 u, _ := req.URL.Parse(resp.Header().Get("Location"))
248 RequestURI: u.RequestURI(),
249 Header: http.Header{},
251 for _, c := range cookies {
257 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
258 s.testVhostRedirectTokenToCookie(c, "GET",
259 arvadostest.FooCollection+".example.com/foo",
260 "?api_token="+arvadostest.ActiveToken,
268 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
269 s.testVhostRedirectTokenToCookie(c, "GET",
270 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
279 // Bad token in URL is 404 Not Found because it doesn't make sense to
280 // retry the same URL with different authorization.
281 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
282 s.testVhostRedirectTokenToCookie(c, "GET",
283 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
292 // Bad token in a cookie (even if it got there via our own
293 // query-string-to-cookie redirect) is, in principle, retryable at the
294 // same URL so it's 401 Unauthorized.
295 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
296 s.testVhostRedirectTokenToCookie(c, "GET",
297 arvadostest.FooCollection+".example.com/foo",
298 "?api_token=thisisabogustoken",
301 http.StatusUnauthorized,
306 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
307 s.testVhostRedirectTokenToCookie(c, "GET",
308 "example.com/c="+arvadostest.FooCollection+"/foo",
309 "?api_token="+arvadostest.ActiveToken,
312 http.StatusBadRequest,
317 // If client requests an attachment by putting ?disposition=attachment
318 // in the query string, and gets redirected, the redirect target
319 // should respond with an attachment.
320 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
321 resp := s.testVhostRedirectTokenToCookie(c, "GET",
322 arvadostest.FooCollection+".example.com/foo",
323 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
329 c.Check(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
332 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
333 s.testServer.Config.TrustAllContent = true
334 s.testVhostRedirectTokenToCookie(c, "GET",
335 "example.com/c="+arvadostest.FooCollection+"/foo",
336 "?api_token="+arvadostest.ActiveToken,
344 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
345 s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
347 s.testVhostRedirectTokenToCookie(c, "GET",
348 "example.com/c="+arvadostest.FooCollection+"/foo",
349 "?api_token="+arvadostest.ActiveToken,
352 http.StatusBadRequest,
356 resp := s.testVhostRedirectTokenToCookie(c, "GET",
357 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
358 "?api_token="+arvadostest.ActiveToken,
364 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
367 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
368 s.testVhostRedirectTokenToCookie(c, "POST",
369 arvadostest.FooCollection+".example.com/foo",
371 "application/x-www-form-urlencoded",
372 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
378 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
379 s.testVhostRedirectTokenToCookie(c, "POST",
380 arvadostest.FooCollection+".example.com/foo",
382 "application/x-www-form-urlencoded",
383 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
389 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
390 s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
391 s.testVhostRedirectTokenToCookie(c, "GET",
392 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
401 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
402 s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
403 s.testVhostRedirectTokenToCookie(c, "GET",
404 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
413 // XHRs can't follow redirect-with-cookie so they rely on method=POST
414 // and disposition=attachment (telling us it's acceptable to respond
415 // with content instead of a redirect) and an Origin header that gets
416 // added automatically by the browser (telling us it's desirable to do
418 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
419 u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
420 req := &http.Request{
424 RequestURI: u.RequestURI(),
426 "Origin": {"https://origin.example"},
427 "Content-Type": {"application/x-www-form-urlencoded"},
429 Body: ioutil.NopCloser(strings.NewReader(url.Values{
430 "api_token": {arvadostest.ActiveToken},
431 "disposition": {"attachment"},
434 resp := httptest.NewRecorder()
435 s.testServer.Handler.ServeHTTP(resp, req)
436 c.Check(resp.Code, check.Equals, http.StatusOK)
437 c.Check(resp.Body.String(), check.Equals, "foo")
438 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
441 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
442 u, _ := url.Parse(`http://` + hostPath + queryString)
443 req := &http.Request{
447 RequestURI: u.RequestURI(),
448 Header: http.Header{"Content-Type": {contentType}},
449 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
452 resp := httptest.NewRecorder()
454 c.Check(resp.Code, check.Equals, expectStatus)
455 c.Check(resp.Body.String(), check.Equals, expectRespBody)
458 s.testServer.Handler.ServeHTTP(resp, req)
459 if resp.Code != http.StatusSeeOther {
462 c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
463 cookies := (&http.Response{Header: resp.Header()}).Cookies()
465 u, _ = u.Parse(resp.Header().Get("Location"))
470 RequestURI: u.RequestURI(),
471 Header: http.Header{},
473 for _, c := range cookies {
477 resp = httptest.NewRecorder()
478 s.testServer.Handler.ServeHTTP(resp, req)
479 c.Check(resp.Header().Get("Location"), check.Equals, "")