5824: Clarify docs.
[arvados.git] / services / keep-web / handler_test.go
1 package main
2
3 import (
4         "html"
5         "io/ioutil"
6         "net/http"
7         "net/http/httptest"
8         "net/url"
9         "regexp"
10         "strings"
11
12         "git.curoverse.com/arvados.git/sdk/go/arvadostest"
13         "git.curoverse.com/arvados.git/sdk/go/auth"
14         check "gopkg.in/check.v1"
15 )
16
17 var _ = check.Suite(&UnitSuite{})
18
19 type UnitSuite struct{}
20
21 func mustParseURL(s string) *url.URL {
22         r, err := url.Parse(s)
23         if err != nil {
24                 panic("parse URL: " + s)
25         }
26         return r
27 }
28
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",
33         } {
34                 resp := httptest.NewRecorder()
35                 req := &http.Request{
36                         Method: "GET",
37                         URL:    mustParseURL(testURL),
38                 }
39                 (&handler{}).ServeHTTP(resp, req)
40                 c.Check(resp.Code, check.Equals, http.StatusNotFound)
41                 c.Check(resp.Body.String(), check.Equals, "")
42         }
43 }
44
45 // An authorizer modifies an HTTP request to make use of the given
46 // token -- by adding it to a header, cookie, query param, or whatever
47 // -- and returns the HTTP status code we should expect from keep-web if
48 // the token is invalid.
49 type authorizer func(*http.Request, string) int
50
51 func (s *IntegrationSuite) TestVhostViaAuthzHeader(c *check.C) {
52         doVhostRequests(c, authzViaAuthzHeader)
53 }
54 func authzViaAuthzHeader(r *http.Request, tok string) int {
55         r.Header.Add("Authorization", "OAuth2 "+tok)
56         return http.StatusUnauthorized
57 }
58
59 func (s *IntegrationSuite) TestVhostViaCookieValue(c *check.C) {
60         doVhostRequests(c, authzViaCookieValue)
61 }
62 func authzViaCookieValue(r *http.Request, tok string) int {
63         r.AddCookie(&http.Cookie{
64                 Name:  "api_token",
65                 Value: auth.EncodeTokenCookie([]byte(tok)),
66         })
67         return http.StatusUnauthorized
68 }
69
70 func (s *IntegrationSuite) TestVhostViaPath(c *check.C) {
71         doVhostRequests(c, authzViaPath)
72 }
73 func authzViaPath(r *http.Request, tok string) int {
74         r.URL.Path = "/t=" + tok + r.URL.Path
75         return http.StatusNotFound
76 }
77
78 func (s *IntegrationSuite) TestVhostViaQueryString(c *check.C) {
79         doVhostRequests(c, authzViaQueryString)
80 }
81 func authzViaQueryString(r *http.Request, tok string) int {
82         r.URL.RawQuery = "api_token=" + tok
83         return http.StatusUnauthorized
84 }
85
86 func (s *IntegrationSuite) TestVhostViaPOST(c *check.C) {
87         doVhostRequests(c, authzViaPOST)
88 }
89 func authzViaPOST(r *http.Request, tok string) int {
90         r.Method = "POST"
91         r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
92         r.Body = ioutil.NopCloser(strings.NewReader(
93                 url.Values{"api_token": {tok}}.Encode()))
94         return http.StatusUnauthorized
95 }
96
97 // Try some combinations of {url, token} using the given authorization
98 // mechanism, and verify the result is correct.
99 func doVhostRequests(c *check.C, authz authorizer) {
100         for _, hostPath := range []string{
101                 arvadostest.FooCollection + ".example.com/foo",
102                 arvadostest.FooCollection + "--dl.example.com/foo",
103                 arvadostest.FooCollection + "--dl.example.com/_/foo",
104                 arvadostest.FooPdh + ".example.com/foo",
105                 strings.Replace(arvadostest.FooPdh, "+", "-", -1) + "--dl.example.com/foo",
106         } {
107                 c.Log("doRequests: ", hostPath)
108                 doVhostRequestsWithHostPath(c, authz, hostPath)
109         }
110 }
111
112 func doVhostRequestsWithHostPath(c *check.C, authz authorizer, hostPath string) {
113         for _, tok := range []string{
114                 arvadostest.ActiveToken,
115                 arvadostest.ActiveToken[:15],
116                 arvadostest.SpectatorToken,
117                 "bogus",
118                 "",
119         } {
120                 u := mustParseURL("http://" + hostPath)
121                 req := &http.Request{
122                         Method: "GET",
123                         Host:   u.Host,
124                         URL:    u,
125                         Header: http.Header{},
126                 }
127                 failCode := authz(req, tok)
128                 resp := doReq(req)
129                 code, body := resp.Code, resp.Body.String()
130                 if tok == arvadostest.ActiveToken {
131                         c.Check(code, check.Equals, http.StatusOK)
132                         c.Check(body, check.Equals, "foo")
133                 } else {
134                         c.Check(code >= 400, check.Equals, true)
135                         c.Check(code < 500, check.Equals, true)
136                         if tok == arvadostest.SpectatorToken {
137                                 // Valid token never offers to retry
138                                 // with different credentials.
139                                 c.Check(code, check.Equals, http.StatusNotFound)
140                         } else {
141                                 // Invalid token can ask to retry
142                                 // depending on the authz method.
143                                 c.Check(code, check.Equals, failCode)
144                         }
145                         c.Check(body, check.Equals, "")
146                 }
147         }
148 }
149
150 func doReq(req *http.Request) *httptest.ResponseRecorder {
151         resp := httptest.NewRecorder()
152         (&handler{}).ServeHTTP(resp, req)
153         if resp.Code != http.StatusSeeOther {
154                 return resp
155         }
156         cookies := (&http.Response{Header: resp.Header()}).Cookies()
157         u, _ := req.URL.Parse(resp.Header().Get("Location"))
158         req = &http.Request{
159                 Method: "GET",
160                 Host:   u.Host,
161                 URL:    u,
162                 Header: http.Header{},
163         }
164         for _, c := range cookies {
165                 req.AddCookie(c)
166         }
167         return doReq(req)
168 }
169
170 func (s *IntegrationSuite) TestVhostRedirectQueryTokenToCookie(c *check.C) {
171         s.testVhostRedirectTokenToCookie(c, "GET",
172                 arvadostest.FooCollection+".example.com/foo",
173                 "?api_token="+arvadostest.ActiveToken,
174                 "text/plain",
175                 "",
176                 http.StatusOK,
177         )
178 }
179
180 func (s *IntegrationSuite) TestVhostRedirectQueryTokenSingleOriginError(c *check.C) {
181         s.testVhostRedirectTokenToCookie(c, "GET",
182                 "example.com/c="+arvadostest.FooCollection+"/foo",
183                 "?api_token="+arvadostest.ActiveToken,
184                 "text/plain",
185                 "",
186                 http.StatusBadRequest,
187         )
188 }
189
190 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
191         defer func(orig bool) {
192                 trustAllContent = orig
193         }(trustAllContent)
194         trustAllContent = true
195         s.testVhostRedirectTokenToCookie(c, "GET",
196                 "example.com/c="+arvadostest.FooCollection+"/foo",
197                 "?api_token="+arvadostest.ActiveToken,
198                 "text/plain",
199                 "",
200                 http.StatusOK,
201         )
202 }
203
204 func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
205         defer func(orig string) {
206                 attachmentOnlyHost = orig
207         }(attachmentOnlyHost)
208         attachmentOnlyHost = "example.com:1234"
209
210         s.testVhostRedirectTokenToCookie(c, "GET",
211                 "example.com/c="+arvadostest.FooCollection+"/foo",
212                 "?api_token="+arvadostest.ActiveToken,
213                 "text/plain",
214                 "",
215                 http.StatusBadRequest,
216         )
217
218         resp := s.testVhostRedirectTokenToCookie(c, "GET",
219                 "example.com:1234/c="+arvadostest.FooCollection+"/foo",
220                 "?api_token="+arvadostest.ActiveToken,
221                 "text/plain",
222                 "",
223                 http.StatusOK,
224         )
225         c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
226 }
227
228 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
229         s.testVhostRedirectTokenToCookie(c, "POST",
230                 arvadostest.FooCollection+".example.com/foo",
231                 "",
232                 "application/x-www-form-urlencoded",
233                 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
234                 http.StatusOK,
235         )
236 }
237
238 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
239         s.testVhostRedirectTokenToCookie(c, "POST",
240                 arvadostest.FooCollection+".example.com/foo",
241                 "",
242                 "application/x-www-form-urlencoded",
243                 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
244                 http.StatusNotFound,
245         )
246 }
247
248 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, body string, expectStatus int) *httptest.ResponseRecorder {
249         u, _ := url.Parse(`http://` + hostPath + queryString)
250         req := &http.Request{
251                 Method: method,
252                 Host:   u.Host,
253                 URL:    u,
254                 Header: http.Header{"Content-Type": {contentType}},
255                 Body:   ioutil.NopCloser(strings.NewReader(body)),
256         }
257
258         resp := httptest.NewRecorder()
259         (&handler{}).ServeHTTP(resp, req)
260         if resp.Code != http.StatusSeeOther {
261                 c.Assert(resp.Code, check.Equals, expectStatus)
262                 return resp
263         }
264         c.Check(resp.Body.String(), check.Matches, `.*href="//`+regexp.QuoteMeta(html.EscapeString(hostPath))+`".*`)
265         cookies := (&http.Response{Header: resp.Header()}).Cookies()
266
267         u, _ = u.Parse(resp.Header().Get("Location"))
268         req = &http.Request{
269                 Method: "GET",
270                 Host:   u.Host,
271                 URL:    u,
272                 Header: http.Header{},
273         }
274         for _, c := range cookies {
275                 req.AddCookie(c)
276         }
277
278         resp = httptest.NewRecorder()
279         (&handler{}).ServeHTTP(resp, req)
280         c.Check(resp.Header().Get("Location"), check.Equals, "")
281         c.Check(resp.Code, check.Equals, expectStatus)
282         if expectStatus == http.StatusOK {
283                 c.Check(resp.Body.String(), check.Equals, "foo")
284         }
285         return resp
286 }