197 lines
7.5 KiB
Go
197 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
// forwardingSecretEnv is the env var the operator injects the Velocity
|
|
// modern-forwarding secret under (mirrors internal/operator.envForwardingSecret).
|
|
const forwardingSecretEnv = "FELIS_FORWARDING_SECRET"
|
|
|
|
// defaultForwardingDataDir is the world PVC mount inside a server pod (mirrors
|
|
// internal/operator.dataMountPath). It is Paper's working directory, so its
|
|
// config/ and server.properties live under it.
|
|
const defaultForwardingDataDir = "/data"
|
|
|
|
// fwd*Mode are the modes the written config lands with. The initContainer runs as
|
|
// the same uid as the server container (naming.GameUID, pinned by the operator in
|
|
// the pod securityContext) after the prepare-data initContainer has handed the
|
|
// whole volume to that uid, so owner read/write is all the server needs to rewrite
|
|
// these files on boot and nothing else on the node gets write access to them.
|
|
const (
|
|
fwdFileMode os.FileMode = 0o644
|
|
fwdDirMode os.FileMode = 0o755
|
|
)
|
|
|
|
// cmdInitForwarding is the felis-image initContainer entrypoint that makes an
|
|
// ARBITRARY Paper image joinable behind a modern-forwarding Velocity proxy,
|
|
// WITHOUT modifying that image: it writes the Velocity block into
|
|
// <data>/config/paper-global.yml and forces online-mode=false in
|
|
// <data>/server.properties before the server container starts. This is the same
|
|
// config deploy/lobby/entrypoint.sh writes for the Felis-built lobby, lifted into
|
|
// Go so it can be applied to an image Felis did not build.
|
|
//
|
|
// It is idempotent and MERGE-based: Paper expands paper-global.yml to its full
|
|
// default tree on first boot, and the panel file editor may change either file
|
|
// between boots, so it only ever sets proxies.velocity.* and the single
|
|
// online-mode key and preserves every other setting.
|
|
//
|
|
// "Preserves" means values, not formatting, and only for the YAML half:
|
|
// sigs.k8s.io/yaml round-trips through JSON, so paper-global.yml comes back with
|
|
// its keys sorted and its comments dropped. Every setting survives and Paper reads
|
|
// it back identically, but a user who annotated that file loses the annotations.
|
|
// Accepted rather than fixed: comment-faithful editing means a yaml.v3 Node walk,
|
|
// which is a lot of machinery for two keys. server.properties is edited line-wise
|
|
// and does keep its comments and ordering.
|
|
//
|
|
// An empty/unset secret is a deliberate no-op (exit 0): a cluster whose proxy is
|
|
// not in modern mode provisions no Secret, and wedging every server's init on a
|
|
// missing optional value would be worse than the pre-forwarding status quo.
|
|
func cmdInitForwarding(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("init-forwarding", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
dataDir := fs.String("data", defaultForwardingDataDir, "server data directory (Paper working dir)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
secret := os.Getenv(forwardingSecretEnv)
|
|
if err := writePaperForwarding(*dataDir, secret); err != nil {
|
|
fmt.Fprintf(stderr, "felis init-forwarding: %v\n", err)
|
|
return 1
|
|
}
|
|
if secret == "" {
|
|
fmt.Fprintln(stdout, "felis init-forwarding: no forwarding secret set; leaving config untouched")
|
|
} else {
|
|
fmt.Fprintln(stdout, "felis init-forwarding: wrote Velocity modern-forwarding config")
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// writePaperForwarding writes both config surfaces (or nothing, when secret == "").
|
|
func writePaperForwarding(dataDir, secret string) error {
|
|
if secret == "" {
|
|
return nil
|
|
}
|
|
if err := writePaperGlobal(dataDir, secret); err != nil {
|
|
return err
|
|
}
|
|
return forceServerPropertyOffline(dataDir)
|
|
}
|
|
|
|
// writePaperGlobal merges the proxies.velocity block into config/paper-global.yml,
|
|
// creating the file and its directory when absent and preserving every other key.
|
|
func writePaperGlobal(dataDir, secret string) error {
|
|
dir := filepath.Join(dataDir, "config")
|
|
if err := os.MkdirAll(dir, fwdDirMode); err != nil {
|
|
return fmt.Errorf("create %s: %w", dir, err)
|
|
}
|
|
// MkdirAll honours the process umask; chmod does not, so a directory an older
|
|
// release left at 0777 is brought back to fwdDirMode here.
|
|
if err := os.Chmod(dir, fwdDirMode); err != nil {
|
|
return fmt.Errorf("chmod %s: %w", dir, err)
|
|
}
|
|
|
|
path := filepath.Join(dir, "paper-global.yml")
|
|
root := map[string]any{}
|
|
if existing, err := os.ReadFile(path); err == nil {
|
|
if err := yaml.Unmarshal(existing, &root); err != nil {
|
|
return fmt.Errorf("parse existing %s: %w", path, err)
|
|
}
|
|
if root == nil { // an empty or "null" document unmarshals to a nil map
|
|
root = map[string]any{}
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
|
|
setVelocity(root, secret)
|
|
out, err := yaml.Marshal(root)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal %s: %w", path, err)
|
|
}
|
|
return writeFileMode(path, out)
|
|
}
|
|
|
|
// setVelocity sets proxies.velocity.{enabled,online-mode,secret}, creating the
|
|
// intermediate maps when missing. proxies.velocity.online-mode is Paper trusting
|
|
// that the proxy verified the player as premium — distinct from server.properties
|
|
// online-mode, which must be false so the backend does not re-authenticate.
|
|
func setVelocity(root map[string]any, secret string) {
|
|
velocity := childMap(childMap(root, "proxies"), "velocity")
|
|
velocity["enabled"] = true
|
|
velocity["online-mode"] = true
|
|
velocity["secret"] = secret
|
|
}
|
|
|
|
// childMap returns parent[key] as a map, replacing a missing or non-map value with
|
|
// a fresh one. sigs.k8s.io/yaml decodes nested objects to map[string]any (JSON
|
|
// semantics), so the assertion holds for any well-formed paper-global.yml.
|
|
func childMap(parent map[string]any, key string) map[string]any {
|
|
if m, ok := parent[key].(map[string]any); ok {
|
|
return m
|
|
}
|
|
m := map[string]any{}
|
|
parent[key] = m
|
|
return m
|
|
}
|
|
|
|
// forceServerPropertyOffline sets online-mode=false in server.properties. A backend
|
|
// behind a modern-forwarding proxy must be offline-mode (the proxy did the Mojang
|
|
// auth); an arbitrary image defaulting to online-mode=true rejects every proxied
|
|
// login.
|
|
func forceServerPropertyOffline(dataDir string) error {
|
|
path := filepath.Join(dataDir, "server.properties")
|
|
var content []byte
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
content = b
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
return writeFileMode(path, upsertProperty(content, "online-mode", "false"))
|
|
}
|
|
|
|
// upsertProperty sets key=value in a java .properties body, replacing an existing
|
|
// uncommented assignment or appending one, and leaving every other line —
|
|
// comments included — untouched. Keys sit at column 0 the way Paper writes them,
|
|
// so a "#key=" comment does not match.
|
|
func upsertProperty(content []byte, key, value string) []byte {
|
|
want := key + "=" + value
|
|
prefix := key + "="
|
|
lines := strings.Split(string(content), "\n")
|
|
found := false
|
|
for i, ln := range lines {
|
|
if strings.HasPrefix(ln, prefix) {
|
|
lines[i] = want
|
|
found = true
|
|
}
|
|
}
|
|
if found {
|
|
return []byte(strings.Join(lines, "\n"))
|
|
}
|
|
body := string(content)
|
|
if body != "" && !strings.HasSuffix(body, "\n") {
|
|
body += "\n"
|
|
}
|
|
return []byte(body + want + "\n")
|
|
}
|
|
|
|
// writeFileMode writes data then forces the mode, since WriteFile honours the
|
|
// umask and leaves an existing file's mode alone: a file an older release wrote
|
|
// world-writable (0666) is tightened back to fwdFileMode on the next boot.
|
|
func writeFileMode(path string, data []byte) error {
|
|
if err := os.WriteFile(path, data, fwdFileMode); err != nil {
|
|
return fmt.Errorf("write %s: %w", path, err)
|
|
}
|
|
if err := os.Chmod(path, fwdFileMode); err != nil {
|
|
return fmt.Errorf("chmod %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|