5824: Add -attachment-only-host feature.
authorTom Clegg <tom@curoverse.com>
Mon, 7 Sep 2015 07:43:59 +0000 (03:43 -0400)
committerTom Clegg <tom@curoverse.com>
Sat, 17 Oct 2015 00:02:16 +0000 (20:02 -0400)
services/keep-web/doc.go
services/keep-web/handler.go
services/keep-web/handler_test.go

index 236820eb730994d5cefa6eba0285c0169117f4ac..cc47ebee63bcd613a44b7d991ffefa4a40d55531 100644 (file)
 // (``https://dl.example.com/'') and upload it to some other site
 // chosen by the author of collection X.
 //
+// Attachment-Only host
+//
+// It is possible to serve untrusted content and accept user
+// credentials at the same origin as long as the content is only
+// downloaded, never executed by browsers. A single origin (hostname
+// and port) can be designated as an "attachment-only" origin: cookies
+// will be accepted and all responses will have a
+// "Content-Disposition: attachment" header. This behavior is invoked
+// only when the designated origin matches exactly the Host header
+// provided by the client or upstream proxy.
+//
+//   keep-web -attachment-only-host domain.example:9999
+//
 // Trust All Content mode
 //
 // In "trust all content" mode, Keep-web will accept credentials (API
index c5d439a399e2b5416e65235f9e56a56f36db1086..b39a941480d25e732ffed93c7a2a0683e06fc435 100644 (file)
@@ -24,11 +24,14 @@ var (
        clientPool      = arvadosclient.MakeClientPool()
        trustAllContent = false
        anonymousTokens []string
+       attachmentOnlyHost = ""
 )
 
 func init() {
        flag.BoolVar(&trustAllContent, "trust-all-content", false,
                "Serve non-public content from a single origin. Dangerous: read docs before using!")
+       flag.StringVar(&attachmentOnlyHost, "attachment-only-host", "",
+               "Accept credentials, and add \"Content-Disposition: attachment\" response headers, for requests at this hostname:port. Prohibiting inline display makes it possible to serve untrusted and non-public content from a single origin, i.e., without wildcard DNS or SSL.")
 }
 
 // return a UUID or PDH if s begins with a UUID or URL-encoded PDH;
@@ -111,8 +114,16 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
        var tokens []string
        var reqTokens []string
        var pathToken bool
+       var attachment bool
        credentialsOK := trustAllContent
 
+       if r.Host != "" && r.Host == attachmentOnlyHost {
+               credentialsOK = true
+               attachment = true
+       } else if r.FormValue("disposition") == "attachment" {
+               attachment = true
+       }
+
        if targetId = parseCollectionIdFromDNSName(r.Host); targetId != "" {
                // http://ID.dl.example/PATH...
                credentialsOK = true
@@ -293,6 +304,9 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                }
        }
        w.Header().Set("Content-Length", fmt.Sprintf("%d", rdr.Len()))
+       if attachment {
+               w.Header().Set("Content-Disposition", "attachment")
+       }
 
        w.WriteHeader(http.StatusOK)
        _, err = io.Copy(w, rdr)
index e2f8edd6b71faad57a4d68cb1842bd481ef9c146..a64aeb5f79e645c03b0eca772ebc4e6d512e484b 100644 (file)
@@ -201,6 +201,30 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C
        )
 }
 
+func (s *IntegrationSuite) TestVhostRedirectQueryTokenAttachmentOnlyHost(c *check.C) {
+       defer func(orig string) {
+               attachmentOnlyHost = orig
+       }(attachmentOnlyHost)
+       attachmentOnlyHost = "example.com:1234"
+
+       s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com/c=" + arvadostest.FooCollection + "/foo",
+               "?api_token=" + arvadostest.ActiveToken,
+               "text/plain",
+               "",
+               http.StatusBadRequest,
+       )
+
+       resp := s.testVhostRedirectTokenToCookie(c, "GET",
+               "example.com:1234/c=" + arvadostest.FooCollection + "/foo",
+               "?api_token=" + arvadostest.ActiveToken,
+               "text/plain",
+               "",
+               http.StatusOK,
+       )
+       c.Check(resp.Header().Get("Content-Disposition"), check.Equals, "attachment")
+}
+
 func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie(c *check.C) {
        s.testVhostRedirectTokenToCookie(c, "POST",
                arvadostest.FooCollection + ".example.com/foo",
@@ -221,7 +245,7 @@ func (s *IntegrationSuite) TestVhostRedirectPOSTFormTokenToCookie404(c *check.C)
        )
 }
 
-func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, body string, expectStatus int) {
+func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, hostPath, queryString, contentType, body string, expectStatus int) *httptest.ResponseRecorder {
        u, _ := url.Parse(`http://` + hostPath + queryString)
        req := &http.Request{
                Method: method,
@@ -235,7 +259,7 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
        (&handler{}).ServeHTTP(resp, req)
        if resp.Code != http.StatusSeeOther {
                c.Assert(resp.Code, check.Equals, expectStatus)
-               return
+               return resp
        }
        c.Check(resp.Body.String(), check.Matches, `.*href="//` + regexp.QuoteMeta(html.EscapeString(hostPath)) + `".*`)
        cookies := (&http.Response{Header: resp.Header()}).Cookies()
@@ -258,4 +282,5 @@ func (s *IntegrationSuite) testVhostRedirectTokenToCookie(c *check.C, method, ho
        if expectStatus == http.StatusOK {
                c.Check(resp.Body.String(), check.Equals, "foo")
        }
+       return resp
 }