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