From c4474e22f68a266cd8f6f7e0aee75e5c6abb9d05 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 5 Apr 2020 18:56:54 -0700 Subject: [PATCH] Add support for downloading a backup --- http.go | 1 - router/router.go | 3 +++ router/router_download.go | 44 ++++++++++++++++++++++++++++++++++++ router/tokens/backup.go | 25 ++++++++++++++++++++ router/tokens/parser.go | 2 +- router/tokens/token_store.go | 41 +++++++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 router/tokens/backup.go create mode 100644 router/tokens/token_store.go diff --git a/http.go b/http.go index 16b5fff..6decab0 100644 --- a/http.go +++ b/http.go @@ -396,7 +396,6 @@ func (rt *Router) ReaderToBytes(r io.Reader) []byte { func (rt *Router) ConfigureRouter() *httprouter.Router { router := httprouter.New() - router.GET("/download/backup", rt.routeDownloadBackup) router.POST("/api/servers/:server/backup", rt.AuthenticateRequest(rt.routeServerBackup)) router.POST("/api/servers/:server/archive", rt.AuthenticateRequest(rt.routeRequestServerArchive)) diff --git a/router/router.go b/router/router.go index dfb78f1..a1d0325 100644 --- a/router/router.go +++ b/router/router.go @@ -7,6 +7,9 @@ func Configure() *gin.Engine { router := gin.Default() router.Use(SetAccessControlHeaders) + // These routes use signed URLs to validate access to the resource being requested. + router.GET("/download/backup", getDownloadBackup) + // This route is special is sits above all of the other requests because we are // using a JWT to authorize access to it, therefore it needs to be publically // accessible. diff --git a/router/router_download.go b/router/router_download.go index 7ef135b..c37d23a 100644 --- a/router/router_download.go +++ b/router/router_download.go @@ -1 +1,45 @@ package router + +import ( + "bufio" + "github.com/gin-gonic/gin" + "github.com/pterodactyl/wings/router/tokens" + "net/http" + "os" + "strconv" +) + +func getDownloadBackup(c *gin.Context) { + token := tokens.BackupPayload{} + 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, st, err := s.LocateBackup(token.BackupUuid) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + + f, err := os.Open(p) + if err != nil { + TrackedServerError(err, s).AbortWithServerError(c) + return + } + defer f.Close() + + 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) +} diff --git a/router/tokens/backup.go b/router/tokens/backup.go new file mode 100644 index 0000000..accd97e --- /dev/null +++ b/router/tokens/backup.go @@ -0,0 +1,25 @@ +package tokens + +import ( + "github.com/gbrlsnchs/jwt/v3" +) + +type BackupPayload struct { + jwt.Payload + ServerUuid string `json:"server_uuid"` + BackupUuid string `json:"backup_uuid"` + UniqueId string `json:"unique_id"` +} + +// Returns the JWT payload. +func (p *BackupPayload) 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 *BackupPayload) IsUniqueRequest() bool { + return getTokenStore().IsValidToken(p.UniqueId) +} \ No newline at end of file diff --git a/router/tokens/parser.go b/router/tokens/parser.go index cad4eb1..5d83969 100644 --- a/router/tokens/parser.go +++ b/router/tokens/parser.go @@ -30,4 +30,4 @@ func ParseToken(token []byte, data TokenData) error { _, err := jwt.Verify(token, alg, &data, verifyOptions) return err -} \ No newline at end of file +} diff --git a/router/tokens/token_store.go b/router/tokens/token_store.go new file mode 100644 index 0000000..7560988 --- /dev/null +++ b/router/tokens/token_store.go @@ -0,0 +1,41 @@ +package tokens + +import ( + cache2 "github.com/patrickmn/go-cache" + "sync" + "time" +) + +type TokenStore struct { + cache *cache2.Cache + mutex *sync.Mutex +} + +var _tokens *TokenStore + +// Returns the global unqiue token store cache. This is used to validate +// one time token usage by storing any received tokens in a local memory +// cache until they are ready to expire. +func getTokenStore() *TokenStore { + if _tokens == nil { + _tokens = &TokenStore{ + cache: cache2.New(time.Minute*60, time.Minute*5), + mutex: &sync.Mutex{}, + } + } + + return _tokens +} + +func (t *TokenStore) IsValidToken(token string) bool { + t.mutex.Lock() + defer t.mutex.Unlock() + + _, exists := t.cache.Get(token) + + if !exists { + t.cache.Add(token, "", time.Minute*60) + } + + return !exists +}