adding mac support mainly
This commit is contained in:
@@ -16,16 +16,31 @@ import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
socketPath = "/run/streamdeck-go/helper.sock"
|
||||
whitelistPath = "/etc/streamdeck-go/privileged.yaml"
|
||||
socketMode = 0660
|
||||
)
|
||||
const socketMode = 0660
|
||||
|
||||
// socketPath returns the Unix socket path for the helper daemon.
|
||||
// /run is standard on Linux; /var/run is used on macOS.
|
||||
func socketPath() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/var/run/streamdeck-go/helper.sock"
|
||||
}
|
||||
return "/run/streamdeck-go/helper.sock"
|
||||
}
|
||||
|
||||
// whitelistPath returns the path to the root-owned command whitelist.
|
||||
// /etc is standard on Linux; /usr/local/etc is the convention on macOS.
|
||||
func whitelistPath() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/usr/local/etc/streamdeck-go/privileged.yaml"
|
||||
}
|
||||
return "/etc/streamdeck-go/privileged.yaml"
|
||||
}
|
||||
|
||||
type whitelist struct {
|
||||
Commands map[string]string `yaml:"commands"`
|
||||
@@ -46,25 +61,28 @@ func main() {
|
||||
log.Fatal("streamdeck-helper must run as root (install as a system service)")
|
||||
}
|
||||
|
||||
wl, err := loadWhitelist(whitelistPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load whitelist %q: %v", whitelistPath, err)
|
||||
}
|
||||
log.Printf("loaded %d whitelisted commands from %s", len(wl.Commands), whitelistPath)
|
||||
sock := socketPath()
|
||||
wlist := whitelistPath()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
|
||||
wl, err := loadWhitelist(wlist)
|
||||
if err != nil {
|
||||
log.Fatalf("load whitelist %q: %v", wlist, err)
|
||||
}
|
||||
log.Printf("loaded %d whitelisted commands from %s", len(wl.Commands), wlist)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(sock), 0755); err != nil {
|
||||
log.Fatalf("create socket dir: %v", err)
|
||||
}
|
||||
// Remove stale socket from a previous run.
|
||||
_ = os.Remove(socketPath)
|
||||
_ = os.Remove(sock)
|
||||
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
ln, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
log.Fatalf("listen on %s: %v", socketPath, err)
|
||||
log.Fatalf("listen on %s: %v", sock, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
if err := os.Chmod(socketPath, socketMode); err != nil {
|
||||
if err := os.Chmod(sock, socketMode); err != nil {
|
||||
log.Fatalf("chmod socket: %v", err)
|
||||
}
|
||||
// Chown the socket to root:streamdeck so group members can connect.
|
||||
@@ -73,11 +91,11 @@ func main() {
|
||||
log.Fatalf("group 'streamdeck' not found — run 'make install-helper' first: %v", err)
|
||||
} else if gid, err := strconv.Atoi(grp.Gid); err != nil {
|
||||
log.Fatalf("invalid gid %q: %v", grp.Gid, err)
|
||||
} else if err := os.Lchown(socketPath, 0, gid); err != nil {
|
||||
} else if err := os.Lchown(sock, 0, gid); err != nil {
|
||||
log.Fatalf("chown socket: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("listening on %s (group: streamdeck)", socketPath)
|
||||
log.Printf("listening on %s (group: streamdeck)", sock)
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
@@ -111,7 +129,7 @@ func handle(conn net.Conn, wl *whitelist) {
|
||||
shell, ok := wl.Commands[req.Command]
|
||||
if !ok {
|
||||
log.Printf("REJECTED unknown command %q", req.Command)
|
||||
send(conn, response{Error: fmt.Sprintf("unknown command %q — add it to %s", req.Command, whitelistPath)})
|
||||
send(conn, response{Error: fmt.Sprintf("unknown command %q — add it to %s", req.Command, whitelistPath())})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
_ "image/jpeg"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -26,9 +26,17 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const helperSocket = "/run/streamdeck-go/helper.sock"
|
||||
// helperSocketPath returns the Unix socket path for the privileged helper.
|
||||
// /run is the standard location on Linux; /var/run is used on macOS.
|
||||
func helperSocketPath() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/var/run/streamdeck-go/helper.sock"
|
||||
}
|
||||
return "/run/streamdeck-go/helper.sock"
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfgPath := flag.String("config", defaultConfigPath(), "path to config file")
|
||||
@@ -467,10 +475,39 @@ func runCommand(cmd string) {
|
||||
}
|
||||
}
|
||||
|
||||
// runPrivileged sends a named command to the helper daemon over its Unix socket.
|
||||
// The helper validates the name against its root-owned whitelist and runs it.
|
||||
// runPrivileged dispatches a named privileged command.
|
||||
// On macOS: reads the user's local whitelist and runs via osascript (admin auth dialog).
|
||||
// On Linux: sends to the root helper daemon over its Unix socket.
|
||||
func runPrivileged(name string) error {
|
||||
conn, err := net.Dial("unix", helperSocket)
|
||||
if runtime.GOOS == "darwin" {
|
||||
return runPrivilegedDarwin(name)
|
||||
}
|
||||
return runPrivilegedHelper(name)
|
||||
}
|
||||
|
||||
// runPrivilegedDarwin looks up name in ~/.config/streamdeck-go/privileged.yaml
|
||||
// and executes it via osascript, which shows the standard macOS admin auth dialog.
|
||||
func runPrivilegedDarwin(name string) error {
|
||||
wlPath := darwinWhitelistPath()
|
||||
commands, err := loadPrivilegedCommands(wlPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load whitelist %q: %w", wlPath, err)
|
||||
}
|
||||
shell, ok := commands[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown command %q — add it to %s", name, wlPath)
|
||||
}
|
||||
script := fmt.Sprintf(`do shell script %q with administrator privileges`, shell)
|
||||
out, err := exec.Command("osascript", "-e", script).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runPrivilegedHelper sends a named command to the root helper daemon over its Unix socket.
|
||||
func runPrivilegedHelper(name string) error {
|
||||
conn, err := net.Dial("unix", helperSocketPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("helper unavailable (is streamdeck-go-helper.service running?): %w", err)
|
||||
}
|
||||
@@ -499,6 +536,32 @@ func runPrivileged(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func darwinWhitelistPath() string {
|
||||
base := os.Getenv("XDG_CONFIG_HOME")
|
||||
if base == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
base = filepath.Join(home, ".config")
|
||||
}
|
||||
return filepath.Join(base, "streamdeck-go", "privileged.yaml")
|
||||
}
|
||||
|
||||
func loadPrivilegedCommands(path string) (map[string]string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var wl struct {
|
||||
Commands map[string]string `yaml:"commands"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &wl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wl.Commands == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
return wl.Commands, nil
|
||||
}
|
||||
|
||||
func defaultConfigPath() string {
|
||||
base := os.Getenv("XDG_CONFIG_HOME")
|
||||
if base == "" {
|
||||
@@ -548,8 +611,3 @@ keys: {}
|
||||
`
|
||||
}
|
||||
|
||||
func blank(w, h int) image.Image {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{color.Black}, image.Point{}, draw.Src)
|
||||
return img
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user