resolve server creation issues

This commit is contained in:
2026-03-19 00:25:25 +01:00
parent 863918a611
commit df2a826624
17 changed files with 179 additions and 121 deletions

View File

@@ -64,12 +64,9 @@ PASSWORD=change-this-default-admin-password
# Docker image name for ACC server containers (built from docker/acc-server/Dockerfile) # Docker image name for ACC server containers (built from docker/acc-server/Dockerfile)
# ACC_IMAGE=acc-wine:latest # ACC_IMAGE=acc-wine:latest
# Host-side path for the shared ACC game installation volume. # Base path where per-server game installations are stored.
# IMPORTANT: must be the HOST path — ACC containers are Docker siblings of the manager. # Each server gets its own subdirectory: ACC_SERVERS_PATH/<service-name>/
# ACC_GAME_VOLUME=/data/acc-game # ACC_SERVERS_PATH=/data/servers
# Host-side base path for per-server config directories.
# ACC_CONFIG_BASE_PATH=/data/configs
# Port pool for Docker-mode ACC server containers (one port per server). # Port pool for Docker-mode ACC server containers (one port per server).
# PORT_POOL_MIN=9600 # PORT_POOL_MIN=9600

View File

@@ -50,14 +50,13 @@ WORKDIR /app
COPY --from=builder /acc-server-manager /app/acc-server-manager COPY --from=builder /acc-server-manager /app/acc-server-manager
# Persistent data directories. # Persistent data directories.
RUN mkdir -p /data/configs /data/acc-game /data/db RUN mkdir -p /data/servers /data/db
ENV PLATFORM=docker ENV PLATFORM=docker
ENV PORT=3000 ENV PORT=3000
ENV DB_NAME=/data/db/acc.db ENV DB_NAME=/data/db/acc.db
ENV STEAMCMD_LINUX_PATH=/usr/games/steamcmd ENV STEAMCMD_LINUX_PATH=/usr/games/steamcmd
ENV ACC_GAME_VOLUME=/data/acc-game ENV ACC_SERVERS_PATH=/data/servers
ENV ACC_CONFIG_BASE_PATH=/data/configs
EXPOSE 3000 EXPOSE 3000

View File

@@ -11,25 +11,22 @@ services:
environment: environment:
PLATFORM: docker PLATFORM: docker
DB_NAME: /data/db/acc.db DB_NAME: /data/db/acc.db
ACC_GAME_VOLUME: /var/lib/docker/volumes/acc-server-manager_acc-game/_data
ACC_CONFIG_BASE_PATH: /var/lib/docker/volumes/acc-server-manager_acc-configs/_data
STEAMCMD_LINUX_PATH: /usr/games/steamcmd STEAMCMD_LINUX_PATH: /usr/games/steamcmd
PORT_POOL_MIN: "9600" PORT_POOL_MIN: "9600"
PORT_POOL_MAX: "9700" PORT_POOL_MAX: "9700"
ACC_IMAGE: acc-wine:latest ACC_IMAGE: acc-wine:latest
ACC_SERVERS_PATH: /data/servers
env_file: env_file:
- .env - .env
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- acc-game:/data/acc-game - acc-servers:/data/servers
- acc-configs:/data/configs
- acc-db:/data/db - acc-db:/data/db
networks: networks:
- acc-net - acc-net
volumes: volumes:
acc-game: acc-servers:
acc-configs:
acc-db: acc-db:
networks: networks:

View File

@@ -24,10 +24,8 @@ WORKDIR /home/accserver
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
# Volumes (bind-mounted by DockerRuntime.Create): # Bind-mounted by DockerRuntime.Create: per-server game install (rw)
# /acc/game — shared game files, mounted read-only (ACC_GAME_VOLUME) VOLUME ["/acc/game"]
# /acc/cfg — per-server config dir, mounted read-write (server.GetConfigPath())
VOLUME ["/acc/game", "/acc/cfg"]
# Default server port — overridden at runtime via SERVER_PORT env var. # Default server port — overridden at runtime via SERVER_PORT env var.
ENV SERVER_PORT=9600 ENV SERVER_PORT=9600

View File

