forked from jstarks/npiperelay
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathnpiperelay.go
305 lines (262 loc) · 8.37 KB
/
npiperelay.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// Package main contains the npiperelay application
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"runtime"
"strconv"
"sync"
"time"
"golang.org/x/sys/windows"
)
// How long to sleep between failures while polling
const (
cSECURITY_SQOS_PRESENT = 0x100000 //nolint:revive,stylecheck // Don't include revive and stylecheck when running golangci-lint to stop complain about use of underscores in Go names
cSECURITY_ANONYMOUS = 0 //nolint:revive,stylecheck // Don't include revive and stylecheck when running golangci-lint to stop complain about use of underscores in Go names
cPOLL_TIMEOUT = 200 * time.Millisecond //nolint:revive,stylecheck // Don't include revive and stylecheck when running golangci-lint to stop complain about use of underscores in Go names
cPOLL_ATTEMPTS = 300 //nolint:revive,stylecheck // Don't include revive and stylecheck when running golangci-lint to stop complain about use of underscores in Go names
)
var (
poll = flag.Bool("p", false, "poll every 200ms until the the named pipe exists and is not busy")
limit = flag.Bool("l", false, "when polling do not poll indefinitely, fail after 300 attempts")
closeWrite = flag.Bool("s", false, "send a 0-byte message to the pipe after EOF on stdin")
closeOnEOF = flag.Bool("ep", false, "terminate on EOF reading from the pipe, even if there is more data to write")
closeOnStdinEOF = flag.Bool("ei", false, "terminate on EOF reading from stdin, even if there is more data to write")
runInBackground = flag.Bool("bg", false, "hide console window and run in background")
assuan = flag.Bool("a", false, "treat the target as an Assuan file socket (used by GnuPG)")
verbose = flag.Bool("v", false, "verbose output on stderr")
version = "0.0.0-dev" // Replaced with value from ldflag in build by GoReleaser: Current Git tag with the v prefix stripped
commit = "unknown" // Replaced with value from ldflag in build by GoReleaser: Current git commit SHA
date = "unknown" // Replaced with value from ldflag in build by GoReleaser: Date according RFC3339
builtBy = "unknown" // Replaced with value from ldflag in build by GoReleaser
)
func hideConsole() error {
getConsoleWindow := windows.NewLazySystemDLL("kernel32.dll").NewProc("GetConsoleWindow")
if err := getConsoleWindow.Find(); err != nil {
return err
}
showWindow := windows.NewLazySystemDLL("user32.dll").NewProc("ShowWindow")
if err := showWindow.Find(); err != nil {
return err
}
hwnd, _, _ := getConsoleWindow.Call()
if hwnd == 0 {
return errors.New("no console associated")
}
iret, _, _ := showWindow.Call(hwnd, 0) // SW_HIDE (0): Hides the window and activates another window.
if iret == 0 {
return errors.New("console window is already hidden")
}
return nil
}
func dialPipe(p string, poll bool, limit bool) (*overlappedFile, error) {
p16, err := windows.UTF16FromString(p)
if err != nil {
return nil, err
}
for attempts := 0; ; {
h, err := windows.CreateFile(&p16[0], windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED|cSECURITY_SQOS_PRESENT|cSECURITY_ANONYMOUS, 0)
if err == nil {
return newOverlappedFile(h), nil
}
if poll && attempts < cPOLL_ATTEMPTS {
if limit {
attempts++
}
if err == windows.ERROR_FILE_NOT_FOUND {
time.Sleep(cPOLL_TIMEOUT)
continue
}
if err == windows.ERROR_PIPE_BUSY {
time.Sleep(cPOLL_TIMEOUT)
continue
}
}
return nil, &os.PathError{Path: p, Op: "open", Err: err}
}
}
func dialPort(p int, _ bool) (*overlappedFile, error) {
if p < 0 || p > 65535 {
return nil, errors.New("invalid port value")
}
h, err := windows.Socket(windows.AF_INET, windows.SOCK_STREAM, 0)
if err != nil {
return nil, err
}
// Create a SockaddrInet4 for connecting to
sa := &windows.SockaddrInet4{Addr: [4]byte{0x7F, 0x00, 0x00, 0x01}, Port: p}
// Bind to a randomly assigned local port
err = windows.Bind(h, &windows.SockaddrInet4{})
if err != nil {
return nil, err
}
// Wrap our socket up to be properly handled
conn := newOverlappedFile(h)
// Connect to the Assuan TCP port using overlapped ConnectEx operation
_, err = conn.asyncIo(func(h windows.Handle, _ *uint32, o *windows.Overlapped) error {
return windows.ConnectEx(h, sa, nil, 0, nil, o)
})
if err != nil {
return nil, err
}
return conn, nil
}
// LibAssaun file socket: Attempt to read contents of the target file and connect to a TCP port
func dialAssuan(p string, poll bool, limit bool) (*overlappedFile, error) {
pipeConn, err := dialPipe(p, poll, limit)
if err != nil {
return nil, err
}
var port int
var nonce [16]byte
reader := bufio.NewReader(pipeConn)
// Read the target port number from the first line
tmp, _, err := reader.ReadLine()
if err == nil {
port, err = strconv.Atoi(string(tmp))
}
if err != nil {
return nil, err
}
// Read the rest of the nonce from the file
n, err := reader.Read(nonce[:])
if err != nil {
return nil, err
}
if n != 16 {
err = fmt.Errorf("Read incorrect number of bytes for nonce. Expected 16, got %d (0x%X)", n, nonce)
return nil, err
}
if *verbose {
log.Printf("Port: %d, Nonce: %X", port, nonce)
}
err = pipeConn.Close()
if err != nil {
return nil, err
}
for {
// Try to connect to the Assuan TCP socket hosted on localhost
conn, err := dialPort(port, poll)
if poll && (err == windows.WSAETIMEDOUT || err == windows.WSAECONNREFUSED || err == windows.WSAENETUNREACH || err == windows.ERROR_CONNECTION_REFUSED) {
time.Sleep(cPOLL_TIMEOUT)
continue
}
if err != nil {
err = os.NewSyscallError("ConnectEx", err)
return nil, err
}
_, err = conn.Write(nonce[:])
if err != nil {
return nil, err
}
return conn, nil
}
}
func main() {
flag.Usage = func() {
// Custom usage message (default documented here: https://pkg.go.dev/flag#pkg-variables)
if _, err := fmt.Fprintf(flag.CommandLine.Output(),
"npiperelay v%s\n"+
" commit %s\n"+
" build date %s\n"+
" built by %s\n"+
" built with %s\n"+
"\nusage:\n",
version, commit, date, builtBy, runtime.Version()); err != nil {
log.Fatalln(err)
}
flag.PrintDefaults()
}
flag.Parse()
args := flag.Args()
if len(args) != 1 {
flag.Usage()
os.Exit(1)
}
if *runInBackground {
if err := hideConsole(); err != nil {
log.Println("hide console window to run in background failed:", err)
}
}
if *verbose {
log.Println("connecting to", args[0])
}
var conn *overlappedFile
var err error
if !*assuan {
conn, err = dialPipe(args[0], *poll, *limit)
} else {
conn, err = dialAssuan(args[0], *poll, *limit)
}
if err != nil {
log.Fatalln(err)
}
if *verbose {
log.Println("connected")
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
if _, err := io.Copy(conn, os.Stdin); err != nil {
log.Fatalln("copy from stdin to pipe failed:", err)
}
if *verbose {
log.Println("copy from stdin to pipe finished")
}
if *closeOnStdinEOF {
os.Exit(0)
}
if *closeWrite {
// A zero-byte write on a message pipe indicates that no more data is coming.
if _, err := conn.Write(nil); err != nil {
log.Println("sending 0-byte message to the pipe after EOF on stdin failed:", err)
}
}
if err := os.Stdin.Close(); err != nil {
log.Println("closing stdin failed:", err)
}
wg.Done()
}()
_, err = io.Copy(os.Stdout, conn)
if errors.Is(err, windows.ERROR_BROKEN_PIPE) || errors.Is(err, windows.ERROR_PIPE_NOT_CONNECTED) {
// The named pipe is closed and there is no more data to read. Since
// named pipes are not bidirectional, there is no way for the other side
// of the pipe to get more data, so do not wait for the stdin copy to
// finish.
if *verbose {
log.Println("copy from pipe to stdout finished: pipe closed")
}
os.Exit(0)
}
if err != nil {
log.Fatalln("copy from pipe to stdout failed:", err)
}
if *verbose {
log.Println("copy from pipe to stdout finished")
}
if !*closeOnEOF {
if err := os.Stdout.Close(); err != nil {
log.Println("closing stdout failed:", err)
}
// Keep reading until we get ERROR_BROKEN_PIPE or the copy from stdin finishes.
go func() {
for {
_, err := conn.Read(nil)
if errors.Is(err, windows.ERROR_BROKEN_PIPE) {
if *verbose {
log.Println("pipe closed")
}
os.Exit(0)
} else if err != nil {
log.Fatalln("pipe error:", err)
}
}
}()
wg.Wait()
}
}