7 Commits

Author SHA1 Message Date
Fran Jurmanović
2a863c51e9 update state history query
All checks were successful
Release and Deploy / build (push) Successful in 3m18s
Release and Deploy / deploy (push) Successful in 27s
2025-09-13 14:41:52 +02:00
Fran Jurmanović
a70d923a6a revert to powershell executable 2025-08-17 16:53:44 +02:00
Fran Jurmanović
f660511b63 change steamCMD executor
All checks were successful
Release and Deploy / build (push) Successful in 2m1s
Release and Deploy / deploy (push) Successful in 23s
2025-08-17 16:42:10 +02:00
Fran Jurmanović
044af60699 steam-crypt app and fix the interactive executor
All checks were successful
Release and Deploy / build (push) Successful in 2m2s
Release and Deploy / deploy (push) Successful in 22s
2025-08-17 16:26:28 +02:00
Fran Jurmanović
384036bcdd remove blocker pattern
All checks were successful
Release and Deploy / build (push) Successful in 2m5s
Release and Deploy / deploy (push) Successful in 26s
2025-08-17 15:53:55 +02:00
Fran Jurmanović
ef300d233b fix wrong userID from context
All checks were successful
Release and Deploy / build (push) Successful in 3m26s
Release and Deploy / deploy (push) Successful in 24s
2025-08-17 13:12:36 +02:00
Fran Jurmanović
edad65d6a9 generate open token using normal token
All checks were successful
Release and Deploy / build (push) Successful in 3m1s
Release and Deploy / deploy (push) Successful in 23s
2025-08-17 12:46:37 +02:00
12 changed files with 206 additions and 39 deletions

93
cmd/steam-crypt/main.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/configs"
"flag"
"fmt"
"os"
)
func main() {
var (
encrypt = flag.Bool("encrypt", false, "Encrypt a password")
decrypt = flag.Bool("decrypt", false, "Decrypt a password")
password = flag.String("password", "", "Password to encrypt/decrypt")
help = flag.Bool("help", false, "Show help")
)
flag.Parse()
if *help || (!*encrypt && !*decrypt) {
showHelp()
return
}
if *encrypt && *decrypt {
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -encrypt and -decrypt\n")
os.Exit(1)
}
if *password == "" {
fmt.Fprintf(os.Stderr, "Error: Password is required\n")
showHelp()
os.Exit(1)
}
// Initialize configs to load encryption key
configs.Init()
if *encrypt {
encrypted, err := model.EncryptPassword(*password)
if err != nil {
fmt.Fprintf(os.Stderr, "Error encrypting password: %v\n", err)
os.Exit(1)
}
fmt.Println(encrypted)
}
if *decrypt {
decrypted, err := model.DecryptPassword(*password)
if err != nil {
fmt.Fprintf(os.Stderr, "Error decrypting password: %v\n", err)
os.Exit(1)
}
fmt.Println(decrypted)
}
}
func showHelp() {
fmt.Println("Steam Credentials Encryption/Decryption Utility")
fmt.Println()
fmt.Println("This utility encrypts and decrypts Steam credentials using the same")
fmt.Println("AES-256-GCM encryption used by the ACC Server Manager application.")
fmt.Println()
fmt.Println("Usage:")
fmt.Println(" steam-crypt -encrypt -password \"your_password\"")
fmt.Println(" steam-crypt -decrypt -password \"encrypted_string\"")
fmt.Println()
fmt.Println("Options:")
fmt.Println(" -encrypt Encrypt the provided password")
fmt.Println(" -decrypt Decrypt the provided encrypted string")
fmt.Println(" -password The password to encrypt or encrypted string to decrypt")
fmt.Println(" -help Show this help message")
fmt.Println()
fmt.Println("Environment Variables Required:")
fmt.Println(" ENCRYPTION_KEY - 32-byte encryption key (same as main application)")
fmt.Println(" APP_SECRET - Application secret (required by configs)")
fmt.Println(" APP_SECRET_CODE - Application secret code (required by configs)")
fmt.Println(" ACCESS_KEY - Access key (required by configs)")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" # Encrypt a password")
fmt.Println(" steam-crypt -encrypt -password \"mysteampassword\"")
fmt.Println()
fmt.Println(" # Decrypt an encrypted password")
fmt.Println(" steam-crypt -decrypt -password \"base64encryptedstring\"")
fmt.Println()
fmt.Println("Security Notes:")
fmt.Println(" - The encryption key must be exactly 32 bytes for AES-256")
fmt.Println(" - Uses AES-256-GCM for authenticated encryption")
fmt.Println(" - Each encryption includes a unique nonce for security")
fmt.Println(" - Passwords are validated for length and basic security")
}

