Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ad98c69
fix: ensure run_bepinex.sh is removed only on Linux if it exists
mitoskalandiel Sep 29, 2025
c5b682b
This is attempt 2 to fix path traversal issues within steamcmd-helper…
mitoskalandiel Sep 29, 2025
515a631
Merge pull request #109 from SteamServerUI/fix-rm_bepinex_win
JacksonTheMaster Sep 29, 2025
8f467a5
Merge pull request #110 from SteamServerUI/fix-zipslips-round2
mitoskalandiel Sep 29, 2025
760d805
Commit Server IP override
akirilov Sep 30, 2025
1c350aa
Fix hardcoded IP
akirilov Sep 30, 2025
bb1ad40
refactored IsServerRunning checks to use a global setting instead of …
JacksonTheMaster Sep 30, 2025
bb2bd34
added an endpoint to get the server status
JacksonTheMaster Sep 30, 2025
ca16e0d
Apply suggestions from code review
akirilov Sep 30, 2025
cf95a31
adds the ability to add apikey- users with a 3 year token expiration …
JacksonTheMaster Sep 30, 2025
13ef650
reflect runState in http code as well
JacksonTheMaster Sep 30, 2025
5ef10ec
added a dedicated endpoint to request an APIKey
JacksonTheMaster Sep 30, 2025
c63caeb
Moving server advertiser to a separate goroutine in the backend init
akirilov Sep 30, 2025
7b56418
Update logger for advertiser
akirilov Sep 30, 2025
04a97c7
Remove unnecessary waits
akirilov Sep 30, 2025
95afab3
Make the advertisement server a const
akirilov Sep 30, 2025
57d0389
improve APIKey handling: support dynamic expiration duration (POST) a…
JacksonTheMaster Sep 30, 2025
e39b3bc
Merge Feat server advertisement (PR #112) from feat-server-advertisement
JacksonTheMaster Sep 30, 2025
9b0d914
Merge branch 'nightly' into feat-monitoring-apikeys
JacksonTheMaster Sep 30, 2025
38213fb
Merge pull request #114 from feat-monitoring-apikeys
JacksonTheMaster Oct 4, 2025
69f66aa
Fix race condition in SSE handling
Oct 5, 2025
93977dc
Updated SSCM.dll - See SSCM project for details
akirilov Oct 8, 2025
730a626
Updated SSCM.dll to optimize thread usage
akirilov Oct 8, 2025
d25a88d
Merge pull request #119 from SteamServerUI/fix-20250150-sscm-crash
mitoskalandiel Oct 8, 2025
d318caa
Merge pull request #117 from semoro/fix-sse-race
JacksonTheMaster Oct 9, 2025
6b86786
Refactor Stationeers server ping endpoint var to use config getter an…
JacksonTheMaster Oct 10, 2025
0c9fd94
update loader for maintainablity
JacksonTheMaster Oct 10, 2025
7f20a99
re-add installer wait group
JacksonTheMaster Oct 10, 2025
c803430
improve advertiser loggin in cmdarg handling
JacksonTheMaster Oct 10, 2025
0c0bcde
Update readme - fixes #124
JacksonTheMaster Oct 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
10 changes: 2 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -25,14 +25,8 @@

<div align="center">

### 🌟 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)
</div>

Expand Down
9 changes: 3 additions & 6 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ 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...")
Expand All @@ -49,11 +48,9 @@ func main() {
setup.Install(&wg)
wg.Wait()
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...")
Expand Down
110 changes: 110 additions & 0 deletions src/advertiser/advertiser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package advertiser

import (
"bytes"
"encoding/json"
"net/http"
"runtime"
"strconv"
"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"
)

var StationeersAdvertisementEndpoint = config.GetStationeersServerPingEndpoint()

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() {
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...")
return
}
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.Advertiser.Errorf("ServerAdvertiser failed to convert max players 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: config.GetOverrideAdvertisedIp(),
Port: config.GetGamePort(),
Players: players,
MaxPlayers: maxplayers,
Type: platform,
}
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
}
// 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
}
defer resp.Body.Close()
// Check the status
if resp.StatusCode != 200 {
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.Advertiser.Errorf("Failed to decode response body: %v", err)
return
}
if adResponse.Status != "Success" {
logger.Advertiser.Warnf("ServerAdvertiser received unexpected 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)
}
}()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akirilov the advertiser should be a singleton and wait on a channel / have some way of stopping it and possibly reloading it. The current implementation will advertise the wrong / default / previous server state (config settings) instead of reloading on config change.

Eg

Suggested change
}()
}(advertiser)

@JacksonTheMaster JacksonTheMaster Oct 10, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also then be added to loader.ReloadBackend()

}
12 changes: 8 additions & 4 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -365,6 +368,7 @@ func safeSaveConfig() error {
AutoStartServerOnStartup: &AutoStartServerOnStartup,
SSUIIdentifier: SSUIIdentifier,
SSUIWebPort: SSUIWebPort,
OverrideAdvertisedIp: OverrideAdvertisedIp,
}

file, err := os.Create(ConfigPath)
Expand Down
17 changes: 17 additions & 0 deletions src/config/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,20 @@ func GetIsDockerContainer() bool {
defer ConfigMu.RUnlock()
return IsDockerContainer
}

func GetIsGameServerRunning() bool {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return IsGameServerRunning
}
func GetOverrideAdvertisedIp() string {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return OverrideAdvertisedIp
}

func GetStationeersServerPingEndpoint() string {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return StationeersServerPingEndpoint
}
16 changes: 16 additions & 0 deletions src/config/setters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -694,3 +702,11 @@ func SetAllowAutoGameServerUpdates(value bool) error {
AllowAutoGameServerUpdates = value
return safeSaveConfig()
}

func SetOverrideAdvertisedIp(value string) error {
ConfigMu.Lock()
defer ConfigMu.Unlock()

OverrideAdvertisedIp = value
return safeSaveConfig()
}
31 changes: 17 additions & 14 deletions src/config/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var (
LanguageSetting string
AutoStartServerOnStartup bool
SSUIIdentifier string
OverrideAdvertisedIp string
)

// Runtime only variables
Expand All @@ -71,6 +72,7 @@ var (
SkipSteamCMD bool // ONLY RUNTIME
IsDockerContainer bool // ONLY RUNTIME
NoSanityCheck bool // ONLY RUNTIME
IsGameServerRunning bool // ONLY RUNTIME
)

// Discord integration
Expand Down Expand Up @@ -129,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
Expand Down
6 changes: 1 addition & 5 deletions src/core/loader/afterstart.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
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"
"github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr"
"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 {
Expand Down
13 changes: 13 additions & 0 deletions src/core/loader/cmdargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -100,6 +102,17 @@ func HandleFlags() {
logger.Main.Info(fmt.Sprintf("Overriding IsDebugMode from command line: Before=%t, Now=true", oldDebug))
}

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.Advertiser.Info(fmt.Sprintf("Overriding Advertised Server IP from command line: Before=%s, Now=%s", oldOverrideAdvertisedIp, overrideAdvertisedIpFlag))
}

if createSSUILogFileFlag {
oldCreateSSUILogFile := config.GetCreateSSUILogFile()
config.SetCreateSSUILogFile(true)
Expand Down
Loading