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"
17 var clientPool = arvadosclient.MakeClientPool()
19 var anonymousTokens []string
24 // TODO(TC): Get anonymousTokens from flags
25 anonymousTokens = []string{}
28 func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
32 w := httpserver.WrapResponseWriter(wOrig)
35 if w.WroteStatus() == 0 {
36 w.WriteHeader(statusCode)
38 httpserver.Log(r.RemoteAddr, "WARNING",
39 fmt.Sprintf("Our status changed from %d to %d after we sent headers", w.WroteStatus(), statusCode))
43 statusText = http.StatusText(statusCode)
45 httpserver.Log(r.RemoteAddr, statusCode, statusText, w.WroteBodyBytes(), r.Method, r.URL.Path)
48 arv := clientPool.Get()
50 statusCode, statusText = http.StatusInternalServerError, "Pool failed: "+clientPool.Err().Error()
53 defer clientPool.Put(arv)
55 pathParts := strings.Split(r.URL.Path[1:], "/")
57 if len(pathParts) < 3 || pathParts[0] != "collections" || pathParts[1] == "" || pathParts[2] == "" {
58 statusCode = http.StatusNotFound
63 var targetPath []string
65 var reqTokens []string
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
72 targetId = pathParts[2]
73 tokens = []string{pathParts[3]}
74 targetPath = pathParts[4:]
77 // "/collections/{id}/path..." form
78 targetId = pathParts[1]
79 reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
80 tokens = append(reqTokens, anonymousTokens...)
81 targetPath = pathParts[2:]
84 tokenResult := make(map[string]int)
85 collection := make(map[string]interface{})
87 for _, arv.ApiToken = range tokens {
88 err := arv.Get("collections", targetId, nil, &collection)
95 if srvErr, ok := err.(arvadosclient.APIServerError); ok {
96 switch srvErr.HttpStatusCode {
98 // Token broken or insufficient to
99 // retrieve collection
100 tokenResult[arv.ApiToken] = srvErr.HttpStatusCode
104 // Something more serious is wrong
105 statusCode, statusText = http.StatusInternalServerError, err.Error()
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
114 statusCode = http.StatusNotFound
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
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]
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\"")
139 filename := strings.Join(targetPath, "/")
140 kc, err := keepclient.MakeKeepClient(arv)
142 statusCode, statusText = http.StatusInternalServerError, err.Error()
145 rdr, err := kc.CollectionFileReader(collection, filename)
146 if os.IsNotExist(err) {
147 statusCode = http.StatusNotFound
149 } else if err != nil {
150 statusCode, statusText = http.StatusBadGateway, err.Error()
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)
165 _, err = io.Copy(w, rdr)
167 statusCode, statusText = http.StatusBadGateway, err.Error()