View File

@@ -95,7 +95,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
// @Router /auth/open-token [post]
func (c *MembershipController) GenerateOpenToken(ctx *fiber.Ctx) error {
token, err := c.service.GenerateOpenToken(ctx.UserContext(), ctx.Locals("userId").(string))
token, err := c.service.GenerateOpenToken(ctx.UserContext(), ctx.Locals("userID").(string))
if err != nil {
return c.errorHandler.HandleAuthError(ctx, err)
}

View File

@@ -106,6 +106,13 @@ func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, isO
})
}
if !jwtHandler.IsOpenToken && claims.IsOpenToken {
logging.Error("Authentication failed: attempting to authenticate with open token")
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Wrong token type used",
})
}
// Additional security: validate user ID format
if claims.UserID == "" || len(claims.UserID) < 10 {
logging.Error("Authentication failed: invalid user ID in token from IP %s", ip)

View File

@@ -187,7 +187,7 @@ func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *
FROM state_histories
WHERE server_id = ? AND date_created BETWEEN ? AND ?
GROUP BY session_id
HAVING COUNT(*) > 1 AND MAX(player_count) > 0
HAVING MAX(player_count) > 0
ORDER BY date DESC
LIMIT 10
`

View File

@@ -63,16 +63,11 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
return "", err
}
return s.jwtHandler.GenerateToken(user)
return s.jwtHandler.GenerateToken(user.ID.String())
}
func (s *MembershipService) GenerateOpenToken(ctx context.Context, userId string) (string, error) {
user, err := s.repo.GetByID(ctx, userId)
if err != nil {
return "", err
}
return s.openJwtHandler.GenerateToken(user)
return s.openJwtHandler.GenerateToken(userId)
}
// CreateUser creates a new user.

View File

@@ -124,33 +124,44 @@ func (s *SteamService) InstallServer(ctx context.Context, installPath string, se
// Get SteamCMD path from environment variable
steamCMDPath := env.GetSteamCMDPath()
// Build SteamCMD command
args := []string{
"-nologo",
"-noprofile",
steamCMDPath,
// Build SteamCMD command arguments
steamCMDArgs := []string{
"+force_install_dir", absPath,
"+login",
}
if creds != nil && creds.Username != "" {
args = append(args, creds.Username)
steamCMDArgs = append(steamCMDArgs, creds.Username)
if creds.Password != "" {
args = append(args, creds.Password)
steamCMDArgs = append(steamCMDArgs, creds.Password)
}
} else {
args = append(args, "anonymous")
steamCMDArgs = append(steamCMDArgs, "anonymous")
}
args = append(args,
steamCMDArgs = append(steamCMDArgs,
"+app_update", ACCServerAppID,
"validate",
"+quit",
)
// Use interactive executor to handle potential 2FA prompts
// Build PowerShell arguments to execute SteamCMD directly
// This matches the format: powershell -nologo -noprofile c:\steamcmd\steamcmd.exe +args...
args := []string{"-nologo", "-noprofile"}
args = append(args, steamCMDPath)
args = append(args, steamCMDArgs...)
// Use interactive executor to handle potential 2FA prompts with timeout
logging.Info("Installing ACC server to %s...", absPath)
if err := s.interactiveExecutor.ExecuteInteractive(ctx, serverID, args...); err != nil {
// Create a context with timeout to prevent hanging indefinitely
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
if err := s.interactiveExecutor.ExecuteInteractive(timeoutCtx, serverID, args...); err != nil {
if timeoutCtx.Err() == context.DeadlineExceeded {
return fmt.Errorf("SteamCMD operation timed out after 10 minutes")
}
return fmt.Errorf("failed to run SteamCMD: %v", err)
}

View File

@@ -61,14 +61,33 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
}
// Create channels for output monitoring
outputDone := make(chan error)
outputDone := make(chan error, 1)
cmdDone := make(chan error, 1)
// Monitor stdout and stderr for 2FA prompts
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
// Wait for either the command to finish or output monitoring to complete
cmdErr := cmd.Wait()
outputErr := <-outputDone
// Wait for the command to finish in a separate goroutine
go func() {
cmdDone <- cmd.Wait()
}()
// Wait for both command and output monitoring to complete
var cmdErr, outputErr error
completedCount := 0
for completedCount < 2 {
select {
case cmdErr = <-cmdDone:
completedCount++
logging.Info("Command execution completed")
case outputErr = <-outputDone:
completedCount++
logging.Info("Output monitoring completed")
case <-ctx.Done():
return ctx.Err()
}
}
if outputErr != nil {
logging.Warn("Output monitoring error: %v", outputErr)
@@ -78,45 +97,85 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
}
func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
defer close(done)
defer func() {
select {
case done <- nil:
default:
}
}()
// Create scanners for both outputs
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputChan := make(chan string)
outputChan := make(chan string, 100) // Buffered channel to prevent blocking
readersDone := make(chan struct{}, 2)
// Read from stdout
go func() {
defer func() { readersDone <- struct{}{} }()
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
if e.LogOutput {
logging.Info("STDOUT: %s", line)
}
outputChan <- line
select {
case outputChan <- line:
case <-ctx.Done():
return
}
}
if err := stdoutScanner.Err(); err != nil {
logging.Warn("Stdout scanner error: %v", err)
}
}()
// Read from stderr
go func() {
defer func() { readersDone <- struct{}{} }()
for stderrScanner.Scan() {
line := stderrScanner.Text()
if e.LogOutput {
logging.Info("STDERR: %s", line)
}
outputChan <- line
select {
case outputChan <- line:
case <-ctx.Done():
return
}
}
if err := stderrScanner.Err(); err != nil {
logging.Warn("Stderr scanner error: %v", err)
}
}()
// Monitor for 2FA prompts
// Monitor for completion and 2FA prompts
readersFinished := 0
for {
select {
case <-ctx.Done():
done <- ctx.Err()
return
case <-readersDone:
readersFinished++
if readersFinished == 2 {
// Both readers are done, close output channel and finish monitoring
close(outputChan)
// Drain any remaining output
for line := range outputChan {
if e.is2FAPrompt(line) {
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
logging.Error("Failed to handle 2FA prompt: %v", err)
done <- err
return
}
}
}
return
}
case line, ok := <-outputChan:
if !ok {
done <- nil
// Channel closed, we're done
return
}

View File

@@ -8,7 +8,7 @@ import (
)
var (
Version = "0.10.5"
Version = "0.10.7"
Prefix = "v1"
Secret string
SecretCode string

View File

@@ -13,7 +13,8 @@ import (
// Claims represents the JWT claims.
type Claims struct {
UserID string `json:"user_id"`
UserID string `json:"user_id"`
IsOpenToken bool `json:"is_open_token"`
jwt.RegisteredClaims
}
@@ -70,13 +71,14 @@ func (jh *JWTHandler) GenerateSecretKey() string {
}
// GenerateToken generates a new JWT for a given user.
func (jh *JWTHandler) GenerateToken(user *model.User) (string, error) {
func (jh *JWTHandler) GenerateToken(userId string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: user.ID.String(),
UserID: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
IsOpenToken: jh.IsOpenToken,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -90,6 +92,7 @@ func (jh *JWTHandler) GenerateTokenWithExpiry(user *model.User, expiry time.Time
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
IsOpenToken: jh.IsOpenToken,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

View File

@@ -16,7 +16,6 @@ type PathValidator struct {
func NewPathValidator() *PathValidator {
blockedPatterns := []*regexp.Regexp{
regexp.MustCompile(`\.\.`),
regexp.MustCompile(`[<>:"|?*]`),
regexp.MustCompile(`^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$`),
regexp.MustCompile(`\x00`),
regexp.MustCompile(`^\\\\`),
@@ -92,4 +91,4 @@ func (pv *PathValidator) AddAllowedBasePath(path string) error {
func (pv *PathValidator) GetAllowedBasePaths() []string {
return append([]string(nil), pv.allowedBasePaths...)
}
}

View File

@@ -28,7 +28,7 @@ func GenerateTestToken() (string, error) {
jwtHandler := jwt.NewJWTHandler(testSecret)
// Generate JWT token
token, err := jwtHandler.GenerateToken(user)
token, err := jwtHandler.GenerateToken(user.ID.String())
if err != nil {
return "", fmt.Errorf("failed to generate test token: %w", err)
}
@@ -55,7 +55,7 @@ func GenerateTestTokenWithExpiry(expiryTime time.Time) (string, error) {
testSecret = "test-secret-that-is-at-least-32-bytes-long-for-security"
}
jwtHandler := jwt.NewJWTHandler(testSecret)
// Create test user
user := &model.User{
ID: uuid.New(),

View File

@@ -26,7 +26,7 @@ func TestJWT_GenerateAndValidateToken(t *testing.T) {
}
// Test JWT generation
token, err := jwtHandler.GenerateToken(user)
token, err := jwtHandler.GenerateToken(user.ID.String())
tests.AssertNoError(t, err)
tests.AssertNotNil(t, token)