mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 12:10:58 +00:00
75c570e951
- Secure CLI credential injection via AES-256-GCM encrypted env vars - API key management with fine-grained RBAC scopes - resolveAuth/requireAuth middleware across all 25+ HTTP handlers - In-memory API key cache with TTL, negative caching, pubsub invalidation - Sandbox-first execution (fails if unavailable, no silent fallback) - Credential scrubbing, constant-time token comparison, Admin-only CLI creds - SQL migration 000020: secure_cli_binaries + api_keys tables - 14 unit tests for cache and RBAC with race detector Closes #197
93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
package http
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/channels/media"
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
)
|
|
|
|
const (
|
|
// maxUploadSize is the default max upload size (50MB).
|
|
maxUploadSize int64 = 50 * 1024 * 1024
|
|
)
|
|
|
|
// MediaUploadHandler handles media file uploads for WebSocket clients.
|
|
type MediaUploadHandler struct {
|
|
token string
|
|
}
|
|
|
|
// NewMediaUploadHandler creates a media upload handler.
|
|
func NewMediaUploadHandler(token string) *MediaUploadHandler {
|
|
return &MediaUploadHandler{token: token}
|
|
}
|
|
|
|
// RegisterRoutes registers the upload endpoint.
|
|
func (h *MediaUploadHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("POST /v1/media/upload", h.auth(h.handleUpload))
|
|
}
|
|
|
|
func (h *MediaUploadHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
|
return requireAuth(h.token, "", next)
|
|
}
|
|
|
|
func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
|
|
|
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgFileTooLarge)})
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgMissingFileField)})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Sanitize filename: strip path, prevent traversal.
|
|
origName := filepath.Base(header.Filename)
|
|
if origName == "." || origName == "/" || strings.Contains(origName, "..") {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidFilename)})
|
|
return
|
|
}
|
|
|
|
ext := filepath.Ext(origName)
|
|
if ext == "" {
|
|
ext = ".bin"
|
|
}
|
|
|
|
// Save to temp with unique name.
|
|
tmpName := fmt.Sprintf("ws_upload_%d%s", time.Now().UnixNano(), ext)
|
|
tmpPath := filepath.Join(os.TempDir(), tmpName)
|
|
|
|
out, err := os.Create(tmpPath)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to create temp file")})
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
if _, err := io.Copy(out, file); err != nil {
|
|
os.Remove(tmpPath)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "failed to save file")})
|
|
return
|
|
}
|
|
|
|
mimeType := media.DetectMIMEType(origName)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"path": tmpPath,
|
|
"mime_type": mimeType,
|
|
"filename": origName,
|
|
})
|
|
}
|