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 mustParseURL(s string) *url.URL {
22 r, err := url.Parse(s)
24 panic("parse URL: " + s)
29 func (s *IntegrationSuite) TestVhost404(c *check.C) {
30 for _, testURL := range []string{
31 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
32 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
34 resp := httptest.NewRecorder()
35 u := mustParseURL(testURL)
39 RequestURI: u.RequestURI(),
41 (&handler{}).ServeHTTP(resp, req)
42 c.Check(resp.Code, check.Equals, http.StatusNotFound)
43 c.Check(resp.Body.String(), check.Equals, "")
47 // An authorizer modifies an HTTP request to make use of the given
48 // token -- by adding it to a header, cookie, query param, or whatever
49 // -- and returns the HTTP status code we should expect from keep-web if
50 // the token is invalid.
51 type authorizer func(*http.Request, string) int
53 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
54 doVhostRequests(c, authzViaAuthzHeader)
56 func authzViaAuthzHeader(r *http.Request, tok string) int {
57 r.Header.Add("Authorization", "OAuth2 "+tok)
58 return http.StatusUnauthorized
61 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
62 doVhostRequests(c, authzViaCookieValue)
64 func authzViaCookieValue(r *http.Request, tok string) int {
65 r.AddCookie(&http.Cookie{
66 Name: "arvados_api_token",
67 Value: auth.EncodeTokenCookie([]byte(tok)),
69 return http.StatusUnauthorized
72 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
73 doVhostRequests(c, authzViaPath)
75 func authzViaPath(r *http.Request, tok string) int {
76 r.URL.Path = "/t=" + tok + r.URL.Path
77 return http.StatusNotFound
80 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
81 doVhostRequests(c, authzViaQueryString)
83 func authzViaQueryString(r *http.Request, tok string) int {
84 r.URL.RawQuery = "api_token=" + tok
85 return http.StatusUnauthorized
88 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
89 doVhostRequests(c, authzViaPOST)
91 func authzViaPOST(r *http.Request, tok string) int {
93 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
94 r.Body = ioutil.NopCloser(strings.NewReader(
95 url.Values{"api_token": {tok}}.Encode()))
96 return http.StatusUnauthorized
99 // Try some combinations of {url, token} using the given authorization
100 // mechanism, and verify the result is correct.
101 func doVhostRequests(c *check.C, authz authorizer) {
102 for _, hostPath := range []string{
103 arvadostest.FooCollection + ".example.com/foo",
104 arvadostest.FooCollection + "--collections.example.com/foo",
105 arvadostest.FooCollection + "--collections.example.com/_/foo",
106 arvadostest.FooPdh + ".example.com/foo",
107 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
108 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
110 c.Log("doRequests: ", hostPath)
111 doVhostRequestsWithHostPath(c, authz, hostPath)
115 func doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
116 for _, tok := range []string{
117 arvadostest.ActiveToken,
118 arvadostest.ActiveToken[:15],
119 arvadostest.SpectatorToken,
123 u := mustParseURL("http://" + hostPath)
124 req := &http.Request{
128 RequestURI: u.RequestURI(),
129 Header: http.Header{},
131 failCode := authz(req, tok)
133 code, body := resp.Code, resp.Body.String()
134 if tok == arvadostest.ActiveToken {
135 c.Check(code, check.Equals, http.StatusOK)
136 c.Check(body, check.Equals, "foo")
138 c.Check(code >= 400, check.Equals, true)
139 c.Check(code < 500, check.Equals, true)
140 if tok == arvadostest.SpectatorToken {
141 // Valid token never offers to retry
142 // with different credentials.
143 c.Check(code, check.Equals, http.StatusNotFound)
145 // Invalid token can ask to retry
146 // depending on the authz method.
147 c.Check(code, check.Equals, failCode)
149 c.Check(body, check.Equals, "")
154 func doReq(req *http.Request) *httptest.ResponseRecorder {
155 resp := httptest.NewRecorder()
156 (&handler{}).ServeHTTP(resp, req)
157 if resp.Code != http.StatusSeeOther {
160 cookies := (&http.Response{Header: resp.Header()}).Cookies()
161 u, _ := req.URL.Parse(resp.Header().Get("Location"))
166 RequestURI: u.RequestURI(),
167 Header: http.Header{},
169 for _, c := range cookies {
175 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
176 s.testVhostRedirectTokenToCookie(c, "GET",
177 arvadostest.FooCollection+".example.com/foo",
178 "?api_token="+arvadostest.ActiveToken,
186 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
187 s.testVhostRedirectTokenToCookie(c, "GET",
188 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
197 // Bad token in URL is 404 Not Found because it doesn't make sense to
198 // retry the same URL with different authorization.
199 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
200 s.testVhostRedirectTokenToCookie(c, "GET",
201 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
210 // Bad token in a cookie (even if it got there via our own
211 // query-string-to-cookie redirect) is, in principle, retryable at the
212 // same URL so it's 401 Unauthorized.
213 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
214 s.testVhostRedirectTokenToCookie(c, "GET",
215 arvadostest.FooCollection+".example.com/foo",
216 "?api_token=thisisabogustoken",
219 http.StatusUnauthorized,
224 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
225 s.testVhostRedirectTokenToCookie(c, "GET",
226 "example.com/c="+arvadostest.FooCollection+"/foo",
227 "?api_token="+arvadostest.ActiveToken,
230 http.StatusBadRequest,
235 // If client requests an attachment by putting ?disposition=attachment
236 // in the query string, and gets redirected, the redirect target
237 // should respond with an attachment.
238 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
239 resp := s.testVhostRedirectTokenToCookie(c, "GET",
240 arvadostest.FooCollection+".example.com/foo",
241 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
247 c.Check(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
250 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
251 defer func(orig bool) {
252 trustAllContent = orig
254 trustAllContent = true
255 s.testVhostRedirectTokenToCookie(c, "GET",
256 "example.com/c="+arvadostest.FooCollection+"/foo",
257 "?api_token="+arvadostest.ActiveToken,
265 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
266 defer func(orig string) {
267 attachmentOnlyHost = orig
268 }(attachmentOnlyHost)
269 attachmentOnlyHost = "example.com:1234"
271 s.testVhostRedirectTokenToCookie(c, "GET",
272 "example.com/c="+arvadostest.FooCollection+"/foo",
273 "?api_token="+arvadostest.ActiveToken,
276 http.StatusBadRequest,
280 resp := s.testVhostRedirectTokenToCookie(c, "GET",
281 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
282 "?api_token="+arvadostest.ActiveToken,
288 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
291 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
292 s.testVhostRedirectTokenToCookie(c, "POST",
293 arvadostest.FooCollection+".example.com/foo",
295 "application/x-www-form-urlencoded",
296 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
302 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
303 s.testVhostRedirectTokenToCookie(c, "POST",
304 arvadostest.FooCollection+".example.com/foo",
306 "application/x-www-form-urlencoded",
307 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
313 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
314 anonymousTokens = []string{arvadostest.AnonymousToken}
315 s.testVhostRedirectTokenToCookie(c, "GET",
316 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
325 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
326 anonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
327 s.testVhostRedirectTokenToCookie(c, "GET",
328 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
337 func (s *IntegrationSuite) TestRange(c *check.C) {
338 u, _ := url.Parse("http://example.com/c=" + arvadostest.HelloWorldCollection + "/Hello%20world.txt")
339 req := &http.Request{
343 RequestURI: u.RequestURI(),
344 Header: http.Header{"Range": {"bytes=0-4"}},
346 resp := httptest.NewRecorder()
347 (&handler{}).ServeHTTP(resp, req)
348 c.Check(resp.Code, check.Equals, http.StatusPartialContent)
349 c.Check(resp.Body.String(), check.Equals, "Hello")
350 c.Check(resp.Header().Get("Content-Length"), check.Equals, "5")
351 c.Check(resp.Header().Get("Content-Range"), check.Equals, "bytes 0-4/12")
353 req.Header.Set("Range", "bytes=0-")
354 resp = httptest.NewRecorder()
355 (&handler{}).ServeHTTP(resp, req)
356 // 200 and 206 are both correct:
357 c.Check(resp.Code, check.Equals, http.StatusOK)
358 c.Check(resp.Body.String(), check.Equals, "Hello world\n")
359 c.Check(resp.Header().Get("Content-Length"), check.Equals, "12")
361 // Unsupported ranges are ignored
362 for _, hdr := range []string{
363 "bytes=5-5", // non-zero start byte
364 "bytes=-5", // last 5 bytes
365 "cubits=0-5", // unsupported unit
366 "bytes=0-340282366920938463463374607431768211456", // 2^128
368 req.Header.Set("Range", hdr)
369 resp = httptest.NewRecorder()
370 (&handler{}).ServeHTTP(resp, req)
371 c.Check(resp.Code, check.Equals, http.StatusOK)
372 c.Check(resp.Body.String(), check.Equals, "Hello world\n")
373 c.Check(resp.Header().Get("Content-Length"), check.Equals, "12")
374 c.Check(resp.Header().Get("Content-Range"), check.Equals, "")
375 c.Check(resp.Header().Get("Accept-Ranges"), check.Equals, "bytes")
379 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
380 u, _ := url.Parse(`http://` + hostPath + queryString)
381 req := &http.Request{
385 RequestURI: u.RequestURI(),
386 Header: http.Header{"Content-Type": {contentType}},
387 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
390 resp := httptest.NewRecorder()
392 c.Check(resp.Code, check.Equals, expectStatus)
393 c.Check(resp.Body.String(), check.Equals, expectRespBody)
396 (&handler{}).ServeHTTP(resp, req)
397 if resp.Code != http.StatusSeeOther {
400 c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
401 cookies := (&http.Response{Header: resp.Header()}).Cookies()
403 u, _ = u.Parse(resp.Header().Get("Location"))
408 RequestURI: u.RequestURI(),
409 Header: http.Header{},
411 for _, c := range cookies {
415 resp = httptest.NewRecorder()
416 (&handler{}).ServeHTTP(resp, req)
417 c.Check(resp.Header().Get("Location"), check.Equals, "")