Directly serve files instead of redirect

Fixes #28
This commit is contained in:
Daan Sprenkels 2019-12-19 23:17:37 +01:00
parent 5e6ce9c2be
commit 245dd64f82
2 changed files with 40 additions and 57 deletions

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings"
"time" "time"
"gitea.hashru.nl/dsprenkels/rushlink/internal/db" "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{}{}) 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) { func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) {
rl.viewPasteHandlerFlags(w, r, 0) rl.viewPasteHandlerFlags(w, r, 0)
} }
@ -158,31 +112,61 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
switch p.State { switch p.State {
case db.PasteStatePresent: case db.PasteStatePresent:
var location string
switch p.Type { switch p.Type {
case db.PasteTypeFileUpload: case db.PasteTypeFileUpload:
if fu == nil { if fu == nil {
panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content))) panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content)))
} }
location = fu.URL().String() rl.viewFileUploadHandler(w, r, fu)
break return
case db.PasteTypeRedirect: case db.PasteTypeRedirect:
location = p.RedirectURL().String() if flags&viewNoRedirect == 0 {
break http.Redirect(w, r, p.RedirectURL().String(), http.StatusSeeOther)
}
return
default: default:
panic("paste type unsupported") panic("paste type unsupported")
} }
if flags&viewNoRedirect == 0 {
http.Redirect(w, r, location, http.StatusSeeOther)
}
fmt.Fprint(w, location)
case db.PasteStateDeleted: case db.PasteStateDeleted:
rl.renderError(w, r, http.StatusGone, "paste has been deleted\n") rl.renderError(w, r, http.StatusGone, "paste has been deleted\n")
return
default: default:
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) 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) { func (rl *rushlink) viewPasteHandlerInnerMeta(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
var cd canDelete var cd canDelete
deleteToken := getDeleteTokenFromRequest(r) deleteToken := getDeleteTokenFromRequest(r)

View File

@ -99,7 +99,6 @@ func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawRootURL
router := mux.NewRouter() router := mux.NewRouter()
router.Use(rl.recoveryMiddleware) router.Use(rl.recoveryMiddleware)
router.Use(rl.metricsMiddleware) 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:img/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/{path:css/"+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") router.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")