Compare commits
7 Commits
feature/sy
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
| 85c5874bc7 | |||
| 4441524447 | |||
| c03234d339 | |||
| d4b16d80f3 | |||
| 9172f0256f | |||
| beb9b479df | |||
| 4aa67db3fd |
29
.env.example
Normal file → Executable file
29
.env.example
Normal file → Executable file
@@ -50,6 +50,35 @@ PASSWORD=change-this-default-admin-password
|
||||
|
||||
# 4. NEVER commit actual secrets to version control!
|
||||
|
||||
# =============================================================================
|
||||
# PLATFORM SELECTION
|
||||
# =============================================================================
|
||||
|
||||
# Platform mode: "windows" (default) or "docker"
|
||||
# PLATFORM=windows
|
||||
|
||||
# =============================================================================
|
||||
# DOCKER MODE SETTINGS (required when PLATFORM=docker)
|
||||
# =============================================================================
|
||||
|
||||
# Docker image name for ACC server containers (built from docker/acc-server/Dockerfile)
|
||||
# ACC_IMAGE=acc-wine:latest
|
||||
|
||||
# Base path where per-server game installations are stored (inside the manager container).
|
||||
# Each server gets its own subdirectory: ACC_SERVERS_PATH/<service-name>/
|
||||
# ACC_SERVERS_PATH=/data/servers
|
||||
|
||||
# Host-side path for the same directory — used when creating sibling ACC server containers.
|
||||
# Must match the host path bound to ACC_SERVERS_PATH in docker-compose.yml.
|
||||
# ACC_SERVERS_HOST_PATH=/opt/acc-servers
|
||||
|
||||
# Port pool for Docker-mode ACC server containers (one port per server).
|
||||
# PORT_POOL_MIN=9600
|
||||
# PORT_POOL_MAX=9700
|
||||
|
||||
# Linux SteamCMD binary path (pre-installed in the manager Docker image).
|
||||
# STEAMCMD_LINUX_PATH=/usr/games/steamcmd
|
||||
|
||||
# =============================================================================
|
||||
# OPTIONAL SETTINGS (These are handled by system config in database)
|
||||
# =============================================================================
|
||||
|
||||
0
.gitea/workflows/release.yml
Normal file → Executable file
0
.gitea/workflows/release.yml
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
63
Dockerfile
Normal file
63
Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Multi-stage build for the acc-server-manager Go backend.
|
||||
# CGO_ENABLED=1 is required for mattn/go-sqlite3 (uses cgo).
|
||||
# The binary is compiled for Linux (Docker mode default).
|
||||
|
||||
# ─── Builder ──────────────────────────────────────────────────────────────────
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies for CGO + sqlite3.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download Go modules first (layer cache).
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source and build.
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||
go build -ldflags="-s -w" -o /acc-server-manager ./cmd/api
|
||||
|
||||
# ─── Runtime ──────────────────────────────────────────────────────────────────
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
|
||||
# Install runtime dependencies and SteamCMD.
|
||||
# steamcmd is not in the standard Debian repo — download directly from Valve.
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
lib32gcc-s1 \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/games && \
|
||||
curl -sSL --retry 3 --retry-delay 5 \
|
||||
-o /tmp/steamcmd.tar.gz \
|
||||
"https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" && \
|
||||
tar -xzf /tmp/steamcmd.tar.gz -C /usr/games && \
|
||||
rm /tmp/steamcmd.tar.gz && \
|
||||
chmod +x /usr/games/steamcmd.sh && \
|
||||
ln -sf /usr/games/steamcmd.sh /usr/games/steamcmd
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /acc-server-manager /app/acc-server-manager
|
||||
|
||||
# Persistent data directories.
|
||||
RUN mkdir -p /data/servers /data/db
|
||||
|
||||
ENV PLATFORM=docker
|
||||
ENV PORT=3000
|
||||
ENV DB_NAME=/data/db/acc.db
|
||||
ENV STEAMCMD_LINUX_PATH=/usr/games/steamcmd
|
||||
ENV ACC_SERVERS_PATH=/data/servers
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/app/acc-server-manager"]
|
||||
0
cmd/api/main.go
Normal file → Executable file
0
cmd/api/main.go
Normal file → Executable file
0
cmd/api/swagger.go
Normal file → Executable file
0
cmd/api/swagger.go
Normal file → Executable file
0
cmd/migrate/main.go
Normal file → Executable file
0
cmd/migrate/main.go
Normal file → Executable file
0
cmd/steam-crypt/main.go
Normal file → Executable file
0
cmd/steam-crypt/main.go
Normal file → Executable file
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
manager:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: acc-server-manager:latest
|
||||
container_name: acc-server-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
PLATFORM: docker
|
||||
DB_NAME: /data/db/acc.db
|
||||
STEAMCMD_LINUX_PATH: /usr/games/steamcmd
|
||||
PORT_POOL_MIN: "9600"
|
||||
PORT_POOL_MAX: "9699"
|
||||
ACC_IMAGE: acc-wine:latest
|
||||
ACC_SERVERS_PATH: /data/servers
|
||||
ACC_SERVERS_HOST_PATH: /opt/acc-servers
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /opt/acc-servers:/data/servers
|
||||
- acc-db:/data/db
|
||||
networks:
|
||||
- acc-net
|
||||
|
||||
volumes:
|
||||
acc-db:
|
||||
|
||||
networks:
|
||||
acc-net:
|
||||
driver: bridge
|
||||
35
docker/acc-server/Dockerfile
Normal file
35
docker/acc-server/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# ACC Server container: Wine + accServer.exe
|
||||
# Game files are bind-mounted from the host at /acc/game.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install Wine, Xvfb (headless display for Wine), and minimal runtime deps.
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wine \
|
||||
wine32 \
|
||||
wine64 \
|
||||
xvfb \
|
||||
ca-certificates \
|
||||
tcpdump \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
ENV WINEARCH=win64
|
||||
ENV WINEPREFIX=/root/.wine
|
||||
ENV WINEDEBUG=-all
|
||||
|
||||
# Copy entrypoint script.
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Bind-mounted by DockerRuntime.Create: per-server game install (rw)
|
||||
VOLUME ["/acc/game"]
|
||||
|
||||
ENV SERVER_PORT=9600
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
78
docker/acc-server/entrypoint.sh
Normal file
78
docker/acc-server/entrypoint.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
DISPLAY_NUM=$((SERVER_PORT % 1000 + 100))
|
||||
export DISPLAY=:${DISPLAY_NUM}
|
||||
|
||||
rm -f /tmp/.X${DISPLAY_NUM}-lock /tmp/.X11-unix/X${DISPLAY_NUM} 2>/dev/null || true
|
||||
|
||||
Xvfb ${DISPLAY} -screen 0 1024x768x16 -nolisten tcp &
|
||||
XVFB_PID=$!
|
||||
|
||||
TCPDUMP_PID=""
|
||||
|
||||
cleanup() {
|
||||
echo "Stopping ACC server..."
|
||||
[ -n "${TCPDUMP_PID}" ] && kill "${TCPDUMP_PID}" 2>/dev/null || true
|
||||
kill "${XVFB_PID}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup SIGTERM SIGINT EXIT
|
||||
|
||||
# Initialize Wine prefix (no-op if already done)
|
||||
wineboot -u 2>/dev/null
|
||||
|
||||
mkdir -p /acc/game/log
|
||||
|
||||
# ─── Debug mode ───────────────────────────────────────────────────────────────
|
||||
# Set DEBUG_MODE=1 on the container to enable extra diagnostics:
|
||||
#
|
||||
# docker run -e DEBUG_MODE=1 ...
|
||||
# or in docker-compose:
|
||||
# environment:
|
||||
# DEBUG_MODE: "1"
|
||||
#
|
||||
# Produces:
|
||||
# /acc/game/log/tcpdump_<port>.pcap — raw packet capture (open in Wireshark)
|
||||
# /acc/game/log/wine_debug.log — Wine debug output
|
||||
# Stderr: Wine version, WINEDEBUG channel, prefix state
|
||||
#
|
||||
# You can also override which Wine debug channels are enabled:
|
||||
# WINEDEBUG_CHANNELS="+loaddll,+seh,+relay" (relay is very verbose)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
if [ "${DEBUG_MODE:-0}" = "1" ]; then
|
||||
echo "[debug] Debug mode enabled for port ${SERVER_PORT}"
|
||||
echo "[debug] Wine: $(wine --version 2>/dev/null || echo unknown)"
|
||||
echo "[debug] WINEPREFIX: ${WINEPREFIX}"
|
||||
|
||||
# Configure Wine debug channels (default: loader + exception + module info)
|
||||
export WINEDEBUG="${WINEDEBUG_CHANNELS:-+loaddll,+module,+seh,+ntdll}"
|
||||
echo "[debug] WINEDEBUG=${WINEDEBUG}"
|
||||
|
||||
# Start packet capture on the server port — captures both TCP and UDP.
|
||||
# The pcap file records every byte Kunos sends/receives so you can inspect
|
||||
# the 2-byte "pakSizeMax" packet and anything that follows.
|
||||
#
|
||||
# NOTE: requires CAP_NET_RAW capability on the container:
|
||||
# docker-compose: cap_add: [NET_RAW]
|
||||
# docker run: --cap-add=NET_RAW
|
||||
# Without it tcpdump will be skipped (no crash, just no capture).
|
||||
CAPTURE_FILE="/acc/game/log/tcpdump_${SERVER_PORT}.pcap"
|
||||
echo "[debug] Starting packet capture → ${CAPTURE_FILE}"
|
||||
echo "[debug] (requires cap_add: [NET_RAW] in docker-compose — will exit silently if missing)"
|
||||
tcpdump -i any -nn -s 0 -w "${CAPTURE_FILE}" \
|
||||
"port ${SERVER_PORT}" 2>/acc/game/log/tcpdump.err &
|
||||
TCPDUMP_PID=$!
|
||||
|
||||
# Redirect Wine debug output to a log file (Wine writes to stderr).
|
||||
WINE_LOG="/acc/game/log/wine_debug.log"
|
||||
echo "[debug] Wine debug log → ${WINE_LOG}"
|
||||
echo "Starting ACC server on port ${SERVER_PORT} (DEBUG)..."
|
||||
cd /acc/game
|
||||
exec wine /acc/game/accServer.exe 2>"${WINE_LOG}"
|
||||
else
|
||||
# Production: suppress all Wine debug output.
|
||||
export WINEDEBUG=-all
|
||||
echo "Starting ACC server on port ${SERVER_PORT}..."
|
||||
cd /acc/game
|
||||
exec wine /acc/game/accServer.exe
|
||||
fi
|
||||
0
docs/API.md
Normal file → Executable file
0
docs/API.md
Normal file → Executable file
0
docs/CONFIG.md
Normal file → Executable file
0
docs/CONFIG.md
Normal file → Executable file
0
docs/DEPLOYMENT.md
Normal file → Executable file
0
docs/DEPLOYMENT.md
Normal file → Executable file
0
docs/MIGRATION.md
Normal file → Executable file
0
docs/MIGRATION.md
Normal file → Executable file
0
docs/SETUP.md
Normal file → Executable file
0
docs/SETUP.md
Normal file → Executable file
0
docs/STEAM_2FA_IMPLEMENTATION.md
Normal file → Executable file
0
docs/STEAM_2FA_IMPLEMENTATION.md
Normal file → Executable file
0
docs/SWAGGER.md
Normal file → Executable file
0
docs/SWAGGER.md
Normal file → Executable file
0
docs/TROUBLESHOOTING.md
Normal file → Executable file
0
docs/TROUBLESHOOTING.md
Normal file → Executable file
24
go.mod
Normal file → Executable file
24
go.mod
Normal file → Executable file
@@ -1,15 +1,19 @@
|
||||
module acc-server-manager
|
||||
|
||||
go 1.23.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v24.0.9+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/gofiber/fiber/v2 v2.52.8
|
||||
github.com/gofiber/swagger v1.1.0
|
||||
github.com/gofiber/websocket/v2 v2.2.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
go.uber.org/dig v1.17.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/sync v0.15.0
|
||||
@@ -20,13 +24,17 @@ require (
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.21 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -36,13 +44,21 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
76
go.sum
Normal file → Executable file
76
go.sum
Normal file → Executable file
@@ -1,9 +1,21 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
|
||||
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
|
||||
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
@@ -20,8 +32,12 @@ github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ
|
||||
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -32,6 +48,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -49,18 +67,31 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1V0dkEkdPelzCdAzCURCfmFArn2/A=
|
||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
@@ -71,22 +102,53 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -96,3 +158,5 @@ gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
|
||||
1
local/api/api.go
Normal file → Executable file
1
local/api/api.go
Normal file → Executable 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()
|
||||
|
||||
0
local/controller/config.go
Normal file → Executable file
0
local/controller/config.go
Normal file → Executable file
5
local/controller/controller.go
Normal file → Executable file
5
local/controller/controller.go
Normal file → Executable 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")
|
||||
}
|
||||
}
|
||||
|
||||
0
local/controller/leaderboard.go
Normal file → Executable file
0
local/controller/leaderboard.go
Normal file → Executable file
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
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})
|
||||
}
|
||||
0
local/controller/system.go
Normal file → Executable file
0
local/controller/system.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
17
local/middleware/auth.go
Normal file → Executable file
17
local/middleware/auth.go
Normal file → Executable 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()
|
||||
}
|
||||
|
||||
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/migrations/001_upgrade_password_security.go
Normal file → Executable file
0
local/migrations/001_upgrade_password_security.go
Normal file → Executable file
0
local/migrations/002_migrate_to_uuid.go
Normal file → Executable file
0
local/migrations/002_migrate_to_uuid.go
Normal file → Executable file
0
local/migrations/003_update_state_history_sessions.go
Normal file → Executable file
0
local/migrations/003_update_state_history_sessions.go
Normal file → Executable file
0
local/migrations/004_fix_ddl_comments.go
Normal file → Executable file
0
local/migrations/004_fix_ddl_comments.go
Normal file → Executable file
33
local/migrations/005_add_platform_columns.go
Normal file
33
local/migrations/005_add_platform_columns.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RunAddPlatformColumnsMigration(db *gorm.DB) error {
|
||||
if err := db.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||
return fmt.Errorf("failed to ensure migration_records table: %v", err)
|
||||
}
|
||||
|
||||
var record MigrationRecord
|
||||
if err := db.Where("migration_name = ?", "005_add_platform_columns").First(&record).Error; err == nil {
|
||||
logging.Info("Platform columns migration already applied, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
record = MigrationRecord{
|
||||
MigrationName: "005_add_platform_columns",
|
||||
AppliedAt: "datetime('now')",
|
||||
Success: true,
|
||||
Notes: "Added Platform and ContainerID columns to servers table via GORM AutoMigrate",
|
||||
}
|
||||
if err := db.Create(&record).Error; err != nil {
|
||||
return fmt.Errorf("failed to record migration 005: %v", err)
|
||||
}
|
||||
|
||||
logging.Info("Platform columns migration recorded")
|
||||
return nil
|
||||
}
|
||||
0
local/model/cache.go
Normal file → Executable file
0
local/model/cache.go
Normal file → Executable file
0
local/model/config.go
Normal file → Executable file
0
local/model/config.go
Normal file → Executable file
0
local/model/filter.go
Normal file → Executable file
0
local/model/filter.go
Normal file → Executable file
0
local/model/leaderboard.go
Normal file → Executable file
0
local/model/leaderboard.go
Normal file → Executable file
0
local/model/lookup.go
Normal file → Executable file
0
local/model/lookup.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
2
local/model/server.go
Normal file → Executable file
2
local/model/server.go
Normal file → Executable file
@@ -42,6 +42,8 @@ type Server struct {
|
||||
Port int `gorm:"not null" json:"-"`
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
ServiceName string `gorm:"not null" json:"serviceName"`
|
||||
Platform string `gorm:"not null;default:'windows'" json:"platform"`
|
||||
ContainerID string `gorm:"default:''" json:"-"`
|
||||
State *ServerState `gorm:"-" json:"state"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
|
||||
|
||||
0
local/model/service_control.go
Normal file → Executable file
0
local/model/service_control.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
28
local/platform/interfaces.go
Normal file
28
local/platform/interfaces.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ServerRuntime interface {
|
||||
Create(ctx context.Context, server *model.Server) error
|
||||
Delete(ctx context.Context, serverID uuid.UUID) error
|
||||
Start(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||
Stop(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||
Restart(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||
Status(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type FirewallManager interface {
|
||||
CreateServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||
DeleteServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||
UpdateServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||
}
|
||||
|
||||
type LogStreamer interface {
|
||||
Start(ctx context.Context, server *model.Server, handleLine func(string)) error
|
||||
Stop(serverID uuid.UUID)
|
||||
}
|
||||
0
local/repository/base.go
Normal file → Executable file
0
local/repository/base.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
0
local/repository/leaderboard.go
Normal file → Executable file
0
local/repository/leaderboard.go
Normal file → Executable file
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
0
local/repository/repository.go
Normal file → Executable file
0
local/repository/repository.go
Normal file → Executable file
0
local/repository/server.go
Normal file → Executable file
0
local/repository/server.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
8
local/service/config.go
Normal file → Executable file
8
local/service/config.go
Normal file → Executable file
@@ -476,6 +476,14 @@ func (as *ConfigService) GetConfiguration(server *model.Server) (*model.Configur
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (as *ConfigService) SetIgnorePrematureDisconnects(server *model.Server, value int) error {
|
||||
configMap := map[string]interface{}{
|
||||
"ignorePrematureDisconnects": value,
|
||||
}
|
||||
_, _, err := as.updateConfigFiles(context.Background(), server, SettingsJson, &configMap, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.Configuration) error {
|
||||
configMap := make(map[string]interface{})
|
||||
configBytes, err := json.Marshal(config)
|
||||
|
||||
74
local/service/docker_log_streamer.go
Normal file
74
local/service/docker_log_streamer.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DockerLogStreamer struct {
|
||||
client *dockerclient.Client
|
||||
cancels sync.Map // uuid.UUID → context.CancelFunc
|
||||
}
|
||||
|
||||
func NewDockerLogStreamer(client *dockerclient.Client) *DockerLogStreamer {
|
||||
return &DockerLogStreamer{client: client}
|
||||
}
|
||||
|
||||
func (s *DockerLogStreamer) Start(ctx context.Context, server *model.Server, handleLine func(string)) error {
|
||||
if _, exists := s.cancels.Load(server.ID); exists {
|
||||
return nil
|
||||
}
|
||||
if server.ContainerID == "" {
|
||||
logging.Warn("DockerLogStreamer.Start: no container ID for server %s, skipping", server.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
s.cancels.Store(server.ID, cancel)
|
||||
|
||||
go func() {
|
||||
defer s.cancels.Delete(server.ID)
|
||||
|
||||
rc, err := s.client.ContainerLogs(streamCtx, server.ContainerID, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Timestamps: false,
|
||||
})
|
||||
if err != nil {
|
||||
logging.Error("DockerLogStreamer: failed to attach to container %s: %v", server.ContainerID[:12], err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
if _, err := stdcopy.StdCopy(pw, pw, rc); err != nil && err != io.EOF {
|
||||
logging.Warn("DockerLogStreamer: stdcopy error for %s: %v", server.ServiceName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(pr)
|
||||
for scanner.Scan() {
|
||||
handleLine(scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DockerLogStreamer) Stop(serverID uuid.UUID) {
|
||||
if v, ok := s.cancels.LoadAndDelete(serverID); ok {
|
||||
v.(context.CancelFunc)()
|
||||
}
|
||||
}
|
||||
238
local/service/docker_runtime.go
Normal file
238
local/service/docker_runtime.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/repository"
|
||||
"acc-server-manager/local/utl/env"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DockerRuntime struct {
|
||||
client *dockerclient.Client
|
||||
repo *repository.ServerRepository
|
||||
}
|
||||
|
||||
func NewDockerRuntime(client *dockerclient.Client, repo *repository.ServerRepository) *DockerRuntime {
|
||||
return &DockerRuntime{
|
||||
client: client,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error {
|
||||
image := env.GetACCImage()
|
||||
// ACC uses the same port number for both TCP (client connections) and UDP (car positions/ping).
|
||||
portStr := strconv.Itoa(server.Port)
|
||||
|
||||
natTCP, err := nat.NewPort("tcp", portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port %d: %v", server.Port, err)
|
||||
}
|
||||
natUDP, err := nat.NewPort("udp", portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port %d: %v", server.Port, err)
|
||||
}
|
||||
|
||||
portBindings := nat.PortMap{
|
||||
natTCP: []nat.PortBinding{{HostPort: portStr}},
|
||||
natUDP: []nat.PortBinding{{HostPort: portStr}},
|
||||
}
|
||||
exposedPorts := nat.PortSet{
|
||||
natTCP: struct{}{},
|
||||
natUDP: struct{}{},
|
||||
}
|
||||
|
||||
containerBase := env.GetACCServersPath()
|
||||
hostBase := env.GetACCServersHostPath()
|
||||
serverSubPath := strings.TrimPrefix(server.GetServerPath(), containerBase)
|
||||
hostServerPath := filepath.Join(hostBase, serverSubPath)
|
||||
|
||||
binds := []string{
|
||||
fmt.Sprintf("%s:/acc/game:rw", hostServerPath),
|
||||
}
|
||||
|
||||
resp, err := r.client.ContainerCreate(ctx, &container.Config{
|
||||
Image: image,
|
||||
ExposedPorts: exposedPorts,
|
||||
Env: []string{
|
||||
fmt.Sprintf("SERVER_PORT=%d", server.Port),
|
||||
},
|
||||
}, &container.HostConfig{
|
||||
Binds: binds,
|
||||
PortBindings: portBindings,
|
||||
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
|
||||
// NET_RAW is required for tcpdump packet capture in debug mode (DEBUG_MODE=1).
|
||||
CapAdd: []string{"NET_RAW"},
|
||||
}, nil, nil, server.ServiceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container %s: %v", server.ServiceName, err)
|
||||
}
|
||||
|
||||
server.ContainerID = resp.ID
|
||||
logging.Info("Created Docker container %s (ID: %s) for server %s", server.ServiceName, resp.ID[:12], server.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Delete(ctx context.Context, serverID uuid.UUID) error {
|
||||
containerID, err := r.resolveContainerID(ctx, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if containerID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = r.client.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{
|
||||
Force: true,
|
||||
RemoveVolumes: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove container %s: %v", containerID[:12], err)
|
||||
}
|
||||
logging.Info("Removed Docker container %s for server %s", containerID[:12], serverID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Start(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||
}
|
||||
if server == nil {
|
||||
return "", fmt.Errorf("server %s not found", serverID)
|
||||
}
|
||||
|
||||
containerID, err := r.ensureContainer(ctx, server)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := r.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
|
||||
return "", fmt.Errorf("failed to start container: %v", err)
|
||||
}
|
||||
return model.StatusRunning.String(), nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Stop(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
containerID, err := r.resolveContainerID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := r.client.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
|
||||
return "", fmt.Errorf("failed to stop container: %v", err)
|
||||
}
|
||||
return model.StatusStopped.String(), nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Restart(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||
}
|
||||
if server == nil {
|
||||
return "", fmt.Errorf("server %s not found", serverID)
|
||||
}
|
||||
|
||||
containerID, err := r.ensureContainer(ctx, server)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{}); err != nil {
|
||||
return "", fmt.Errorf("failed to restart container: %v", err)
|
||||
}
|
||||
return model.StatusRunning.String(), nil
|
||||
}
|
||||
|
||||
// ensureContainer returns the container ID for a server, recreating the container
|
||||
// from the server's current DB record if it no longer exists in Docker.
|
||||
func (r *DockerRuntime) ensureContainer(ctx context.Context, server *model.Server) (string, error) {
|
||||
if server.ContainerID != "" {
|
||||
if _, err := r.client.ContainerInspect(ctx, server.ContainerID); err == nil {
|
||||
return server.ContainerID, nil
|
||||
}
|
||||
logging.Info("Container %s for server %s no longer exists, recreating", server.ContainerID[:min(12, len(server.ContainerID))], server.ID)
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, server); err != nil {
|
||||
return "", fmt.Errorf("failed to recreate container: %v", err)
|
||||
}
|
||||
if err := r.repo.Update(ctx, server); err != nil {
|
||||
return "", fmt.Errorf("container recreated but failed to persist container ID: %v", err)
|
||||
}
|
||||
return server.ContainerID, nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) Status(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil || server == nil {
|
||||
return model.StatusUnknown.String(), err
|
||||
}
|
||||
|
||||
containerID := server.ContainerID
|
||||
if containerID == "" {
|
||||
containerID, err = r.findContainerByName(ctx, server.ServiceName)
|
||||
if err != nil || containerID == "" {
|
||||
return model.StatusStopped.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
info, err := r.client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return model.StatusUnknown.String(), nil
|
||||
}
|
||||
|
||||
return dockerStateToStatus(info.State.Status), nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) resolveContainerID(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||
}
|
||||
if server == nil {
|
||||
return "", fmt.Errorf("server %s not found", serverID)
|
||||
}
|
||||
return server.ContainerID, nil
|
||||
}
|
||||
|
||||
func (r *DockerRuntime) findContainerByName(ctx context.Context, name string) (string, error) {
|
||||
containers, err := r.client.ContainerList(ctx, types.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("name", "/"+name)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return containers[0].ID, nil
|
||||
}
|
||||
|
||||
func dockerStateToStatus(state string) string {
|
||||
switch state {
|
||||
case "running":
|
||||
return model.StatusRunning.String()
|
||||
case "exited", "dead":
|
||||
return model.StatusStopped.String()
|
||||
case "restarting":
|
||||
return model.StatusRestarting.String()
|
||||
case "paused":
|
||||
return model.StatusStopped.String()
|
||||
default:
|
||||
return model.StatusUnknown.String()
|
||||
}
|
||||
}
|
||||
38
local/service/file_log_streamer.go
Normal file
38
local/service/file_log_streamer.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/tracking"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type FileLogStreamer struct {
|
||||
tailers sync.Map // uuid.UUID → *tracking.LogTailer
|
||||
}
|
||||
|
||||
func NewFileLogStreamer() *FileLogStreamer {
|
||||
return &FileLogStreamer{}
|
||||
}
|
||||
|
||||
func (s *FileLogStreamer) Start(_ context.Context, server *model.Server, handleLine func(string)) error {
|
||||
if _, exists := s.tailers.Load(server.ID); exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
||||
tailer := tracking.NewLogTailer(logPath, handleLine)
|
||||
s.tailers.Store(server.ID, tailer)
|
||||
|
||||
go tailer.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileLogStreamer) Stop(serverID uuid.UUID) {
|
||||
if v, exists := s.tailers.LoadAndDelete(serverID); exists {
|
||||
v.(*tracking.LogTailer).Stop()
|
||||
}
|
||||
}
|
||||
0
local/service/firewall_service.go
Normal file → Executable file
0
local/service/firewall_service.go
Normal file → Executable file
0
local/service/leaderboard.go
Normal file → Executable file
0
local/service/leaderboard.go
Normal file → Executable file
0
local/service/lookup.go
Normal file → Executable file
0
local/service/lookup.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
7
local/service/noop_firewall.go
Normal file
7
local/service/noop_firewall.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package service
|
||||
|
||||
type NoOpFirewall struct{}
|
||||
|
||||
func (f *NoOpFirewall) CreateServerRules(_ string, _, _ []int) error { return nil }
|
||||
func (f *NoOpFirewall) DeleteServerRules(_ string, _, _ []int) error { return nil }
|
||||
func (f *NoOpFirewall) UpdateServerRules(_ string, _, _ []int) error { return nil }
|
||||
143
local/service/server.go
Normal file → Executable file
143
local/service/server.go
Normal file → Executable file
@@ -2,9 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/platform"
|
||||
"acc-server-manager/local/repository"
|
||||
"acc-server-manager/local/utl/env"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"acc-server-manager/local/utl/network"
|
||||
"acc-server-manager/local/utl/tracking"
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -12,8 +14,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"acc-server-manager/local/utl/network"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -29,14 +29,15 @@ type ServerService struct {
|
||||
apiService *ServiceControlService
|
||||
configService *ConfigService
|
||||
steamService *SteamService
|
||||
windowsService *WindowsService
|
||||
firewallService *FirewallService
|
||||
runtime platform.ServerRuntime
|
||||
firewall platform.FirewallManager
|
||||
logStreamer platform.LogStreamer
|
||||
portPool *network.PortPoolManager
|
||||
webSocketService *WebSocketService
|
||||
instances sync.Map // Track instances per server
|
||||
lastInsertTimes sync.Map // Track last insert time per server
|
||||
debouncers sync.Map // Track debounce timers per server
|
||||
logTailers sync.Map // Track log tailers per server
|
||||
sessionIDs sync.Map // Track current session ID per server
|
||||
instances sync.Map // uuid.UUID → *tracking.AccServerInstance
|
||||
lastInsertTimes sync.Map // uuid.UUID → time.Time
|
||||
debouncers sync.Map // uuid.UUID → *pendingState
|
||||
sessionIDs sync.Map // uuid.UUID → uuid.UUID
|
||||
}
|
||||
|
||||
type pendingState struct {
|
||||
@@ -45,16 +46,10 @@ type pendingState struct {
|
||||
}
|
||||
|
||||
func (s *ServerService) ensureLogTailing(server *model.Server, instance *tracking.AccServerInstance) {
|
||||
if _, exists := s.logTailers.Load(server.ID); exists {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
||||
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
|
||||
s.logTailers.Store(server.ID, tailer)
|
||||
|
||||
tailer.Start()
|
||||
if err := s.logStreamer.Start(context.Background(), server, instance.HandleLogLine); err != nil {
|
||||
logging.Error("Failed to start log streaming for server %s: %v", server.ID, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -64,8 +59,10 @@ func NewServerService(
|
||||
apiService *ServiceControlService,
|
||||
configService *ConfigService,
|
||||
steamService *SteamService,
|
||||
windowsService *WindowsService,
|
||||
firewallService *FirewallService,
|
||||
runtime platform.ServerRuntime,
|
||||
firewall platform.FirewallManager,
|
||||
logStreamer platform.LogStreamer,
|
||||
portPool *network.PortPoolManager,
|
||||
webSocketService *WebSocketService,
|
||||
) *ServerService {
|
||||
service := &ServerService{
|
||||
@@ -74,8 +71,10 @@ func NewServerService(
|
||||
apiService: apiService,
|
||||
configService: configService,
|
||||
steamService: steamService,
|
||||
windowsService: windowsService,
|
||||
firewallService: firewallService,
|
||||
runtime: runtime,
|
||||
firewall: firewall,
|
||||
logStreamer: logStreamer,
|
||||
portPool: portPool,
|
||||
webSocketService: webSocketService,
|
||||
}
|
||||
|
||||
@@ -195,9 +194,19 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
|
||||
}
|
||||
|
||||
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
||||
steamCMDPath := env.GetSteamCMDDirPath()
|
||||
server.Path = server.GenerateServerPath(steamCMDPath)
|
||||
server.FromSteamCMD = true
|
||||
server.GenerateUUID()
|
||||
if server.ServiceName == "" {
|
||||
server.ServiceName = server.GenerateServiceName()
|
||||
}
|
||||
|
||||
if env.IsDockerPlatform() {
|
||||
server.Path = filepath.Join(env.GetACCServersPath(), server.ServiceName)
|
||||
server.FromSteamCMD = true
|
||||
} else {
|
||||
steamCMDPath := env.GetSteamCMDDirPath()
|
||||
server.Path = server.GenerateServerPath(steamCMDPath)
|
||||
server.FromSteamCMD = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
||||
@@ -250,9 +259,6 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
|
||||
s.ensureLogTailing(server, instance)
|
||||
}
|
||||
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) {
|
||||
servers, err := s.repository.GetAll(ctx.UserContext(), filter)
|
||||
if err != nil {
|
||||
@@ -281,9 +287,6 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// context.Context: Application context
|
||||
// Returns:
|
||||
// string: Application version
|
||||
func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Server, error) {
|
||||
server, err := as.repository.GetByID(ctx.UserContext(), serverID)
|
||||
if err != nil {
|
||||
@@ -291,7 +294,7 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Ser
|
||||
}
|
||||
status, err := as.apiService.GetCachedStatus(server.ServiceName)
|
||||
if err != nil {
|
||||
logging.Error(err.Error())
|
||||
logging.Error("Failed to get cached status: %v", err)
|
||||
}
|
||||
server.Status = model.ParseServiceStatus(status)
|
||||
instance, ok := as.instances.Load(server.ID)
|
||||
@@ -377,12 +380,24 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
||||
important: true,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find available ports: %v", err)
|
||||
var port int
|
||||
var err error
|
||||
|
||||
if env.IsDockerPlatform() {
|
||||
port, err = s.portPool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to acquire port from pool: %v", err)
|
||||
}
|
||||
} else {
|
||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find available ports: %v", err)
|
||||
}
|
||||
port = ports[0]
|
||||
}
|
||||
|
||||
serverPort = ports[0]
|
||||
serverPort = port
|
||||
server.Port = port
|
||||
|
||||
if err := s.updateServerPort(server, serverPort); err != nil {
|
||||
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
||||
@@ -396,12 +411,11 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
||||
important: true,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
|
||||
serverWorkingDir := filepath.Join(server.GetServerPath(), "server")
|
||||
if err := s.windowsService.CreateService(ctx, server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
|
||||
return "", fmt.Errorf("failed to create Windows service: %v", err)
|
||||
server.Platform = env.GetPlatform()
|
||||
if err := s.runtime.Create(ctx, server); err != nil {
|
||||
return "", fmt.Errorf("failed to create server instance: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Windows service '%s' created successfully", server.ServiceName), nil
|
||||
return fmt.Sprintf("Server instance '%s' created successfully", server.ServiceName), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -409,13 +423,12 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
||||
important: false,
|
||||
description: "",
|
||||
callback: func() (string, error) {
|
||||
s.configureFirewall(server)
|
||||
tcpPorts = []int{serverPort}
|
||||
udpPorts = []int{serverPort}
|
||||
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
if err := s.firewall.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
return "", fmt.Errorf("failed to create firewall rules: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("Firewall rules created for port %d", serverPort), nil
|
||||
return fmt.Sprintf("Firewall rules configured for port %d", serverPort), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -469,12 +482,18 @@ func (s *ServerService) rollbackSteps(ctx context.Context, server *model.Server,
|
||||
s.repository.Delete(ctx, server.ID)
|
||||
case model.StepFirewallRules:
|
||||
if len(tcpPorts) > 0 && len(udpPorts) > 0 {
|
||||
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||
s.firewall.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||
}
|
||||
case model.StepServiceCreation:
|
||||
s.windowsService.DeleteService(ctx, server.ServiceName)
|
||||
s.runtime.Delete(ctx, server.ID)
|
||||
case model.StepSteamDownload:
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
case model.StepDirectoryCreation:
|
||||
s.steamService.UninstallServer(server.Path)
|
||||
case model.StepConfigGeneration:
|
||||
if env.IsDockerPlatform() && server.Port > 0 {
|
||||
s.portPool.Release(ctx, server.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,32 +504,35 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
|
||||
return fmt.Errorf("failed to get server details: %v", err)
|
||||
}
|
||||
|
||||
if err := s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName); err != nil {
|
||||
logging.Error("Failed to delete Windows service: %v", err)
|
||||
if err := s.runtime.Delete(ctx.UserContext(), server.ID); err != nil {
|
||||
logging.Error("Failed to delete server instance: %v", err)
|
||||
}
|
||||
|
||||
configuration, err := s.configService.GetConfiguration(server)
|
||||
if err != nil {
|
||||
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
|
||||
}
|
||||
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
||||
udpPorts := []int{configuration.UdpPort.ToInt()}
|
||||
if err := s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
logging.Error("Failed to delete firewall rules: %v", err)
|
||||
if configuration != nil {
|
||||
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
||||
udpPorts := []int{configuration.UdpPort.ToInt()}
|
||||
if err := s.firewall.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||
logging.Error("Failed to delete firewall rules: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.steamService.UninstallServer(server.Path); err != nil {
|
||||
logging.Error("Failed to uninstall server: %v", err)
|
||||
}
|
||||
|
||||
if env.IsDockerPlatform() && server.Port > 0 {
|
||||
s.portPool.Release(ctx.UserContext(), server.Port)
|
||||
}
|
||||
|
||||
if err := s.repository.Delete(ctx.UserContext(), serverID); err != nil {
|
||||
return fmt.Errorf("failed to delete server from database: %v", err)
|
||||
}
|
||||
|
||||
if tailer, exists := s.logTailers.Load(server.ID); exists {
|
||||
tailer.(*tracking.LogTailer).Stop()
|
||||
s.logTailers.Delete(server.ID)
|
||||
}
|
||||
s.logStreamer.Stop(server.ID)
|
||||
s.instances.Delete(server.ID)
|
||||
s.lastInsertTimes.Delete(server.ID)
|
||||
s.debouncers.Delete(server.ID)
|
||||
@@ -533,7 +555,7 @@ func (s *ServerService) configureFirewall(server *model.Server) error {
|
||||
|
||||
logging.Info("Configuring firewall for server %d with port %d", server.ID, serverPort)
|
||||
|
||||
if err := s.firewallService.UpdateServerRules(server.Name, tcpPorts, udpPorts); err != nil {
|
||||
if err := s.firewall.UpdateServerRules(server.Name, tcpPorts, udpPorts); err != nil {
|
||||
return fmt.Errorf("failed to configure firewall: %v", err)
|
||||
}
|
||||
|
||||
@@ -552,10 +574,17 @@ func (s *ServerService) updateServerPort(server *model.Server, port int) error {
|
||||
|
||||
config.TcpPort = model.IntString(port)
|
||||
config.UdpPort = model.IntString(port)
|
||||
config.RegisterToLobby = model.IntString(1)
|
||||
|
||||
if err := s.configService.SaveConfiguration(server, config); err != nil {
|
||||
return fmt.Errorf("failed to save server configuration: %v", err)
|
||||
}
|
||||
|
||||
if env.IsDockerPlatform() {
|
||||
if err := s.configService.SetIgnorePrematureDisconnects(server, 0); err != nil {
|
||||
logging.Error("Failed to set ignorePrematureDisconnects for Docker server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
70
local/service/service.go
Normal file → Executable file
70
local/service/service.go
Normal file → Executable file
@@ -1,13 +1,22 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/platform"
|
||||
"acc-server-manager/local/repository"
|
||||
"acc-server-manager/local/utl/docker"
|
||||
"acc-server-manager/local/utl/env"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"acc-server-manager/local/utl/network"
|
||||
"fmt"
|
||||
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"go.uber.org/dig"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// *dig.Container: Dig Container
|
||||
// InitializeServices registers all service providers into the DI container.
|
||||
// Platform-specific components (ServerRuntime, FirewallManager, LogStreamer)
|
||||
// are conditionally provided based on the PLATFORM environment variable.
|
||||
func InitializeServices(c *dig.Container) {
|
||||
logging.Debug("Initializing repositories")
|
||||
repository.InitializeRepositories(c)
|
||||
@@ -19,21 +28,74 @@ func InitializeServices(c *dig.Container) {
|
||||
c.Provide(NewServiceControlService)
|
||||
c.Provide(NewConfigService)
|
||||
c.Provide(NewLookupService)
|
||||
c.Provide(NewWindowsService)
|
||||
c.Provide(NewFirewallService)
|
||||
c.Provide(NewMembershipService)
|
||||
c.Provide(NewWebSocketService)
|
||||
c.Provide(NewLeaderboardService)
|
||||
|
||||
// Always provide the PortPoolManager; in Windows mode it seeds from an
|
||||
// empty result set and sits idle.
|
||||
c.Provide(func(db *gorm.DB) (*network.PortPoolManager, error) {
|
||||
return network.NewPortPoolManager(db)
|
||||
})
|
||||
|
||||
if env.IsDockerPlatform() {
|
||||
registerDockerProviders(c)
|
||||
} else {
|
||||
registerWindowsProviders(c)
|
||||
}
|
||||
|
||||
logging.Debug("Initializing service dependencies")
|
||||
err := c.Invoke(func(server *ServerService, api *ServiceControlService, config *ConfigService) {
|
||||
logging.Debug("Setting up service cross-references")
|
||||
api.SetServerService(server)
|
||||
config.SetServerService(server)
|
||||
|
||||
})
|
||||
if err != nil {
|
||||
logging.Panic("unable to initialize services: " + err.Error())
|
||||
}
|
||||
logging.Debug("Completed service initialization")
|
||||
}
|
||||
|
||||
func registerWindowsProviders(c *dig.Container) {
|
||||
logging.Debug("Registering Windows platform providers")
|
||||
|
||||
c.Provide(NewWindowsService)
|
||||
|
||||
c.Provide(func(ws *WindowsService, repo *repository.ServerRepository) platform.ServerRuntime {
|
||||
return NewWindowsRuntime(ws, repo)
|
||||
})
|
||||
|
||||
c.Provide(func() platform.FirewallManager {
|
||||
return NewFirewallService()
|
||||
})
|
||||
|
||||
c.Provide(func() platform.LogStreamer {
|
||||
return NewFileLogStreamer()
|
||||
})
|
||||
}
|
||||
|
||||
func registerDockerProviders(c *dig.Container) {
|
||||
logging.Debug("Registering Docker platform providers")
|
||||
|
||||
if err := c.Provide(func() (*dockerclient.Client, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Docker daemon: %v", err)
|
||||
}
|
||||
return client, nil
|
||||
}); err != nil {
|
||||
logging.Panic("failed to provide Docker client: " + err.Error())
|
||||
}
|
||||
|
||||
c.Provide(func(client *dockerclient.Client, repo *repository.ServerRepository) platform.ServerRuntime {
|
||||
return NewDockerRuntime(client, repo)
|
||||
})
|
||||
|
||||
c.Provide(func() platform.FirewallManager {
|
||||
return &NoOpFirewall{}
|
||||
})
|
||||
|
||||
c.Provide(func(client *dockerclient.Client) platform.LogStreamer {
|
||||
return NewDockerLogStreamer(client)
|
||||
})
|
||||
}
|
||||
|
||||
52
local/service/service_control.go
Normal file → Executable file
52
local/service/service_control.go
Normal file → Executable file
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/platform"
|
||||
"acc-server-manager/local/repository"
|
||||
"context"
|
||||
"errors"
|
||||
@@ -15,20 +16,23 @@ type ServiceControlService struct {
|
||||
serverRepository *repository.ServerRepository
|
||||
serverService *ServerService
|
||||
statusCache *model.ServerStatusCache
|
||||
windowsService *WindowsService
|
||||
runtime platform.ServerRuntime
|
||||
}
|
||||
|
||||
func NewServiceControlService(repository *repository.ServiceControlRepository,
|
||||
serverRepository *repository.ServerRepository) *ServiceControlService {
|
||||
func NewServiceControlService(
|
||||
repository *repository.ServiceControlRepository,
|
||||
serverRepository *repository.ServerRepository,
|
||||
runtime platform.ServerRuntime,
|
||||
) *ServiceControlService {
|
||||
return &ServiceControlService{
|
||||
repository: repository,
|
||||
serverRepository: serverRepository,
|
||||
runtime: runtime,
|
||||
statusCache: model.NewServerStatusCache(model.CacheConfig{
|
||||
ExpirationTime: 30 * time.Second,
|
||||
ThrottleTime: 5 * time.Second,
|
||||
DefaultStatus: model.StatusRunning,
|
||||
}),
|
||||
windowsService: NewWindowsService(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +127,11 @@ func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (st
|
||||
}
|
||||
|
||||
func (as *ServiceControlService) StatusServer(serviceName string) (string, error) {
|
||||
return as.windowsService.Status(context.Background(), serviceName)
|
||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return as.runtime.Status(context.Background(), server.ID)
|
||||
}
|
||||
|
||||
func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, error) {
|
||||
@@ -142,44 +150,46 @@ func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, er
|
||||
}
|
||||
|
||||
func (as *ServiceControlService) StartServer(serviceName string) (string, error) {
|
||||
status, err := as.windowsService.Start(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
status, err := as.runtime.Start(context.Background(), server.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
as.serverService.StartAccServerRuntime(server)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (as *ServiceControlService) StopServer(serviceName string) (string, error) {
|
||||
status, err := as.windowsService.Stop(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
as.serverService.instances.Delete(server.ID)
|
||||
|
||||
status, err := as.runtime.Stop(context.Background(), server.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
as.serverService.instances.Delete(server.ID)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (as *ServiceControlService) RestartServer(serviceName string) (string, error) {
|
||||
status, err := as.windowsService.Restart(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
status, err := as.runtime.Restart(context.Background(), server.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
as.serverService.StartAccServerRuntime(server)
|
||||
return status, err
|
||||
}
|
||||
|
||||
0
local/service/service_manager.go
Normal file → Executable file
0
local/service/service_manager.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
41
local/service/steam_service.go
Normal file → Executable file
41
local/service/steam_service.go
Normal file → Executable file
@@ -44,6 +44,14 @@ func NewSteamService(repository *repository.SteamCredentialsRepository, tfaManag
|
||||
}
|
||||
}
|
||||
|
||||
// getSteamCMDPath returns the platform-appropriate SteamCMD binary path.
|
||||
func getSteamCMDPath() string {
|
||||
if env.IsDockerPlatform() {
|
||||
return env.GetLinuxSteamCMDPath()
|
||||
}
|
||||
return env.GetSteamCMDPath()
|
||||
}
|
||||
|
||||
func (s *SteamService) GetCredentials(ctx context.Context) (*model.SteamCredentials, error) {
|
||||
return s.repository.GetCurrent(ctx)
|
||||
}
|
||||
@@ -56,6 +64,11 @@ func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCr
|
||||
}
|
||||
|
||||
func (s *SteamService) ensureSteamCMD(_ context.Context) error {
|
||||
// In Docker mode, SteamCMD is pre-installed in the manager container image.
|
||||
if env.IsDockerPlatform() {
|
||||
return nil
|
||||
}
|
||||
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
steamCMDDir := filepath.Dir(steamCMDPath)
|
||||
|
||||
@@ -92,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)
|
||||
@@ -117,7 +132,7 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||
}
|
||||
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
steamCMDPath := getSteamCMDPath()
|
||||
|
||||
steamCMDArgs := []string{
|
||||
"+force_install_dir", absPath,
|
||||
@@ -135,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",
|
||||
@@ -216,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)
|
||||
@@ -241,7 +262,7 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||
}
|
||||
|
||||
steamCMDPath := env.GetSteamCMDPath()
|
||||
steamCMDPath := getSteamCMDPath()
|
||||
|
||||
steamCMDArgs := []string{
|
||||
"+force_install_dir", absPath,
|
||||
@@ -259,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",
|
||||
|
||||
13
local/service/websocket.go
Normal file → Executable file
13
local/service/websocket.go
Normal file → Executable 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 {
|
||||
|
||||
68
local/service/windows_runtime.go
Normal file
68
local/service/windows_runtime.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/repository"
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type WindowsRuntime struct {
|
||||
windowsService *WindowsService
|
||||
repo *repository.ServerRepository
|
||||
}
|
||||
|
||||
func NewWindowsRuntime(windowsService *WindowsService, repo *repository.ServerRepository) *WindowsRuntime {
|
||||
return &WindowsRuntime{
|
||||
windowsService: windowsService,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Create(ctx context.Context, server *model.Server) error {
|
||||
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
|
||||
serverWorkingDir := server.GetServerPath()
|
||||
return r.windowsService.CreateService(ctx, server.ServiceName, execPath, serverWorkingDir, nil)
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Delete(ctx context.Context, serverID uuid.UUID) error {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.windowsService.DeleteService(ctx, server.ServiceName)
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Start(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.windowsService.Start(ctx, server.ServiceName)
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Stop(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.windowsService.Stop(ctx, server.ServiceName)
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Restart(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.windowsService.Restart(ctx, server.ServiceName)
|
||||
}
|
||||
|
||||
func (r *WindowsRuntime) Status(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||
server, err := r.repo.GetByID(ctx, serverID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.windowsService.Status(ctx, server.ServiceName)
|
||||
}
|
||||
0
local/service/windows_service.go
Normal file → Executable file
0
local/service/windows_service.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
49
local/utl/command/callback_executor.go
Normal file → Executable file
49
local/utl/command/callback_executor.go
Normal file → Executable 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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
69
local/utl/command/interactive_executor.go
Normal file → Executable file
69
local/utl/command/interactive_executor.go
Normal file → Executable 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
|
||||
}
|
||||
|
||||
|
||||
1
local/utl/common/common.go
Normal file → Executable file
1
local/utl/common/common.go
Normal file → Executable file
@@ -27,6 +27,7 @@ type RouteGroups struct {
|
||||
System fiber.Router
|
||||
WebSocket fiber.Router
|
||||
Leaderboard fiber.Router
|
||||
Steam fiber.Router
|
||||
}
|
||||
|
||||
func CheckError(err error) {
|
||||
|
||||
0
local/utl/configs/configs.go
Normal file → Executable file
0
local/utl/configs/configs.go
Normal file → Executable file
4
local/utl/db/db.go
Normal file → Executable file
4
local/utl/db/db.go
Normal file → Executable file
@@ -76,6 +76,10 @@ func runMigrations(db *gorm.DB) {
|
||||
logging.Error("Failed to run password security migration: %v", err)
|
||||
}
|
||||
|
||||
if err := migrations.RunAddPlatformColumnsMigration(db); err != nil {
|
||||
logging.Error("Failed to run platform columns migration: %v", err)
|
||||
}
|
||||
|
||||
logging.Info("Custom database migrations completed")
|
||||
}
|
||||
|
||||
|
||||
12
local/utl/docker/client.go
Normal file
12
local/utl/docker/client.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
func NewDockerClient() (*dockerclient.Client, error) {
|
||||
return dockerclient.NewClientWithOpts(
|
||||
dockerclient.FromEnv,
|
||||
dockerclient.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
63
local/utl/env/env.go
vendored
Normal file → Executable file
63
local/utl/env/env.go
vendored
Normal file → Executable file
@@ -6,10 +6,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSteamCMDPath = "c:\\steamcmd\\steamcmd.exe"
|
||||
DefaultNSSMPath = ".\\nssm.exe"
|
||||
DefaultSteamCMDPath = "c:\\steamcmd\\steamcmd.exe"
|
||||
DefaultNSSMPath = ".\\nssm.exe"
|
||||
DefaultPlatform = "windows"
|
||||
DefaultACCImage = "acc-wine:latest"
|
||||
DefaultACCServersPath = "/data/servers"
|
||||
DefaultLinuxSteamCMDPath = "/usr/games/steamcmd"
|
||||
)
|
||||
|
||||
func GetPlatform() string {
|
||||
if p := os.Getenv("PLATFORM"); p != "" {
|
||||
return p
|
||||
}
|
||||
return DefaultPlatform
|
||||
}
|
||||
|
||||
func IsDockerPlatform() bool {
|
||||
return GetPlatform() == "docker"
|
||||
}
|
||||
|
||||
func GetSteamCMDPath() string {
|
||||
if path := os.Getenv("STEAMCMD_PATH"); path != "" {
|
||||
return path
|
||||
@@ -17,6 +32,13 @@ func GetSteamCMDPath() string {
|
||||
return DefaultSteamCMDPath
|
||||
}
|
||||
|
||||
func GetLinuxSteamCMDPath() string {
|
||||
if path := os.Getenv("STEAMCMD_LINUX_PATH"); path != "" {
|
||||
return path
|
||||
}
|
||||
return DefaultLinuxSteamCMDPath
|
||||
}
|
||||
|
||||
func GetSteamCMDDirPath() string {
|
||||
steamCMDPath := GetSteamCMDPath()
|
||||
return filepath.Dir(steamCMDPath)
|
||||
@@ -29,17 +51,40 @@ func GetNSSMPath() string {
|
||||
return DefaultNSSMPath
|
||||
}
|
||||
|
||||
func GetACCImage() string {
|
||||
if img := os.Getenv("ACC_IMAGE"); img != "" {
|
||||
return img
|
||||
}
|
||||
return DefaultACCImage
|
||||
}
|
||||
|
||||
func GetACCServersPath() string {
|
||||
if p := os.Getenv("ACC_SERVERS_PATH"); p != "" {
|
||||
return p
|
||||
}
|
||||
return DefaultACCServersPath
|
||||
}
|
||||
|
||||
func GetACCServersHostPath() string {
|
||||
if p := os.Getenv("ACC_SERVERS_HOST_PATH"); p != "" {
|
||||
return p
|
||||
}
|
||||
return GetACCServersPath()
|
||||
}
|
||||
|
||||
func ValidatePaths() map[string]error {
|
||||
errors := make(map[string]error)
|
||||
|
||||
steamCMDPath := GetSteamCMDPath()
|
||||
if _, err := os.Stat(steamCMDPath); os.IsNotExist(err) {
|
||||
errors["STEAMCMD_PATH"] = err
|
||||
}
|
||||
if !IsDockerPlatform() {
|
||||
steamCMDPath := GetSteamCMDPath()
|
||||
if _, err := os.Stat(steamCMDPath); os.IsNotExist(err) {
|
||||
errors["STEAMCMD_PATH"] = err
|
||||
}
|
||||
|
||||
nssmPath := GetNSSMPath()
|
||||
if _, err := os.Stat(nssmPath); os.IsNotExist(err) {
|
||||
errors["NSSM_PATH"] = err
|
||||
nssmPath := GetNSSMPath()
|
||||
if _, err := os.Stat(nssmPath); os.IsNotExist(err) {
|
||||
errors["NSSM_PATH"] = err
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user