feat: complete Windows client control and recovery paths
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
//go:build windows
|
||||
|
||||
package windowstray
|
||||
|
||||
// This file is the small native service endpoint. It intentionally uses one
|
||||
// request per pipe connection: the tray is a presentation client, not a
|
||||
// long-lived command channel, and a bounded connection makes cancellation and
|
||||
// peer verification straightforward. The service never hands the tray a
|
||||
// store handle or command payload.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
winapi "golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
pipeBufferBytes = 64 << 10
|
||||
pipeInstances = 8
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupported = errors.New("Windows tray IPC is unavailable on this platform")
|
||||
pipeSDDL = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)"
|
||||
)
|
||||
|
||||
// Serve accepts bounded tray requests until ctx is cancelled. It is safe to
|
||||
// run before any interactive user logs in; in that state no client can pass
|
||||
// the interactive-peer authorization check.
|
||||
func Serve(ctx context.Context, handler Handler) error {
|
||||
if handler == nil {
|
||||
return errors.New("tray handler is required")
|
||||
}
|
||||
for {
|
||||
pipe, err := newTrayPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create tray pipe: %w", err)
|
||||
}
|
||||
connected := make(chan error, 1)
|
||||
go func() { connected <- winapi.ConnectNamedPipe(winapi.Handle(pipe.Fd()), nil) }()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = pipe.Close()
|
||||
return nil
|
||||
case err := <-connected:
|
||||
if err != nil && !errors.Is(err, winapi.ERROR_PIPE_CONNECTED) {
|
||||
_ = pipe.Close()
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
go serveTrayPipe(ctx, pipe, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTrayPipe() (*os.File, error) {
|
||||
name, err := winapi.UTF16PtrFromString(PipeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
descriptor, err := winapi.SecurityDescriptorFromString(pipeSDDL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes := &winapi.SecurityAttributes{
|
||||
Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})),
|
||||
SecurityDescriptor: descriptor,
|
||||
}
|
||||
mode := uint32(winapi.PIPE_ACCESS_DUPLEX | winapi.PIPE_TYPE_MESSAGE | winapi.PIPE_READMODE_MESSAGE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS | winapi.SECURITY_IDENTIFICATION)
|
||||
handle, err := winapi.CreateNamedPipe(name, mode, pipeInstances, pipeBufferBytes, pipeBufferBytes, 0, 0, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(handle), "rvbox-tray-pipe"), nil
|
||||
}
|
||||
|
||||
func serveTrayPipe(ctx context.Context, pipe *os.File, handler Handler) {
|
||||
if pipe == nil {
|
||||
return
|
||||
}
|
||||
defer pipe.Close()
|
||||
// A blocked Read must be interrupted when service shutdown cancels ctx.
|
||||
readDone := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = pipe.Close()
|
||||
case <-readDone:
|
||||
}
|
||||
}()
|
||||
defer close(readDone)
|
||||
peer, err := peerFromPipe(winapi.Handle(pipe.Fd()))
|
||||
if err != nil {
|
||||
writeTrayResponse(pipe, err)
|
||||
return
|
||||
}
|
||||
request, err := readTrayFrame(pipe)
|
||||
if err != nil {
|
||||
writeTrayResponse(pipe, err)
|
||||
return
|
||||
}
|
||||
if err := Authorize(peer, request.Action); err != nil {
|
||||
writeTrayResponse(pipe, err)
|
||||
return
|
||||
}
|
||||
response, err := handler(ctx, peer, request)
|
||||
if err != nil {
|
||||
writeTrayResponse(pipe, err)
|
||||
return
|
||||
}
|
||||
if response.Action == 0 {
|
||||
response.Action = ActionStatus
|
||||
}
|
||||
if response.Action != ActionStatus {
|
||||
response = Frame{Action: ActionStatus}
|
||||
}
|
||||
writeTrayResponse(pipe, response)
|
||||
}
|
||||
|
||||
func readTrayFrame(reader io.Reader) (Frame, error) {
|
||||
buffer := make([]byte, maxFrameBytes)
|
||||
count, err := reader.Read(buffer)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
if count == len(buffer) {
|
||||
return Frame{}, ErrFrameTooLarge
|
||||
}
|
||||
return Decode(buffer[:count])
|
||||
}
|
||||
|
||||
func writeTrayResponse(writer *os.File, value any) {
|
||||
frame := Frame{Action: ActionStatus}
|
||||
switch response := value.(type) {
|
||||
case Frame:
|
||||
frame = response
|
||||
case error:
|
||||
message := response.Error()
|
||||
if len(message) > maxPayloadBytes {
|
||||
message = message[:maxPayloadBytes]
|
||||
}
|
||||
frame.Payload = []byte("error: " + message)
|
||||
}
|
||||
encoded, err := Encode(frame)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write(encoded)
|
||||
_ = winapi.FlushFileBuffers(winapi.Handle(writer.Fd()))
|
||||
}
|
||||
|
||||
func peerFromPipe(pipe winapi.Handle) (Peer, error) {
|
||||
if pipe == 0 || pipe == winapi.InvalidHandle {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
var pid uint32
|
||||
if err := winapi.GetNamedPipeClientProcessId(pipe, &pid); err != nil || pid == 0 {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
process, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
defer winapi.CloseHandle(process)
|
||||
var token winapi.Token
|
||||
if err := winapi.OpenProcessToken(process, winapi.TOKEN_QUERY, &token); err != nil {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
defer token.Close()
|
||||
user, err := token.GetTokenUser()
|
||||
if err != nil || user.User.Sid == nil {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
var sessionID uint32
|
||||
var returned uint32
|
||||
if err := winapi.GetTokenInformation(token, winapi.TokenSessionId, (*byte)(unsafe.Pointer(&sessionID)), uint32(unsafe.Sizeof(sessionID)), &returned); err != nil || returned != uint32(unsafe.Sizeof(sessionID)) {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
adminSID, err := winapi.CreateWellKnownSid(winapi.WinBuiltinAdministratorsSid)
|
||||
if err != nil {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
admin, err := token.IsMember(adminSID)
|
||||
if err != nil {
|
||||
return Peer{}, ErrInvalidPeer
|
||||
}
|
||||
sid := user.User.Sid.String()
|
||||
return Peer{PID: pid, SessionID: sessionID, SID: sid, TokenVerified: true, Interactive: sessionID != 0, Administrator: admin, System: sid == "S-1-5-18"}, nil
|
||||
}
|
||||
|
||||
// Request opens exactly one local pipe connection and exchanges one frame.
|
||||
// It retries only the transient ERROR_PIPE_BUSY state and never falls back to
|
||||
// an arbitrary filesystem/socket path.
|
||||
func Request(ctx context.Context, request Frame) (Frame, error) {
|
||||
encoded, err := Encode(request)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
var pipe *os.File
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return Frame{}, ctx.Err()
|
||||
}
|
||||
name, nameErr := winapi.UTF16PtrFromString(PipeName)
|
||||
if nameErr != nil {
|
||||
return Frame{}, nameErr
|
||||
}
|
||||
handle, openErr := winapi.CreateFile(name, winapi.GENERIC_READ|winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, 0, 0)
|
||||
if openErr == nil {
|
||||
pipe = os.NewFile(uintptr(handle), "rvbox-tray-client")
|
||||
break
|
||||
}
|
||||
if !errors.Is(openErr, winapi.ERROR_PIPE_BUSY) {
|
||||
return Frame{}, openErr
|
||||
}
|
||||
timer := time.NewTimer(100 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return Frame{}, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
defer pipe.Close()
|
||||
state := uint32(winapi.PIPE_READMODE_MESSAGE)
|
||||
_ = winapi.SetNamedPipeHandleState(winapi.Handle(pipe.Fd()), &state, nil, nil)
|
||||
if _, err := pipe.Write(encoded); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
if err := winapi.FlushFileBuffers(winapi.Handle(pipe.Fd())); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
return readTrayFrame(pipe)
|
||||
}
|
||||
Reference in New Issue
Block a user