diff --git a/cmd/gbrgames/boottest.go b/cmd/gbrgames/boottest.go index 6477bca..2b6167e 100644 --- a/cmd/gbrgames/boottest.go +++ b/cmd/gbrgames/boottest.go @@ -21,6 +21,10 @@ const bootTestNetwork = "gbr-boottest" var publicPort = regexp.MustCompile(`\{PUBLIC_PORT_(\d+)\}`) var savedValue = regexp.MustCompile(`\{saved:[A-Za-z0-9_.-]+\}`) +// Narrower than the validator's sensitiveKey: that one also matches settings like RCON_PORT, which +// are fine to flag as "don't put secrets here" but must never be overwritten with a password. +var passwordKey = regexp.MustCompile(`(?i)(pass(word)?|secret|token)`) + // runBootTestPayload writes the DEPLOY_SERVER message the panel would send for this game, plus the // boot-test settings the node-agent's BootTest reads. Secrets come from environment variables named // in test.yaml's 'secrets'. @@ -71,10 +75,12 @@ func runBootTestPayload(args []string) error { // would type in, and some games refuse to start without) get a throwaway value too for key, p := range schemaProperties(g) { prop, _ := p.(map[string]any) - if prop["x-generate"] != nil || sensitiveKey.MatchString(key) { - if s, _ := config[key].(string); s == "" { - config[key] = "boot-" + randomSuffix() - } + if prop["x-generate"] == nil && !passwordKey.MatchString(key) { + continue + } + // Only missing or empty text: ports, booleans and other values are never touched + if v, present := config[key]; !present || v == "" { + config[key] = "boot-" + randomSuffix() } } for k, v := range config { diff --git a/cmd/gbrgames/boottest_test.go b/cmd/gbrgames/boottest_test.go new file mode 100644 index 0000000..21e6fb9 --- /dev/null +++ b/cmd/gbrgames/boottest_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestBootTestPayloadOnlyFillsBlankPasswords(t *testing.T) { + dir := t.TempDir() + game := filepath.Join(dir, "games", "demo") + if err := os.MkdirAll(game, 0o755); err != nil { + t.Fatal(err) + } + os.WriteFile(filepath.Join(game, "game.yaml"), []byte(`id: demo +name: Demo +defaultDockerImage: debian:12 +installMethod: NONE +defaultConfig: + RCON_PORT: 25575 + ENABLE_RCON: true + RCON_PASSWORD: "" + ServerPassword: "" + AdminPassword: kept-by-owner + MOTD: "{SERVER_NAME} on {PUBLIC_PORT_25565}" +configSchema: + properties: + RCON_PORT: {type: integer} + ENABLE_RCON: {type: boolean} + RCON_PASSWORD: {type: string, x-generate: {type: password}} + ServerPassword: {type: string} + AdminPassword: {type: string} + MOTD: {type: string} +`), 0o644) + + out := filepath.Join(dir, "payload.json") + if err := runBootTestPayload([]string{"-games", filepath.Join(dir, "games"), "-out", out, "demo"}); err != nil { + t.Fatal(err) + } + raw, _ := os.ReadFile(out) + var payload struct{ GameConfig map[string]any } + if err := json.Unmarshal(raw, &payload); err != nil { + t.Fatal(err) + } + c := payload.GameConfig + + if c["RCON_PORT"] != float64(25575) { + t.Errorf("RCON_PORT changed to %v", c["RCON_PORT"]) + } + if c["ENABLE_RCON"] != true { + t.Errorf("ENABLE_RCON changed to %v", c["ENABLE_RCON"]) + } + for _, k := range []string{"RCON_PASSWORD", "ServerPassword"} { + if s, _ := c[k].(string); !strings.HasPrefix(s, "boot-") { + t.Errorf("%s not filled: %v", k, c[k]) + } + } + if c["AdminPassword"] != "kept-by-owner" { + t.Errorf("AdminPassword overwritten: %v", c["AdminPassword"]) + } + if c["MOTD"] != "gbr-boot-test on 25565" { + t.Errorf("placeholders not resolved: %v", c["MOTD"]) + } +}