From 245dd64f82a85da281254d8c6c1d2ac181dd47a2 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Thu, 19 Dec 2019 23:17:37 +0100 Subject: [PATCH] Directly serve files instead of redirect Fixes #28 --- handlers.go | 96 ++++++++++++++++++++++------------------------------- router.go | 1 - 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/handlers.go b/handlers.go index e24fed6..7fad054 100644 --- a/handlers.go +++ b/handlers.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "strings" "time" "gitea.hashru.nl/dsprenkels/rushlink/internal/db" @@ -60,53 +61,6 @@ func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) { rl.render(w, r, "index", map[string]interface{}{}) } -func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - var fu *db.FileUpload - var badID bool - if err := rl.db.Bolt.View(func(tx *bolt.Tx) error { - fuID, err := uuid.Parse(id) - if err != nil { - badID = true - return err - } - fu, err = db.GetFileUpload(tx, fuID) - return err - }); err != nil { - if badID { - rl.renderError(w, r, http.StatusNotFound, "malformed file id") - return - } - // unexpected error - panic(err) - } - - filePath := fu.Path(rl.fs) - file, err := os.Open(filePath) - if err != nil { - if os.IsNotExist(err) { - log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath) - rl.renderError(w, r, http.StatusNotFound, "file not found") - return - } - // unexpected error - panic(err) - } - info, err := file.Stat() - var modtime time.Time - if err != nil { - log.Printf("error: %v", errors.Wrapf(err, "could not stat file '%v'", filePath)) - } else { - modtime = info.ModTime() - } - // We use http.ServeContent (instead of http.ServeFile) because we cannot - // use http.ServeFile together with the assertion that the file exists, - // without introducing a TOCTOU flaw. - http.ServeContent(w, r, fu.FileName, modtime, file) -} - func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) { rl.viewPasteHandlerFlags(w, r, 0) } @@ -158,31 +112,61 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request switch p.State { case db.PasteStatePresent: - var location string switch p.Type { case db.PasteTypeFileUpload: if fu == nil { panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content))) } - location = fu.URL().String() - break + rl.viewFileUploadHandler(w, r, fu) + return case db.PasteTypeRedirect: - location = p.RedirectURL().String() - break + if flags&viewNoRedirect == 0 { + http.Redirect(w, r, p.RedirectURL().String(), http.StatusSeeOther) + } + return default: panic("paste type unsupported") } - if flags&viewNoRedirect == 0 { - http.Redirect(w, r, location, http.StatusSeeOther) - } - fmt.Fprint(w, location) + case db.PasteStateDeleted: rl.renderError(w, r, http.StatusGone, "paste has been deleted\n") + return default: panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) } } +func (rl *rushlink) viewFileUploadHandler(w http.ResponseWriter, r *http.Request, fu *db.FileUpload) { + filePath := fu.Path(rl.fs) + file, err := os.Open(filePath) + if err != nil { + if os.IsNotExist(err) { + log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath) + rl.renderError(w, r, http.StatusNotFound, "file not found") + return + } + // unexpected error + panic(err) + } + + var modtime time.Time + info, err := file.Stat() + if err != nil { + log.Printf("error: %v", errors.Wrapf(err, "could not stat file '%v'", filePath)) + } else { + modtime = info.ModTime() + } + + // Provide the real filename to the client (to be used in Ctrl+S etc.) + quotedName := strings.ReplaceAll(fu.FileName, "\"", "\\\"") + w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", quotedName)) + + // We use http.ServeContent (instead of http.ServeFile) because we cannot + // use http.ServeFile together with the assertion that the file exists, + // without introducing a TOCTOU flaw. + http.ServeContent(w, r, fu.FileName, modtime, file) +} + func (rl *rushlink) viewPasteHandlerInnerMeta(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) { var cd canDelete deleteToken := getDeleteTokenFromRequest(r) diff --git a/router.go b/router.go index 8d04d3e..64cdfd1 100644 --- a/router.go +++ b/router.go @@ -99,7 +99,6 @@ func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawRootURL router := mux.NewRouter() router.Use(rl.recoveryMiddleware) router.Use(rl.metricsMiddleware) - router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET", "HEAD") router.HandleFunc("/{path:img/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD") router.HandleFunc("/{path:css/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD") router.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")