Panel — public source drop (v0.9.0)
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user