Files

157 lines
4.3 KiB
Go

package storage
import (
"crypto/rand"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
const slugChars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
var safeFilenameRegex = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
func GenerateSlug(length int) string {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return "x" + strings.ToLower(slugChars[:length-1])
}
charsLen := byte(len(slugChars))
for i := 0; i < length; i++ {
b[i] = slugChars[b[i]%charsLen]
}
return string(b)
}
func SanitizeFilename(name string) string {
// Normalizar separadores de ruta (incluyendo Windows \)
normalized := strings.ReplaceAll(name, "\\", "/")
base := filepath.Base(normalized)
// Eliminar secuencias '..' para evitar cualquier ambigüedad
for strings.Contains(base, "..") {
base = strings.ReplaceAll(base, "..", ".")
}
sanitized := safeFilenameRegex.ReplaceAllString(base, "_")
// Evitar archivos ocultos o nombres que empiecen por punto
sanitized = strings.TrimLeft(sanitized, "._-")
if len(sanitized) > 100 {
ext := filepath.Ext(sanitized)
sanitized = sanitized[:100-len(ext)] + ext
}
if sanitized == "" || sanitized == "." {
sanitized = "archivo.bin"
}
return sanitized
}
func DetectMimeType(filename string, reader io.ReadSeeker) string {
ext := strings.ToLower(filepath.Ext(filename))
// 1. Extensiones peligrosas de scripts, código y plantillas: NUNCA ejecutables como text/html o php
switch ext {
case ".html", ".htm", ".shtml", ".xhtml", ".hta":
return "text/plain; charset=utf-8"
case ".php", ".phtml", ".php3", ".php4", ".php5", ".pht", ".phar", ".inc", ".cgi", ".pl", ".py", ".sh", ".bash", ".sql":
return "text/plain; charset=utf-8"
case ".js", ".mjs", ".ts", ".jsx", ".tsx":
return "text/plain; charset=utf-8"
case ".txt", ".log", ".env", ".ini", ".conf", ".yaml", ".yml", ".md", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".css":
return "text/plain; charset=utf-8"
case ".json":
return "application/json"
case ".pdf":
return "application/pdf"
case ".mp4":
return "video/mp4"
case ".webm":
return "video/webm"
case ".mp3":
return "audio/mpeg"
case ".wav":
return "audio/wav"
case ".ogg":
return "audio/ogg"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".webp":
return "image/webp"
case ".svg":
return "image/svg+xml"
}
// 2. Consulta MIME del sistema para otras extensiones
if m := mime.TypeByExtension(ext); m != "" {
// Doble verificación: neutralizar si el sistema operativo clasifica como HTML o PHP
if strings.HasPrefix(m, "text/html") || strings.Contains(m, "php") || strings.Contains(m, "javascript") {
return "text/plain; charset=utf-8"
}
return m
}
// 3. Sniff de primeros 512 bytes
if reader != nil {
buf := make([]byte, 512)
n, err := reader.Read(buf)
_, _ = reader.Seek(0, io.SeekStart)
if err == nil && n > 0 {
detected := http.DetectContentType(buf[:n])
if detected != "application/octet-stream" {
if strings.HasPrefix(detected, "text/html") {
return "text/plain; charset=utf-8"
}
return detected
}
}
}
return "application/octet-stream"
}
func SaveFile(uploadDir, slug, originalFilename string, src io.Reader) (string, int64, error) {
if err := os.MkdirAll(uploadDir, 0750); err != nil {
return "", 0, err
}
// Crear archivo preventivo .htaccess para bloquear cualquier motor de script si Apache apuntara aquí
htaccessPath := filepath.Join(uploadDir, ".htaccess")
if _, err := os.Stat(htaccessPath); os.IsNotExist(err) {
htaccessContent := `# Seguridad drop.sigmac.app: Desactivar ejecución de scripts
Options -ExecCGI -Indexes
<FilesMatch "\.(php|phtml|phar|pl|py|cgi|sh|bash)$">
Require all denied
</FilesMatch>
`
_ = os.WriteFile(htaccessPath, []byte(htaccessContent), 0644)
}
safeName := SanitizeFilename(originalFilename)
storedFilename := slug + "_" + safeName
destPath := filepath.Join(uploadDir, storedFilename)
// Crear con permisos 0640 (sin flag +x ejecutable)
dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return "", 0, err
}
defer dest.Close()
written, err := io.Copy(dest, src)
if err != nil {
_ = os.Remove(destPath)
return "", 0, err
}
return storedFilename, written, nil
}