resolve server creation issues

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

View File

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

View File

@@ -64,4 +64,9 @@ func InitializeControllers(c *dig.Container) {
if err != nil {
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 {
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 {
image := env.GetACCImage()
gameVolume := env.GetACCGameVolume()
configPath := server.GetConfigPath()
portStr := strconv.Itoa(server.Port)
natPort, err := nat.NewPort("tcp", portStr)
if err != nil {
@@ -54,8 +51,7 @@ func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error
}
binds := []string{
fmt.Sprintf("%s:/acc/game:ro", gameVolume),
fmt.Sprintf("%s:/acc/cfg:rw", configPath),
fmt.Sprintf("%s:/acc/game:rw", server.GetServerPath()),
}
resp, err := r.client.ContainerCreate(ctx, &container.Config{

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ type WebSocketConnection struct {
conn *websocket.Conn
serverID *uuid.UUID
userID *uuid.UUID
mu sync.Mutex
}
type WebSocketService struct {
@@ -131,7 +132,7 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
ws.connections.Range(func(key, value interface{}) bool {
if wsConn, ok := value.(*WebSocketConnection); ok {
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)
ws.RemoveConnection(key.(string))
} 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) {
ws.connections.Range(func(key, value interface{}) bool {
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)
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 {
if wsConn, ok := value.(*WebSocketConnection); ok {
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)
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 {
count := 0
ws.connections.Range(func(key, value interface{}) bool {

View File

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

View File

@@ -106,11 +106,12 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan string, 100)
outputChan := make(chan string, 10000)
readersDone := make(chan struct{}, 2)
steamConsoleStarted := false
tfaRequestCreated := false
tfaAutoCompleted := false
go func() {
defer func() { readersDone <- struct{}{} }()
@@ -165,12 +166,13 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if readersFinished == 2 {
close(outputChan)
for line := range outputChan {
if e.is2FAPrompt(line) {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
done <- err
return
}
if e.is2FAPrompt(line) && !tfaRequestCreated {
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)
}
}
return
@@ -188,28 +190,29 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
if e.is2FAPrompt(line) {
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
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")
tfaAutoCompleted = true
e.autoCompletePendingRequests(serverID)
}
case <-time.After(15 * time.Second):
if steamConsoleStarted && !tfaRequestCreated {
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
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)
done <- err
return
}
tfaRequestCreated = true
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 {
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
}
}()
} else if !steamConsoleStarted {
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 {
twoFAKeywords := []string{
"please enter your steam guard code",
"steam guard",
"two-factor",
"authentication code",
"steam guard code",
"two-factor authentication",
"please check your steam mobile app",
"confirm in application",
"enter the current code from your steam mobile app",
"steam guard mobile authenticator",
"waiting for user info",
"login failure",
"two factor code required",
"enter steam guard code",
"mobile authenticator code",
"authenticator app",
"guard code",
"mobile app",
"confirmation required",
"2fa prompt detected",
}
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
}

View File

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

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

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