@@ -12,6 +12,5 @@ trap cleanup SIGTERM SIGINT
echo "Starting ACC server on port ${SERVER_PORT}..." echo "Starting ACC server on port ${SERVER_PORT}..."
exec wine /acc/game/server/accServer.exe \ exec wine /acc/game/accServer.exe \
-serverCfgPath /acc/cfg \
-port "${SERVER_PORT}" -port "${SERVER_PORT}"

View File

@@ -33,6 +33,7 @@ func Init(di *dig.Container, app *fiber.App) {
System: groups.Group("/system"), System: groups.Group("/system"),
WebSocket: groups.Group("/ws"), WebSocket: groups.Group("/ws"),
Leaderboard: serverIdGroup.Group("/leaderboard"), Leaderboard: serverIdGroup.Group("/leaderboard"),
Steam: groups.Group("/steam"),
} }
accessKeyMiddleware := middleware.NewAccessKeyMiddleware() accessKeyMiddleware := middleware.NewAccessKeyMiddleware()

View File

@@ -64,4 +64,9 @@ func InitializeControllers(c *dig.Container) {
if err != nil { if err != nil {
logging.Panic("unable to initialize leaderboard controller") logging.Panic("unable to initialize leaderboard controller")
} }
err = c.Invoke(NewSteamController)
if err != nil {
logging.Panic("unable to initialize steam controller")
}
} }

57
local/controller/steam.go Normal file
View File

@@ -0,0 +1,57 @@
package controller
import (
"acc-server-manager/local/middleware"
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
)
type SteamController struct {
steamService *service.SteamService
auth *middleware.AuthMiddleware
errorHandler *error_handler.ControllerErrorHandler
}
func NewSteamController(steamService *service.SteamService, auth *middleware.AuthMiddleware, routeGroups *common.RouteGroups) *SteamController {
sc := &SteamController{
steamService: steamService,
auth: auth,
errorHandler: error_handler.NewControllerErrorHandler(),
}
routeGroups.Steam.Use(auth.Authenticate, auth.RequireSuperAdmin())
routeGroups.Steam.Post("/credentials", sc.SetCredentials)
return sc
}
func (sc *SteamController) SetCredentials(c *fiber.Ctx) error {
type request struct {
Username string `json:"username"`
Password string `json:"password"`
}
var req request
if err := c.BodyParser(&req); err != nil {
return sc.errorHandler.HandleParsingError(c, err)
}
creds := &model.SteamCredentials{
Username: req.Username,
Password: req.Password,
}
if err := creds.Validate(); err != nil {
return sc.errorHandler.HandleServiceError(c, err)
}
if err := sc.steamService.SaveCredentials(c.UserContext(), creds); err != nil {
return sc.errorHandler.HandleServiceError(c, err)
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": creds.ID})
}

View File

@@ -178,6 +178,23 @@ func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler
} }
} }
func (m *AuthMiddleware) RequireSuperAdmin() fiber.Handler {
return func(ctx *fiber.Ctx) error {
if os.Getenv("TESTING_ENV") == "true" {
return ctx.Next()
}
userInfo, ok := ctx.Locals("userInfo").(*CachedUserInfo)
if !ok || userInfo.RoleName != "Super Admin" {
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Forbidden",
})
}
return ctx.Next()
}
}
func (m *AuthMiddleware) AuthRateLimit() fiber.Handler { func (m *AuthMiddleware) AuthRateLimit() fiber.Handler {
return m.securityMW.AuthRateLimit() return m.securityMW.AuthRateLimit()
} }

View File

