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 mustParseURL(s string) *url.URL {
53 r, err := url.Parse(s)
55 panic("parse URL: " + s)
60 func (s *IntegrationSuite) TestVhost404(c *check.C) {
61 for _, testURL := range []string{
62 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
63 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
65 resp := httptest.NewRecorder()
66 u := mustParseURL(testURL)
70 RequestURI: u.RequestURI(),
72 s.testServer.Handler.ServeHTTP(resp, req)
73 c.Check(resp.Code, check.Equals, http.StatusNotFound)
74 c.Check(resp.Body.String(), check.Equals, "")
78 // An authorizer modifies an HTTP request to make use of the given
79 // token -- by adding it to a header, cookie, query param, or whatever
80 // -- and returns the HTTP status code we should expect from keep-web if
81 // the token is invalid.
82 type authorizer func(*http.Request, string) int
84 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
85 s.doVhostRequests(c, authzViaAuthzHeader)
87 func authzViaAuthzHeader(r *http.Request, tok string) int {
88 r.Header.Add("Authorization", "OAuth2 "+tok)
89 return http.StatusUnauthorized
92 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
93 s.doVhostRequests(c, authzViaCookieValue)
95 func authzViaCookieValue(r *http.Request, tok string) int {
96 r.AddCookie(&http.Cookie{
97 Name: "arvados_api_token",
98 Value: auth.EncodeTokenCookie([]byte(tok)),
100 return http.StatusUnauthorized
103 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
104 s.doVhostRequests(c, authzViaPath)
106 func authzViaPath(r *http.Request, tok string) int {
107 r.URL.Path = "/t=" + tok + r.URL.Path
108 return http.StatusNotFound
111 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
112 s.doVhostRequests(c, authzViaQueryString)
114 func authzViaQueryString(r *http.Request, tok string) int {
115 r.URL.RawQuery = "api_token=" + tok
116 return http.StatusUnauthorized
119 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
120 s.doVhostRequests(c, authzViaPOST)
122 func authzViaPOST(r *http.Request, tok string) int {
124 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
125 r.Body = ioutil.NopCloser(strings.NewReader(
126 url.Values{"api_token": {tok}}.Encode()))
127 return http.StatusUnauthorized
130 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
131 s.doVhostRequests(c, authzViaPOST)
133 func authzViaXHRPOST(r *http.Request, tok string) int {
135 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
136 r.Header.Add("Origin", "https://origin.example")
137 r.Body = ioutil.NopCloser(strings.NewReader(
140 "disposition": {"attachment"},
142 return http.StatusUnauthorized
145 // Try some combinations of {url, token} using the given authorization
146 // mechanism, and verify the result is correct.
147 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
148 for _, hostPath := range []string{
149 arvadostest.FooCollection + ".example.com/foo",
150 arvadostest.FooCollection + "--collections.example.com/foo",
151 arvadostest.FooCollection + "--collections.example.com/_/foo",
152 arvadostest.FooPdh + ".example.com/foo",
153 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
154 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
156 c.Log("doRequests: ", hostPath)
157 s.doVhostRequestsWithHostPath(c, authz, hostPath)
161 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
162 for _, tok := range []string{
163 arvadostest.ActiveToken,
164 arvadostest.ActiveToken[:15],
165 arvadostest.SpectatorToken,
169 u := mustParseURL("http://" + hostPath)
170 req := &http.Request{
174 RequestURI: u.RequestURI(),
175 Header: http.Header{},
177 failCode := authz(req, tok)
178 req, resp := s.doReq(req)
179 code, body := resp.Code, resp.Body.String()
181 // If the initial request had a (non-empty) token
182 // showing in the query string, we should have been
183 // redirected in order to hide it in a cookie.
184 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
186 if tok == arvadostest.ActiveToken {
187 c.Check(code, check.Equals, http.StatusOK)
188 c.Check(body, check.Equals, "foo")
191 c.Check(code >= 400, check.Equals, true)
192 c.Check(code < 500, check.Equals, true)
193 if tok == arvadostest.SpectatorToken {
194 // Valid token never offers to retry
195 // with different credentials.
196 c.Check(code, check.Equals, http.StatusNotFound)
198 // Invalid token can ask to retry
199 // depending on the authz method.
200 c.Check(code, check.Equals, failCode)
202 c.Check(body, check.Equals, "")
207 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
208 resp := httptest.NewRecorder()
209 s.testServer.Handler.ServeHTTP(resp, req)
210 if resp.Code != http.StatusSeeOther {
213 cookies := (&http.Response{Header: resp.Header()}).Cookies()
214 u, _ := req.URL.Parse(resp.Header().Get("Location"))
219 RequestURI: u.RequestURI(),
220 Header: http.Header{},
222 for _, c := range cookies {
228 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
229 s.testVhostRedirectTokenToCookie(c, "GET",
230 arvadostest.FooCollection+".example.com/foo",
231 "?api_token="+arvadostest.ActiveToken,
239 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
240 s.testVhostRedirectTokenToCookie(c, "GET",
241 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
250 // Bad token in URL is 404 Not Found because it doesn't make sense to
251 // retry the same URL with different authorization.
252 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
253 s.testVhostRedirectTokenToCookie(c, "GET",
254 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
263 // Bad token in a cookie (even if it got there via our own
264 // query-string-to-cookie redirect) is, in principle, retryable at the
265 // same URL so it's 401 Unauthorized.
266 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
267 s.testVhostRedirectTokenToCookie(c, "GET",
268 arvadostest.FooCollection+".example.com/foo",
269 "?api_token=thisisabogustoken",
272 http.StatusUnauthorized,
277 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
278 s.testVhostRedirectTokenToCookie(c, "GET",
279 "example.com/c="+arvadostest.FooCollection+"/foo",
280 "?api_token="+arvadostest.ActiveToken,
283 http.StatusBadRequest,
288 // If client requests an attachment by putting ?disposition=attachment
289 // in the query string, and gets redirected, the redirect target
290 // should respond with an attachment.
291 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
292 resp := s.testVhostRedirectTokenToCookie(c, "GET",
293 arvadostest.FooCollection+".example.com/foo",
294 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
300 c.Check(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
303 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
304 s.testServer.Config.TrustAllContent = true
305 s.testVhostRedirectTokenToCookie(c, "GET",
306 "example.com/c="+arvadostest.FooCollection+"/foo",
307 "?api_token="+arvadostest.ActiveToken,
315 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
316 s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
318 s.testVhostRedirectTokenToCookie(c, "GET",
319 "example.com/c="+arvadostest.FooCollection+"/foo",
320 "?api_token="+arvadostest.ActiveToken,
323 http.StatusBadRequest,
327 resp := s.testVhostRedirectTokenToCookie(c, "GET",
328 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
329 "?api_token="+arvadostest.ActiveToken,
335 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
338 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
339 s.testVhostRedirectTokenToCookie(c, "POST",
340 arvadostest.FooCollection+".example.com/foo",
342 "application/x-www-form-urlencoded",
343 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
349 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
350 s.testVhostRedirectTokenToCookie(c, "POST",
351 arvadostest.FooCollection+".example.com/foo",
353 "application/x-www-form-urlencoded",
354 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
360 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
361 s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
362 s.testVhostRedirectTokenToCookie(c, "GET",
363 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
372 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
373 s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
374 s.testVhostRedirectTokenToCookie(c, "GET",
375 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
384 // XHRs can't follow redirect-with-cookie so they rely on method=POST
385 // and disposition=attachment (telling us it's acceptable to respond
386 // with content instead of a redirect) and an Origin header that gets
387 // added automatically by the browser (telling us it's desirable to do
389 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
390 u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
391 req := &http.Request{
395 RequestURI: u.RequestURI(),
397 "Origin": {"https://origin.example"},
398 "Content-Type": {"application/x-www-form-urlencoded"},
400 Body: ioutil.NopCloser(strings.NewReader(url.Values{
401 "api_token": {arvadostest.ActiveToken},
402 "disposition": {"attachment"},
405 resp := httptest.NewRecorder()
406 s.testServer.Handler.ServeHTTP(resp, req)
407 c.Check(resp.Code, check.Equals, http.StatusOK)
408 c.Check(resp.Body.String(), check.Equals, "foo")
409 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
412 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
413 u, _ := url.Parse(`http://` + hostPath + queryString)
414 req := &http.Request{
418 RequestURI: u.RequestURI(),
419 Header: http.Header{"Content-Type": {contentType}},
420 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
423 resp := httptest.NewRecorder()
425 c.Check(resp.Code, check.Equals, expectStatus)
426 c.Check(resp.Body.String(), check.Equals, expectRespBody)
429 s.testServer.Handler.ServeHTTP(resp, req)
430 if resp.Code != http.StatusSeeOther {
433 c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
434 cookies := (&http.Response{Header: resp.Header()}).Cookies()
436 u, _ = u.Parse(resp.Header().Get("Location"))
441 RequestURI: u.RequestURI(),
442 Header: http.Header{},
444 for _, c := range cookies {
448 resp = httptest.NewRecorder()
449 s.testServer.Handler.ServeHTTP(resp, req)
450 c.Check(resp.Header().Get("Location"), check.Equals, "")