Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 80 additions & 36 deletions components/backend/handlers/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"ambient-code-backend/git"
"ambient-code-backend/pathutil"

"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -61,7 +62,7 @@ func ContentGitPush(c *gin.Context) {
}

// Basic safety: repoDir must be under StateBaseDir
if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
log.Printf("contentGitPush: invalid repoPath resolved=%q stateBaseDir=%q", repoDir, StateBaseDir)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
return
Expand Down Expand Up @@ -101,7 +102,7 @@ func ContentGitAbandon(c *gin.Context) {
repoDir = StateBaseDir
}

if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
log.Printf("contentGitAbandon: invalid repoPath resolved=%q base=%q", repoDir, StateBaseDir)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
return
Expand All @@ -126,7 +127,7 @@ func ContentGitDiff(c *gin.Context) {
}

repoDir := filepath.Clean(filepath.Join(StateBaseDir, repoPath))
if !strings.HasPrefix(repoDir+string(os.PathSeparator), StateBaseDir+string(os.PathSeparator)) && repoDir != StateBaseDir {
if !pathutil.IsPathWithinBase(repoDir, StateBaseDir) && repoDir != StateBaseDir {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid repoPath"})
return
}
Expand Down Expand Up @@ -159,13 +160,13 @@ func ContentGitDiff(c *gin.Context) {
// ContentGitStatus handles GET /content/git-status?path=
func ContentGitStatus(c *gin.Context) {
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}

abs := filepath.Join(StateBaseDir, path)

// Check if directory exists
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
c.JSON(http.StatusOK, gin.H{
Expand Down Expand Up @@ -224,13 +225,13 @@ func ContentGitConfigureRemote(c *gin.Context) {
}

path := filepath.Clean("/" + body.Path)
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}

abs := filepath.Join(StateBaseDir, path)

// Check if directory exists
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
c.JSON(http.StatusBadRequest, gin.H{"error": "directory not found"})
Expand Down Expand Up @@ -301,13 +302,13 @@ func ContentGitSync(c *gin.Context) {
}

path := filepath.Clean("/" + body.Path)
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}

abs := filepath.Join(StateBaseDir, path)

// Check if git repo exists
gitDir := filepath.Join(abs, ".git")
if _, err := os.Stat(gitDir); err != nil {
Expand Down Expand Up @@ -345,12 +346,13 @@ func ContentWrite(c *gin.Context) {
log.Printf("ContentWrite: path=%q contentLen=%d encoding=%q StateBaseDir=%q", req.Path, len(req.Content), req.Encoding, StateBaseDir)

path := filepath.Clean("/" + strings.TrimSpace(req.Path))
if path == "/" || strings.Contains(path, "..") {
log.Printf("ContentWrite: invalid path rejected: path=%q", path)
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
log.Printf("ContentWrite: path traversal attempt rejected: path=%q abs=%q", path, abs)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
abs := filepath.Join(StateBaseDir, path)
log.Printf("ContentWrite: absolute path=%q", abs)

if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
Expand Down Expand Up @@ -385,12 +387,13 @@ func ContentRead(c *gin.Context) {
log.Printf("ContentRead: requested path=%q StateBaseDir=%q", c.Query("path"), StateBaseDir)
log.Printf("ContentRead: cleaned path=%q", path)

if path == "/" || strings.Contains(path, "..") {
log.Printf("ContentRead: invalid path rejected: path=%q", path)
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
log.Printf("ContentRead: path traversal attempt rejected: path=%q abs=%q", path, abs)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
abs := filepath.Join(StateBaseDir, path)
log.Printf("ContentRead: absolute path=%q", abs)

b, err := os.ReadFile(abs)
Expand All @@ -414,12 +417,13 @@ func ContentList(c *gin.Context) {
log.Printf("ContentList: cleaned path=%q", path)
log.Printf("ContentList: StateBaseDir=%q", StateBaseDir)

if path == "/" || strings.Contains(path, "..") {
log.Printf("ContentList: invalid path rejected: path=%q", path)
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
log.Printf("ContentList: path traversal attempt rejected: path=%q abs=%q", path, abs)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
abs := filepath.Join(StateBaseDir, path)
log.Printf("ContentList: absolute path=%q", abs)

info, err := os.Stat(abs)
Expand Down Expand Up @@ -672,7 +676,9 @@ func ContentGitMergeStatus(c *gin.Context) {
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))
branch := strings.TrimSpace(c.Query("branch"))

if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
Expand All @@ -681,8 +687,6 @@ func ContentGitMergeStatus(c *gin.Context) {
branch = "main"
}

abs := filepath.Join(StateBaseDir, path)

// Check if git repo exists
gitDir := filepath.Join(abs, ".git")
if _, err := os.Stat(gitDir); err != nil {
Expand Down Expand Up @@ -722,7 +726,9 @@ func ContentGitPull(c *gin.Context) {
}

path := filepath.Clean("/" + body.Path)
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
Expand All @@ -731,8 +737,6 @@ func ContentGitPull(c *gin.Context) {
body.Branch = "main"
}

abs := filepath.Join(StateBaseDir, path)

if err := GitPullRepo(c.Request.Context(), abs, body.Branch); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
Expand All @@ -757,7 +761,9 @@ func ContentGitPushToBranch(c *gin.Context) {
}

path := filepath.Clean("/" + body.Path)
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
Expand All @@ -770,8 +776,6 @@ func ContentGitPushToBranch(c *gin.Context) {
body.Message = "Session artifacts update"
}

abs := filepath.Join(StateBaseDir, path)

if err := GitPushToRepo(c.Request.Context(), abs, body.Branch, body.Message); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
Expand All @@ -795,7 +799,9 @@ func ContentGitCreateBranch(c *gin.Context) {
}

path := filepath.Clean("/" + body.Path)
if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
Expand All @@ -805,8 +811,6 @@ func ContentGitCreateBranch(c *gin.Context) {
return
}

abs := filepath.Join(StateBaseDir, path)

if err := GitCreateBranch(c.Request.Context(), abs, body.BranchName); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
Expand All @@ -820,13 +824,13 @@ func ContentGitCreateBranch(c *gin.Context) {
func ContentGitListBranches(c *gin.Context) {
path := filepath.Clean("/" + strings.TrimSpace(c.Query("path")))

if path == "/" || strings.Contains(path, "..") {
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}

abs := filepath.Join(StateBaseDir, path)

branches, err := GitListRemoteBranches(c.Request.Context(), abs)
if err != nil {
// Log actual error for debugging, but return generic message to avoid leaking internal details
Expand All @@ -837,3 +841,43 @@ func ContentGitListBranches(c *gin.Context) {

c.JSON(http.StatusOK, gin.H{"branches": branches})
}

// ContentDelete handles DELETE /content/delete when running in CONTENT_SERVICE_MODE
func ContentDelete(c *gin.Context) {
var req struct {
Path string `json:"path"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("ContentDelete: bind JSON failed: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("ContentDelete: path=%q StateBaseDir=%q", req.Path, StateBaseDir)

path := filepath.Clean("/" + strings.TrimSpace(req.Path))
abs := filepath.Join(StateBaseDir, path)
// Verify abs is within StateBaseDir to prevent path traversal
if !pathutil.IsPathWithinBase(abs, StateBaseDir) {
log.Printf("ContentDelete: path traversal attempt rejected: path=%q abs=%q", path, abs)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
log.Printf("ContentDelete: absolute path=%q", abs)

// Check if file exists
if _, err := os.Stat(abs); os.IsNotExist(err) {
log.Printf("ContentDelete: file not found: %q", abs)
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
return
}

// Delete the file
if err := os.Remove(abs); err != nil {
log.Printf("ContentDelete: delete failed for %q: %v", abs, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete file"})
return
}

log.Printf("ContentDelete: successfully deleted %q", abs)
c.JSON(http.StatusOK, gin.H{"message": "file deleted successfully"})
}
Loading
Loading