Support one-time downloads of server backups
This commit is contained in:
parent
4ce2b73490
commit
4ad57af990
47
download_tokens.go
Normal file
47
download_tokens.go
Normal file
|
@ -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
|
||||||
|
}
|
1
http.go
1
http.go
|
@ -613,6 +613,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||||
})
|
})
|
||||||
|
|
||||||
router.GET("/", rt.routeIndex)
|
router.GET("/", rt.routeIndex)
|
||||||
|
router.GET("/download/backup", rt.routeDownloadBackup)
|
||||||
router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation))
|
router.GET("/api/system", rt.AuthenticateToken(rt.routeSystemInformation))
|
||||||
router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers))
|
router.GET("/api/servers", rt.AuthenticateToken(rt.routeAllServers))
|
||||||
router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer))
|
router.GET("/api/servers/:server", rt.AuthenticateRequest(rt.routeServer))
|
||||||
|
|
84
http_download.go
Normal file
84
http_download.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -37,6 +37,23 @@ func (s *Server) NewBackup(data []byte) (*Backup, error) {
|
||||||
return backup, nil
|
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.
|
// Ensures that the local backup destination for files exists.
|
||||||
func (b *Backup) ensureLocalBackupLocation() error {
|
func (b *Backup) ensureLocalBackupLocation() error {
|
||||||
if _, err := os.Stat(b.localDirectory); err != nil {
|
if _, err := os.Stat(b.localDirectory); err != nil {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user