Files
ArmA-3-web-server/backend/internal/api/mods.go
T
MrFastwind 4d6f162b8f fix: code quality, memory safety, and install improvements
Critical fixes:
- Fix Dockerfile: reorder stages so frontend assets embed into Go binary
- Fix Go version 1.25 (nonexistent) to 1.24 across Dockerfile, go.mod, CI
- Add graceful game server shutdown on SIGTERM/SIGINT
- Order startup tasks: updates complete before auto-start
- Fix TOCTOU race in UpdateSettings with atomic Update() method

Security:
- Add optional AUTH_TOKEN bearer auth middleware on API/WS routes
- Fix path traversal in DeleteMod using filepath.Rel instead of HasPrefix
- Add input validation for IPPort, ServerParameters, ScheduledUpdate

Memory safety:
- Cap RPT buffer allocation to 64KB to prevent OOM on large logs
- Cap GetLog file read to 10MB
- Fix context cancel leak in SteamCmdManager.run()
- Remove data-raced cancel field in steamcmd.go
- Atomic file writes (write-temp-then-rename) across all managers

Reliability:
- Log save errors in ProcessManager.Stop()
- Atomic file writes prevent corruption on crash

Tests:
- Add mod_manager_test.go (12 tests: ListWorkshopMods, ListLocalMods,
  BuildUsageMap, RemoveMod, dirSize)
- Add scheduler_test.go (6 tests: Start/Stop, Refresh with empty,
  invalid, valid, and replaced cron expressions)
- Add TestRestart to server_process_test.go

CI/Docs:
- Add -race flag to go test in CI and Makefile
- Add npm lint step to CI
- Add Go/npm module caching to CI
- Update README: prerequisites, AUTH_TOKEN/GIN_MODE/SERVERS_DIR docs,
  fix manual quickstart to use make build
2026-07-25 02:47:04 +02:00

111 lines
2.6 KiB
Go

package api
import (
"net/http"
"path/filepath"
"strings"
"arma3-web-server/internal/services"
"github.com/gin-gonic/gin"
)
type deleteModInput struct {
Path string `json:"path" binding:"required"`
}
func (h *Handler) ListMods(c *gin.Context) {
workshopMods := services.ListWorkshopMods(h.serverfileDir)
localMods := services.ListLocalMods(h.modsDir)
usedWorkshop, usedLocal := services.BuildUsageMap(h.modlists)
for i := range workshopMods {
if lists, ok := usedWorkshop[workshopMods[i].ID]; ok {
workshopMods[i].InUse = true
workshopMods[i].Modlists = lists
// pull name from first modlist that has it
if workshopMods[i].Name == "" {
workshopMods[i].Name = lists[0]
}
}
}
for i := range localMods {
// check both the display name and the @name
if lists, ok := usedLocal[localMods[i].ID]; ok {
localMods[i].InUse = true
localMods[i].Modlists = lists
} else if lists, ok := usedLocal[localMods[i].Name]; ok {
localMods[i].InUse = true
localMods[i].Modlists = lists
}
}
all := append(workshopMods, localMods...)
if all == nil {
all = []services.ModInfo{}
}
c.JSON(http.StatusOK, all)
}
func isUnderDir(path, dir string) bool {
rel, err := filepath.Rel(dir, path)
if err != nil {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func (h *Handler) DeleteMod(c *gin.Context) {
var input deleteModInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
absPath, err := filepath.Abs(input.Path)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"})
return
}
if !isUnderDir(absPath, h.serverfileDir) && !isUnderDir(absPath, h.modsDir) {
c.JSON(http.StatusForbidden, gin.H{"error": "path outside allowed directories"})
return
}
if err := services.RemoveMod(absPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusNoContent, nil)
}
func (h *Handler) CleanupMods(c *gin.Context) {
workshopMods := services.ListWorkshopMods(h.serverfileDir)
localMods := services.ListLocalMods(h.modsDir)
usedWorkshop, usedLocal := services.BuildUsageMap(h.modlists)
var deleted int
for _, m := range workshopMods {
if _, ok := usedWorkshop[m.ID]; !ok {
if err := services.RemoveMod(m.Path); err == nil {
deleted++
}
}
}
for _, m := range localMods {
if _, ok := usedLocal[m.ID]; !ok {
if _, ok2 := usedLocal[m.Name]; !ok2 {
if err := services.RemoveMod(m.Path); err == nil {
deleted++
}
}
}
}
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
}