diff --git a/router/router.go b/router/router.go index fbc6d8a..ed3a5a9 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 da3d996..8624232 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -5,12 +5,16 @@ import ( "context" "errors" "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" ) @@ -342,3 +346,73 @@ func postServerDecompressFiles(c *gin.Context) { c.Status(http.StatusNoContent) } + +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 + } + + headers, ok := form.File["files"] + if !ok { + c.AbortWithStatusJSON(http.StatusNotModified, gin.H{ + "error": "No files were attached to the request.", + }) + return + } + + directory := c.Query("directory") + + for _, header := range headers { + p, err := s.Filesystem.SafePath(filepath.Join(directory, header.Filename)) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + // 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 { + 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) +}