Files
phishingclub/backend/service/file.go
Ronni Skansing 16bbcd3b81 replace securejoin with os.openroot
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-10-04 18:09:12 +02:00

418 lines
11 KiB
Go

package service
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"mime/multipart"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-errors/errors"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/validate"
)
// FileUpload is a file upload
type FileUpload struct {
Path string
File *multipart.FileHeader
}
// RootFileUpload is a file upload using os.Root
type RootFileUpload struct {
Root *os.Root
RelativePath string
File *multipart.FileHeader
}
// NewFileUpload creates a new file upload
func NewFileUpload(path string, file *multipart.FileHeader) *FileUpload {
return &FileUpload{
Path: path,
File: file,
}
}
// NewRootFileUpload creates a new secure file upload
func NewRootFileUpload(root *os.Root, relativePath string, file *multipart.FileHeader) *RootFileUpload {
return &RootFileUpload{
Root: root,
RelativePath: relativePath,
File: file,
}
}
// File is a File service
type File struct {
Common
}
// checkFilePathIsValidForUpload checks if the file path is valid for upload
func (f *File) checkFilePathIsValidForUpload(path string) error {
parts := strings.Split(path, "/")
// Check each part of the path
for i := 1; i < len(parts); i++ {
partPath := strings.Join(parts[:i], "/")
info, err := os.Stat(partPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// The path part does not exist, which is expected as we are still constructing the full path
continue
} else {
// Some other error occurred
return fmt.Errorf("failed to check asset (%s) path info: %w", partPath, err)
}
}
if !info.IsDir() {
return fmt.Errorf(
"part of the path is a file: '%s' of '%s'",
partPath,
path,
)
}
}
return nil
}
func (f *File) UploadLegacy(
g *gin.Context,
files []*FileUpload,
) (int, error) {
for _, fileUpload := range files {
path := fileUpload.Path
file := fileUpload.File
f.Logger.Debugw("checking if file exists", "path", path)
err := f.checkFilePathIsValidForUpload(path)
if err != nil && !errors.Is(err, errs.ErrValidationFailed) {
return 0, errs.Wrap(err)
}
if err != nil {
f.Logger.Errorw("failed to check file path for upload", "error", err)
return 0, errs.Wrap(err)
}
// check if the file exists
_, err = os.Stat(path)
pathDoesNotExists := errors.Is(err, fs.ErrNotExist)
if err != nil && !pathDoesNotExists {
f.Logger.Errorw("failed to get asset path info", "error", err)
return 0, errs.Wrap(err)
}
// a file or folder already exists
if !pathDoesNotExists {
filePathNotExistsMsg := fmt.Sprintf("file already exists at '%s'", path)
f.Logger.Debug(filePathNotExistsMsg)
return 0, validate.WrapErrorWithField(
errs.NewValidationError(
errors.New("a file already exists with that name"),
),
"file",
)
}
// Upload the file
err = g.SaveUploadedFile(file, path)
if err != nil {
f.Logger.Errorw("failed to save uploaded file", "error", err)
return 0, errs.Wrap(err)
}
f.Logger.Debugw("file uploaded", "error", path)
}
return len(files), nil
}
func (f *File) UploadFileLegacy(
ctx context.Context,
path string,
contents *bytes.Buffer,
overwrite bool,
) error {
f.Logger.Debugw("checking if file exists", "path", path)
err := f.checkFilePathIsValidForUpload(path)
if err != nil && !errors.Is(err, errs.ErrValidationFailed) {
return err
}
if err != nil {
f.Logger.Errorw("failed to check file path for upload", "error", err)
return err
}
// check if the file exists
_, err = os.Stat(path)
pathDoesNotExists := errors.Is(err, fs.ErrNotExist)
if err != nil && !pathDoesNotExists {
f.Logger.Errorw("failed to get asset path info", "error", err)
return err
}
// a file or folder already exists
if !overwrite && !pathDoesNotExists {
filePathNotExistsMsg := fmt.Sprintf("file already exists at '%s'", path)
f.Logger.Debug(filePathNotExistsMsg)
return validate.WrapErrorWithField(
errs.NewValidationError(
errors.New("a file already exists with that name"),
),
"file",
)
}
// Create directories if they don't exist
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
f.Logger.Errorw("failed to create directories", "error", err)
return err
}
if overwrite {
f.Logger.Debug("removing existing file...")
err = os.RemoveAll(path)
if err != nil {
f.Logger.Errorw("failed to remove existing file", "error", err)
return err
}
}
// #nosec
outFile, err := os.Create(path)
if err != nil {
f.Logger.Errorw("failed to create file", "error", err)
return err
}
// #nosec
_, err = contents.WriteTo(outFile)
_ = outFile.Close()
if err != nil {
f.Logger.Errorw("failed to write file", "error", err)
return err
}
f.Logger.Debugw("file uploaded", "path", path)
return nil
}
// Delete deletes a file
func (f *File) Delete(
path string,
) error {
err := os.Remove(path)
if err != nil {
f.Logger.Errorw("failed to delete file", "error", err)
return err
}
return nil
}
// DeleteAll deletes a file or folder recursively
func (f *File) DeleteAll(
path string,
) error {
err := os.RemoveAll(path)
if err != nil {
f.Logger.Errorw("failed to delete path", "error", err)
return err
}
return nil
}
// RemoveEmptyFolderRecursively folders recursively deletes all empty folders
// until it hits an non-empty folder or the root
func (f *File) RemoveEmptyFolderRecursively(
rootPath string,
path string,
) error {
f.Logger.Debugw("Checking if empty folders should be removed, root: %s, path: %s",
"rootPath", rootPath,
"path", path,
)
// check if the path is the root
if path == rootPath {
f.Logger.Debug("path is the root, stopping recursion")
return nil
}
// check if the path is empty
entries, err := os.ReadDir(path)
if err != nil {
f.Logger.Errorw("failed to read directory", "error", err)
return nil
}
if len(entries) > 0 {
f.Logger.Debug("path is not empty, stopping recursion")
return nil
}
// delete the empty folder
f.Logger.Debugw("deleting empty folder", "path", path)
err = os.Remove(path)
if err != nil {
f.Logger.Errorw("failed to delete empty folder", "error", err)
return err
}
// check the parent folder
parent := filepath.Dir(path)
return f.RemoveEmptyFolderRecursively(rootPath, parent)
}
// Upload uploads files using os.Root for security
func (f *File) Upload(
g *gin.Context,
files []*RootFileUpload,
) (int, error) {
for _, fileUpload := range files {
root := fileUpload.Root
relativePath := fileUpload.RelativePath
file := fileUpload.File
f.Logger.Debugw("checking if file path is valid", "path", relativePath)
// validate path is safe through root
_, err := root.Stat(relativePath)
if err != nil && !os.IsNotExist(err) {
f.Logger.Errorw("invalid file path", "path", relativePath, "error", err)
return 0, errs.Wrap(err)
}
// check if file already exists
pathDoesNotExist := os.IsNotExist(err)
if !pathDoesNotExist {
f.Logger.Debugw("file already exists", "path", relativePath)
return 0, errs.NewValidationError(fmt.Errorf("file already exists: %s", relativePath))
}
// create directory structure if needed
dirPath := filepath.Dir(relativePath)
if dirPath != "." {
// create directories step by step
parts := strings.Split(dirPath, "/")
currentPath := ""
for _, part := range parts {
if part != "" {
if currentPath == "" {
currentPath = part
} else {
currentPath = filepath.Join(currentPath, part)
}
// check if directory exists
_, err = root.Stat(currentPath)
if err != nil && !os.IsNotExist(err) {
f.Logger.Errorw("invalid directory path", "path", currentPath, "error", err)
return 0, errs.Wrap(err)
}
// create directory if it doesn't exist
if os.IsNotExist(err) {
err = root.Mkdir(currentPath, 0755)
if err != nil {
f.Logger.Errorw("failed to create directory", "path", currentPath, "error", err)
return 0, errs.Wrap(err)
}
f.Logger.Debugw("created directory", "path", currentPath)
}
}
}
}
// open file for writing through root
dst, err := root.OpenFile(relativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
f.Logger.Errorw("failed to create file", "path", relativePath, "error", err)
return 0, errs.Wrap(err)
}
defer dst.Close()
// open uploaded file
src, err := file.Open()
if err != nil {
f.Logger.Errorw("failed to open uploaded file", "error", err)
return 0, errs.Wrap(err)
}
defer src.Close()
// copy file content
_, err = io.Copy(dst, src)
if err != nil {
f.Logger.Errorw("failed to copy file content", "error", err)
return 0, errs.Wrap(err)
}
f.Logger.Debugw("file uploaded successfully", "path", relativePath)
}
return len(files), nil
}
// UploadFile uploads a file using os.Root for security
func (f *File) UploadFile(
root *os.Root,
relativePath string,
contents *bytes.Buffer,
overwrite bool,
) error {
f.Logger.Debugw("checking if file path is valid", "path", relativePath)
// validate path is safe through root
_, err := root.Stat(relativePath)
if err != nil && !os.IsNotExist(err) {
f.Logger.Errorw("invalid file path", "path", relativePath, "error", err)
return errs.Wrap(err)
}
// check if file exists
pathDoesNotExist := os.IsNotExist(err)
if !pathDoesNotExist && !overwrite {
f.Logger.Debugw("file already exists and overwrite is false", "path", relativePath)
return errs.NewValidationError(fmt.Errorf("file already exists: %s", relativePath))
}
// create directory structure if needed
dirPath := filepath.Dir(relativePath)
if dirPath != "." {
// create directories step by step
parts := strings.Split(dirPath, "/")
currentPath := ""
for _, part := range parts {
if part != "" {
if currentPath == "" {
currentPath = part
} else {
currentPath = filepath.Join(currentPath, part)
}
// check if directory exists
_, err = root.Stat(currentPath)
if err != nil && !os.IsNotExist(err) {
f.Logger.Errorw("invalid directory path", "path", currentPath, "error", err)
return errs.Wrap(err)
}
// create directory if it doesn't exist
if os.IsNotExist(err) {
err = root.Mkdir(currentPath, 0755)
if err != nil {
f.Logger.Errorw("failed to create directory", "path", currentPath, "error", err)
return errs.Wrap(err)
}
f.Logger.Debugw("created directory", "path", currentPath)
}
}
}
}
// open file for writing through root
dst, err := root.OpenFile(relativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
f.Logger.Errorw("failed to create file", "path", relativePath, "error", err)
return errs.Wrap(err)
}
defer dst.Close()
// write content
_, err = io.Copy(dst, contents)
if err != nil {
f.Logger.Errorw("failed to write file content", "error", err)
return errs.Wrap(err)
}
f.Logger.Debugw("file uploaded successfully", "path", relativePath)
return nil
}