From 4ad57af9906a453cd7bc74898e94a23a4876ce74 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 4 Apr 2020 19:55:23 -0700 Subject: [PATCH] Support one-time downloads of server backups --- download_tokens.go | 47 ++++++++++++++++++++++++++ http.go | 1 + http_download.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++ server/backup.go | 17 ++++++++++ 4 files changed, 149 insertions(+) create mode 100644 download_tokens.go create mode 100644 http_download.go diff --git a/download_tokens.go b/download_tokens.go new file mode 100644 index 0000000..b7b0d02 --- /dev/null +++ b/download_tokens.go @@ -0,0 +1,47 @@ +package main + +import ( + "github.com/gbrlsnchs/jwt/v3" + cache2 "github.com/patrickmn/go-cache" + "sync" + "time" +) + +type JWTokens struct { + cache *cache2.Cache + mutex *sync.Mutex +} + +var _tokens *JWTokens + +type DownloadBackupPayload struct { + jwt.Payload + ServerUuid string `json:"server_uuid"` + BackupUuid string `json:"backup_uuid"` + UniqueId string `json:"unique_id"` +} + +func getTokenStore() *JWTokens { + if _tokens == nil { + _tokens = &JWTokens{ + cache: cache2.New(time.Minute*60, time.Minute*5), + mutex: &sync.Mutex{}, + } + } + + return _tokens +} + +// Determines if a given JWT unique token is valid. +func (tokens *JWTokens) IsValidToken(token string) bool { + tokens.mutex.Lock() + defer tokens.mutex.Unlock() + + _, exists := tokens.cache.Get(token) + + if !exists { + _tokens.cache.Add(token, "", time.Minute*60) + } + + return !exists +} diff --git a/http.go b/http.go index 8a126f2..6a5af59 100644 --- a/http.go +++ b/http.go @@ -613,6 +613,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router { }) router.GET("/", rt.routeIndex) + router.GET("/download/backup", rt.routeDownloadBackup) router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation)) router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers)) router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer)) diff --git a/http_download.go b/http_download.go new file mode 100644 index 0000000..7d3611a --- /dev/null +++ b/http_download.go @@ -0,0 +1,84 @@ +package main + +import ( + "bufio" + "github.com/gbrlsnchs/jwt/v3" + "github.com/julienschmidt/httprouter" + "github.com/pterodactyl/wings/config" + "go.uber.org/zap" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +// Validates the provided JWT against the known secret for the Daemon and returns the +// parsed data. +func (rt *Router) parseBackupToken(token []byte) (*DownloadBackupPayload, error) { + var payload DownloadBackupPayload + if alg == nil { + alg = jwt.NewHS256([]byte(config.Get().AuthenticationToken)) + } + + now := time.Now() + verifyOptions := jwt.ValidatePayload( + &payload.Payload, + jwt.ExpirationTimeValidator(now), + ) + + _, err := jwt.Verify(token, alg, &payload, verifyOptions) + if err != nil { + return nil, err + } + + return &payload, nil +} + + +func (rt *Router) routeDownloadBackup(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + defer r.Body.Close() + + payload, err := rt.parseBackupToken([]byte(r.URL.Query().Get("token"))) + // Some type of payload issue with the JWT, bail out here. + if err != nil { + zap.S().Warnw("failed to validate token for downloading backup", zap.Error(err)) + http.Error(w, "failed to open backup for download", http.StatusForbidden) + + return + } + + store := getTokenStore() + // The one-time-token in the payload is no longer valid, request is likely a repeat + // so block it. + if ok := store.IsValidToken(payload.UniqueId); !ok { + http.NotFound(w, r) + return + } + + s := rt.GetServer(payload.ServerUuid) + p, st, err := s.LocateBackup(payload.BackupUuid) + if err != nil { + if !os.IsNotExist(err) && !strings.HasPrefix(err.Error(), "invalid archive found") { + zap.S().Warnw("failed to locate a backup for download", zap.String("path", p), zap.String("server", s.Uuid), zap.Error(err)) + } + + http.NotFound(w, r) + return + } + + f, err := os.OpenFile(p, os.O_RDONLY, 0) + if err != nil { + zap.S().Errorw("failed to open file for reading", zap.String("path", ps.ByName("path")), zap.String("server", s.Uuid), zap.Error(err)) + + http.Error(w, "failed to open backup for download", http.StatusInternalServerError) + return + } + defer f.Close() + + w.Header().Set("Content-Length", strconv.Itoa(int(st.Size()))) + w.Header().Set("Content-Disposition", "attachment; filename="+st.Name()) + w.Header().Set("Content-Type", "application/octet-stream") + + bufio.NewReader(f).WriteTo(w) +} diff --git a/server/backup.go b/server/backup.go index e41e844..fe6f7a5 100644 --- a/server/backup.go +++ b/server/backup.go @@ -37,6 +37,23 @@ func (s *Server) NewBackup(data []byte) (*Backup, error) { return backup, nil } +// Locates the backup for a server and returns the local path. This will obviously only +// work if the backup was created as a local backup. +func (s *Server) LocateBackup(uuid string) (string, os.FileInfo, error) { + p := path.Join(config.Get().System.BackupDirectory, s.Uuid, uuid + ".tar.gz") + + st, err := os.Stat(p) + if err != nil { + return "", nil, err + } + + if st.IsDir() { + return "", nil, errors.New("invalid archive found; is directory") + } + + return p, st, nil +} + // Ensures that the local backup destination for files exists. func (b *Backup) ensureLocalBackupLocation() error { if _, err := os.Stat(b.localDirectory); err != nil {