package handler
import (
"fmt"
"mime"
"os"
"path/filepath"
"slices"
"strings"
"github.com/gabriel-vasile/mimetype"
)
// isPathInAllowedDirs checks if a path is within any of the allowed directories
func (fs *FilesystemHandler) isPathInAllowedDirs(path string) bool {
// Ensure path is absolute and clean
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
// Add trailing separator to ensure we're checking a directory or a file within a directory
// and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar)
if !strings.HasSuffix(absPath, string(filepath.Separator)) {
// If it's a file, we need to check its directory
if info, err := os.Stat(absPath); err == nil && !info.IsDir() {
absPath = filepath.Dir(absPath) + string(filepath.Separator)
} else {
absPath = absPath + string(filepath.Separator)
}
}
// Check if the path is within any of the allowed directories
for _, dir := range fs.allowedDirs {
if strings.HasPrefix(absPath, dir) {
return true
}
}
return false
}
func (fs *FilesystemHandler) validatePath(requestedPath string) (string, error) {
// Always convert to absolute path first
abs, err := filepath.Abs(requestedPath)
if err != nil {
return "", fmt.Errorf("invalid path: %w", err)
}
// Check if path is within allowed directories
if !fs.isPathInAllowedDirs(abs) {
return "", fmt.Errorf(
"access denied - path outside allowed directories: %s",
abs,
)
}
// Handle symlinks
realPath, err := filepath.EvalSymlinks(abs)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
// For new files, check parent directory
parent := filepath.Dir(abs)
realParent, err := filepath.EvalSymlinks(parent)
if err != nil {
return "", fmt.Errorf("parent directory does not exist: %s", parent)
}
if !fs.isPathInAllowedDirs(realParent) {
return "", fmt.Errorf(
"access denied - parent directory outside allowed directories",
)
}
return abs, nil
}
// Check if the real path (after resolving symlinks) is still within allowed directories
if !fs.isPathInAllowedDirs(realPath) {
return "", fmt.Errorf(
"access denied - symlink target outside allowed directories",
)
}
return realPath, nil
}
// detectMimeType tries to determine the MIME type of a file
func detectMimeType(path string) string {
// Use mimetype library for more accurate detection
mtype, err := mimetype.DetectFile(path)
if err != nil {
// Fallback to extension-based detection if file can't be read
ext := filepath.Ext(path)
if ext != "" {
mimeType := mime.TypeByExtension(ext)
if mimeType != "" {
return mimeType
}
}
return "application/octet-stream" // Default
}
return mtype.String()
}
// isTextFile determines if a file is likely a text file based on MIME type
func isTextFile(mimeType string) bool {
// Check for common text MIME types
if strings.HasPrefix(mimeType, "text/") {
return true
}
// Common application types that are text-based
textApplicationTypes := []string{
"application/json",
"application/xml",
"application/javascript",
"application/x-javascript",
"application/typescript",
"application/x-typescript",
"application/x-yaml",
"application/yaml",
"application/toml",
"application/x-sh",
"application/x-shellscript",
}
if slices.Contains(textApplicationTypes, mimeType) {
return true
}
// Check for +format types
if strings.Contains(mimeType, "+xml") ||
strings.Contains(mimeType, "+json") ||
strings.Contains(mimeType, "+yaml") {
return true
}
// Common code file types that might be misidentified
if strings.HasPrefix(mimeType, "text/x-") {
return true
}
if strings.HasPrefix(mimeType, "application/x-") &&
(strings.Contains(mimeType, "script") ||
strings.Contains(mimeType, "source") ||
strings.Contains(mimeType, "code")) {
return true
}
return false
}
// isImageFile determines if a file is an image based on MIME type
func isImageFile(mimeType string) bool {
return strings.HasPrefix(mimeType, "image/") ||
(mimeType == "application/xml" && strings.HasSuffix(strings.ToLower(mimeType), ".svg"))
}