Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/config/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@ func GetIsDockerContainer() bool {
return IsDockerContainer
}

func GetIsGameServerRunning() bool {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return IsGameServerRunning
}
func GetOverrideAdvertisedIp() string {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
Expand Down
8 changes: 8 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
1 change: 1 addition & 0 deletions src/config/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var (
SkipSteamCMD bool // ONLY RUNTIME
IsDockerContainer bool // ONLY RUNTIME
NoSanityCheck bool // ONLY RUNTIME
IsGameServerRunning bool // ONLY RUNTIME
)

// Discord integration
Expand Down
6 changes: 6 additions & 0 deletions src/core/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,6 +28,7 @@ func InitBackend() {
ReloadAppInfoPoller()
ReloadDiscordBot()
InitDetector()
StartIsGameServerRunningCheck()
if config.GetOverrideAdvertisedIp() != "" {
logger.Advertiser.Info("Starting server advertiser...")
advertiser.StartAdvertiser()
Expand Down Expand Up @@ -93,6 +95,10 @@ func ReloadLocalizer() {
localization.ReloadLocalizer()
}

func StartIsGameServerRunningCheck() {
gamemgr.StartIsGameServerRunningCheck()
}

func ReloadAppInfoPoller() {
steamcmd.AppInfoPoller()
}
Expand Down
12 changes: 10 additions & 2 deletions src/core/security/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package security
//repurposed from a Jacksonthemaster private repo

import (
"strings"
"time"

"github.com/JacksonTheMaster/StationeersServerUI/v5/src/config"
Expand All @@ -19,8 +20,16 @@ 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-") {
durationMonths := 1
if len(apikeyduration) > 0 {
durationMonths = apikeyduration[0]
}
expirationTime = time.Now().AddDate(0, durationMonths, 0)
}

claims := &jwt.MapClaims{
"exp": expirationTime.Unix(),
"iss": "StationeersServerUI",
Expand All @@ -37,7 +46,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
Expand Down
2 changes: 2 additions & 0 deletions src/managers/gamemgr/processmanagement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -217,6 +218,7 @@ func InternalStopServer() error {

// Process is confirmed stopped, clear cmd
cmd = nil
config.SetIsGameServerRunning(false)
clearGameServerUUID()
return nil
}
19 changes: 18 additions & 1 deletion src/managers/gamemgr/runcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/web/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
92 changes: 92 additions & 0 deletions src/web/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,6 +173,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 {
Expand Down Expand Up @@ -240,3 +248,87 @@ 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
}

// 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
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, durationMonths)
expires := time.Now().AddDate(0, durationMonths, 0)
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),
})
logger.Security.Infof("APIKey %s registered successfully. Expires: %s ", creds.Username, expires.Format(time.RFC3339))
}
21 changes: 21 additions & 0 deletions src/web/monitoring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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,
}
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
}
}
4 changes: 4 additions & 0 deletions src/web/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ 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
protectedMux.HandleFunc("/api/v2/monitor/gameserver/status", HandleMonitorStatus)

return mux, protectedMux
}