5824: Assign MIME type by file extension. closes #6327
[arvados.git] / services / keep-web / handler.go
1 package main
2
3 import (
4         "fmt"
5         "io"
6         "mime"
7         "net/http"
8         "os"
9         "strings"
10
11         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
12         "git.curoverse.com/arvados.git/sdk/go/auth"
13         "git.curoverse.com/arvados.git/sdk/go/httpserver"
14 )
15
16 var clientPool = arvadosclient.MakeClientPool()
17
18 var anonymousTokens []string
19
20 type handler struct{}
21
22 func init() {
23         // TODO(TC): Get anonymousTokens from flags
24         anonymousTokens = []string{}
25 }
26
27 func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
28         var statusCode int
29         var statusText string
30
31         w := httpserver.WrapResponseWriter(wOrig)
32         defer func() {
33                 if statusCode > 0 {
34                         if w.WroteStatus() == 0 {
35                                 w.WriteHeader(statusCode)
36                         } else {
37                                 httpserver.Log(r.RemoteAddr, "WARNING",
38                                         fmt.Sprintf("Our status changed from %d to %d after we sent headers", w.WroteStatus(), statusCode))
39                         }
40                 }
41                 if statusText == "" {
42                         statusText = http.StatusText(statusCode)
43                 }
44                 httpserver.Log(r.RemoteAddr, statusCode, statusText, w.WroteBodyBytes(), r.Method, r.URL.Path)
45         }()
46
47         arv := clientPool.Get()
48         if arv == nil {
49                 statusCode, statusText = http.StatusInternalServerError, "Pool failed: "+clientPool.Err().Error()
50                 return
51         }
52         defer clientPool.Put(arv)
53
54         pathParts := strings.Split(r.URL.Path[1:], "/")
55
56         if len(pathParts) < 3 || pathParts[0] != "collections" || pathParts[1] == "" || pathParts[2] == "" {
57                 statusCode = http.StatusNotFound
58                 return
59         }
60
61         var targetId string
62         var targetPath []string
63         var tokens []string
64         var reqTokens []string
65         var pathToken bool
66         if len(pathParts) >= 5 && pathParts[1] == "download" {
67                 // "/collections/download/{id}/{token}/path..." form:
68                 // Don't use our configured anonymous tokens,
69                 // Authorization headers, etc.  Just use the token in
70                 // the path.
71                 targetId = pathParts[2]
72                 tokens = []string{pathParts[3]}
73                 targetPath = pathParts[4:]
74                 pathToken = true
75         } else {
76                 // "/collections/{id}/path..." form
77                 targetId = pathParts[1]
78                 reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
79                 tokens = append(reqTokens, anonymousTokens...)
80                 targetPath = pathParts[2:]
81         }
82
83         tokenResult := make(map[string]int)
84         collection := make(map[string]interface{})
85         found := false
86         for _, arv.ApiToken = range tokens {
87                 err := arv.Get("collections", targetId, nil, &collection)
88                 httpserver.Log(err)
89                 if err == nil {
90                         // Success
91                         found = true
92                         break
93                 }
94                 if srvErr, ok := err.(arvadosclient.APIServerError); ok {
95                         switch srvErr.HttpStatusCode {
96                         case 404, 401:
97                                 // Token broken or insufficient to
98                                 // retrieve collection
99                                 tokenResult[arv.ApiToken] = srvErr.HttpStatusCode
100                                 continue
101                         }
102                 }
103                 // Something more serious is wrong
104                 statusCode, statusText = http.StatusInternalServerError, err.Error()
105                 return
106         }
107         if !found {
108                 if pathToken {
109                         // The URL is a "secret sharing link", but it
110                         // didn't work out. Asking the client for
111                         // additional credentials would just be
112                         // confusing.
113                         statusCode = http.StatusNotFound
114                         return
115                 }
116                 for _, t := range reqTokens {
117                         if tokenResult[t] == 404 {
118                                 // The client provided valid token(s), but the
119                                 // collection was not found.
120                                 statusCode = http.StatusNotFound
121                                 return
122                         }
123                 }
124                 // The client's token was invalid (e.g., expired), or
125                 // the client didn't even provide one.  Propagate the
126                 // 401 to encourage the client to use a [different]
127                 // token.
128                 //
129                 // TODO(TC): This response would be confusing to
130                 // someone trying (anonymously) to download public
131                 // data that has been deleted.  Allow a referrer to
132                 // provide this context somehow?
133                 statusCode = http.StatusUnauthorized
134                 w.Header().Add("WWW-Authenticate", "Basic realm=\"dl\"")
135                 return
136         }
137
138         filename := strings.Join(targetPath, "/")
139         rdr, err := arvadosclient.CollectionFileReader(collection, filename)
140         if os.IsNotExist(err) {
141                 statusCode = http.StatusNotFound
142                 return
143         } else if err == arvadosclient.ErrNotImplemented {
144                 statusCode = http.StatusNotImplemented
145                 return
146         } else if err != nil {
147                 statusCode, statusText = http.StatusBadGateway, err.Error()
148                 return
149         }
150
151         // One or both of these can be -1 if not found:
152         basenamePos := strings.LastIndex(filename, "/")
153         extPos := strings.LastIndex(filename, ".")
154         if extPos > basenamePos {
155                 // Now extPos is safely >= 0.
156                 if t := mime.TypeByExtension(filename[extPos:]); t != "" {
157                         w.Header().Set("Content-Type", t)
158                 }
159         }
160
161         _, err = io.Copy(w, rdr)
162         if err != nil {
163                 statusCode, statusText = http.StatusBadGateway, err.Error()
164         }
165 }