X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/0e2f3e506566b1ceb54bd764d3f32c004e45f8b3..b041a675c577e174680913e0da0bf69b1cca83b6:/services/keep-web/s3.go diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go index 5af7ebb5d5..d92828e066 100644 --- a/services/keep-web/s3.go +++ b/services/keep-web/s3.go @@ -2,18 +2,20 @@ // // SPDX-License-Identifier: AGPL-3.0 -package main +package keepweb import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/json" "encoding/xml" "errors" "fmt" "hash" "io" "net/http" + "net/textproto" "net/url" "os" "path/filepath" @@ -222,8 +224,8 @@ func (h *handler) checks3signature(r *http.Request) (string, error) { } client := (&arvados.Client{ - APIHost: h.Config.cluster.Services.Controller.ExternalURL.Host, - Insecure: h.Config.cluster.TLS.Insecure, + APIHost: h.Cluster.Services.Controller.ExternalURL.Host, + Insecure: h.Cluster.TLS.Insecure, }).WithRequestID(r.Header.Get("X-Request-Id")) var aca arvados.APIClientAuthorization var secret string @@ -231,7 +233,7 @@ func (h *handler) checks3signature(r *http.Request) (string, error) { if len(key) == 27 && key[5:12] == "-gj3su-" { // Access key is the UUID of an Arvados token, secret // key is the secret part. - ctx := arvados.ContextWithAuthorization(r.Context(), "Bearer "+h.Config.cluster.SystemRootToken) + ctx := arvados.ContextWithAuthorization(r.Context(), "Bearer "+h.Cluster.SystemRootToken) err = client.RequestAndDecodeContext(ctx, &aca, "GET", "arvados/v1/api_client_authorizations/"+key, nil, nil) secret = aca.APIToken } else { @@ -316,7 +318,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { // Use a single session (cached FileSystem) across // multiple read requests. var sess *cachedSession - fs, sess, err = h.Config.Cache.GetSession(token) + fs, sess, err = h.Cache.GetSession(token) if err != nil { s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusInternalServerError) return true @@ -336,13 +338,13 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { } defer release() fs = client.SiteFileSystem(kc) - fs.ForwardSlashNameSubstitution(h.Config.cluster.Collections.ForwardSlashNameSubstitution) + fs.ForwardSlashNameSubstitution(h.Cluster.Collections.ForwardSlashNameSubstitution) } var objectNameGiven bool var bucketName string fspath := "/by_id" - if id := parseCollectionIDFromDNSName(r.Host); id != "" { + if id := arvados.CollectionIDFromDNSName(r.Host); id != "" { fspath += "/" + id bucketName = id objectNameGiven = strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") > 0 @@ -365,7 +367,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { w.Header().Set("Content-Type", "application/xml") io.WriteString(w, xml.Header) fmt.Fprintln(w, ``+ - h.Config.cluster.ClusterID+ + h.Cluster.ClusterID+ ``) } else if reRawQueryIndicatesAPI.MatchString(r.URL.RawQuery) { // GetBucketWebsite ("GET /bucketid/?website"), GetBucketTagging, etc. @@ -385,6 +387,11 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { if r.Method == "HEAD" && !objectNameGiven { // HeadBucket if err == nil && fi.IsDir() { + err = setFileInfoHeaders(w.Header(), fs, fspath) + if err != nil { + s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway) + return true + } w.WriteHeader(http.StatusOK) } else if os.IsNotExist(err) { s3ErrorResponse(w, NoSuchBucket, "The specified bucket does not exist.", r.URL.Path, http.StatusNotFound) @@ -393,7 +400,12 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { } return true } - if err == nil && fi.IsDir() && objectNameGiven && strings.HasSuffix(fspath, "/") && h.Config.cluster.Collections.S3FolderObjects { + if err == nil && fi.IsDir() && objectNameGiven && strings.HasSuffix(fspath, "/") && h.Cluster.Collections.S3FolderObjects { + err = setFileInfoHeaders(w.Header(), fs, fspath) + if err != nil { + s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway) + return true + } w.Header().Set("Content-Type", "application/x-directory") w.WriteHeader(http.StatusOK) return true @@ -405,7 +417,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { return true } - tokenUser, err := h.Config.Cache.GetTokenUser(token) + tokenUser, err := h.Cache.GetTokenUser(token) if !h.userPermittedToUploadOrDownload(r.Method, tokenUser) { http.Error(w, "Not permitted", http.StatusForbidden) return true @@ -415,6 +427,11 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { // shallow copy r, and change URL path r := *r r.URL.Path = fspath + err = setFileInfoHeaders(w.Header(), fs, fspath) + if err != nil { + s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway) + return true + } http.FileServer(fs).ServeHTTP(w, &r) return true case r.Method == http.MethodPut: @@ -429,7 +446,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { } var objectIsDir bool if strings.HasSuffix(fspath, "/") { - if !h.Config.cluster.Collections.S3FolderObjects { + if !h.Cluster.Collections.S3FolderObjects { s3ErrorResponse(w, InvalidArgument, "invalid object name: trailing slash", r.URL.Path, http.StatusBadRequest) return true } @@ -496,7 +513,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { } defer f.Close() - tokenUser, err := h.Config.Cache.GetTokenUser(token) + tokenUser, err := h.Cache.GetTokenUser(token) if !h.userPermittedToUploadOrDownload(r.Method, tokenUser) { http.Error(w, "Not permitted", http.StatusForbidden) return true @@ -523,7 +540,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { return true } // Ensure a subsequent read operation will see the changes. - h.Config.Cache.ResetSession(token) + h.Cache.ResetSession(token) w.WriteHeader(http.StatusOK) return true case r.Method == http.MethodDelete: @@ -577,7 +594,7 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { return true } // Ensure a subsequent read operation will see the changes. - h.Config.Cache.ResetSession(token) + h.Cache.ResetSession(token) w.WriteHeader(http.StatusNoContent) return true default: @@ -586,6 +603,52 @@ func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool { } } +func setFileInfoHeaders(header http.Header, fs arvados.CustomFileSystem, path string) error { + path = strings.TrimSuffix(path, "/") + var props map[string]interface{} + for { + fi, err := fs.Stat(path) + if err != nil { + return err + } + switch src := fi.Sys().(type) { + case *arvados.Collection: + props = src.Properties + case *arvados.Group: + props = src.Properties + default: + if err, ok := src.(error); ok { + return err + } + // Try parent + cut := strings.LastIndexByte(path, '/') + if cut < 0 { + return nil + } + path = path[:cut] + continue + } + break + } + for k, v := range props { + if !validMIMEHeaderKey(k) { + continue + } + k = "x-amz-meta-" + k + if s, ok := v.(string); ok { + header.Set(k, s) + } else if j, err := json.Marshal(v); err == nil { + header.Set(k, string(j)) + } + } + return nil +} + +func validMIMEHeaderKey(k string) bool { + check := "z-" + k + return check != textproto.CanonicalMIMEHeaderKey(check) +} + // Call fn on the given path (directory) and its contents, in // lexicographic order. // @@ -756,7 +819,7 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request, if path < params.marker || path < params.prefix || path <= params.startAfter { return nil } - if fi.IsDir() && !h.Config.cluster.Collections.S3FolderObjects { + if fi.IsDir() && !h.Cluster.Collections.S3FolderObjects { // Note we don't add anything to // commonPrefixes here even if delimiter is // "/". We descend into the directory, and