X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/cc3794e611e3941eab0b6f359a0d866ae6a40fd6..5cc1710b57f98905469225c68d975ad2e3e7e56d:/services/keep-web/handler.go diff --git a/services/keep-web/handler.go b/services/keep-web/handler.go index c16f01a1c4..27981c487d 100644 --- a/services/keep-web/handler.go +++ b/services/keep-web/handler.go @@ -20,6 +20,7 @@ import ( "sync" "git.arvados.org/arvados.git/lib/cmd" + "git.arvados.org/arvados.git/lib/webdavfs" "git.arvados.org/arvados.git/sdk/go/arvados" "git.arvados.org/arvados.git/sdk/go/arvadosclient" "git.arvados.org/arvados.git/sdk/go/auth" @@ -34,13 +35,12 @@ type handler struct { Cache cache Cluster *arvados.Cluster setupOnce sync.Once - webdavLS webdav.LockSystem } var urlPDHDecoder = strings.NewReplacer(" ", "+", "-", "+") var notFoundMessage = "Not Found" -var unauthorizedMessage = "401 Unauthorized\r\n\r\nA valid Arvados token must be provided to access this resource.\r\n" +var unauthorizedMessage = "401 Unauthorized\n\nA valid Arvados token must be provided to access this resource." // parseCollectionIDFromURL returns a UUID or PDH if s is a UUID or a // PDH (even if it is a PDH with "+" replaced by " " or "-"); @@ -57,10 +57,6 @@ func parseCollectionIDFromURL(s string) string { func (h *handler) setup() { keepclient.DefaultBlockCache.MaxBlocks = h.Cluster.Collections.WebDAVCache.MaxBlockEntries - - // Even though we don't accept LOCK requests, every webdav - // handler must have a non-nil LockSystem. - h.webdavLS = &noLockSystem{} } func (h *handler) serveStatus(w http.ResponseWriter, r *http.Request) { @@ -117,7 +113,7 @@ var ( corsAllowHeadersHeader = strings.Join([]string{ "Authorization", "Content-Type", "Range", // WebDAV request headers: - "Depth", "Destination", "If", "Lock-Token", "Overwrite", "Timeout", + "Depth", "Destination", "If", "Lock-Token", "Overwrite", "Timeout", "Cache-Control", }, ", ") writeMethod = map[string]bool{ "COPY": true, @@ -217,7 +213,26 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { return } - pathParts := strings.Split(r.URL.Path[1:], "/") + webdavPrefix := "" + arvPath := r.URL.Path + if prefix := r.Header.Get("X-Webdav-Prefix"); prefix != "" { + // Enable a proxy (e.g., container log handler in + // controller) to satisfy a request for path + // "/foo/bar/baz.txt" using content from + // "//abc123-4.internal/bar/baz.txt", by adding a + // request header "X-Webdav-Prefix: /foo" + if !strings.HasPrefix(arvPath, prefix) { + http.Error(w, "X-Webdav-Prefix header is not a prefix of the requested path", http.StatusBadRequest) + return + } + arvPath = r.URL.Path[len(prefix):] + if arvPath == "" { + arvPath = "/" + } + w.Header().Set("Vary", "X-Webdav-Prefix, "+w.Header().Get("Vary")) + webdavPrefix = prefix + } + pathParts := strings.Split(arvPath[1:], "/") var stripParts int var collectionID string @@ -329,7 +344,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { fsprefix := "" if useSiteFS { if writeMethod[r.Method] { - http.Error(w, errReadOnly.Error(), http.StatusMethodNotAllowed) + http.Error(w, webdavfs.ErrReadOnly.Error(), http.StatusMethodNotAllowed) return } if len(reqTokens) == 0 { @@ -345,6 +360,10 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { fsprefix = "by_id/" + collectionID + "/" } + if src := r.Header.Get("X-Webdav-Source"); strings.HasPrefix(src, "/") && !strings.Contains(src, "//") && !strings.Contains(src, "/../") { + fsprefix += src[1:] + } + if tokens == nil { tokens = reqTokens if h.Cluster.Users.AnonymousUserToken != "" { @@ -352,15 +371,6 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { } } - if tokens == nil { - if !credentialsOK { - http.Error(w, fmt.Sprintf("Authorization tokens are not accepted here: %v, and no anonymous user token is configured.", reasonNotAcceptingCredentials), http.StatusUnauthorized) - } else { - http.Error(w, fmt.Sprintf("No authorization token in request, and no anonymous user token is configured."), http.StatusUnauthorized) - } - return - } - if len(targetPath) > 0 && targetPath[0] == "_" { // If a collection has a directory called "t=foo" or // "_", it can be served at @@ -377,7 +387,8 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { dirOpenMode = os.O_RDWR } - validToken := make(map[string]bool) + var tokenValid bool + var tokenScopeProblem bool var token string var tokenUser *arvados.User var sessionFS arvados.CustomFileSystem @@ -393,14 +404,18 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { http.Error(w, "cache error: "+err.Error(), http.StatusInternalServerError) return } + if token != h.Cluster.Users.AnonymousUserToken { + tokenValid = true + } f, err := fs.OpenFile(fsprefix, dirOpenMode, 0) - if errors.As(err, &statusErr) && statusErr.HTTPStatus() == http.StatusForbidden { - // collection id is outside token scope - validToken[token] = true + if errors.As(err, &statusErr) && + statusErr.HTTPStatus() == http.StatusForbidden && + token != h.Cluster.Users.AnonymousUserToken { + // collection id is outside scope of supplied + // token + tokenScopeProblem = true continue - } - validToken[token] = true - if os.IsNotExist(err) { + } else if os.IsNotExist(err) { // collection does not exist or is not // readable using this token continue @@ -425,22 +440,27 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { } } if session == nil { - if pathToken || !credentialsOK { - // Either the URL is a "secret sharing link" - // that didn't work out (and asking the client - // for additional credentials would just be - // confusing), or we don't even accept - // credentials at this path. + if pathToken { + // The URL is a "secret sharing link" that + // didn't work out. Asking the client for + // additional credentials would just be + // confusing. http.Error(w, notFoundMessage, http.StatusNotFound) return } - for _, t := range reqTokens { - if validToken[t] { - // The client provided valid token(s), - // but the collection was not found. - http.Error(w, notFoundMessage, http.StatusNotFound) - return - } + if tokenValid { + // The client provided valid token(s), but the + // collection was not found. + http.Error(w, notFoundMessage, http.StatusNotFound) + return + } + if tokenScopeProblem { + // The client provided a valid token but + // fetching a collection returned 401, which + // means the token scope doesn't permit + // fetching that collection. + http.Error(w, notFoundMessage, http.StatusForbidden) + return } // The client's token was invalid (e.g., expired), or // the client didn't even provide one. Redirect to @@ -477,10 +497,18 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { target.RawQuery = redirkey + "=" + callback w.Header().Add("Location", target.String()) w.WriteHeader(http.StatusSeeOther) - } else { - w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"") - http.Error(w, unauthorizedMessage, http.StatusUnauthorized) + return + } + if !credentialsOK { + http.Error(w, fmt.Sprintf("Authorization tokens are not accepted here: %v, and no anonymous user token is configured.", reasonNotAcceptingCredentials), http.StatusUnauthorized) + return } + // If none of the above cases apply, suggest the + // user-agent (which is either a non-browser agent + // like wget, or a browser that can't redirect through + // a login flow) prompt the user for credentials. + w.Header().Add("WWW-Authenticate", "Basic realm=\"collections\"") + http.Error(w, unauthorizedMessage, http.StatusUnauthorized) return } @@ -501,7 +529,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { basename = targetPath[len(targetPath)-1] } if arvadosclient.PDHMatch(collectionID) && writeMethod[r.Method] { - http.Error(w, errReadOnly.Error(), http.StatusMethodNotAllowed) + http.Error(w, webdavfs.ErrReadOnly.Error(), http.StatusMethodNotAllowed) return } if !h.userPermittedToUploadOrDownload(r.Method, tokenUser) { @@ -556,17 +584,20 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { applyContentDispositionHdr(w, r, basename, attachment) } + if webdavPrefix == "" { + webdavPrefix = "/" + strings.Join(pathParts[:stripParts], "/") + } wh := webdav.Handler{ - Prefix: "/" + strings.Join(pathParts[:stripParts], "/"), - FileSystem: &webdavFS{ - collfs: sessionFS, - prefix: fsprefix, - writing: writeMethod[r.Method], - alwaysReadEOF: r.Method == "PROPFIND", + Prefix: webdavPrefix, + FileSystem: &webdavfs.FS{ + FileSystem: sessionFS, + Prefix: fsprefix, + Writing: writeMethod[r.Method], + AlwaysReadEOF: r.Method == "PROPFIND", }, - LockSystem: h.webdavLS, + LockSystem: webdavfs.NoLockSystem, Logger: func(r *http.Request, err error) { - if err != nil { + if err != nil && !os.IsNotExist(err) { ctxlog.FromContext(r.Context()).WithError(err).Error("error reported by webdav handler") } },