feat: customize login and lobby spaces from the panel

This commit is contained in:
Lemon-miaow committed 2026-10-04 12:38:39 +08:00
1 parent efbbe27629
commit 10bd2ddad0
54 files changed
+1146 -158

No files matched your search

+8 -3
View File
@@ -456,7 +456,7 @@ const (
redactedValue = "<redacted by felis>"
)
// redactSecretProps blanks the RCON password when server.properties is read.
// redactSecretProps hides RCON and Limbo forwarding secrets when server.properties is read.
//
// Unlike secretConfigPath this is a value redaction rather than a whole-file
// denial, because the file is not platform material that merely happens to sit in
@@ -468,6 +468,9 @@ const (
// regardless of blast radius, and because the console already gives an owner every
// capability the password would.
//
// Limbo stores its cluster forwarding key in this file too; that value is
// withheld by the same redaction and refreshed by the Limbo entrypoint.
//
// The write path is left alone on purpose, mirroring the reasoning at
// secretConfigPath: felis-lobby's entrypoint rewrites all three rcon keys from the
// injected Secret on every boot, so saving the placeholder back cannot lock the
@@ -481,8 +484,10 @@ func redactSecretProps(name string, content []byte) []byte {
for i, line := range lines {
// TrimSpace before matching: a properties key may be indented, and the
// trailing \r of a CRLF file would otherwise ride along into the value.
if bytes.HasPrefix(bytes.TrimSpace(line), []byte(rconPasswordKey+"=")) {
lines[i] = []byte(rconPasswordKey + "=" + redactedValue)
for _, key := range []string{rconPasswordKey, "forwarding-secrets"} {
if bytes.HasPrefix(bytes.TrimSpace(line), []byte(key+"=")) {
lines[i] = []byte(key + "=" + redactedValue)
}
}
}
return bytes.Join(lines, []byte("\n"))
+20
View File
@@ -678,3 +678,23 @@ func assertNoTemporaries(t *testing.T, dir string) {
}
}
}
func TestReadRedactsLimboForwardingSecret(t *testing.T) {
root := t.TempDir()
props := "spawn-x=8\nforwarding-secrets=shared-key\nvelocity-modern=true\n"
if err := os.WriteFile(filepath.Join(root, "server.properties"), []byte(props), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Link(filepath.Join(root, "server.properties"), filepath.Join(root, "copy.properties")); err != nil {
t.Fatal(err)
}
for _, path := range []string{"server.properties", "copy.properties"} {
res, err := run(root, OpRead, path, nil, "")
if err != nil || res.Code != "" {
t.Fatalf("read: %+v %v", res, err)
}
if strings.Contains(string(res.Content), "shared-key") || !strings.Contains(string(res.Content), "spawn-x=8") {
t.Fatalf("unsafe redaction: %s", res.Content)
}
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import (
// What leaves a world mount — a read, a download, a world export, a backup
// export — passes the same two guards: the forwarding-secret file
// (secretConfigPath) is withheld, and server.properties has its RCON password
// redacted (propsPath).
// redacted (propsPath), along with Limbo's forwarding-secrets.
//
// On a live mount both are matched by the file itself (os.SameFile), not by the
// name it was reached under. A plugin runs arbitrary code as the game uid and can
@@ -51,7 +51,7 @@ func ArchiveRule(name string) (withhold, redact bool) {
return name == secretConfigPath, name == propsPath
}
// RedactProps replaces the RCON password in server.properties content with
// RedactProps replaces RCON and Limbo forwarding secrets in server.properties with
// redactedValue (see redactSecretProps for why a placeholder and not a blank).
func RedactProps(content []byte) []byte {
return redactSecretProps(propsPath, content)