arblant
firefox-vpn-client / core / internal / tunservice / client_windows.go
client_windows.go159 lines · 4.2 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
package tunservice

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"

	"github.com/armanlife/firefox-vpn-client/core/internal/tunapi"
)

// InstallForUser installs the service for the user running this process.
// Windows' prompt names the program itself, so prompt (macOS') is not
// used.
func InstallForUser(prompt string) error {
	user, err := currentUser()
	if err != nil {
		return err
	}
	if windows.GetCurrentProcessToken().IsElevated() {
		return Install(user)
	}
	return runElevated("install-as-admin", "-user", user)
}

// UninstallForUser removes the service: through its pipe while it runs,
// with no prompt; else elevated.
func UninstallForUser(prompt string) error {
	if installed, err := tunapi.Installed(); err == nil && !installed {
		return nil
	}
	if err := askService(tunapi.Message{Type: tunapi.TypeUninstall}, tunapi.TypeUninstalling); err == nil {
		return waitRemoved(30 * time.Second)
	}
	if windows.GetCurrentProcessToken().IsElevated() {
		return Uninstall()
	}
	return runElevated("uninstall-as-admin")
}

func currentUser() (string, error) {
	u, err := windows.GetCurrentProcessToken().GetTokenUser()
	if err != nil {
		return "", err
	}
	return u.User.Sid.String(), nil
}

// runElevated runs this executable with args as an administrator, after
// Windows' prompt, and waits for it. The elevated copy writes why it
// failed to a report file, since it has no console of this process.
func runElevated(args ...string) error {
	self, err := os.Executable()
	if err != nil {
		return err
	}
	report, err := os.CreateTemp("", "firefox-vpn-tun-*.txt")
	if err != nil {
		return err
	}
	report.Close()
	defer os.Remove(report.Name())
	args = append(args, "-report", report.Name())
	quoted := make([]string, len(args))
	for i, a := range args {
		quoted[i] = windows.EscapeArg(a)
	}
	code, err := shellRunAs(self, strings.Join(quoted, " "))
	if err != nil {
		if errors.Is(err, windows.ERROR_CANCELLED) {
			return ErrCancelled
		}
		return err
	}
	if code == 0 {
		return nil
	}
	if text := readReport(report.Name()); text != "" {
		return errors.New(text)
	}
	return fmt.Errorf("%s failed with exit code %d", filepath.Base(self), code)
}

// shellRunAs starts file with params through ShellExecuteEx's "runas",
// which asks for administrator rights, and returns its exit code. The
// prompt belongs to the foreground window: the app or Setup that asked.
func shellRunAs(file, params string) (uint32, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	if err := windows.CoInitializeEx(0, windows.COINIT_APARTMENTTHREADED|windows.COINIT_DISABLE_OLE1DDE); err == nil {
		defer windows.CoUninitialize()
	}
	verb, _ := windows.UTF16PtrFromString("runas")
	f, err := windows.UTF16PtrFromString(file)
	if err != nil {
		return 0, err
	}
	p, err := windows.UTF16PtrFromString(params)
	if err != nil {
		return 0, err
	}
	info := shellExecuteInfo{
		mask:       seeMaskNoCloseProcess | seeMaskNoAsync | seeMaskFlagNoUI,
		hwnd:       windows.GetForegroundWindow(),
		verb:       verb,
		file:       f,
		parameters: p,
		show:       windows.SW_HIDE,
	}
	info.size = uint32(unsafe.Sizeof(info))
	if r, _, e := procShellExecuteExW.Call(uintptr(unsafe.Pointer(&info))); r == 0 {
		return 0, e
	}
	if info.process == 0 {
		return 0, errors.New("the elevated process did not start")
	}
	defer windows.CloseHandle(info.process)
	if _, err := windows.WaitForSingleObject(info.process, windows.INFINITE); err != nil {
		return 0, err
	}
	var code uint32
	if err := windows.GetExitCodeProcess(info.process, &code); err != nil {
		return 0, err
	}
	return code, nil
}

var procShellExecuteExW = windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteExW")

const (
	seeMaskNoCloseProcess = 0x40
	seeMaskNoAsync        = 0x100
	seeMaskFlagNoUI       = 0x400
)

// shellExecuteInfo is SHELLEXECUTEINFOW.
type shellExecuteInfo struct {
	size          uint32
	mask          uint32
	hwnd          windows.HWND
	verb          *uint16
	file          *uint16
	parameters    *uint16
	directory     *uint16
	show          int32
	instApp       windows.Handle
	idList        uintptr
	class         *uint16
	keyClass      windows.Handle
	hotKey        uint32
	iconOrMonitor windows.Handle
	process       windows.Handle
}