From 7a6397bf17adae1698c01eeb2ed667d443f33251 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 12 Jul 2020 16:43:25 -0600 Subject: [PATCH] Add basic file upload support --- router/router.go | 1 + router/router_server_files.go | 94 ++++++++++++++++++++++++++++++++++- router/tokens/backup.go | 3 +- router/tokens/upload.go | 25 ++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 router/tokens/upload.go diff --git a/router/router.go b/router/router.go index 12d270e..8c534ed 100644 --- a/router/router.go +++ b/router/router.go @@ -35,6 +35,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) + router.POST("/upload/file", postServerUploadFiles) // 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_server_files.go b/router/router_server_files.go index d9635e7..c1ac534 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -3,13 +3,18 @@ package router import ( "bufio" "context" + "github.com/apex/log" "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/pterodactyl/wings/router/tokens" "github.com/pterodactyl/wings/server" "golang.org/x/sync/errgroup" + "mime/multipart" "net/http" "net/url" "os" "path" + "path/filepath" "strconv" "strings" ) @@ -170,7 +175,7 @@ func postServerDeleteFiles(c *gin.Context) { if len(data.Files) == 0 { c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{ - "error": "No files were specififed for deletion.", + "error": "No files were specified for deletion.", }) return } @@ -277,3 +282,90 @@ func postServerCompressFiles(c *gin.Context) { Mimetype: "application/tar+gzip", }) } + +func postServerUploadFiles(c *gin.Context) { + token := tokens.UploadPayload{} + 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 + } + + if !s.Filesystem.HasSpaceAvailable() { + c.AbortWithStatusJSON(http.StatusConflict, gin.H{ + "error": "This server does not have enough available disk space to accept any file uploads.", + }) + return + } + + form, err := c.MultipartForm() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ + "error": "Failed to get multipart form.", + }) + return + } + + for i := range form.File { + log.Debug(i) + } + + headers, ok := form.File["files"] + if !ok { + c.AbortWithStatusJSON(http.StatusNotModified, gin.H{ + "error": "No files were attached to the request.", + }) + return + } + + // TODO: Make sure directory is safe. + directory := c.Query("directory") + + for _, header := range headers { + // TODO: Make sure header#Filename is clean. + p, err := s.Filesystem.SafePath(filepath.Join(directory, header.Filename)) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + log.Debug(p) + + // We run this in a different method so I can use defer without any of + // the consequences caused by calling it in a loop. + if err := handleFileUpload(p, s, header); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + } +} + +func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error { + _, err := s.Filesystem.Stat(header.Filename) + if err == nil { + // TODO: Figure out how to better handle this + + // This means the file exists, not 100% sure what to do in this situation, but for now we will skip the file. + return nil + } else if !os.IsNotExist(err) { + return errors.WithStack(err) + } + + file, err := header.Open() + if err != nil { + return errors.WithStack(err) + } + defer file.Close() + + if err := s.Filesystem.Writefile(p, file); err != nil { + return errors.WithStack(err) + } + + return nil +} diff --git a/router/tokens/backup.go b/router/tokens/backup.go index accd97e..5408574 100644 --- a/router/tokens/backup.go +++ b/router/tokens/backup.go @@ -6,6 +6,7 @@ import ( type BackupPayload struct { jwt.Payload + ServerUuid string `json:"server_uuid"` BackupUuid string `json:"backup_uuid"` UniqueId string `json:"unique_id"` @@ -22,4 +23,4 @@ func (p *BackupPayload) GetPayload() *jwt.Payload { // 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/upload.go b/router/tokens/upload.go new file mode 100644 index 0000000..5321fea --- /dev/null +++ b/router/tokens/upload.go @@ -0,0 +1,25 @@ +package tokens + +import ( + "github.com/gbrlsnchs/jwt/v3" +) + +type UploadPayload struct { + jwt.Payload + + ServerUuid string `json:"server_uuid"` + UniqueId string `json:"unique_id"` +} + +// Returns the JWT payload. +func (p *UploadPayload) 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 *UploadPayload) IsUniqueRequest() bool { + return getTokenStore().IsValidToken(p.UniqueId) +}