Sybren A. Stüvel
aa4d3cff75
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.
154 lines
4.3 KiB
Go
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
|
|
}
|