resolve server creation issues
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
57
local/controller/steam.go
Normal 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})
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if !env.IsDockerPlatform() {
|
||||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
@@ -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,9 +235,11 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if !env.IsDockerPlatform() {
|
||||||
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
go func(prompt string) {
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
done <- err
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
return
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||||
}
|
}
|
||||||
|
}(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 {
|
||||||
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
|
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")
|
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)
|
||||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
go func() {
|
||||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||||
done <- err
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
||||||
return
|
}
|
||||||
}
|
}()
|
||||||
tfaRequestCreated = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
go func(prompt string) {
|
||||||
done <- err
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
return
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
}
|
}
|
||||||
|
}(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 {
|
||||||
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
|
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 {
|
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
16
local/utl/env/env.go
vendored
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user