package module import ( "bytes" "crypto/rand" "encoding/base64" "fmt" "os" "path/filepath" "text/template" ) // Render writes out each ConfigFile declared by the manifest into dataPath, // applying `values` (which typically combines user config_values + generated // secrets) via Go text/template. Existing files are overwritten. // // Templates are resolved relative to the manifest's Dir. If a ConfigFile // declares no Template, it is skipped — operators can also manage those // files by direct edit via the (future) SFTP/file manager. // // Render uses each ConfigFile's default Template. Callers that know the // instance's Steam branch should use RenderForBranch so version-specific // templates (7DTD 3.0's serverconfig.v3.xml.tmpl) are selected. func Render(manifest *Manifest, dataPath string, values map[string]string) error { return RenderForBranch(manifest, dataPath, values, "") } // RenderForBranch is Render with branch-aware template selection: each // ConfigFile renders cf.TemplateForBranch(branch) instead of cf.Template, so // an instance on a 3.0 branch picks serverconfig.v3.xml.tmpl while ≤2.6 // instances (branch "") keep the default template. branch is the normalized // Steam branch (see resolveCreateBranch); "" means the module default. func RenderForBranch(manifest *Manifest, dataPath string, values map[string]string, branch string) error { if manifest == nil { return fmt.Errorf("render: manifest is nil") } if manifest.Dir == "" { return fmt.Errorf("render: manifest.Dir is empty (loader must set it)") } for _, cf := range manifest.ConfigFiles { tmpl := cf.TemplateForBranch(branch) if tmpl == "" { continue } if err := renderOne(manifest.Dir, cf, tmpl, dataPath, values); err != nil { return err } } return nil } func renderOne(moduleDir string, cf ConfigFile, templateRel string, dataPath string, values map[string]string) error { templatePath := filepath.Join(moduleDir, templateRel) tmplData, err := os.ReadFile(templatePath) if err != nil { return fmt.Errorf("read template %s: %w", templatePath, err) } tmpl, err := template.New(cf.Path).Option("missingkey=zero").Parse(string(tmplData)) if err != nil { return fmt.Errorf("parse template %s: %w", templateRel, err) } var buf bytes.Buffer ctx := struct { Values map[string]string }{Values: values} if err := tmpl.Execute(&buf, ctx); err != nil { return fmt.Errorf("execute template %s: %w", templateRel, err) } outPath := filepath.Join(dataPath, cf.Path) if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { return fmt.Errorf("mkdir %s: %w", filepath.Dir(outPath), err) } if err := os.WriteFile(outPath, buf.Bytes(), 0o644); err != nil { return fmt.Errorf("write %s: %w", outPath, err) } return nil } // GenerateSecrets emits a random value for each manifest Secret flagged // Generated. Values are URL-safe base64 of 24 bytes — safe in XML, shell, // URLs, and long enough to resist brute force on a local RCON surface. // // Callers typically merge this with user-supplied config_values before // rendering templates and resolving RCON passwords. func GenerateSecrets(manifest *Manifest) (map[string]string, error) { if manifest == nil { return nil, nil } out := map[string]string{} for _, s := range manifest.Secrets { if !s.Generated { continue } val, err := randomToken(24) if err != nil { return nil, fmt.Errorf("generate secret %q: %w", s.Name, err) } out[s.Name] = val } return out, nil } // randomToken returns a URL-safe base64 string of n random bytes. func randomToken(nBytes int) (string, error) { b := make([]byte, nBytes) if _, err := rand.Read(b); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil }