Files
jbergner 1d8e36c53a
Some checks failed
release-tag / release-image (push) Has been cancelled
Init
2026-08-22 08:10:11 +02:00

168 lines
4.8 KiB
Go

package templates
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"unicode/utf16"
"github.com/example/sessionguard/internal/model"
)
func Apply(profile string, item model.TemplateItem) (changed bool, err error) {
if item.ID == "" {
return false, errors.New("template item id is required")
}
target, err := safeTarget(profile, item.Target)
if err != nil {
return false, err
}
switch strings.ToLower(item.Kind) {
case "directory":
if st, err := os.Stat(target); err == nil && st.IsDir() {
return false, nil
}
return true, os.MkdirAll(target, 0o755)
case "file":
data, err := sourceData(item)
if err != nil {
return false, err
}
return ensureFile(target, data, item.Overwrite)
case "url":
if item.URL == "" {
return false, errors.New("url template requires url")
}
data := []byte("[InternetShortcut]\r\nURL=" + item.URL + "\r\n")
return ensureFile(target, data, item.Overwrite)
case "shortcut":
if item.Shortcut == nil || item.Shortcut.Target == "" {
return false, errors.New("shortcut template requires shortcut.target")
}
if runtime.GOOS != "windows" {
return false, errors.New("shortcut generation is Windows-only")
}
if _, err := os.Stat(target); err == nil && !item.Overwrite {
return false, nil
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return false, err
}
return ensureShortcut(target, *item.Shortcut)
default:
return false, fmt.Errorf("unknown template kind %q", item.Kind)
}
}
func safeTarget(profile, rel string) (string, error) {
if rel == "" || filepath.IsAbs(rel) {
return "", errors.New("template target must be relative to the user profile")
}
root, err := filepath.Abs(profile)
if err != nil {
return "", err
}
t, err := filepath.Abs(filepath.Join(root, rel))
if err != nil {
return "", err
}
rp := strings.ToLower(filepath.Clean(root)) + string(os.PathSeparator)
tp := strings.ToLower(filepath.Clean(t))
if tp != strings.TrimSuffix(rp, string(os.PathSeparator)) && !strings.HasPrefix(tp, rp) {
return "", errors.New("template target escapes profile root")
}
return t, nil
}
func sourceData(item model.TemplateItem) ([]byte, error) {
if item.Source != "" {
return os.ReadFile(item.Source)
}
if item.ContentBase64 != "" {
return base64.StdEncoding.DecodeString(item.ContentBase64)
}
return []byte(item.Content), nil
}
func ensureFile(path string, data []byte, overwrite bool) (bool, error) {
if old, err := os.ReadFile(path); err == nil {
if hash(old) == hash(data) {
return false, nil
}
if !overwrite {
return false, nil
}
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return false, err
}
tmp := path + ".sessionguard.tmp"
if err := os.WriteFile(tmp, data, 0o644); err != nil {
return false, err
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return false, err
}
return true, nil
}
func hash(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
func psQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
func ensureShortcut(path string, s model.ShortcutSpec) (bool, error) {
script := "$w=New-Object -ComObject WScript.Shell;" +
"$p=" + psQuote(path) + ";" +
"if(Test-Path -LiteralPath $p){$x=$w.CreateShortcut($p);" +
"if(($x.TargetPath -eq " + psQuote(s.Target) + ") -and ($x.Arguments -eq " + psQuote(s.Arguments) + ") -and ($x.WorkingDirectory -eq " + psQuote(s.WorkingDirectory) + ") -and ($x.IconLocation -eq " + psQuote(s.IconLocation) + ") -and ($x.Description -eq " + psQuote(s.Description) + ")){Write-Output 'UNCHANGED';exit 0}};" +
"$l=$w.CreateShortcut($p);" +
"$l.TargetPath=" + psQuote(s.Target) + ";" +
"$l.Arguments=" + psQuote(s.Arguments) + ";" +
"$l.WorkingDirectory=" + psQuote(s.WorkingDirectory) + ";" +
"$l.IconLocation=" + psQuote(s.IconLocation) + ";" +
"$l.Description=" + psQuote(s.Description) + ";$l.Save();Write-Output 'CHANGED'"
u16 := utf16.Encode([]rune(script))
bytes := make([]byte, len(u16)*2)
for i, v := range u16 {
bytes[i*2] = byte(v)
bytes[i*2+1] = byte(v >> 8)
}
enc := base64.StdEncoding.EncodeToString(bytes)
cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", enc)
out, err := cmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("ensure shortcut: %w: %s", err, strings.TrimSpace(string(out)))
}
return strings.Contains(string(out), "CHANGED"), nil
}
func CopyFile(dst, src string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
_, cpErr := io.Copy(out, in)
closeErr := out.Close()
if cpErr != nil {
return cpErr
}
return closeErr
}