X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/654ee9154fe85832a0862c27fd7b982831a75a0d..987457b7e545f4ad1e32c7a07c00c29c24326421:/services/keep-web/handler.go diff --git a/services/keep-web/handler.go b/services/keep-web/handler.go index 432fc21f4e..19a2040b4a 100644 --- a/services/keep-web/handler.go +++ b/services/keep-web/handler.go @@ -10,6 +10,7 @@ import ( "html" "html/template" "io" + "log" "net/http" "net/url" "os" @@ -82,26 +83,82 @@ func (h *handler) setup() { Prefix: "/_health/", } - h.webdavLS = webdav.NewMemLS() + // 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) { status := struct { cacheStats + Version string }{ cacheStats: h.Config.Cache.Stats(), + Version: version, } json.NewEncoder(w).Encode(status) } +// updateOnSuccess wraps httpserver.ResponseWriter. If the handler +// sends an HTTP header indicating success, updateOnSuccess first +// calls the provided update func. If the update func fails, a 500 +// response is sent, and the status code and body sent by the handler +// are ignored (all response writes return the update error). +type updateOnSuccess struct { + httpserver.ResponseWriter + update func() error + sentHeader bool + err error +} + +func (uos *updateOnSuccess) Write(p []byte) (int, error) { + if uos.err != nil { + return 0, uos.err + } + if !uos.sentHeader { + uos.WriteHeader(http.StatusOK) + } + return uos.ResponseWriter.Write(p) +} + +func (uos *updateOnSuccess) WriteHeader(code int) { + if !uos.sentHeader { + uos.sentHeader = true + if code >= 200 && code < 400 { + if uos.err = uos.update(); uos.err != nil { + code := http.StatusInternalServerError + if err, ok := uos.err.(*arvados.TransactionError); ok { + code = err.StatusCode + } + log.Printf("update() changes response to HTTP %d: %T %q", code, uos.err, uos.err) + http.Error(uos.ResponseWriter, uos.err.Error(), code) + return + } + } + } + uos.ResponseWriter.WriteHeader(code) +} + var ( + writeMethod = map[string]bool{ + "COPY": true, + "DELETE": true, + "MKCOL": true, + "MOVE": true, + "PUT": true, + "RMCOL": true, + } webdavMethod = map[string]bool{ + "COPY": true, + "DELETE": true, + "MKCOL": true, + "MOVE": true, "OPTIONS": true, "PROPFIND": true, - "LOCK": true, - "UNLOCK": true, + "PUT": true, + "RMCOL": true, } - fsMethod = map[string]bool{ + browserMethod = map[string]bool{ "GET": true, "HEAD": true, "POST": true, @@ -142,19 +199,19 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { } if method := r.Header.Get("Access-Control-Request-Method"); method != "" && r.Method == "OPTIONS" { - if !fsMethod[method] && !webdavMethod[method] { + if !browserMethod[method] && !webdavMethod[method] { statusCode = http.StatusMethodNotAllowed return } - w.Header().Set("Access-Control-Allow-Headers", "Range") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PROPFIND, LOCK, UNLOCK") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Range") + w.Header().Set("Access-Control-Allow-Methods", "COPY, DELETE, GET, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PUT, RMCOL") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Max-Age", "86400") statusCode = http.StatusOK return } - if !fsMethod[r.Method] && !webdavMethod[r.Method] { + if !browserMethod[r.Method] && !webdavMethod[r.Method] { statusCode, statusText = http.StatusMethodNotAllowed, r.Method return } @@ -239,7 +296,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { // * The token isn't embedded in the URL, so we don't // need to worry about bookmarks and copy/paste. tokens = append(tokens, formToken) - } else if formToken != "" { + } else if formToken != "" && browserMethod[r.Method] { // The client provided an explicit token in the query // string, or a form in POST body. We must put the // token in an HttpOnly cookie, and redirect to the @@ -346,24 +403,52 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { return } - basename := targetPath[len(targetPath)-1] + var basename string + if len(targetPath) > 0 { + basename = targetPath[len(targetPath)-1] + } applyContentDispositionHdr(w, r, basename, attachment) - fs := collection.FileSystem(&arvados.Client{ + client := &arvados.Client{ APIHost: arv.ApiServer, AuthToken: arv.ApiToken, Insecure: arv.ApiInsecure, - }, kc) + } + fs, err := collection.FileSystem(client, kc) + if err != nil { + statusCode, statusText = http.StatusInternalServerError, err.Error() + return + } + + targetIsPDH := arvadosclient.PDHMatch(targetID) + if targetIsPDH && writeMethod[r.Method] { + statusCode, statusText = http.StatusMethodNotAllowed, errReadOnly.Error() + return + } + if webdavMethod[r.Method] { + if writeMethod[r.Method] { + // Save the collection only if/when all + // webdav->filesystem operations succeed -- + // and send a 500 error if the modified + // collection can't be saved. + w = &updateOnSuccess{ + ResponseWriter: w, + update: func() error { + return h.Config.Cache.Update(client, *collection, fs) + }} + } h := webdav.Handler{ - Prefix: "/" + strings.Join(pathParts[:stripParts], "/"), - FileSystem: &webdavFS{httpfs: fs}, + Prefix: "/" + strings.Join(pathParts[:stripParts], "/"), + FileSystem: &webdavFS{ + collfs: fs, + writing: writeMethod[r.Method], + alwaysReadEOF: r.Method == "PROPFIND", + }, LockSystem: h.webdavLS, Logger: func(_ *http.Request, err error) { - if os.IsNotExist(err) { - statusCode, statusText = http.StatusNotFound, err.Error() - } else if err != nil { - statusCode, statusText = http.StatusInternalServerError, err.Error() + if err != nil { + log.Printf("error from webdav handler: %q", err) } }, } @@ -386,7 +471,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { // ".../dirname/". This way, relative links in the // listing for "dirname" can always be "fnm", never // "dirname/fnm". - h.seeOtherWithCookie(w, r, basename+"/", credentialsOK) + h.seeOtherWithCookie(w, r, r.URL.Path+"/", credentialsOK) } else if stat.IsDir() { h.serveDirectory(w, r, collection.Name, fs, openPath, stripParts) } else { @@ -547,16 +632,16 @@ func applyContentDispositionHdr(w http.ResponseWriter, r *http.Request, filename } func (h *handler) seeOtherWithCookie(w http.ResponseWriter, r *http.Request, location string, credentialsOK bool) { - if !credentialsOK { - // It is not safe to copy the provided token - // into a cookie unless the current vhost - // (origin) serves only a single collection or - // we are in TrustAllContent mode. - w.WriteHeader(http.StatusBadRequest) - return - } - if formToken := r.FormValue("api_token"); formToken != "" { + if !credentialsOK { + // It is not safe to copy the provided token + // into a cookie unless the current vhost + // (origin) serves only a single collection or + // we are in TrustAllContent mode. + w.WriteHeader(http.StatusBadRequest) + return + } + // The HttpOnly flag is necessary to prevent // JavaScript code (included in, or loaded by, a page // in the collection being served) from employing the @@ -568,7 +653,6 @@ func (h *handler) seeOtherWithCookie(w http.ResponseWriter, r *http.Request, loc // bar, and in the case of a POST request to avoid // raising warnings when the user refreshes the // resulting page. - http.SetCookie(w, &http.Cookie{ Name: "arvados_api_token", Value: auth.EncodeTokenCookie([]byte(formToken)),