@@ -31,9 +31,6 @@ func NewDockerRuntime(client *dockerclient.Client, repo *repository.ServerReposi
func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error { func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error {
image := env.GetACCImage() image := env.GetACCImage()
gameVolume := env.GetACCGameVolume()
configPath := server.GetConfigPath()
portStr := strconv.Itoa(server.Port) portStr := strconv.Itoa(server.Port)
natPort, err := nat.NewPort("tcp", portStr) natPort, err := nat.NewPort("tcp", portStr)
if err != nil { if err != nil {
@@ -54,8 +51,7 @@ func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error
} }
binds := []string{ binds := []string{
fmt.Sprintf("%s:/acc/game:ro", gameVolume), fmt.Sprintf("%s:/acc/game:rw", server.GetServerPath()),
fmt.Sprintf("%s:/acc/cfg:rw", configPath),
} }
resp, err := r.client.ContainerCreate(ctx, &container.Config{ resp, err := r.client.ContainerCreate(ctx, &container.Config{

View File

@@ -200,8 +200,8 @@ func (s *ServerService) GenerateServerPath(server *model.Server) {
} }
if env.IsDockerPlatform() { if env.IsDockerPlatform() {
server.Path = filepath.Join(env.GetACCConfigBasePath(), server.ServiceName) server.Path = filepath.Join(env.GetACCServersPath(), server.ServiceName)
server.FromSteamCMD = false server.FromSteamCMD = true
} else { } else {
steamCMDPath := env.GetSteamCMDDirPath() steamCMDPath := env.GetSteamCMDDirPath()
server.Path = server.GenerateServerPath(steamCMDPath) server.Path = server.GenerateServerPath(steamCMDPath)
@@ -369,11 +369,7 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
important: true, important: true,
description: "Server files downloaded successfully", description: "Server files downloaded successfully",
callback: func() (string, error) { callback: func() (string, error) {
installPath := server.Path if err := s.steamService.InstallServerWithWebSocket(ctx, server.Path, &server.ID, s.webSocketService); err != nil {
if env.IsDockerPlatform() {
installPath = env.GetACCGameVolume()
}
if err := s.steamService.InstallServerWithWebSocket(ctx, installPath, &server.ID, s.webSocketService); err != nil {
return "", fmt.Errorf("failed to install server: %v", err) return "", fmt.Errorf("failed to install server: %v", err)
} }
return "Server files downloaded successfully", nil return "Server files downloaded successfully", nil

View File

@@ -105,10 +105,12 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
return err return err
} }
if !env.IsDockerPlatform() {
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil { if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true) wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
return fmt.Errorf("invalid installation path: %v", err) return fmt.Errorf("invalid installation path: %v", err)
} }
}
absPath, err := filepath.Abs(installPath) absPath, err := filepath.Abs(installPath)
if err != nil { if err != nil {
@@ -148,6 +150,10 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
steamCMDArgs = append(steamCMDArgs, "anonymous") steamCMDArgs = append(steamCMDArgs, "anonymous")
} }
if env.IsDockerPlatform() {
steamCMDArgs = append(steamCMDArgs, "+@sSteamCmdForcePlatformType", "windows")
}
steamCMDArgs = append(steamCMDArgs, steamCMDArgs = append(steamCMDArgs,
"+app_update", ACCServerAppID, "+app_update", ACCServerAppID,
"validate", "validate",
@@ -229,10 +235,12 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
return err return err
} }
if !env.IsDockerPlatform() {
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil { if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true) outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
return fmt.Errorf("invalid installation path: %v", err) return fmt.Errorf("invalid installation path: %v", err)
} }
}
absPath, err := filepath.Abs(installPath) absPath, err := filepath.Abs(installPath)
if err != nil { if err != nil {
@@ -272,6 +280,10 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
steamCMDArgs = append(steamCMDArgs, "anonymous") steamCMDArgs = append(steamCMDArgs, "anonymous")
} }
if env.IsDockerPlatform() {
steamCMDArgs = append(steamCMDArgs, "+@sSteamCmdForcePlatformType", "windows")
}
steamCMDArgs = append(steamCMDArgs, steamCMDArgs = append(steamCMDArgs,
"+app_update", ACCServerAppID, "+app_update", ACCServerAppID,
"validate", "validate",

View File

@@ -15,6 +15,7 @@ type WebSocketConnection struct {
conn *websocket.Conn conn *websocket.Conn
serverID *uuid.UUID serverID *uuid.UUID
userID *uuid.UUID userID *uuid.UUID
mu sync.Mutex
} }
type WebSocketService struct { type WebSocketService struct {
@@ -131,7 +132,7 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
ws.connections.Range(func(key, value interface{}) bool { ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok { if wsConn, ok := value.(*WebSocketConnection); ok {
if wsConn.serverID != nil && *wsConn.serverID == serverID { if wsConn.serverID != nil && *wsConn.serverID == serverID {
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil { if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err) logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
ws.RemoveConnection(key.(string)) ws.RemoveConnection(key.(string))
} else { } else {
@@ -145,7 +146,7 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
if !sentToAssociatedConnections && (message.Type == model.MessageTypeStep || message.Type == model.MessageTypeError || message.Type == model.MessageTypeComplete) { if !sentToAssociatedConnections && (message.Type == model.MessageTypeStep || message.Type == model.MessageTypeError || message.Type == model.MessageTypeComplete) {
ws.connections.Range(func(key, value interface{}) bool { ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok { if wsConn, ok := value.(*WebSocketConnection); ok {
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil { if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err) logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
ws.RemoveConnection(key.(string)) ws.RemoveConnection(key.(string))
} }
@@ -165,7 +166,7 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
ws.connections.Range(func(key, value interface{}) bool { ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok { if wsConn, ok := value.(*WebSocketConnection); ok {
if wsConn.userID != nil && *wsConn.userID == userID { if wsConn.userID != nil && *wsConn.userID == userID {
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil { if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err) logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
ws.RemoveConnection(key.(string)) ws.RemoveConnection(key.(string))
} }
@@ -175,6 +176,12 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
}) })
} }
func (wc *WebSocketConnection) writeMessage(messageType int, data []byte) error {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.conn.WriteMessage(messageType, data)
}
func (ws *WebSocketService) GetActiveConnections() int { func (ws *WebSocketService) GetActiveConnections() int {
count := 0 count := 0
ws.connections.Range(func(key, value interface{}) bool { ws.connections.Range(func(key, value interface{}) bool {

View File

@@ -123,11 +123,12 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
stdoutScanner := bufio.NewScanner(stdout) stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr) stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan outputLine, 100) outputChan := make(chan outputLine, 10000)
readersDone := make(chan struct{}, 2) readersDone := make(chan struct{}, 2)
steamConsoleStarted := false steamConsoleStarted := false
tfaRequestCreated := false tfaRequestCreated := false
tfaAutoCompleted := false
go func() { go func() {
defer func() { readersDone <- struct{}{} }() defer func() { readersDone <- struct{}{} }()
@@ -182,13 +183,14 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
if readersFinished == 2 { if readersFinished == 2 {
close(outputChan) close(outputChan)
for lineData := range outputChan { for lineData := range outputChan {
if e.is2FAPrompt(lineData.text) { if e.is2FAPrompt(lineData.text) && !tfaRequestCreated {
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil { tfaRequestCreated = true
go func(prompt string) {
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err) logging.Error("Failed to handle 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true) e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
} }
}(lineData.text)
} }
} }
return return
@@ -207,33 +209,34 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
if e.is2FAPrompt(lineData.text) { if e.is2FAPrompt(lineData.text) {
if !tfaRequestCreated { if !tfaRequestCreated {
tfaRequestCreated = true
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false) e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil { go func(prompt string) {
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err) logging.Error("Failed to handle 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true) e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
done <- err
return
} }
tfaRequestCreated = true }(lineData.text)
} }
} }
if tfaRequestCreated && e.isSteamContinuing(lineData.text) { if tfaRequestCreated && !tfaAutoCompleted && e.isSteamContinuing(lineData.text) {
logging.Info("Steam CMD appears to have continued after 2FA confirmation") logging.Info("Steam CMD appears to have continued after 2FA confirmation")
tfaAutoCompleted = true
e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false) e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false)
e.autoCompletePendingRequests(serverID) e.autoCompletePendingRequests(serverID)
} }
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
if steamConsoleStarted && !tfaRequestCreated { if steamConsoleStarted && !tfaRequestCreated {
tfaRequestCreated = true
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA") logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false) e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false)
go func() {
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil { if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err) logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true) e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
done <- err
return
} }
tfaRequestCreated = true }()
} }
} }
} }

View File

@@ -106,11 +106,12 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
stdoutScanner := bufio.NewScanner(stdout) stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr) stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan string, 100) outputChan := make(chan string, 10000)
readersDone := make(chan struct{}, 2) readersDone := make(chan struct{}, 2)
steamConsoleStarted := false steamConsoleStarted := false
tfaRequestCreated := false tfaRequestCreated := false
tfaAutoCompleted := false
go func() { go func() {
defer func() { readersDone <- struct{}{} }() defer func() { readersDone <- struct{}{} }()
@@ -165,12 +166,13 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if readersFinished == 2 { if readersFinished == 2 {
close(outputChan) close(outputChan)
for line := range outputChan { for line := range outputChan {
if e.is2FAPrompt(line) { if e.is2FAPrompt(line) && !tfaRequestCreated {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil { tfaRequestCreated = true
go func(prompt string) {
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err) logging.Error("Failed to handle 2FA prompt: %v", err)
done <- err
return
} }
}(line)
} }
} }
return return
@@ -188,28 +190,29 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if e.is2FAPrompt(line) { if e.is2FAPrompt(line) {
if !tfaRequestCreated { if !tfaRequestCreated {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
done <- err
return
}
tfaRequestCreated = true tfaRequestCreated = true
go func(prompt string) {
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
}
}(line)
} }
} }
if tfaRequestCreated && e.isSteamContinuing(line) { if tfaRequestCreated && !tfaAutoCompleted && e.isSteamContinuing(line) {
logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request") logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request")
tfaAutoCompleted = true
e.autoCompletePendingRequests(serverID) e.autoCompletePendingRequests(serverID)
} }
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
if steamConsoleStarted && !tfaRequestCreated { if steamConsoleStarted && !tfaRequestCreated {
tfaRequestCreated = true
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA") logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
go func() {
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil { if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err) logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
done <- err
return
} }
tfaRequestCreated = true }()
} else if !steamConsoleStarted { } else if !steamConsoleStarted {
logging.Info("No output for 15 seconds (Steam Console not yet started)") logging.Info("No output for 15 seconds (Steam Console not yet started)")
} }
@@ -220,22 +223,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool { func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
twoFAKeywords := []string{ twoFAKeywords := []string{
"please enter your steam guard code", "please enter your steam guard code",
"steam guard", "steam guard code",
"two-factor", "two-factor authentication",
"authentication code",
"please check your steam mobile app", "please check your steam mobile app",
"confirm in application",
"enter the current code from your steam mobile app", "enter the current code from your steam mobile app",
"steam guard mobile authenticator", "steam guard mobile authenticator",
"waiting for user info",
"login failure",
"two factor code required", "two factor code required",
"enter steam guard code", "enter steam guard code",
"mobile authenticator code", "mobile authenticator code",
"authenticator app", "2fa prompt detected",
"guard code",
"mobile app",
"confirmation required",
} }
lowerLine := strings.ToLower(line) lowerLine := strings.ToLower(line)
@@ -246,21 +242,6 @@ func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
} }
} }
waitingPatterns := []string{
"waiting for",
"please enter",
"enter code",
"code:",
"authenticator:",
}
for _, pattern := range waitingPatterns {
if strings.Contains(lowerLine, pattern) {
logging.Info("Potential 2FA waiting pattern found: '%s' in line: '%s'", pattern, line)
return true
}
}
return false return false
} }

View File

@@ -27,6 +27,7 @@ type RouteGroups struct {
System fiber.Router System fiber.Router
WebSocket fiber.Router WebSocket fiber.Router
Leaderboard fiber.Router Leaderboard fiber.Router
Steam fiber.Router
} }
func CheckError(err error) { func CheckError(err error) {

16
local/utl/env/env.go vendored
View File

@@ -10,8 +10,7 @@ const (
DefaultNSSMPath = ".\\nssm.exe" DefaultNSSMPath = ".\\nssm.exe"
DefaultPlatform = "windows" DefaultPlatform = "windows"
DefaultACCImage = "acc-wine:latest" DefaultACCImage = "acc-wine:latest"
DefaultACCGameVolume = "/data/acc-game" DefaultACCServersPath = "/data/servers"
DefaultACCConfigBasePath = "/data/configs"
DefaultLinuxSteamCMDPath = "/usr/games/steamcmd" DefaultLinuxSteamCMDPath = "/usr/games/steamcmd"
) )
@@ -59,18 +58,11 @@ func GetACCImage() string {
return DefaultACCImage return DefaultACCImage
} }
func GetACCGameVolume() string { func GetACCServersPath() string {
if v := os.Getenv("ACC_GAME_VOLUME"); v != "" { if p := os.Getenv("ACC_SERVERS_PATH"); p != "" {
return v
}
return DefaultACCGameVolume
}
func GetACCConfigBasePath() string {
if p := os.Getenv("ACC_CONFIG_BASE_PATH"); p != "" {
return p return p
} }
return DefaultACCConfigBasePath return DefaultACCServersPath
} }
func ValidatePaths() map[string]error { func ValidatePaths() map[string]error {