5824: Fail at startup if ARVADOS_API_HOST is not set.
[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) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
181         s.testVhostRedirectTokenToCookie(c, "POST",
182                 arvadostest.FooCollection + ".example.com/foo",
183                 "",
184                 "application/x-www-form-urlencoded",
185                 url.Values{"api_token": {arvadostest.ActiveToken}}.Encode(),
186                 http.StatusOK,
187         )
188 }
189
190 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C) {
191         s.testVhostRedirectTokenToCookie(c, "POST",
192                 arvadostest.FooCollection + ".example.com/foo",
193                 "",
194                 "application/x-www-form-urlencoded",
195                 url.Values{"api_token": {arvadostest.SpectatorToken}}.Encode(),
196                 http.StatusNotFound,
197         )
198 }
199
200 func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, body string, expectStatus int) {
201         u, _ := url.Parse(`http://` + hostPath + queryString)
202         req := &http.Request{
203                 Method: method,
204                 Host: u.Host,
205                 URL: u,
206                 Header: http.Header{"Content-Type": {contentType}},
207                 Body: ioutil.NopCloser(strings.NewReader(body)),
208         }
209
210         resp := httptest.NewRecorder()
211         (&handler{}).ServeHTTP(resp, req)
212         c.Assert(resp.Code, check.Equals, http.StatusSeeOther)
213         c.Check(resp.Body.String(), check.Matches, `.*href="//` + regexp.QuoteMeta(html.EscapeString(hostPath)) + `".*`)
214         cookies := (&http.Response{Header: resp.Header()}).Cookies()
215
216         u, _ = u.Parse(resp.Header().Get("Location"))
217         req = &http.Request{
218                 Method: "GET",
219                 Host: u.Host,
220                 URL: u,
221                 Header: http.Header{},
222         }
223         for _, c := range cookies {
224                 req.AddCookie(c)
225         }
226
227         resp = httptest.NewRecorder()
228         (&handler{}).ServeHTTP(resp, req)
229         c.Check(resp.Header().Get("Location"), check.Equals, "")
230         c.Check(resp.Code, check.Equals, expectStatus)
231         if expectStatus == http.StatusOK {
232                 c.Check(resp.Body.String(), check.Equals, "foo")
233         }
234 }