From ad98c69898e118190c982d42e6caee58bfa64f09 Mon Sep 17 00:00:00 2001 From: Sebastian Meier zu Biesen Date: Mon, 29 Sep 2025 16:29:41 +0200 Subject: [PATCH 01/23] 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 c5b682b8205f6474210139bfe8a7c47a8ba921a4 Mon Sep 17 00:00:00 2001 From: Sebastian Meier zu Biesen Date: Mon, 29 Sep 2025 16:41:01 +0200 Subject: [PATCH 02/23] 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 760d8057bd58dca615d9709377dffd4a3adc9371 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 08:28:41 +0200 Subject: [PATCH 03/23] 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 1c350aa180dcb69c970f8569bd845b96b74a7e32 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 09:41:50 +0200 Subject: [PATCH 04/23] 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 bb1ad400b06c9ceb1b2b18869536b8918dd6c147 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/23] 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 bb2bd3454dc535906d8cdc7228848fa322c9a1b2 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/23] 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 ca16e0d9c2f032cfc3292323affc13769578b1d6 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 19:56:55 +0900 Subject: [PATCH 07/23] 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 cf95a315c6e0480f13c9d3bb11f0a7f73187f328 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/23] 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 13ef650f129cd51254a380239b1ff5fd55bafb21 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/23] 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 5ef10ec3c57871712b8241f92620a20bdd37d1ec 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/23] 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 c63caeb4218877b06f848e0febb89c1c907a46ef Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:14:22 +0200 Subject: [PATCH 11/23] 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 7b56418322a6c34216b0069b6242e66a897b6b49 Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:23:46 +0200 Subject: [PATCH 12/23] 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 04a97c79b87ac0e7fa946d79d9bd7d96be97b88a Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 15:59:11 +0200 Subject: [PATCH 13/23] 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 95afab3a695041c705d7dcfe5781e03d70a0769a Mon Sep 17 00:00:00 2001 From: akirilov Date: Tue, 30 Sep 2025 17:09:18 +0200 Subject: [PATCH 14/23] 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 57d0389a37a47b7c217626649f191f77d54638aa 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/23] 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 69f66aa1440542b772796b0c51792260177bfbee Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 5 Oct 2025 23:05:31 +0200 Subject: [PATCH 16/23] 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 93977dce58f44fc1dbd5d4be6ea4dea6d88422ad Mon Sep 17 00:00:00 2001 From: akirilov Date: Wed, 8 Oct 2025 02:06:02 +0200 Subject: [PATCH 17/23] 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 730a6264d41e1db3dda649005e14db7ac6977903 Mon Sep 17 00:00:00 2001 From: akirilov Date: Wed, 8 Oct 2025 05:32:32 +0200 Subject: [PATCH 18/23] 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 6b86786d36b63985e50c82c192f47846939a3e72 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/23] 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 0c9fd946c3f17ef0beaad039467979bc6c599ee5 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/23] 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 7f20a999139a11f55d52aedd5f362f6ffe647114 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/23] 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 c80343022f754a92f17ed62f6af0a6156753970f 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/23] 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 0c0bcdef7e69df09e81ee529c864d6395725c2e2 Mon Sep 17 00:00:00 2001 From: JLangisch Date: Sat, 11 Oct 2025 04:25:51 +0200 Subject: [PATCH 23/23] 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)