-
Notifications
You must be signed in to change notification settings - Fork 5
/
pageant.go
223 lines (207 loc) · 5.7 KB
/
pageant.go
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Package pageant provides native Go support for using PuTTY Pageant as an
// SSH agent with the golang.org/x/crypto/ssh/agent package.
// Based loosely on the Java JNA package jsch-agent-proxy-pageant.
package pageant
import (
"encoding/binary"
"fmt"
"io"
"reflect"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
const (
agentCopydataID = 0x804e50ba
agentMaxMsglen = 8192
noError = syscall.Errno(0)
wmCopyData = 0x004a
)
var (
pageantWindowName = utf16Ptr("Pageant")
user32 = windows.NewLazySystemDLL("user32.dll")
findWindow = user32.NewProc("FindWindowW")
sendMessage = user32.NewProc("SendMessageW")
)
// Conn is a shared-memory connection to Pageant.
// Conn implements io.Reader, io.Writer, and io.Closer.
// It is not safe to use Conn in multiple concurrent goroutines.
type Conn struct {
window windows.Handle
sharedFile windows.Handle
sharedMem uintptr
readOffset int
readLimit int
mapName string
}
var _ io.ReadWriteCloser = &Conn{}
// NewConn creates a new connection to Pageant.
// Ensure Close gets called on the returned Conn when it is no longer needed.
func NewConn() (*Conn, error) {
return &Conn{}, nil
}
// Close frees resources used by Conn.
func (c *Conn) Close() error {
if c.sharedMem == 0 {
return nil
}
errUnmap := windows.UnmapViewOfFile(c.sharedMem)
errClose := windows.CloseHandle(c.sharedFile)
if errUnmap != nil {
return errUnmap
} else if errClose != nil {
return errClose
}
c.sharedMem = 0
c.sharedFile = windows.InvalidHandle
return nil
}
func (c *Conn) Read(p []byte) (n int, err error) {
if c.sharedMem == 0 {
return 0, fmt.Errorf("not connected to Pageant")
} else if c.readLimit == 0 {
return 0, fmt.Errorf("must send request to Pageant before reading response")
} else if c.readOffset == c.readLimit {
return 0, io.EOF
}
bytesToRead := minInt(len(p), c.readLimit-c.readOffset)
src := toSlice(c.sharedMem+uintptr(c.readOffset), bytesToRead)
copy(p, src)
c.readOffset += bytesToRead
return bytesToRead, nil
}
func (c *Conn) Write(p []byte) (n int, err error) {
if len(p) > agentMaxMsglen {
return 0, fmt.Errorf("size of request message (%d) exceeds max length (%d)", len(p), agentMaxMsglen)
} else if len(p) == 0 {
return 0, fmt.Errorf("message to send is empty")
}
if c.sharedMem != 0 {
err := c.Close()
if c.sharedMem != 0 {
return 0, fmt.Errorf("failed to close previous connection: %s", err)
}
}
if err := c.establishConn(); err != nil {
return 0, fmt.Errorf("failed to connect to Pageant: %s", err)
}
dst := toSlice(c.sharedMem, len(p))
copy(dst, p)
data := make([]byte, len(c.mapName)+1)
copy(data, c.mapName)
result, err := c.sendMessage(data)
if result == 0 {
if err != nil {
return 0, fmt.Errorf("failed to send request to Pageant: %s", err)
} else {
return 0, fmt.Errorf("request refused by Pageant")
}
}
messageSize := binary.BigEndian.Uint32(toSlice(c.sharedMem, 4))
if messageSize > agentMaxMsglen-4 {
return 0, fmt.Errorf("size of response message (%d) exceeds max length (%d)", messageSize+4, agentMaxMsglen)
}
c.readOffset = 0
c.readLimit = 4 + int(messageSize)
return len(p), nil
}
// establishConn creates a new connection to Pageant.
func (c *Conn) establishConn() error {
window, _, err := findWindow.Call(
uintptr(unsafe.Pointer(pageantWindowName)),
uintptr(unsafe.Pointer(pageantWindowName)),
)
if window == 0 {
if err != nil && err != noError {
return fmt.Errorf("cannot find Pageant window: %s", err)
} else {
return fmt.Errorf("cannot find Pageant window, ensure Pageant is running")
}
}
mapName := fmt.Sprintf("PageantRequest%08x", windows.GetCurrentThreadId())
mapNameUTF16 := utf16Ptr(mapName)
sharedFile, err := windows.CreateFileMapping(
windows.InvalidHandle,
nil,
windows.PAGE_READWRITE,
0,
agentMaxMsglen,
mapNameUTF16,
)
if err != nil {
return fmt.Errorf("failed to create shared file: %s", err)
}
sharedMem, err := windows.MapViewOfFile(
sharedFile,
windows.FILE_MAP_WRITE,
0,
0,
0,
)
if err != nil {
return fmt.Errorf("failed to map file into shared memory: %s", err)
}
*c = Conn{
window: windows.Handle(window),
sharedFile: sharedFile,
sharedMem: sharedMem,
mapName: mapName,
}
return nil
}
// sendMessage invokes user32.SendMessage to alert Pageant that data
// is available for it to read.
func (c *Conn) sendMessage(data []byte) (uintptr, error) {
cds := copyData{
dwData: agentCopydataID,
cbData: uintptr(len(data)),
lpData: uintptr(unsafe.Pointer(&data[0])),
}
result, _, err := sendMessage.Call(
uintptr(c.window),
wmCopyData,
0,
uintptr(unsafe.Pointer(&cds)),
)
if err == noError {
return result, nil
}
return result, err
}
// copyData is equivalent to COPYDATASTRUCT.
// Unlike Java, Go has a native type that matches the bit width of the
// platform, so there is no need for separate 32-bit and 64-bit versions.
// Curiously, the MSDN definition of COPYDATASTRUCT says dwData is ULONG_PTR
// and cbData is DWORD, which seems to be backwards.
type copyData struct {
dwData uint32
cbData uintptr
lpData uintptr
}
// minInt returns the lesser of x and y.
func minInt(x, y int) int {
if x < y {
return x
} else {
return y
}
}
// toSlice creates a fake slice header that allows copying to/from the block
// of memory from addr to addr+size.
func toSlice(addr uintptr, size int) []byte {
header := reflect.SliceHeader{
Len: size,
Cap: size,
Data: addr,
}
return *(*[]byte)(unsafe.Pointer(&header))
}
// utf16Ptr converts a static string not containing any zero bytes to a
// sequence of UTF-16 code units, represented as a pointer to the first one.
func utf16Ptr(s string) *uint16 {
result, err := windows.UTF16PtrFromString(s)
if err != nil {
panic(err)
}
return result
}