Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5ef7bfb
fix: ensure run_bepinex.sh is removed only on Linux if it exists
mitoskalandiel Sep 29, 2025
3e14b63
This is attempt 2 to fix path traversal issues within steamcmd-helper…
mitoskalandiel Sep 29, 2025
29642b6
Merge pull request #109 from SteamServerUI/fix-rm_bepinex_win
JacksonTheMaster Sep 29, 2025
9f3c8f1
Merge pull request #110 from SteamServerUI/fix-zipslips-round2
mitoskalandiel Sep 29, 2025
961a980
Commit Server IP override
akirilov Sep 30, 2025
acefe2d
Fix hardcoded IP
akirilov Sep 30, 2025
fe9d444
refactored IsServerRunning checks to use a global setting instead of …
JacksonTheMaster Sep 30, 2025
ea03021
added an endpoint to get the server status
JacksonTheMaster Sep 30, 2025
d1189a2
Apply suggestions from code review
akirilov Sep 30, 2025
483ab81
adds the ability to add apikey- users with a 3 year token expiration …
JacksonTheMaster Sep 30, 2025
50b007f
reflect runState in http code as well
JacksonTheMaster Sep 30, 2025
71109e5
added a dedicated endpoint to request an APIKey
JacksonTheMaster Sep 30, 2025
c92e40f
Moving server advertiser to a separate goroutine in the backend init
akirilov Sep 30, 2025
9931466
Update logger for advertiser
akirilov Sep 30, 2025
ade0b30
Remove unnecessary waits
akirilov Sep 30, 2025
d87f67f
Make the advertisement server a const
akirilov Sep 30, 2025
bc6e0ee
improve APIKey handling: support dynamic expiration duration (POST) a…
JacksonTheMaster Sep 30, 2025
4a6e9a5
Merge Feat server advertisement (PR #112) from feat-server-advertisement
JacksonTheMaster Sep 30, 2025
ae91853
Merge branch 'nightly' into feat-monitoring-apikeys
JacksonTheMaster Sep 30, 2025
e1e7a95
Merge pull request #114 from feat-monitoring-apikeys
JacksonTheMaster Oct 4, 2025
77c7ec9
Fix race condition in SSE handling
Oct 5, 2025
a16e1ba
Updated SSCM.dll - See SSCM project for details
akirilov Oct 8, 2025
16a277f
Updated SSCM.dll to optimize thread usage
akirilov Oct 8, 2025
96355a4
Merge pull request #119 from SteamServerUI/fix-20250150-sscm-crash
mitoskalandiel Oct 8, 2025
b9a07b8
Merge pull request #117 from semoro/fix-sse-race
JacksonTheMaster Oct 9, 2025
eb11022
Refactor Stationeers server ping endpoint var to use config getter an…
JacksonTheMaster Oct 10, 2025
b3fb8eb
update loader for maintainablity
JacksonTheMaster Oct 10, 2025
8598243
re-add installer wait group
JacksonTheMaster Oct 10, 2025
c63e915
improve advertiser loggin in cmdarg handling
JacksonTheMaster Oct 10, 2025
0b5bd91
Update readme - fixes #124
JacksonTheMaster Oct 11, 2025
8d51689
updated error message for "NoSanityChecK" to be in line with the expe…
mitoskalandiel Oct 22, 2025
2a3c212
Fix command line flag (again) for sanity check warning - fixes #132
JacksonTheMaster Oct 22, 2025
edf60ea
Update advertiser for DNS resolution and auto (public IP) modes
akirilov Oct 27, 2025
37d3d88
rename advertiser config variable
akirilov Oct 27, 2025
f0f06d5
Update src/advertiser/advertiser.go
akirilov Oct 27, 2025
2847c64
Added more checks for IPv6
akirilov Oct 27, 2025
8c344b7
Apply suggestions from code review
akirilov Oct 27, 2025
91b6117
Fixed memory leak and improved resilience
akirilov Oct 27, 2025
62bc5f0
Add ServerVisible info to help output of AdvertiserOverride flag
JacksonTheMaster Oct 28, 2025
adc0f51
Merge pull request #135 from feat-advertiser-modes
JacksonTheMaster Oct 28, 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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
"console": "integratedTerminal",
"showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm
"args": ["--NoSteamCMD"]
}
},
]
}
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
180 changes: 180 additions & 0 deletions src/advertiser/advertiser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package advertiser

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

const maxTransientErrors = 5
const advertiserIntervalSeconds = 30

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 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")
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 ip := net.ParseIP(address); ip != nil {
if ip.To4() != nil {
return ip.To4().String(), 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
}
// Return the first resolved IPv4 address
for _, ip := range ips {
if ip.To4() != nil {
return ip.To4().String(), nil
}
}
// If the address is invalid, return an error
return "", errors.New("unable to resolve IP from advertiser override")
}

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
// 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 {
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
}
// Get IP address
ipAddress, err := getIpFromAdvertiserOverride(config.GetAdvertiserOverride())
if err != nil {
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,
Name: config.GetServerName(),
Password: config.GetServerPassword() != "",
Version: config.GetExtractedGameVersion(),
Address: ipAddress,
Port: config.GetGamePort(),
Players: players,
MaxPlayers: maxplayers,
Type: platform,
}
body, err := json.Marshal(adMessage)
if err != nil {
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.Warnf("ServerAdvertiser failed to send request: %v", err)
transientErrors++
time.Sleep(advertiserIntervalSeconds * time.Second)
continue
}
// 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.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(advertiserIntervalSeconds * time.Second)
}
}()
}
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"`
AdvertiserOverride string `json:"AdvertiserOverride"`

// 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")

AdvertiserOverride = getString(cfg.AdvertiserOverride, "ADVERTISER_OVERRIDE", "")

safeSaveConfig()
}

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

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 GetAdvertiserOverride() string {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return AdvertiserOverride
}

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 SetAdvertiserOverride(value string) error {
ConfigMu.Lock()
defer ConfigMu.Unlock()

AdvertiserOverride = 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
AdvertiserOverride 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
Loading
Loading