From 3edcd5f9c3c53913731d733bb51b702423663b55 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 6 Apr 2020 20:27:57 -0700 Subject: [PATCH] Add support for direct downloads of server files --- router/router.go | 1 + router/router_download.go | 44 +++++++++++++++++++++++++++++++++++++++ router/tokens/file.go | 25 ++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 router/tokens/file.go diff --git a/router/router.go b/router/router.go index 8e3ad0c..991ec7b 100644 --- a/router/router.go +++ b/router/router.go @@ -15,6 +15,7 @@ func Configure() *gin.Engine { // These routes use signed URLs to validate access to the resource being requested. router.GET("/download/backup", getDownloadBackup) + router.GET("/download/file", getDownloadFile) // This route is special it sits above all of the other requests because we are // using a JWT to authorize access to it, therefore it needs to be publicly diff --git a/router/router_download.go b/router/router_download.go index c37d23a..6ae0d3d 100644 --- a/router/router_download.go +++ b/router/router_download.go @@ -9,6 +9,7 @@ import ( "strconv" ) +// Handle a download request for a server backup. func getDownloadBackup(c *gin.Context) { token := tokens.BackupPayload{} if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { @@ -43,3 +44,46 @@ func getDownloadBackup(c *gin.Context) { bufio.NewReader(f).WriteTo(c.Writer) } + +// Handles downloading a specific file for a server. +func getDownloadFile(c *gin.Context) { + token := tokens.FilePayload{} + if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil { + TrackedError(err).AbortWithServerError(c) + return + } + + s := GetServer(token.ServerUuid) + if s == nil || !token.IsUniqueRequest() { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested resource was not found on this server.", + }) + return + } + + p, _ := s.Filesystem.SafePath(token.FilePath) + st, err := os.Stat(p) + // If there is an error or we're somehow trying to download a directory, just + // respond with the appropriate error. + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } else if st.IsDir() { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "The requested resource was not found on this server.", + }) + return + } + + f, err := os.Open(p) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + c.Header("Content-Length", strconv.Itoa(int(st.Size()))) + c.Header("Content-Disposition", "attachment; filename="+st.Name()) + c.Header("Content-Type", "application/octet-stream") + + bufio.NewReader(f).WriteTo(c.Writer) +} \ No newline at end of file diff --git a/router/tokens/file.go b/router/tokens/file.go new file mode 100644 index 0000000..97b991d --- /dev/null +++ b/router/tokens/file.go @@ -0,0 +1,25 @@ +package tokens + +import ( + "github.com/gbrlsnchs/jwt/v3" +) + +type FilePayload struct { + jwt.Payload + FilePath string `json:"file_path"` + ServerUuid string `json:"server_uuid"` + UniqueId string `json:"unique_id"` +} + +// Returns the JWT payload. +func (p *FilePayload) GetPayload() *jwt.Payload { + return &p.Payload +} + +// Determines if this JWT is valid for the given request cycle. If the +// unique ID passed in the token has already been seen before this will +// return false. This allows us to use this JWT as a one-time token that +// validates all of the request. +func (p *FilePayload) IsUniqueRequest() bool { + return getTokenStore().IsValidToken(p.UniqueId) +}