package filemanager import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "imuslab.com/zoraxy/mod/utils" ) /* File Manager This is a simple package that handles file management under the web server directory */ type FileManager struct { Directory string } // Create a new file manager with directory as root func NewFileManager(directory string) *FileManager { return &FileManager{ Directory: directory, } } // Handle listing of a given directory func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) { directory, err := utils.GetPara(r, "dir") if err != nil { utils.SendErrorResponse(w, "invalid directory given") return } // Construct the absolute path to the target directory targetDir := filepath.Join(fm.Directory, directory) // Clean path to prevent path escape #274 isValidRequest := validatePathEscape(targetDir, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Open the target directory dirEntries, err := os.ReadDir(targetDir) if err != nil { utils.SendErrorResponse(w, "unable to open directory") return } // Create a slice to hold the file information var files []map[string]interface{} = []map[string]interface{}{} // Iterate through the directory entries for _, dirEntry := range dirEntries { fileInfo := make(map[string]interface{}) fileInfo["filename"] = dirEntry.Name() fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name()) fileInfo["isDir"] = dirEntry.IsDir() // Get file size and last modified time finfo, err := dirEntry.Info() if err != nil { //unable to load its info. Skip this file continue } fileInfo["lastModified"] = finfo.ModTime().Unix() if !dirEntry.IsDir() { // If it's a file, get its size fileInfo["size"] = finfo.Size() } else { // If it's a directory, set size to 0 fileInfo["size"] = 0 } // Append file info to the list files = append(files, fileInfo) } // Serialize the file info slice to JSON jsonData, err := json.Marshal(files) if err != nil { utils.SendErrorResponse(w, "unable to marshal JSON") return } // Set response headers and send the JSON response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonData) } // Handle upload of a file (multi-part), 25MB max func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) { dir, err := utils.PostPara(r, "dir") if err != nil { log.Println("no dir given") utils.SendErrorResponse(w, "invalid dir given") return } // Parse the multi-part form data err = r.ParseMultipartForm(25 << 20) if err != nil { utils.SendErrorResponse(w, "unable to parse form data") return } // Get the uploaded file file, fheader, err := r.FormFile("file") if err != nil { log.Println(err.Error()) utils.SendErrorResponse(w, "unable to get uploaded file") return } defer file.Close() // Specify the directory where you want to save the uploaded file uploadDir := filepath.Join(fm.Directory, dir) // Clean path to prevent path escape #274 isValidRequest := validatePathEscape(uploadDir, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } if !utils.FileExists(uploadDir) { utils.SendErrorResponse(w, "upload target directory not exists") return } filename := sanitizeFilename(fheader.Filename) if !isValidFilename(filename) { utils.SendErrorResponse(w, "filename contain invalid or reserved characters") return } // Create the file on the server filePath := filepath.Join(uploadDir, filepath.Base(filename)) out, err := os.Create(filePath) if err != nil { utils.SendErrorResponse(w, "unable to create file on the server") return } defer out.Close() // Copy the uploaded file to the server _, err = io.Copy(out, file) if err != nil { utils.SendErrorResponse(w, "unable to copy file to server") return } // Respond with a success message or appropriate response utils.SendOK(w) } // Handle download of a selected file, serve with content dispose header func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) { filename, err := utils.GetPara(r, "file") if err != nil { utils.SendErrorResponse(w, "invalid filepath given") return } filePath := filepath.Join(fm.Directory, filename) // Clean path to prevent path escape #274 isValidRequest := validatePathEscape(filePath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } previewMode, _ := utils.GetPara(r, "preview") if previewMode == "true" { // Serve the file using http.ServeFile http.ServeFile(w, r, filePath) } else { // Trigger a download with content disposition headers w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename)) http.ServeFile(w, r, filePath) } } // HandleNewFolder creates a new folder in the specified directory func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) { // Parse the directory name from the request dirName, err := utils.PostPara(r, "path") if err != nil { utils.SendErrorResponse(w, "invalid directory name") return } //Prevent path escape dirName = strings.ReplaceAll(dirName, "\\", "/") dirName = strings.ReplaceAll(dirName, "../", "") // Specify the directory where you want to create the new folder newFolderPath := filepath.Join(fm.Directory, dirName) isValidRequest := validatePathEscape(newFolderPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Check if the folder already exists if _, err := os.Stat(newFolderPath); os.IsNotExist(err) { // Create the new folder err := os.Mkdir(newFolderPath, os.ModePerm) if err != nil { utils.SendErrorResponse(w, "unable to create the new folder") return } // Respond with a success message or appropriate response utils.SendOK(w) } else { // If the folder already exists, respond with an error utils.SendErrorResponse(w, "folder already exists") } } // HandleFileCopy copies a file or directory from the source path to the destination path func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) { // Parse the source and destination paths from the request srcPath, err := utils.PostPara(r, "srcpath") if err != nil { utils.SendErrorResponse(w, "invalid source path") return } destPath, err := utils.PostPara(r, "destpath") if err != nil { utils.SendErrorResponse(w, "invalid destination path") return } // Validate and sanitize the source and destination paths srcPath = filepath.Clean(srcPath) destPath = filepath.Clean(destPath) // Construct the absolute paths absSrcPath := filepath.Join(fm.Directory, srcPath) absDestPath := filepath.Join(fm.Directory, destPath) //Make sure the copy source and dest are within web directory folder isValidRequest := validatePathEscape(absSrcPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } isValidRequest = validatePathEscape(absDestPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Check if the source path exists if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { utils.SendErrorResponse(w, "source path does not exist") return } // Check if the destination path exists if _, err := os.Stat(absDestPath); os.IsNotExist(err) { utils.SendErrorResponse(w, "destination path does not exist") return } //Join the name to create final paste filename absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath)) //Reject opr if already exists if utils.FileExists(absDestPath) { utils.SendErrorResponse(w, "target already exists") return } // Perform the copy operation based on whether the source is a file or directory if isDir(absSrcPath) { // Recursive copy for directories err := copyDirectory(absSrcPath, absDestPath) if err != nil { utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err)) return } } else { // Copy a single file err := copyFile(absSrcPath, absDestPath) if err != nil { utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err)) return } } utils.SendOK(w) } func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) { // Parse the source and destination paths from the request srcPath, err := utils.PostPara(r, "srcpath") if err != nil { utils.SendErrorResponse(w, "invalid source path") return } destPath, err := utils.PostPara(r, "destpath") if err != nil { utils.SendErrorResponse(w, "invalid destination path") return } // Validate and sanitize the source and destination paths srcPath = filepath.Clean(srcPath) destPath = filepath.Clean(destPath) // Construct the absolute paths absSrcPath := filepath.Join(fm.Directory, srcPath) absDestPath := filepath.Join(fm.Directory, destPath) //Make sure move source and target are within web server directory isValidRequest := validatePathEscape(absSrcPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } isValidRequest = validatePathEscape(absDestPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Check if the source path exists if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { utils.SendErrorResponse(w, "source path does not exist") return } // Check if the destination path exists if _, err := os.Stat(absDestPath); !os.IsNotExist(err) { utils.SendErrorResponse(w, "destination path already exists") return } // Rename the source to the destination err = os.Rename(absSrcPath, absDestPath) if err != nil { utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err)) return } utils.SendOK(w) } func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) { // Parse the target file or directory path from the request filePath, err := utils.GetPara(r, "file") if err != nil { utils.SendErrorResponse(w, "invalid file path") return } // Construct the absolute path to the target file or directory absPath := filepath.Join(fm.Directory, filePath) isValidRequest := validatePathEscape(absPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Check if the target path exists _, err = os.Stat(absPath) if err != nil { utils.SendErrorResponse(w, "file or directory does not exist") return } // Initialize a map to hold file properties fileProps := make(map[string]interface{}) fileProps["filename"] = filepath.Base(absPath) fileProps["filepath"] = filePath fileProps["isDir"] = isDir(absPath) // Get file size and last modified time finfo, err := os.Stat(absPath) if err != nil { utils.SendErrorResponse(w, "unable to retrieve file properties") return } fileProps["lastModified"] = finfo.ModTime().Unix() if !isDir(absPath) { // If it's a file, get its size fileProps["size"] = finfo.Size() } else { // If it's a directory, calculate its total size containing all child files and folders totalSize, err := calculateDirectorySize(absPath) if err != nil { utils.SendErrorResponse(w, "unable to calculate directory size") return } fileProps["size"] = totalSize } // Count the number of sub-files and sub-folders numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath) if err != nil { utils.SendErrorResponse(w, "unable to count sub-files and sub-folders") return } fileProps["fileCounts"] = numSubFiles fileProps["folderCounts"] = numSubFolders // Serialize the file properties to JSON jsonData, err := json.Marshal(fileProps) if err != nil { utils.SendErrorResponse(w, "unable to marshal JSON") return } // Set response headers and send the JSON response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(jsonData) } // HandleFileDelete deletes a file or directory func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) { // Parse the target file or directory path from the request filePath, err := utils.PostPara(r, "target") if err != nil { utils.SendErrorResponse(w, "invalid file path") return } // Construct the absolute path to the target file or directory absPath := filepath.Join(fm.Directory, filePath) isValidRequest := validatePathEscape(absPath, fm.Directory) if !isValidRequest { http.Error(w, "403 - Forbidden", http.StatusForbidden) return } // Check if the target path exists _, err = os.Stat(absPath) if err != nil { utils.SendErrorResponse(w, "file or directory does not exist") return } // Delete the file or directory err = os.RemoveAll(absPath) if err != nil { utils.SendErrorResponse(w, "error deleting file or directory") return } // Respond with a success message or appropriate response utils.SendOK(w) } // Return true if the path is within the root path func validatePathEscape(reqestPath string, rootPath string) bool { reqestPath = filepath.ToSlash(filepath.Clean(reqestPath)) rootPath = filepath.ToSlash(filepath.Clean(rootPath)) requestPathAbs, err := filepath.Abs(reqestPath) if err != nil { return false } rootPathAbs, err := filepath.Abs(rootPath) if err != nil { return false } if strings.HasPrefix(requestPathAbs, rootPathAbs) { return true } return false }