From 5ef7bfbc8286a0dafc15f76789392f248f82117c Mon Sep 17 00:00:00 2001 From: Sebastian Meier zu Biesen Date: Mon, 29 Sep 2025 16:29:41 +0200 Subject: [PATCH 01/32] fix: ensure run_bepinex.sh is removed only on Linux if it exists --- src/setup/sscm.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/setup/sscm.go b/src/setup/sscm.go index a41cb8f3..d278fa97 100644 --- a/src/setup/sscm.go +++ b/src/setup/sscm.go @@ -150,10 +150,12 @@ func downloadAndInstallBepInEx(url string) error { } } - if runtime.GOOS != "linux" { - err = os.Remove("./run_bepinex.sh") - if err != nil { - logger.Install.Warn(fmt.Sprintf("⚠️Failed to remove obsoleterun_bepinex.sh: %v", err)) + if runtime.GOOS == "linux" { + if _, err := os.Stat("./run_bepinex.sh"); err == nil { + err = os.Remove("./run_bepinex.sh") + if err != nil { + logger.Install.Warn(fmt.Sprintf("⚠️Failed to remove run_bepinex.sh: %v", err)) + } } } From 3e14b63b349f7811fb7f443e80c159d912125644 Mon Sep 17 00:00:00 2001 From: Sebastian Meier zu Biesen Date: Mon, 29 Sep 2025 16:41:01 +0200 Subject: [PATCH 02/32] This is attempt 2 to fix path traversal issues within steamcmd-helper.go so we don't slip on those zips apparently zips are a slippery slope, ok I'll stop now :rofl: --- src/steamcmd/steamcmd-helper.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/steamcmd/steamcmd-helper.go b/src/steamcmd/steamcmd-helper.go index 617aeacb..4caee4fe 100644 --- a/src/steamcmd/steamcmd-helper.go +++ b/src/steamcmd/steamcmd-helper.go @@ -44,6 +44,15 @@ func isSymlinkInsideRoot(name, link, root string) bool { return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(abs, string(os.PathSeparator)) } +func isPathInsideRoot(path, root string) bool { + cleanPath := filepath.Clean(path) + rel, err := filepath.Rel(root, cleanPath) + if err != nil { + return false + } + return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(cleanPath, string(os.PathSeparator)) +} + // createSteamCMDDirectory creates the SteamCMD directory. func createSteamCMDDirectory(steamCMDDir string) error { if err := os.MkdirAll(steamCMDDir, os.ModePerm); err != nil { @@ -183,6 +192,9 @@ func untar(dest string, r io.Reader) error { return fmt.Errorf("failed to create directory %s: %v", target, err) } case tar.TypeReg: + if !isPathInsideRoot(target, dest) { + return fmt.Errorf("invalid file path attempts to write outside root directory: %s", target) + } outFile, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) if err != nil { return fmt.Errorf("failed to create file %s: %v", target, err) @@ -230,6 +242,9 @@ func Unzip(zipReader io.ReaderAt, size int64, dest string) error { fpath := filepath.Join(dest, f.Name) // Ensure the file path is within the destination directory + if !isPathInsideRoot(fpath, dest) { + return fmt.Errorf("invalid file path attempts to write outside root directory: %s", fpath) + } relPath, err := filepath.Rel(dest, fpath) if err != nil || strings.HasPrefix(relPath, "..") || strings.HasPrefix(relPath, string(os.PathSeparator)) { return fmt.Errorf("invalid file path: %s", fpath) From 961a980c011bf4db78fae8e9ef7d1d5af58eee8f Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 08:28:41 +0200 Subject: [PATCH 03/32] Commit Server IP override --- .vscode/launch.json | 10 ++++ server.go | 6 ++ src/advertiser/advertiser.go | 109 +++++++++++++++++++++++++++++++++++ src/config/config.go | 12 ++-- src/config/getters.go | 6 ++ src/config/setters.go | 8 +++ src/config/vars.go | 1 + src/core/loader/cmdargs.go | 8 +++ 8 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 src/advertiser/advertiser.go diff --git a/.vscode/launch.json b/.vscode/launch.json index b5d9d441..23382b36 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,16 @@ "console": "integratedTerminal", "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm "args": ["--NoSteamCMD"] + }, + { + "name": "Debug Go Server noSteamCMD noSvelte overrideAdvertisedIp", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/server.go", + "console": "integratedTerminal", + "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm + "args": ["--NoSteamCMD", "--OverrideAdvertisedIp=127.0.0.1"] } ] } \ No newline at end of file diff --git a/server.go b/server.go index 94cb1576..2bac71db 100644 --- a/server.go +++ b/server.go @@ -24,7 +24,9 @@ import ( "embed" "sync" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/advertiser" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" @@ -58,5 +60,9 @@ func main() { web.StartWebServer(&wg) logger.Main.Debug("Initializing SSUICLI...") cli.StartConsole(&wg) + if config.GetOverrideAdvertisedIp() != "" { + logger.Main.Debug("Starting server advertiser...") + advertiser.StartAdvertiser(&wg) + } wg.Wait() } diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go new file mode 100644 index 00000000..c7172865 --- /dev/null +++ b/src/advertiser/advertiser.go @@ -0,0 +1,109 @@ +package advertiser + +import ( + "bytes" + "encoding/json" + "net/http" + "runtime" + "strconv" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" +) + +type ServerAdMessage struct { + SessionId int + Name string + Password bool + Version string + Address string + Port string + Players int + MaxPlayers int + Type int +} + +type ServerAdResponse struct { + SessionId int + Status string +} + +func StartAdvertiser(wg *sync.WaitGroup) { + if config.GetServerVisible() { + logger.Core.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") + return + } + wg.Go(func() { + sessionId := -1 + for { + // Only advertise if we are running + if gamemgr.InternalIsServerRunning() { + // Get max players + maxplayers, err := strconv.Atoi(config.GetServerMaxPlayers()) + if err != nil { + logger.Core.Errorf("ServerAdvertiser failed to convert port number to int: %s", config.GetServerMaxPlayers()) + return + } + // Get connected players + detector := detectionmgr.GetDetector() + players := len(detectionmgr.GetPlayers(detector)) + // Get platform + platform := 0 + switch runtime.GOOS { + case "windows": + platform = 1 + case "linux": + platform = 2 + } + adMessage := ServerAdMessage{ + SessionId: sessionId, + Name: config.GetServerName(), + Password: config.GetServerPassword() != "", + Version: config.GetExtractedGameVersion(), + Address: "127.0.0.1", //TODO - we need to pass this in from the command line + Port: config.GetGamePort(), + Players: players, + MaxPlayers: maxplayers, + Type: platform, + } + body, err := json.Marshal(adMessage) + if err != nil { + logger.Core.Errorf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) + return + } + // Send advertisement + resp, err := http.Post("http://40.82.200.175:8081/Ping", "application/json", bytes.NewBuffer(body)) + // Check for errors + if err != nil { + logger.Core.Errorf("ServerAdvertiser failed to send request: %v", err) + return + } + defer resp.Body.Close() + // Check the status + if resp.StatusCode != 200 { + logger.Core.Warnf("ServerAdvertiser received non-200 status: %d", resp.StatusCode) + } + // Read the response and update our sessionId if needed + adResponse := ServerAdResponse{} + err = json.NewDecoder(resp.Body).Decode(&adResponse) + if err != nil { + logger.Core.Errorf("Failed to decode response body: %v", err) + return + } + if adResponse.Status != "Success" { + logger.Core.Warnf("ServerAdvertiser received unexpeted status: %s", adResponse.Status) + } + sessionId = adResponse.SessionId + } else { + // Reset sessionid for the next run + sessionId = -1 + } + // Sleep for 30 seconds to follow the standard advertisement timer + time.Sleep(30 * time.Second) + } + }) +} diff --git a/src/config/config.go b/src/config/config.go index 26fcf72f..d1a6f52b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -51,10 +51,11 @@ type JsonConfig struct { StartLocation string `json:"StartLocation"` // Logging and debug settings - Debug *bool `json:"Debug"` - CreateSSUILogFile *bool `json:"CreateSSUILogFile"` - LogLevel int `json:"LogLevel"` - SubsystemFilters []string `json:"subsystemFilters"` + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + SubsystemFilters []string `json:"subsystemFilters"` + OverrideAdvertisedIp string `json:"OverrideAdvertisedIp"` // Authentication Settings Users map[string]string `json:"users"` // Map of username to hashed password @@ -296,6 +297,8 @@ func applyConfig(cfg *JsonConfig) { // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", SaveName, "Safebackups") + OverrideAdvertisedIp = getString(cfg.OverrideAdvertisedIp, "OVERRIDE_ADVERTISED_IP", "") + safeSaveConfig() } @@ -365,6 +368,7 @@ func safeSaveConfig() error { AutoStartServerOnStartup: &AutoStartServerOnStartup, SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, + OverrideAdvertisedIp: OverrideAdvertisedIp, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 27067b83..d5b78aaa 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -513,3 +513,9 @@ func GetIsDockerContainer() bool { defer ConfigMu.RUnlock() return IsDockerContainer } + +func GetOverrideAdvertisedIp() string { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return OverrideAdvertisedIp +} diff --git a/src/config/setters.go b/src/config/setters.go index 58627abb..438482e7 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -694,3 +694,11 @@ func SetAllowAutoGameServerUpdates(value bool) error { AllowAutoGameServerUpdates = value return safeSaveConfig() } + +func SetOverrideAdvertisedIp(value string) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + OverrideAdvertisedIp = value + return safeSaveConfig() +} diff --git a/src/config/vars.go b/src/config/vars.go index a2f3a507..a3aacc24 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,6 +61,7 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string + OverrideAdvertisedIp string ) // Runtime only variables diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 1b04418c..303ac507 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -21,6 +21,7 @@ var recoveryPasswordFlag string var devModeFlag bool var skipSteamCMDFlag bool var sanityCheckFlag bool +var overrideAdvertisedIpFlag string // ParseFlags parses command-line arguments ONCE at startup (called from func main) func ParseFlags() { @@ -39,6 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") + flag.StringVar(&overrideAdvertisedIpFlag, "OverrideAdvertisedIp", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse() @@ -100,6 +102,12 @@ func HandleFlags() { logger.Main.Info(fmt.Sprintf("Overriding IsDebugMode from command line: Before=%t, Now=true", oldDebug)) } + if overrideAdvertisedIpFlag != "" { + oldOverrideAdvertisedIp := config.GetOverrideAdvertisedIp() + config.SetOverrideAdvertisedIp(overrideAdvertisedIpFlag) + logger.Main.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=true", oldOverrideAdvertisedIp)) + } + if createSSUILogFileFlag { oldCreateSSUILogFile := config.GetCreateSSUILogFile() config.SetCreateSSUILogFile(true) From acefe2d7a4b90f8948e716409974869c7d613886 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 09:41:50 +0200 Subject: [PATCH 04/32] Fix hardcoded IP --- src/advertiser/advertiser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index c7172865..86cd38e7 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -64,7 +64,7 @@ func StartAdvertiser(wg *sync.WaitGroup) { Name: config.GetServerName(), Password: config.GetServerPassword() != "", Version: config.GetExtractedGameVersion(), - Address: "127.0.0.1", //TODO - we need to pass this in from the command line + Address: config.GetOverrideAdvertisedIp(), Port: config.GetGamePort(), Players: players, MaxPlayers: maxplayers, From fe9d4443275badf090e81a762ea71fe87fe9fd21 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:36:48 +0200 Subject: [PATCH 05/32] refactored IsServerRunning checks to use a global setting instead of individually getting the state, reducing load on the backend. --- src/config/getters.go | 6 ++++++ src/config/setters.go | 8 ++++++++ src/config/vars.go | 1 + src/core/loader/loader.go | 6 ++++++ src/managers/gamemgr/processmanagement.go | 2 ++ src/managers/gamemgr/runcheck.go | 19 ++++++++++++++++++- src/web/http.go | 2 +- src/web/routes.go | 3 +++ 8 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/config/getters.go b/src/config/getters.go index 27067b83..85ba8dc7 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -513,3 +513,9 @@ func GetIsDockerContainer() bool { defer ConfigMu.RUnlock() return IsDockerContainer } + +func GetIsGameServerRunning() bool { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return IsGameServerRunning +} diff --git a/src/config/setters.go b/src/config/setters.go index 58627abb..7c934dea 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -86,6 +86,14 @@ func SetWorldID(value string) error { return nil } +func SetIsGameServerRunning(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + IsGameServerRunning = value + return nil +} + // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT diff --git a/src/config/vars.go b/src/config/vars.go index a2f3a507..7aa37788 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -71,6 +71,7 @@ var ( SkipSteamCMD bool // ONLY RUNTIME IsDockerContainer bool // ONLY RUNTIME NoSanityCheck bool // ONLY RUNTIME + IsGameServerRunning bool // ONLY RUNTIME ) // Discord integration diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 6c224cc4..9f0790c3 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -13,6 +13,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" @@ -29,6 +30,7 @@ func InitBackend(wg *sync.WaitGroup) { ReloadAppInfoPoller() ReloadDiscordBot() InitDetector() + StartIsGameServerRunningCheck() } // use this to reload backend at runtime @@ -91,6 +93,10 @@ func ReloadLocalizer() { localization.ReloadLocalizer() } +func StartIsGameServerRunningCheck() { + gamemgr.StartIsGameServerRunningCheck() +} + func ReloadAppInfoPoller() { steamcmd.AppInfoPoller() } diff --git a/src/managers/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go index 06e1a296..280eba9c 100644 --- a/src/managers/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -129,6 +129,7 @@ func InternalStartServer() error { } // create a UUID for this specific run createGameServerUUID() + config.SetIsGameServerRunning(true) // Start auto-restart goroutine if AutoRestartServerTimer is set greater than 0 if config.GetAutoRestartServerTimer() != "0" { @@ -217,6 +218,7 @@ func InternalStopServer() error { // Process is confirmed stopped, clear cmd cmd = nil + config.SetIsGameServerRunning(false) clearGameServerUUID() return nil } diff --git a/src/managers/gamemgr/runcheck.go b/src/managers/gamemgr/runcheck.go index a57734f4..cf83544e 100644 --- a/src/managers/gamemgr/runcheck.go +++ b/src/managers/gamemgr/runcheck.go @@ -3,16 +3,33 @@ package gamemgr import ( "runtime" "syscall" + "time" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) +func StartIsGameServerRunningCheck() { + go func() { + for { + if InternalIsServerRunning() { + config.SetIsGameServerRunning(true) + } else { + config.SetIsGameServerRunning(false) + } + time.Sleep(4 * time.Second) + } + }() +} + // InternalIsServerRunning checks if the server process is running. // Safe to call standalone as it manages its own locking. func InternalIsServerRunning() bool { mu.Lock() defer mu.Unlock() - return internalIsServerRunningNoLock() + status := internalIsServerRunningNoLock() + config.SetIsGameServerRunning(status) + return status } // internalIsServerRunningNoLock checks if the server process is running. diff --git a/src/web/http.go b/src/web/http.go index fd010f65..1b214fe6 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -48,7 +48,7 @@ func StopServer(w http.ResponseWriter, r *http.Request) { } func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { - runState := gamemgr.InternalIsServerRunning() + runState := config.GetIsGameServerRunning() response := map[string]interface{}{ "isRunning": runState, "uuid": gamemgr.GameServerUUID.String(), diff --git a/src/web/routes.go b/src/web/routes.go index 9b7a45eb..4975bb86 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -79,5 +79,8 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/auth/setup/register", RegisterUserHandler) // user registration protectedMux.HandleFunc("/api/v2/auth/setup/finalize", SetupFinalizeHandler) + // Monitoring + protectedMux.HandleFunc("/api/v2/monitor/status", HandleMonitorStatus) + return mux, protectedMux } From ea0302162797eded5263b47a2bc55d7df2d6dd9e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:37:26 +0200 Subject: [PATCH 06/32] added an endpoint to get the server status --- src/web/monitoring.go | 20 ++++++++++++++++++++ src/web/routes.go | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/web/monitoring.go diff --git a/src/web/monitoring.go b/src/web/monitoring.go new file mode 100644 index 00000000..74dff40f --- /dev/null +++ b/src/web/monitoring.go @@ -0,0 +1,20 @@ +package web + +import ( + "encoding/json" + "net/http" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +func HandleMonitorStatus(w http.ResponseWriter, r *http.Request) { + runState := config.GetIsGameServerRunning() + response := map[string]interface{}{ + "isRunning": runState, + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to respond with Game Server status", http.StatusInternalServerError) + return + } +} diff --git a/src/web/routes.go b/src/web/routes.go index 4975bb86..f52dcd65 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -80,7 +80,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/auth/setup/finalize", SetupFinalizeHandler) // Monitoring - protectedMux.HandleFunc("/api/v2/monitor/status", HandleMonitorStatus) + protectedMux.HandleFunc("/api/v2/monitor/gameserver/status", HandleMonitorStatus) return mux, protectedMux } From d1189a24bf3d3a059fa58b17e80e9da965d692f3 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 19:56:55 +0900 Subject: [PATCH 07/32] Apply suggestions from code review Co-authored-by: JLangisch Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/advertiser/advertiser.go | 4 ++-- src/core/loader/cmdargs.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 86cd38e7..02c3144d 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -45,7 +45,7 @@ func StartAdvertiser(wg *sync.WaitGroup) { // Get max players maxplayers, err := strconv.Atoi(config.GetServerMaxPlayers()) if err != nil { - logger.Core.Errorf("ServerAdvertiser failed to convert port number to int: %s", config.GetServerMaxPlayers()) + logger.Core.Errorf("ServerAdvertiser failed to convert max players number to int: %s", config.GetServerMaxPlayers()) return } // Get connected players @@ -95,7 +95,7 @@ func StartAdvertiser(wg *sync.WaitGroup) { return } if adResponse.Status != "Success" { - logger.Core.Warnf("ServerAdvertiser received unexpeted status: %s", adResponse.Status) + logger.Core.Warnf("ServerAdvertiser received unexpected status: %s", adResponse.Status) } sessionId = adResponse.SessionId } else { diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 303ac507..d264fa27 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -105,7 +105,7 @@ func HandleFlags() { if overrideAdvertisedIpFlag != "" { oldOverrideAdvertisedIp := config.GetOverrideAdvertisedIp() config.SetOverrideAdvertisedIp(overrideAdvertisedIpFlag) - logger.Main.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=true", oldOverrideAdvertisedIp)) + logger.Main.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag)) } if createSSUILogFileFlag { From 483ab814410aea34ade6f05515021c7583baf675 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:30:20 +0200 Subject: [PATCH 08/32] adds the ability to add apikey- users with a 3 year token expiration time --- src/core/security/auth.go | 6 +++++- src/web/login.go | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/security/auth.go b/src/core/security/auth.go index a052eb24..88f76be0 100644 --- a/src/core/security/auth.go +++ b/src/core/security/auth.go @@ -4,6 +4,7 @@ package security //repurposed from a Jacksonthemaster private repo import ( + "strings" "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" @@ -21,6 +22,10 @@ type UserCredentials struct { // GenerateJWT creates a JWT for a given username func GenerateJWT(username string) (string, error) { expirationTime := time.Now().Add(time.Duration(config.GetAuthTokenLifetime()) * time.Minute) + if strings.HasPrefix(username, "apikey-") { + expirationTime = time.Now().Add(3 * 365 * 24 * time.Hour) + } + claims := &jwt.MapClaims{ "exp": expirationTime.Unix(), "iss": "StationeersServerUI", @@ -37,7 +42,6 @@ func GenerateJWT(username string) (string, error) { // ValidateCredentials checks username and password against stored users func ValidateCredentials(creds UserCredentials) (bool, error) { - // Placeholder: assumes config.Users is a map[string]string (username -> hashed password) storedHash, exists := config.GetUsers()[creds.Username] if !exists { return false, nil diff --git a/src/web/login.go b/src/web/login.go index e2c8f0db..a4e82d1a 100644 --- a/src/web/login.go +++ b/src/web/login.go @@ -172,6 +172,13 @@ func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { return } + if strings.HasPrefix(creds.Username, "apikey-") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid Username"}) + return + } + // Hash the password hashedPassword, err := security.HashPassword(creds.Password) if err != nil { From 50b007f0f12be54d79d70c22393901c77940fc45 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:30:57 +0200 Subject: [PATCH 09/32] reflect runState in http code as well --- src/web/monitoring.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/web/monitoring.go b/src/web/monitoring.go index 74dff40f..ab5ad023 100644 --- a/src/web/monitoring.go +++ b/src/web/monitoring.go @@ -13,6 +13,13 @@ func HandleMonitorStatus(w http.ResponseWriter, r *http.Request) { "isRunning": runState, } w.Header().Set("Content-Type", "application/json") + + if !runState { + w.WriteHeader(http.StatusServiceUnavailable) // 503 + } else { + w.WriteHeader(http.StatusOK) // 200 + } + if err := json.NewEncoder(w).Encode(response); err != nil { http.Error(w, "Failed to respond with Game Server status", http.StatusInternalServerError) return From 71109e5a5b279f67e0a87c9ca5202169e733be5d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:32:15 +0200 Subject: [PATCH 10/32] added a dedicated endpoint to request an APIKey --- src/web/login.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++ src/web/routes.go | 1 + 2 files changed, 57 insertions(+) diff --git a/src/web/login.go b/src/web/login.go index a4e82d1a..a3c38ba4 100644 --- a/src/web/login.go +++ b/src/web/login.go @@ -14,6 +14,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/google/uuid" ) var setupReminderCount = 0 // to limit the number of setup reminders shown to the user @@ -247,3 +248,58 @@ func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { }) loader.ReloadBackend() } + +func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { + + // Handle preflight OPTIONS requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + // reject requests with non-GET methods + if r.Method != http.MethodGet { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method Not Allowed"}) + return + } + + var creds security.UserCredentials + + // Generate a random UUID as the username + creds.Username = "apikey-" + uuid.NewString() + + // Hash a random UUID as the password + hashedPassword, err := security.HashPassword(uuid.NewString()) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + + // Initialize Users map if nil + if config.GetUsers() == nil { + config.SetUsers(make(map[string]string)) + } + + // Add or update the user + config.SetUsers(map[string]string{creds.Username: hashedPassword}) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + apikey, err := security.GenerateJWT(creds.Username) + expires := time.Now().Add(3 * 365 * 24 * time.Hour) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + json.NewEncoder(w).Encode(map[string]string{ + "message": "APIKey registered successfully", + "apikey": apikey, + "expires": expires.Format(time.RFC3339), + }) +} diff --git a/src/web/routes.go b/src/web/routes.go index f52dcd65..394858ce 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -77,6 +77,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // Setup protectedMux.HandleFunc("/setup", ServeTwoBoxFormTemplate) protectedMux.HandleFunc("/api/v2/auth/setup/register", RegisterUserHandler) // user registration + protectedMux.HandleFunc("/api/v2/auth/setup/apikey", RegisterAPIKeyHandler) // API Key registration protectedMux.HandleFunc("/api/v2/auth/setup/finalize", SetupFinalizeHandler) // Monitoring From c92e40f92cbac675e621b720720d76d0d6638941 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:14:22 +0200 Subject: [PATCH 11/32] Moving server advertiser to a separate goroutine in the backend init --- server.go | 6 ------ src/advertiser/advertiser.go | 7 +++---- src/core/loader/loader.go | 5 +++++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/server.go b/server.go index 2bac71db..94cb1576 100644 --- a/server.go +++ b/server.go @@ -24,9 +24,7 @@ import ( "embed" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/advertiser" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" @@ -60,9 +58,5 @@ func main() { web.StartWebServer(&wg) logger.Main.Debug("Initializing SSUICLI...") cli.StartConsole(&wg) - if config.GetOverrideAdvertisedIp() != "" { - logger.Main.Debug("Starting server advertiser...") - advertiser.StartAdvertiser(&wg) - } wg.Wait() } diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 02c3144d..fa0b6ad0 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -6,7 +6,6 @@ import ( "net/http" "runtime" "strconv" - "sync" "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" @@ -32,12 +31,12 @@ type ServerAdResponse struct { Status string } -func StartAdvertiser(wg *sync.WaitGroup) { +func StartAdvertiser() { if config.GetServerVisible() { logger.Core.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") return } - wg.Go(func() { + go func() { sessionId := -1 for { // Only advertise if we are running @@ -105,5 +104,5 @@ func StartAdvertiser(wg *sync.WaitGroup) { // Sleep for 30 seconds to follow the standard advertisement timer time.Sleep(30 * time.Second) } - }) + }() } diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 6c224cc4..61df63cf 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/advertiser" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" @@ -29,6 +30,10 @@ func InitBackend(wg *sync.WaitGroup) { ReloadAppInfoPoller() ReloadDiscordBot() InitDetector() + if config.GetOverrideAdvertisedIp() != "" { + logger.Main.Debug("Starting server advertiser...") + advertiser.StartAdvertiser() + } } // use this to reload backend at runtime From 993146631fe368f190c000b280ff118c44c7a696 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:23:46 +0200 Subject: [PATCH 12/32] Update logger for advertiser --- src/advertiser/advertiser.go | 14 +++++++------- src/core/loader/loader.go | 2 +- src/logger/logger.go | 3 +++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index fa0b6ad0..a96e150e 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -33,7 +33,7 @@ type ServerAdResponse struct { func StartAdvertiser() { if config.GetServerVisible() { - logger.Core.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") + logger.Advertiser.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") return } go func() { @@ -44,7 +44,7 @@ func StartAdvertiser() { // Get max players maxplayers, err := strconv.Atoi(config.GetServerMaxPlayers()) if err != nil { - logger.Core.Errorf("ServerAdvertiser failed to convert max players number to int: %s", config.GetServerMaxPlayers()) + logger.Advertiser.Errorf("ServerAdvertiser failed to convert max players number to int: %s", config.GetServerMaxPlayers()) return } // Get connected players @@ -71,30 +71,30 @@ func StartAdvertiser() { } body, err := json.Marshal(adMessage) if err != nil { - logger.Core.Errorf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) + logger.Advertiser.Errorf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) return } // Send advertisement resp, err := http.Post("http://40.82.200.175:8081/Ping", "application/json", bytes.NewBuffer(body)) // Check for errors if err != nil { - logger.Core.Errorf("ServerAdvertiser failed to send request: %v", err) + logger.Advertiser.Errorf("ServerAdvertiser failed to send request: %v", err) return } defer resp.Body.Close() // Check the status if resp.StatusCode != 200 { - logger.Core.Warnf("ServerAdvertiser received non-200 status: %d", resp.StatusCode) + logger.Advertiser.Warnf("ServerAdvertiser received non-200 status: %d", resp.StatusCode) } // Read the response and update our sessionId if needed adResponse := ServerAdResponse{} err = json.NewDecoder(resp.Body).Decode(&adResponse) if err != nil { - logger.Core.Errorf("Failed to decode response body: %v", err) + logger.Advertiser.Errorf("Failed to decode response body: %v", err) return } if adResponse.Status != "Success" { - logger.Core.Warnf("ServerAdvertiser received unexpected status: %s", adResponse.Status) + logger.Advertiser.Warnf("ServerAdvertiser received unexpected status: %s", adResponse.Status) } sessionId = adResponse.SessionId } else { diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 61df63cf..df1a62c8 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -31,7 +31,7 @@ func InitBackend(wg *sync.WaitGroup) { ReloadDiscordBot() InitDetector() if config.GetOverrideAdvertisedIp() != "" { - logger.Main.Debug("Starting server advertiser...") + logger.Advertiser.Info("Starting server advertiser...") advertiser.StartAdvertiser() } } diff --git a/src/logger/logger.go b/src/logger/logger.go index 958d3971..a505e154 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -24,6 +24,7 @@ var ( SSE = &Logger{suffix: SYS_SSE} Security = &Logger{suffix: SYS_SECURITY} Localization = &Logger{suffix: SYS_LOCALIZATION} + Advertiser = &Logger{suffix: SYS_ADVERTISER} ) // Severity Levels @@ -48,6 +49,7 @@ const ( SYS_SSE = "SSE" SYS_SECURITY = "SECURITY" SYS_LOCALIZATION = "LOCALIZATION" + SYS_ADVERTISER = "ADVERTISER" ) const ( @@ -73,6 +75,7 @@ var subsystemColors = map[string]string{ SYS_SSE: colorCyan, // Matches WEB, streaming vibe SYS_SECURITY: colorRed, // Screams "pay attention" SYS_LOCALIZATION: colorCyan, // Matches WEB, localization-related + SYS_ADVERTISER: colorYellow, // Matches Config, advanced feature } // Global channels and mutex for all loggers From ade0b30bfe3baa20fc0a88a752c3784a564e01c0 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:59:11 +0200 Subject: [PATCH 13/32] Remove unnecessary waits --- server.go | 12 ++++-------- src/core/loader/afterstart.go | 6 +----- src/core/loader/loader.go | 9 ++------- src/setup/install.go | 5 +---- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/server.go b/server.go index 94cb1576..a11365b5 100644 --- a/server.go +++ b/server.go @@ -39,21 +39,17 @@ func main() { logger.ConfigureConsole() loader.ParseFlags() loader.HandleSanityCheckFlag() - loader.SanityCheck(&wg) - wg.Wait() + loader.SanityCheck() logger.Main.Info("Initializing resources...") loader.InitVirtFS(v1uiFS) logger.Install.Info("Starting setup...") loader.ReloadConfig() // Load the config file before starting the setup process loader.HandleFlags() - setup.Install(&wg) - wg.Wait() + setup.Install() logger.Main.Debug("Initializing Backend...") - loader.InitBackend(&wg) - wg.Wait() + loader.InitBackend() logger.Main.Debug("Initializing after start tasks...") - loader.AfterStartComplete(&wg) - wg.Wait() + loader.AfterStartComplete() logger.Main.Debug("Starting webserver...") web.StartWebServer(&wg) logger.Main.Debug("Initializing SSUICLI...") diff --git a/src/core/loader/afterstart.go b/src/core/loader/afterstart.go index 91c3824b..1e72fee7 100644 --- a/src/core/loader/afterstart.go +++ b/src/core/loader/afterstart.go @@ -1,8 +1,6 @@ package loader import ( - "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordrpc" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" @@ -10,9 +8,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" ) -func AfterStartComplete(wg *sync.WaitGroup) { - wg.Add(1) - defer wg.Done() +func AfterStartComplete() { config.SetSaveConfig() // Save config after startup through setters err := setup.CleanUpOldUIModFolderFiles() if err != nil { diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index df1a62c8..fa6e1d3c 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -4,7 +4,6 @@ package loader import ( "embed" "os" - "sync" "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/advertiser" @@ -20,9 +19,7 @@ import ( ) // only call this once at startup -func InitBackend(wg *sync.WaitGroup) { - wg.Add(1) - defer wg.Done() +func InitBackend() { ReloadConfig() ReloadSSCM() ReloadBackupManager() @@ -105,9 +102,7 @@ func InitVirtFS(v1uiFS embed.FS) { config.SetV1UIFS(v1uiFS) } -func SanityCheck(wg *sync.WaitGroup) { - wg.Add(1) - defer wg.Done() +func SanityCheck() { err := runSanityCheck() if err != nil { logger.Main.Error("Sanity check failed, exiting in 10 secconds: " + err.Error()) diff --git a/src/setup/install.go b/src/setup/install.go index 99e1d82d..052338ad 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -23,10 +23,7 @@ import ( var downloadBranch string // Holds the branch to download from // Install performs the entire installation process and ensures the server waits for it to complete -func Install(wg *sync.WaitGroup) { - wg.Add(1) - defer wg.Done() // Signal that installation is complete - +func Install() { // Step 0: Check for updates if err := update.UpdateExecutable(); err != nil { logger.Install.Error("❌Update check went sideways: " + err.Error()) From d87f67f8a5fd888231032311487eb8a99a1c64c3 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 17:09:18 +0200 Subject: [PATCH 14/32] Make the advertisement server a const --- src/advertiser/advertiser.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index a96e150e..eba2d9e8 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -14,6 +14,8 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" ) +const StationeersAdvertisementEndpoint = "http://40.82.200.175:8081/Ping" + type ServerAdMessage struct { SessionId int Name string @@ -75,7 +77,7 @@ func StartAdvertiser() { return } // Send advertisement - resp, err := http.Post("http://40.82.200.175:8081/Ping", "application/json", bytes.NewBuffer(body)) + resp, err := http.Post(StationeersAdvertisementEndpoint, "application/json", bytes.NewBuffer(body)) // Check for errors if err != nil { logger.Advertiser.Errorf("ServerAdvertiser failed to send request: %v", err) From bc6e0ee7d5b99b8aff1174bdd6ddc748a6b04c22 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:58:39 +0200 Subject: [PATCH 15/32] improve APIKey handling: support dynamic expiration duration (POST) and adjust JWT generation accordingly. Defaults to 1 month if user sends a GET instead. --- src/core/security/auth.go | 8 ++++++-- src/web/login.go | 37 +++++++++++++++++++++++++++++++++---- src/web/monitoring.go | 10 ++-------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/core/security/auth.go b/src/core/security/auth.go index 88f76be0..01ccd6b0 100644 --- a/src/core/security/auth.go +++ b/src/core/security/auth.go @@ -20,10 +20,14 @@ type UserCredentials struct { } // GenerateJWT creates a JWT for a given username -func GenerateJWT(username string) (string, error) { +func GenerateJWT(username string, apikeyduration ...int) (string, error) { expirationTime := time.Now().Add(time.Duration(config.GetAuthTokenLifetime()) * time.Minute) if strings.HasPrefix(username, "apikey-") { - expirationTime = time.Now().Add(3 * 365 * 24 * time.Hour) + durationMonths := 1 + if len(apikeyduration) > 0 { + durationMonths = apikeyduration[0] + } + expirationTime = time.Now().AddDate(0, durationMonths, 0) } claims := &jwt.MapClaims{ diff --git a/src/web/login.go b/src/web/login.go index a3c38ba4..a442a023 100644 --- a/src/web/login.go +++ b/src/web/login.go @@ -257,14 +257,42 @@ func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { return } - // reject requests with non-GET methods - if r.Method != http.MethodGet { + // Allow only GET or POST methods + if r.Method != http.MethodGet && r.Method != http.MethodPost { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusMethodNotAllowed) json.NewEncoder(w).Encode(map[string]string{"error": "Method Not Allowed"}) return } + // Set default duration for GET requests, require duration for POST + durationMonths := 1 + if r.Method == http.MethodPost { + var reqBody struct { + DurationMonths *int `json:"durationMonths"` // Use pointer to distinguish between 0 and unspecified + } + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) + return + } + if reqBody.DurationMonths == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - durationMonths is required for POST"}) + return + } + if *reqBody.DurationMonths <= 0 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Duration must be positive"}) + return + } + durationMonths = *reqBody.DurationMonths + } + var creds security.UserCredentials // Generate a random UUID as the username @@ -289,8 +317,8 @@ func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - apikey, err := security.GenerateJWT(creds.Username) - expires := time.Now().Add(3 * 365 * 24 * time.Hour) + apikey, err := security.GenerateJWT(creds.Username, durationMonths) + expires := time.Now().AddDate(0, durationMonths, 0) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) @@ -302,4 +330,5 @@ func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { "apikey": apikey, "expires": expires.Format(time.RFC3339), }) + logger.Security.Infof("APIKey %s registered successfully. Expires: %s ", creds.Username, expires.Format(time.RFC3339)) } diff --git a/src/web/monitoring.go b/src/web/monitoring.go index ab5ad023..3f524abb 100644 --- a/src/web/monitoring.go +++ b/src/web/monitoring.go @@ -12,15 +12,9 @@ func HandleMonitorStatus(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "isRunning": runState, } - w.Header().Set("Content-Type", "application/json") - - if !runState { - w.WriteHeader(http.StatusServiceUnavailable) // 503 - } else { - w.WriteHeader(http.StatusOK) // 200 - } - if err := json.NewEncoder(w).Encode(response); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) // 200 OK http.Error(w, "Failed to respond with Game Server status", http.StatusInternalServerError) return } From 77c7ec94f413f8c56053295a0f8129b4690795d8 Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 5 Oct 2025 23:05:31 +0200 Subject: [PATCH 16/32] Fix race condition in SSE handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The race condition between writing to the Http2 Response Writer occurs when SSE connection is closed, but the Fprintf formatting code is still executing. ``` panic: Write called after Handler finished goroutine 9953 [running]: net/http.(*http2responseWriter).write(0x2?, 0xc0000944e0?, {0xc000096500?, 0x7ff6153450be?, 0x7ff615d95060?}, {0x0?, 0xc000500808?}) /usr/local/go/src/net/http/h2_bundle.go:6987 +0x13f net/http.(*http2responseWriter).Write(0xc0000944e0?, {0xc000096500?, 0xa?, 0xc000391f50?}) /usr/local/go/src/net/http/h2_bundle.go:6976 +0x2a fmt.Fprintf({0x2426bb5a1a0, 0xc0001982d0}, {0x7ff61584992c, 0xa}, {0xc000391f50, 0x1, 0x1}) /usr/local/go/src/fmt/print.go:225 +0x97 github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream.(*SSEManager).streamMessages(0x0?, {0x7ff6159deac0, 0xc0001982d0}, {0x2426b765b58, 0xc0001982d0}, 0xc0001708a0, {0x0?, 0x0?}, 0xc0002f0150) src/core/ssestream/ssemanager.go:113 +0x1b6 created by github.com/JacksonTheMaster/StationeersServerUI/v5/src/web.GetLogOutput.StartConsoleStream.(*SSEManager).CreateStreamHandler.func1 in goroutine 9952 src/core/ssestream/ssemanager.go:93 +0x409 ``` This is due to concurrent closing of the underlaying Http2 Response Writer and Formatting message Reproducer via artificial delay: ``` for { select { case msg := <-client.messages: time.Sleep(300 * time.Millisecond) _, err := fmt.Fprintf(w, "data: %s\n\n", msg) if err != nil { logger.SSE.Error(" ❌ Failed to send message: " + err.Error()) return } flusher.Flush() case <-notify: return } } ``` --- src/core/ssestream/ssemanager.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/ssestream/ssemanager.go b/src/core/ssestream/ssemanager.go index 16e86919..7d11efef 100644 --- a/src/core/ssestream/ssemanager.go +++ b/src/core/ssestream/ssemanager.go @@ -106,10 +106,7 @@ func (m *SSEManager) CreateStreamHandler(streamType string) http.HandlerFunc { notify := r.Context().Done() // Start streaming messages - go m.streamMessages(w, flusher, client, streamType, notify) - - // Wait for client disconnection - <-notify + m.streamMessages(w, flusher, client, streamType, notify) } } From a16e1baa057cd0663110cf22a4fdea68c3910c2f Mon Sep 17 00:00:00 2001 From: akirilov Date: Wed, 8 Oct 2025 02:06:02 +0200 Subject: [PATCH 17/32] Updated SSCM.dll - See SSCM project for details --- sscm/SSCM.dll | Bin 10240 -> 11776 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sscm/SSCM.dll b/sscm/SSCM.dll index 766d35c798b5909fa4bfb8de124b5b9f99d77c8b..29ab16049728fb2bee7a824a66ee52a87860e3c8 100644 GIT binary patch literal 11776 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 XziMEI^EU?P+JDsy`+s}>FFf$y?psBx delta 3915 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 O=jzkG=qKz@v*Uke9HN~7 From 16a277f9e3b0df4d0f0cdedc030c89e6439a8349 Mon Sep 17 00:00:00 2001 From: akirilov Date: Wed, 8 Oct 2025 05:32:32 +0200 Subject: [PATCH 18/32] Updated SSCM.dll to optimize thread usage --- sscm/SSCM.dll | Bin 11776 -> 12288 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sscm/SSCM.dll b/sscm/SSCM.dll index 29ab16049728fb2bee7a824a66ee52a87860e3c8..62b505696c033d94f411115a29fdc0d17dd3d065 100644 GIT binary patch delta 4375 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 OP}R6mKEcK!+W!D|YO$mM From eb11022ec92ac450da556e6babd93c378e78ec99 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:40:44 +0200 Subject: [PATCH 19/32] Refactor Stationeers server ping endpoint var to use config getter and hardcode in config package for improved maintainability Co-authored-by: mitoskalandiel --- src/advertiser/advertiser.go | 2 +- src/config/getters.go | 6 ++++++ src/config/vars.go | 29 +++++++++++++++-------------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index eba2d9e8..f622b731 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -14,7 +14,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" ) -const StationeersAdvertisementEndpoint = "http://40.82.200.175:8081/Ping" +var StationeersAdvertisementEndpoint = config.GetStationeersServerPingEndpoint() type ServerAdMessage struct { SessionId int diff --git a/src/config/getters.go b/src/config/getters.go index fff0e4e8..2b8a7f7f 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -524,3 +524,9 @@ func GetOverrideAdvertisedIp() string { defer ConfigMu.RUnlock() return OverrideAdvertisedIp } + +func GetStationeersServerPingEndpoint() string { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return StationeersServerPingEndpoint +} diff --git a/src/config/vars.go b/src/config/vars.go index 5113f7c7..50ecb59e 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -131,20 +131,21 @@ var ( // File paths var ( - TLSCertPath = "./UIMod/tls/cert.pem" - TLSKeyPath = "./UIMod/tls/key.pem" - ConfigPath = "./UIMod/config/config.json" - CustomDetectionsFilePath = "./UIMod/config/customdetections.json" - LogFolder = "./UIMod/logs/" - UIModFolder = "./UIMod/" - TwoBoxFormFolder = "./UIMod/twoboxform/" - ConfigHtmlPath = "./UIMod/ui/config.html" - DetectionManagerHtmlPath = "./UIMod/ui/detectionmanager.html" - TwoBoxFormHtmlPath = "./UIMod/twoboxform/twoboxform.html" - IndexHtmlPath = "./UIMod/ui/index.html" - SSCMWebDir = "./UIMod/sscm/" - SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" - SSCMPluginDir = "./BepInEx/plugins/SSCM/" + TLSCertPath = "./UIMod/tls/cert.pem" + TLSKeyPath = "./UIMod/tls/key.pem" + ConfigPath = "./UIMod/config/config.json" + CustomDetectionsFilePath = "./UIMod/config/customdetections.json" + LogFolder = "./UIMod/logs/" + UIModFolder = "./UIMod/" + TwoBoxFormFolder = "./UIMod/twoboxform/" + ConfigHtmlPath = "./UIMod/ui/config.html" + DetectionManagerHtmlPath = "./UIMod/ui/detectionmanager.html" + TwoBoxFormHtmlPath = "./UIMod/twoboxform/twoboxform.html" + IndexHtmlPath = "./UIMod/ui/index.html" + SSCMWebDir = "./UIMod/sscm/" + SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" + SSCMPluginDir = "./BepInEx/plugins/SSCM/" + StationeersServerPingEndpoint = "http://40.82.200.175:8081/Ping" ) // Bundled Assets From b3fb8ebb41a05b97eddb26d66d517f43c68f9786 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:55:51 +0200 Subject: [PATCH 20/32] update loader for maintainablity Co-authored-by: mitoskalandiel --- src/core/loader/loader.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index b8d79ce7..3e9916e7 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -29,10 +29,7 @@ func InitBackend() { ReloadDiscordBot() InitDetector() StartIsGameServerRunningCheck() - if config.GetOverrideAdvertisedIp() != "" { - logger.Advertiser.Info("Starting server advertiser...") - advertiser.StartAdvertiser() - } + LoadAdvertiser() } // use this to reload backend at runtime @@ -103,6 +100,13 @@ func ReloadAppInfoPoller() { steamcmd.AppInfoPoller() } +func LoadAdvertiser() { + if config.GetOverrideAdvertisedIp() != "" { + logger.Advertiser.Info("Starting server advertiser...") + advertiser.StartAdvertiser() + } +} + // InitBundler initialized the onboard bundled assets for the web UI func InitVirtFS(v1uiFS embed.FS) { config.SetV1UIFS(v1uiFS) From 85982433228702596e88f2563c722831baa460a2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:07:33 +0200 Subject: [PATCH 21/32] re-add installer wait group --- server.go | 3 ++- src/setup/install.go | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/server.go b/server.go index a11365b5..4a079f17 100644 --- a/server.go +++ b/server.go @@ -45,7 +45,8 @@ func main() { logger.Install.Info("Starting setup...") loader.ReloadConfig() // Load the config file before starting the setup process loader.HandleFlags() - setup.Install() + setup.Install(&wg) + wg.Wait() logger.Main.Debug("Initializing Backend...") loader.InitBackend() logger.Main.Debug("Initializing after start tasks...") diff --git a/src/setup/install.go b/src/setup/install.go index 052338ad..99e1d82d 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -23,7 +23,10 @@ import ( var downloadBranch string // Holds the branch to download from // Install performs the entire installation process and ensures the server waits for it to complete -func Install() { +func Install(wg *sync.WaitGroup) { + wg.Add(1) + defer wg.Done() // Signal that installation is complete + // Step 0: Check for updates if err := update.UpdateExecutable(); err != nil { logger.Install.Error("❌Update check went sideways: " + err.Error()) From c63e915c3031644181f24601b0eda62e59959185 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:15:06 +0200 Subject: [PATCH 22/32] improve advertiser loggin in cmdarg handling --- src/core/loader/cmdargs.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index d264fa27..b6839216 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -104,8 +104,13 @@ func HandleFlags() { if overrideAdvertisedIpFlag != "" { oldOverrideAdvertisedIp := config.GetOverrideAdvertisedIp() + + if overrideAdvertisedIpFlag == oldOverrideAdvertisedIp { + logger.Advertiser.Info(fmt.Sprintf("Advertised Server IP is already set to %s", overrideAdvertisedIpFlag)) + return + } config.SetOverrideAdvertisedIp(overrideAdvertisedIpFlag) - logger.Main.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag)) + logger.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag)) } if createSSUILogFileFlag { From 0b5bd913222f6c750f85fce1ed62a5ad48250218 Mon Sep 17 00:00:00 2001 From: JLangisch Date: Sat, 11 Oct 2025 04:25:51 +0200 Subject: [PATCH 23/32] Update readme - fixes #124 --- readme.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index a94a5ec7..c299aeed 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,7 @@ # Stationeers Server UI -![Go](https://img.shields.io/badge/Go-1.24.2-blue?logo=go&logoColor=white) +![Go](https://img.shields.io/badge/Go-1.25.0-blue?logo=go&logoColor=white) ![Version](https://img.shields.io/github/v/release/jacksonthemaster/StationeersServerUI?logo=github&logoColor=white) ![Issues](https://img.shields.io/github/issues/jacksonthemaster/StationeersServerUI?logo=github&logoColor=white) ![Stars](https://img.shields.io/github/stars/jacksonthemaster/StationeersServerUI?style=social&logo=github) @@ -25,14 +25,8 @@
-### 🌟 Live UI Preview 🌟 +### 🌟 This is a WebUi, you don't need a graphical OS to run this 🌟 -Explore your fututre Stationeers Server UI in action—no setup required! -And the "best part?" The Demo current is not on the V5 but V4, so if you are convinced by the Demo, it will only get better when you actually try it out! - -### Preview is not available any more, but will be available soon! - -[![v4 Live Preview - Stationeers UI](https://img.shields.io/badge/Live%20Preview-Stationeers%20Server%20UI-blueviolet?style=for-the-badge&logo=github)](https://jacksonthemaster.github.io/StationeersServerUI/server.html) [![Download latest Version](https://img.shields.io/badge/Download-Stationeers%20Server%20UI-orange?style=for-the-badge)](https://jacksonthemaster.github.io/StationeersServerUI)
From 8d51689cf4349a8217f93b6c0a455b83637488f6 Mon Sep 17 00:00:00 2001 From: Sebastian Stein Date: Wed, 22 Oct 2025 17:35:50 +0100 Subject: [PATCH 24/32] updated error message for "NoSanityChecK" to be in line with the expected parameter. Fixes wrong "nosanitycheck" flag in log output Fixes #132 --- src/core/loader/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 3e9916e7..db7c1a3a 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -116,7 +116,7 @@ func SanityCheck() { err := runSanityCheck() if err != nil { logger.Main.Error("Sanity check failed, exiting in 10 secconds: " + err.Error()) - logger.Main.Info("If you want to continue anyway, run SSUI with the --noSanityCheck flag, but be aware there may be Dragons ahead.") + logger.Main.Info("If you want to continue anyway, run SSUI with the -NoSanityCheck flag, but be aware there may be Dragons ahead.") logger.Main.Info("This is not recommended nor supported and may cause unexpected behavior, including potential data loss!") time.Sleep(10 * time.Second) os.Exit(1) From 2a3c2121a343544c5bf97e28f260a5b56e0ad935 Mon Sep 17 00:00:00 2001 From: JLangisch Date: Wed, 22 Oct 2025 21:35:07 +0200 Subject: [PATCH 25/32] Fix command line flag (again) for sanity check warning - fixes #132 --- src/core/loader/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index db7c1a3a..2a47bb47 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -116,7 +116,7 @@ func SanityCheck() { err := runSanityCheck() if err != nil { logger.Main.Error("Sanity check failed, exiting in 10 secconds: " + err.Error()) - logger.Main.Info("If you want to continue anyway, run SSUI with the -NoSanityCheck flag, but be aware there may be Dragons ahead.") + logger.Main.Info("If you want to continue anyway, run SSUI with the --NoSanityCheck flag, but be aware there may be Dragons ahead.") logger.Main.Info("This is not recommended nor supported and may cause unexpected behavior, including potential data loss!") time.Sleep(10 * time.Second) os.Exit(1) From edf60ea713d1de2689a254f77ded58fc2d59b5d5 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 12:52:04 +0100 Subject: [PATCH 26/32] Update advertiser for DNS resolution and auto (public IP) modes --- src/advertiser/advertiser.go | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index f622b731..ced924b4 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -3,6 +3,8 @@ package advertiser import ( "bytes" "encoding/json" + "errors" + "net" "net/http" "runtime" "strconv" @@ -33,6 +35,33 @@ type ServerAdResponse struct { Status string } +func getIpFromAddressConfig(address string) (string, error) { + // If the address is "auto", we need to check our public IPv4 via ipify + if address == "auto" { + resp, err := http.Get("https://api4.ipify.org") + if err != nil { + return "", err + } + defer resp.Body.Close() + buf := new(bytes.Buffer) + buf.ReadFrom(resp.Body) + return buf.String(), nil + } + // If the address is an IP quad, return it as is + if net.ParseIP(address) != nil { + return address, nil + } + // If the address is a DNS name, resolve it + ips, err := net.LookupIP(address) + if err != nil { + return "", err + } else if len(ips) > 0 { + return ips[0].String(), nil + } + // If the address is invalid, return an error + return "", errors.New("invalid address") +} + func StartAdvertiser() { if config.GetServerVisible() { logger.Advertiser.Warn("Server advertisement is enabled. Disable it in the config and restart SSUI to use manual advertisement. Skipping for now...") @@ -60,12 +89,18 @@ func StartAdvertiser() { case "linux": platform = 2 } + // Get IP address + ipAddress, err := getIpFromAddressConfig(config.GetOverrideAdvertisedIp()) + if err != nil { + logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetOverrideAdvertisedIp(), err) + return + } adMessage := ServerAdMessage{ SessionId: sessionId, Name: config.GetServerName(), Password: config.GetServerPassword() != "", Version: config.GetExtractedGameVersion(), - Address: config.GetOverrideAdvertisedIp(), + Address: ipAddress, Port: config.GetGamePort(), Players: players, MaxPlayers: maxplayers, From 37d3d88479d66fd1f7334a459c2a7dee821f4b43 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 14:24:10 +0100 Subject: [PATCH 27/32] rename advertiser config variable --- src/advertiser/advertiser.go | 6 +++--- src/config/config.go | 14 +++++++------- src/config/getters.go | 4 ++-- src/config/setters.go | 4 ++-- src/config/vars.go | 2 +- src/core/loader/cmdargs.go | 16 ++++++++-------- src/core/loader/loader.go | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index ced924b4..ffa7168a 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -35,7 +35,7 @@ type ServerAdResponse struct { Status string } -func getIpFromAddressConfig(address string) (string, error) { +func getIpFromAdvertiserOverride(address string) (string, error) { // If the address is "auto", we need to check our public IPv4 via ipify if address == "auto" { resp, err := http.Get("https://api4.ipify.org") @@ -90,9 +90,9 @@ func StartAdvertiser() { platform = 2 } // Get IP address - ipAddress, err := getIpFromAddressConfig(config.GetOverrideAdvertisedIp()) + ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride()) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetOverrideAdvertisedIp(), err) + logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) return } adMessage := ServerAdMessage{ diff --git a/src/config/config.go b/src/config/config.go index d1a6f52b..d9d01a0b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -51,11 +51,11 @@ type JsonConfig struct { StartLocation string `json:"StartLocation"` // Logging and debug settings - Debug *bool `json:"Debug"` - CreateSSUILogFile *bool `json:"CreateSSUILogFile"` - LogLevel int `json:"LogLevel"` - SubsystemFilters []string `json:"subsystemFilters"` - OverrideAdvertisedIp string `json:"OverrideAdvertisedIp"` + Debug *bool `json:"Debug"` + CreateSSUILogFile *bool `json:"CreateSSUILogFile"` + LogLevel int `json:"LogLevel"` + SubsystemFilters []string `json:"subsystemFilters"` + AdvertiserOverride string `json:"AdvertiserOverride"` // Authentication Settings Users map[string]string `json:"users"` // Map of username to hashed password @@ -297,7 +297,7 @@ func applyConfig(cfg *JsonConfig) { // use Safebackups folder either way. ConfiguredSafeBackupDir = filepath.Join("./saves/", SaveName, "Safebackups") - OverrideAdvertisedIp = getString(cfg.OverrideAdvertisedIp, "OVERRIDE_ADVERTISED_IP", "") + AdvertiserOverride = getString(cfg.AdvertiserOverride, "ADVERTISER_OVERRIDE", "") safeSaveConfig() } @@ -368,7 +368,7 @@ func safeSaveConfig() error { AutoStartServerOnStartup: &AutoStartServerOnStartup, SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, - OverrideAdvertisedIp: OverrideAdvertisedIp, + AdvertiserOverride: AdvertiserOverride, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 2b8a7f7f..a71d7ab0 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -519,10 +519,10 @@ func GetIsGameServerRunning() bool { defer ConfigMu.RUnlock() return IsGameServerRunning } -func GetOverrideAdvertisedIp() string { +func GetAdvertiserOverride() string { ConfigMu.RLock() defer ConfigMu.RUnlock() - return OverrideAdvertisedIp + return AdvertiserOverride } func GetStationeersServerPingEndpoint() string { diff --git a/src/config/setters.go b/src/config/setters.go index c0f305b1..3f8b603b 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -703,10 +703,10 @@ func SetAllowAutoGameServerUpdates(value bool) error { return safeSaveConfig() } -func SetOverrideAdvertisedIp(value string) error { +func SetAdvertiserOverride(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() - OverrideAdvertisedIp = value + AdvertiserOverride = value return safeSaveConfig() } diff --git a/src/config/vars.go b/src/config/vars.go index 50ecb59e..f1e6e192 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,7 +61,7 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string - OverrideAdvertisedIp string + AdvertiserOverride string ) // Runtime only variables diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index b6839216..1567c50c 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -21,7 +21,7 @@ var recoveryPasswordFlag string var devModeFlag bool var skipSteamCMDFlag bool var sanityCheckFlag bool -var overrideAdvertisedIpFlag string +var advertiserOverrideFlag string // ParseFlags parses command-line arguments ONCE at startup (called from func main) func ParseFlags() { @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&overrideAdvertisedIpFlag, "OverrideAdvertisedIp", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse() @@ -102,15 +102,15 @@ func HandleFlags() { logger.Main.Info(fmt.Sprintf("Overriding IsDebugMode from command line: Before=%t, Now=true", oldDebug)) } - if overrideAdvertisedIpFlag != "" { - oldOverrideAdvertisedIp := config.GetOverrideAdvertisedIp() + if advertiserOverrideFlag != "" { + oldAdvertiserOverride := config.GetAdvertiserOverride() - if overrideAdvertisedIpFlag == oldOverrideAdvertisedIp { - logger.Advertiser.Info(fmt.Sprintf("Advertised Server IP is already set to %s", overrideAdvertisedIpFlag)) + if advertiserOverrideFlag == oldAdvertiserOverride { + logger.Advertiser.Info(fmt.Sprintf("Advertised Server IP is already set to %s", advertiserOverrideFlag)) return } - config.SetOverrideAdvertisedIp(overrideAdvertisedIpFlag) - logger.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag)) + config.SetAdvertiserOverride(advertiserOverrideFlag) + logger.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldAdvertiserOverride, advertiserOverrideFlag)) } if createSSUILogFileFlag { diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 2a47bb47..ca2c3810 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -101,7 +101,7 @@ func ReloadAppInfoPoller() { } func LoadAdvertiser() { - if config.GetOverrideAdvertisedIp() != "" { + if config.GetAdvertiserOverride() != "" { logger.Advertiser.Info("Starting server advertiser...") advertiser.StartAdvertiser() } From f0f06d52c06950c5e748402bacb68f16ca4a212b Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 22:49:46 +0900 Subject: [PATCH 28/32] Update src/advertiser/advertiser.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/advertiser/advertiser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index ffa7168a..6f547d71 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -48,7 +48,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { return buf.String(), nil } // If the address is an IP quad, return it as is - if net.ParseIP(address) != nil { + if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { return address, nil } // If the address is a DNS name, resolve it From 2847c642547678192a8b7ebde3f570548fb52fd6 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 15:18:39 +0100 Subject: [PATCH 29/32] Added more checks for IPv6 --- .vscode/launch.json | 10 ---------- src/advertiser/advertiser.go | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 23382b36..ed0a4c87 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,15 +30,5 @@ "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm "args": ["--NoSteamCMD"] }, - { - "name": "Debug Go Server noSteamCMD noSvelte overrideAdvertisedIp", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/server.go", - "console": "integratedTerminal", - "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm - "args": ["--NoSteamCMD", "--OverrideAdvertisedIp=127.0.0.1"] - } ] } \ No newline at end of file diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 6f547d71..f76ccece 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -48,18 +48,27 @@ func getIpFromAdvertiserOverride(address string) (string, error) { return buf.String(), nil } // If the address is an IP quad, return it as is - if ip := net.ParseIP(address); ip != nil && ip.To4() != nil { - return address, nil + if ip := net.ParseIP(address); ip != nil { + if ip.To4() != nil { + return address, nil + } else if ip.To16() != nil { + return "", errors.New("IPv6 addresses are not supported for advertiser override") + } } // If the address is a DNS name, resolve it ips, err := net.LookupIP(address) if err != nil { return "", err } else if len(ips) > 0 { - return ips[0].String(), nil + // Return the first resolved IPv4 address + for _, ip := range ips { + if ip.To4() != nil { + return ip.String(), nil + } + } } // If the address is invalid, return an error - return "", errors.New("invalid address") + return "", errors.New("unable to resolve IP from advertiser override") } func StartAdvertiser() { From 8c344b7be5cd2a896e9a7ea52ef9fc8cf534f2f2 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 23:24:25 +0900 Subject: [PATCH 30/32] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/advertiser/advertiser.go | 11 +++++------ src/core/loader/cmdargs.go | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index f76ccece..0f3f5ede 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -59,12 +59,11 @@ func getIpFromAdvertiserOverride(address string) (string, error) { ips, err := net.LookupIP(address) if err != nil { return "", err - } else if len(ips) > 0 { - // Return the first resolved IPv4 address - for _, ip := range ips { - if ip.To4() != nil { - return ip.String(), nil - } + } + // Return the first resolved IPv4 address + for _, ip := range ips { + if ip.To4() != nil { + return ip.String(), nil } } // If the address is invalid, return an error diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 1567c50c..8fd43619 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse() From 91b6117a3a4ceda9de109b07da5d127e6925c615 Mon Sep 17 00:00:00 2001 From: akirilov Date: Mon, 27 Oct 2025 16:02:02 +0100 Subject: [PATCH 31/32] Fixed memory leak and improved resilience --- src/advertiser/advertiser.go | 53 +++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/advertiser/advertiser.go b/src/advertiser/advertiser.go index 0f3f5ede..28782aae 100644 --- a/src/advertiser/advertiser.go +++ b/src/advertiser/advertiser.go @@ -18,6 +18,9 @@ import ( var StationeersAdvertisementEndpoint = config.GetStationeersServerPingEndpoint() +const maxTransientErrors = 5 +const advertiserIntervalSeconds = 30 + type ServerAdMessage struct { SessionId int Name string @@ -50,7 +53,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { // If the address is an IP quad, return it as is if ip := net.ParseIP(address); ip != nil { if ip.To4() != nil { - return address, nil + return ip.To4().String(), nil } else if ip.To16() != nil { return "", errors.New("IPv6 addresses are not supported for advertiser override") } @@ -63,7 +66,7 @@ func getIpFromAdvertiserOverride(address string) (string, error) { // Return the first resolved IPv4 address for _, ip := range ips { if ip.To4() != nil { - return ip.String(), nil + return ip.To4().String(), nil } } // If the address is invalid, return an error @@ -77,9 +80,16 @@ func StartAdvertiser() { } go func() { sessionId := -1 + // Track accumulated transient errors and kill the advertiser if we exceed a threshold + transientErrors := 0 for { // Only advertise if we are running if gamemgr.InternalIsServerRunning() { + // If we have exceeded the max transient errors, exit the advertiser + if transientErrors >= maxTransientErrors { + logger.Advertiser.Errorf("ServerAdvertiser exceeded max transient errors (%d). Stopping advertiser...", maxTransientErrors) + return + } // Get max players maxplayers, err := strconv.Atoi(config.GetServerMaxPlayers()) if err != nil { @@ -100,8 +110,10 @@ func StartAdvertiser() { // Get IP address ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride()) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to get IP address from config value '%s': %v", config.GetAdvertiserOverride(), err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } adMessage := ServerAdMessage{ SessionId: sessionId, @@ -116,38 +128,53 @@ func StartAdvertiser() { } body, err := json.Marshal(adMessage) if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to Serialize to JSON from native Go struct type: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } // Send advertisement resp, err := http.Post(StationeersAdvertisementEndpoint, "application/json", bytes.NewBuffer(body)) // Check for errors if err != nil { - logger.Advertiser.Errorf("ServerAdvertiser failed to send request: %v", err) - return + logger.Advertiser.Warnf("ServerAdvertiser failed to send request: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } - defer resp.Body.Close() - // Check the status + // Check for non-200 status codes if resp.StatusCode != 200 { logger.Advertiser.Warnf("ServerAdvertiser received non-200 status: %d", resp.StatusCode) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + resp.Body.Close() // Close the response body + continue } // Read the response and update our sessionId if needed adResponse := ServerAdResponse{} err = json.NewDecoder(resp.Body).Decode(&adResponse) + resp.Body.Close() if err != nil { - logger.Advertiser.Errorf("Failed to decode response body: %v", err) - return + logger.Advertiser.Warnf("Failed to decode response body: %v", err) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } if adResponse.Status != "Success" { logger.Advertiser.Warnf("ServerAdvertiser received unexpected status: %s", adResponse.Status) + transientErrors++ + time.Sleep(advertiserIntervalSeconds * time.Second) + continue } + // Reset transient errors on success sessionId = adResponse.SessionId + transientErrors = 0 } else { // Reset sessionid for the next run sessionId = -1 } // Sleep for 30 seconds to follow the standard advertisement timer - time.Sleep(30 * time.Second) + time.Sleep(advertiserIntervalSeconds * time.Second) } }() } From 62bc5f0e13c118ed69f28ea433acb2f24e828885 Mon Sep 17 00:00:00 2001 From: JLangisch Date: Tue, 28 Oct 2025 21:54:35 +0100 Subject: [PATCH 32/32] Add ServerVisible info to help output of AdvertiserOverride flag --- src/core/loader/cmdargs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 8fd43619..82b472dd 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -40,7 +40,7 @@ func ParseFlags() { flag.BoolVar(&createSSUILogFileFlag, "lf", false, "(Alias) Create log files for SSUI") flag.BoolVar(&skipSteamCMDFlag, "NoSteamCMD", false, "Skips SteamCMD installation") flag.BoolVar(&sanityCheckFlag, "NoSanityCheck", false, "Skips the sanity check. Not recommended.") - flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") + flag.StringVar(&advertiserOverrideFlag, "AdvertiserOverride", "", "Override the advertised server IP. For this, the ServerVisible setting must be set to false. Use \"auto\" for automatic public IP detection, an IPv4 address, or a DNS hostname (to allow server advertisement if you are behind a reverse proxy)") // Parse command-line flags flag.Parse()