Flamenco/pkg/sysinfo/sysinfo_windows.go
Sybren A. Stüvel aa4d3cff75 Windows: check user's system privileges when Shaman is enabled
Symbolic links on Windows require some special user privilege, and
Shaman can now check for this at startup. Hopefully this helps in guiding
people towards a working Shaman system.
2023-10-16 21:36:16 +02:00

154 lines
4.3 KiB
Go

package sysinfo
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
"github.com/rs/zerolog/log"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
// canSymlink tries to determine whether the running system can use symbolic
// links, based on information from the Windows registry.
func canSymlink() (bool, error) {
if isDeveloperModeActive() {
return true, nil
}
return hasSystemPrivilege("SeCreateSymbolicLinkPrivilege")
}
func description() (string, error) {
productName, err := registryReadString(
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
"ProductName",
)
if err != nil {
return "", err
}
editionID, err := windowsEditionID()
if err != nil {
return "", err
}
description := fmt.Sprintf("%s (%s)", productName, editionID)
return description, nil
}
func windowsEditionID() (string, error) {
// Values seen so far:
// - "Professional"
// - "Core"
return registryReadString(
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
"EditionID",
)
}
func registryReadString(keyPath, valueName string) (string, error) {
regkey, err := registry.OpenKey(
registry.LOCAL_MACHINE,
keyPath,
registry.QUERY_VALUE)
if err != nil {
return "", fmt.Errorf("opening registry key %q: %w", keyPath, err)
}
defer regkey.Close()
value, _, err := regkey.GetStringValue(valueName)
if err != nil {
return "", fmt.Errorf("reading registry key %q: %w", valueName, err)
}
return value, nil
}
// isDeveloperModeActive checks whether or not the developer mode is active on Windows 10.
// Returns false for prior Windows versions.
// see https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development
// Copied from https://github.com/golang/go/pull/24307/files
func isDeveloperModeActive() bool {
key, err := registry.OpenKey(
registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock`,
registry.READ)
if err != nil {
return false
}
val, _, err := key.GetIntegerValue("AllowDevelopmentWithoutDevLicense")
if err != nil {
return false
}
return val != 0
}
// hasSystemPrivilege checks whether the user has the
// SeCreateSymbolicLinkPrivilege, which is necessary to create symbolic links.
func hasSystemPrivilege(privilegeName string) (bool, error) {
// This doesn't fail, and just returns -1. The Microsoft docs still recommend
// calling this function, though, instead of just hard-coding the -1 value.
hProcess := windows.CurrentProcess()
// Open a process token, necessary for the subsequent calls.
var processToken windows.Token
err := windows.OpenProcessToken(hProcess, windows.TOKEN_READ, &processToken)
if err != nil {
return false, fmt.Errorf("calling OpenProcessToken: %w", err)
}
defer func() {
_ = processToken.Close()
}()
privilegeNameU16, err := syscall.UTF16PtrFromString(privilegeName)
if err != nil {
return false, fmt.Errorf("invalid privilege name %q: %w", privilegeName, err)
}
// Look up the LUID for the privilege.
var privilegeLUID windows.LUID
err = windows.LookupPrivilegeValue(nil, privilegeNameU16, &privilegeLUID)
if err != nil {
return false, fmt.Errorf("calling LookupPrivilegeValue: %w", err)
}
// Get the size of the buffer needed for the actual data.
var TokenPrivileges uint32 = 3
var bufferSize uint32
err = windows.GetTokenInformation(processToken, TokenPrivileges, nil, 0, &bufferSize)
if errno, ok := err.(syscall.Errno); !ok || errno != 122 {
return false, fmt.Errorf("unexpected error from first GetTokenInformation call: %w", err)
}
// Get the list of user's privileges.
buffer := make([]byte, bufferSize)
err = windows.GetTokenInformation(processToken, TokenPrivileges, &buffer[0], bufferSize, &bufferSize)
if err != nil {
return false, fmt.Errorf("unexpected error from second GetTokenInformation call: %w", err)
}
// Decode the privileges.
privCount := int(binary.LittleEndian.Uint32(buffer))
offset := int(unsafe.Sizeof(uint32(0)))
structSize := int(unsafe.Sizeof(windows.LUIDAndAttributes{}))
for i := 0; i < privCount; i++ {
structPtr := &buffer[offset+structSize*i]
luidAndAttr := *(*windows.LUIDAndAttributes)(unsafe.Pointer(structPtr))
if luidAndAttr.Luid != privilegeLUID {
continue
}
log.Debug().Str("privilege", privilegeName).Msg("found privilege")
return true, nil
}
return false, nil
}