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 resp := httptest.NewRecorder()
36 h.ServeHTTP(resp, req)
37 c.Check(resp.Code, check.Equals, http.StatusOK)
38 c.Check(resp.Body.String(), check.Equals, "")
39 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
40 c.Check(resp.Header().Get("Access-Control-Allow-Methods"), check.Equals, "GET, POST")
41 c.Check(resp.Header().Get("Access-Control-Allow-Headers"), check.Equals, "Range")
43 resp = httptest.NewRecorder()
44 req.Header.Set("Access-Control-Request-Method", "DELETE")
45 h.ServeHTTP(resp, req)
46 c.Check(resp.Body.String(), check.Equals, "")
47 c.Check(resp.Code, check.Equals, http.StatusMethodNotAllowed)
50 func mustParseURL(s string) *url.URL {
51 r, err := url.Parse(s)
53 panic("parse URL: " + s)
58 func (s *IntegrationSuite) TestVhost404(c *check.C) {
59 for _, testURL := range []string{
60 arvadostest.NonexistentCollection + ".example.com/theperthcountyconspiracy",
61 arvadostest.NonexistentCollection + ".example.com/t=" + arvadostest.ActiveToken + "/theperthcountyconspiracy",
63 resp := httptest.NewRecorder()
64 u := mustParseURL(testURL)
68 RequestURI: u.RequestURI(),
70 s.testServer.Handler.ServeHTTP(resp, req)
71 c.Check(resp.Code, check.Equals, http.StatusNotFound)
72 c.Check(resp.Body.String(), check.Equals, "")
76 // An authorizer modifies an HTTP request to make use of the given
77 // token -- by adding it to a header, cookie, query param, or whatever
78 // -- and returns the HTTP status code we should expect from keep-web if
79 // the token is invalid.
80 type authorizer func(*http.Request, string) int
82 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
83 s.doVhostRequests(c, authzViaAuthzHeader)
85 func authzViaAuthzHeader(r *http.Request, tok string) int {
86 r.Header.Add("Authorization", "OAuth2 "+tok)
87 return http.StatusUnauthorized
90 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
91 s.doVhostRequests(c, authzViaCookieValue)
93 func authzViaCookieValue(r *http.Request, tok string) int {
94 r.AddCookie(&http.Cookie{
95 Name: "arvados_api_token",
96 Value: auth.EncodeTokenCookie([]byte(tok)),
98 return http.StatusUnauthorized
101 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
102 s.doVhostRequests(c, authzViaPath)
104 func authzViaPath(r *http.Request, tok string) int {
105 r.URL.Path = "/t=" + tok + r.URL.Path
106 return http.StatusNotFound
109 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
110 s.doVhostRequests(c, authzViaQueryString)
112 func authzViaQueryString(r *http.Request, tok string) int {
113 r.URL.RawQuery = "api_token=" + tok
114 return http.StatusUnauthorized
117 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
118 s.doVhostRequests(c, authzViaPOST)
120 func authzViaPOST(r *http.Request, tok string) int {
122 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
123 r.Body = ioutil.NopCloser(strings.NewReader(
124 url.Values{"api_token": {tok}}.Encode()))
125 return http.StatusUnauthorized
128 func (s *IntegrationSuite) TestVhostViaXHRPOST(c *check.C) {
129 s.doVhostRequests(c, authzViaPOST)
131 func authzViaXHRPOST(r *http.Request, tok string) int {
133 r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
134 r.Header.Add("Origin", "https://origin.example")
135 r.Body = ioutil.NopCloser(strings.NewReader(
138 "disposition": {"attachment"},
140 return http.StatusUnauthorized
143 // Try some combinations of {url, token} using the given authorization
144 // mechanism, and verify the result is correct.
145 func (s *IntegrationSuite) doVhostRequests(c *check.C, authz authorizer) {
146 for _, hostPath := range []string{
147 arvadostest.FooCollection + ".example.com/foo",
148 arvadostest.FooCollection + "--collections.example.com/foo",
149 arvadostest.FooCollection + "--collections.example.com/_/foo",
150 arvadostest.FooPdh + ".example.com/foo",
151 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--collections.example.com/foo",
152 arvadostest.FooBarDirCollection + ".example.com/dir1/foo",
154 c.Log("doRequests: ", hostPath)
155 s.doVhostRequestsWithHostPath(c, authz, hostPath)
159 func (s *IntegrationSuite) doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
160 for _, tok := range []string{
161 arvadostest.ActiveToken,
162 arvadostest.ActiveToken[:15],
163 arvadostest.SpectatorToken,
167 u := mustParseURL("http://" + hostPath)
168 req := &http.Request{
172 RequestURI: u.RequestURI(),
173 Header: http.Header{},
175 failCode := authz(req, tok)
176 req, resp := s.doReq(req)
177 code, body := resp.Code, resp.Body.String()
179 // If the initial request had a (non-empty) token
180 // showing in the query string, we should have been
181 // redirected in order to hide it in a cookie.
182 c.Check(req.URL.String(), check.Not(check.Matches), `.*api_token=.+`)
184 if tok == arvadostest.ActiveToken {
185 c.Check(code, check.Equals, http.StatusOK)
186 c.Check(body, check.Equals, "foo")
189 c.Check(code >= 400, check.Equals, true)
190 c.Check(code < 500, check.Equals, true)
191 if tok == arvadostest.SpectatorToken {
192 // Valid token never offers to retry
193 // with different credentials.
194 c.Check(code, check.Equals, http.StatusNotFound)
196 // Invalid token can ask to retry
197 // depending on the authz method.
198 c.Check(code, check.Equals, failCode)
200 c.Check(body, check.Equals, "")
205 func (s *IntegrationSuite) doReq(req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
206 resp := httptest.NewRecorder()
207 s.testServer.Handler.ServeHTTP(resp, req)
208 if resp.Code != http.StatusSeeOther {
211 cookies := (&http.Response{Header: resp.Header()}).Cookies()
212 u, _ := req.URL.Parse(resp.Header().Get("Location"))
217 RequestURI: u.RequestURI(),
218 Header: http.Header{},
220 for _, c := range cookies {
226 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
227 s.testVhostRedirectTokenToCookie(c, "GET",
228 arvadostest.FooCollection+".example.com/foo",
229 "?api_token="+arvadostest.ActiveToken,
237 func (s *IntegrationSuite) TestSingleOriginSecretLink(c *check.C) {
238 s.testVhostRedirectTokenToCookie(c, "GET",
239 "example.com/c="+arvadostest.FooCollection+"/t="+arvadostest.ActiveToken+"/foo",
248 // Bad token in URL is 404 Not Found because it doesn't make sense to
249 // retry the same URL with different authorization.
250 func (s *IntegrationSuite) TestSingleOriginSecretLinkBadToken(c *check.C) {
251 s.testVhostRedirectTokenToCookie(c, "GET",
252 "example.com/c="+arvadostest.FooCollection+"/t=bogus/foo",
261 // Bad token in a cookie (even if it got there via our own
262 // query-string-to-cookie redirect) is, in principle, retryable at the
263 // same URL so it's 401 Unauthorized.
264 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToBogusCookie(c *check.C) {
265 s.testVhostRedirectTokenToCookie(c, "GET",
266 arvadostest.FooCollection+".example.com/foo",
267 "?api_token=thisisabogustoken",
270 http.StatusUnauthorized,
275 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
276 s.testVhostRedirectTokenToCookie(c, "GET",
277 "example.com/c="+arvadostest.FooCollection+"/foo",
278 "?api_token="+arvadostest.ActiveToken,
281 http.StatusBadRequest,
286 // If client requests an attachment by putting ?disposition=attachment
287 // in the query string, and gets redirected, the redirect target
288 // should respond with an attachment.
289 func (s *IntegrationSuite) TestVhostRedirectQueryTokenRequestAttachment(c *check.C) {
290 resp := s.testVhostRedirectTokenToCookie(c, "GET",
291 arvadostest.FooCollection+".example.com/foo",
292 "?disposition=attachment&api_token="+arvadostest.ActiveToken,
298 c.Check(strings.Split(resp.Header().Get("Content-Disposition"), ";")[0], check.Equals, "attachment")
301 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
302 s.testServer.Config.TrustAllContent = true
303 s.testVhostRedirectTokenToCookie(c, "GET",
304 "example.com/c="+arvadostest.FooCollection+"/foo",
305 "?api_token="+arvadostest.ActiveToken,
313 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
314 s.testServer.Config.AttachmentOnlyHost = "example.com:1234"
316 s.testVhostRedirectTokenToCookie(c, "GET",
317 "example.com/c="+arvadostest.FooCollection+"/foo",
318 "?api_token="+arvadostest.ActiveToken,
321 http.StatusBadRequest,
325 resp := s.testVhostRedirectTokenToCookie(c, "GET",
326 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
327 "?api_token="+arvadostest.ActiveToken,
333 c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
336 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
337 s.testVhostRedirectTokenToCookie(c, "POST",
338 arvadostest.FooCollection+".example.com/foo",
340 "application/x-www-form-urlencoded",
341 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
347 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
348 s.testVhostRedirectTokenToCookie(c, "POST",
349 arvadostest.FooCollection+".example.com/foo",
351 "application/x-www-form-urlencoded",
352 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
358 func (s *IntegrationSuite) TestAnonymousTokenOK(c *check.C) {
359 s.testServer.Config.AnonymousTokens = []string{arvadostest.AnonymousToken}
360 s.testVhostRedirectTokenToCookie(c, "GET",
361 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
370 func (s *IntegrationSuite) TestAnonymousTokenError(c *check.C) {
371 s.testServer.Config.AnonymousTokens = []string{"anonymousTokenConfiguredButInvalid"}
372 s.testVhostRedirectTokenToCookie(c, "GET",
373 "example.com/c="+arvadostest.HelloWorldCollection+"/Hello%20world.txt",
382 // XHRs can't follow redirect-with-cookie so they rely on method=POST
383 // and disposition=attachment (telling us it's acceptable to respond
384 // with content instead of a redirect) and an Origin header that gets
385 // added automatically by the browser (telling us it's desirable to do
387 func (s *IntegrationSuite) TestXHRNoRedirect(c *check.C) {
388 u, _ := url.Parse("http://example.com/c=" + arvadostest.FooCollection + "/foo")
389 req := &http.Request{
393 RequestURI: u.RequestURI(),
395 "Origin": {"https://origin.example"},
396 "Content-Type": {"application/x-www-form-urlencoded"},
398 Body: ioutil.NopCloser(strings.NewReader(url.Values{
399 "api_token": {arvadostest.ActiveToken},
400 "disposition": {"attachment"},
403 resp := httptest.NewRecorder()
404 s.testServer.Handler.ServeHTTP(resp, req)
405 c.Check(resp.Code, check.Equals, http.StatusOK)
406 c.Check(resp.Body.String(), check.Equals, "foo")
407 c.Check(resp.Header().Get("Access-Control-Allow-Origin"), check.Equals, "*")
410 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, reqBody string, expectStatus int, expectRespBody string) *httptest.ResponseRecorder {
411 u, _ := url.Parse(`http://` + hostPath + queryString)
412 req := &http.Request{
416 RequestURI: u.RequestURI(),
417 Header: http.Header{"Content-Type": {contentType}},
418 Body: ioutil.NopCloser(strings.NewReader(reqBody)),
421 resp := httptest.NewRecorder()
423 c.Check(resp.Code, check.Equals, expectStatus)
424 c.Check(resp.Body.String(), check.Equals, expectRespBody)
427 s.testServer.Handler.ServeHTTP(resp, req)
428 if resp.Code != http.StatusSeeOther {
431 c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`(\?[^"]*)?".*`)
432 cookies := (&http.Response{Header: resp.Header()}).Cookies()
434 u, _ = u.Parse(resp.Header().Get("Location"))
439 RequestURI: u.RequestURI(),
440 Header: http.Header{},
442 for _, c := range cookies {
446 resp = httptest.NewRecorder()
447 s.testServer.Handler.ServeHTTP(resp, req)
448 c.Check(resp.Header().Get("Location"), check.Equals, "")