forked from secsy/goftp
-
Notifications
You must be signed in to change notification settings - Fork 1
/
client.go
442 lines (366 loc) · 10.7 KB
/
client.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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
// Copyright 2015 Muir Manders. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package goftp
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
)
// Error is an expanded error interface returned by all Client methods.
// It allows discerning callers to discover potentially actionable qualities
// of the error.
type Error interface {
error
// Whether the error was transient and attempting the same operation
// again may be succesful. This includes timeouts.
Temporary() bool
// If the error originated from an unexpected response from the server, this
// will return the FTP response code. Otherwise it will return 0.
Code() int
// Similarly, this will return the text response from the server, or empty
// string.
Message() string
}
type ftpError struct {
err error
code int
msg string
timeout bool
temporary bool
}
func (e ftpError) Error() string {
if e.err != nil {
return e.err.Error()
} else {
return fmt.Sprintf("unexpected response: %d-%s", e.code, e.msg)
}
}
func (e ftpError) Temporary() bool {
return e.temporary || transientNegativeCompletionReply(e.code)
}
func (e ftpError) Timeout() bool {
return e.timeout
}
func (e ftpError) Code() int {
if fe, _ := e.err.(Error); fe != nil {
return fe.Code()
}
return e.code
}
func (e ftpError) Message() string {
if fe, _ := e.err.(Error); fe != nil {
return fe.Message()
}
return e.msg
}
// TLSMode represents the FTPS connection strategy. Servers cannot support
// both modes on the same port.
type TLSMode int
const (
// TLSExplicit means the client first runs an explicit command ("AUTH TLS")
// before switching to TLS.
TLSExplicit TLSMode = 0
// TLSImplicit means both sides already implicitly agree to use TLS, and the
// client connects directly using TLS.
TLSImplicit TLSMode = 1
)
// for testing
type stubResponse struct {
code int
msg string
}
// Config contains configuration for a Client object.
type Config struct {
// User name. Defaults to "anonymous".
User string
// User password. Defaults to "anonymous" if required.
Password string
// Maximum number of FTP connections to open per-host. Defaults to 5. Keep in
// mind that FTP servers typically limit how many connections a single user
// may have open at once, so you may need to lower this if you are doing
// concurrent transfers.
ConnectionsPerHost int
// Timeout for opening connections, sending control commands, and each read/write
// of data transfers. Defaults to 5 seconds.
Timeout time.Duration
// TLS Config used for FTPS. If provided, it will be an error if the server
// does not support TLS. Both the control and data connection will use TLS.
TLSConfig *tls.Config
// FTPS mode. TLSExplicit means connect non-TLS, then upgrade connection to
// TLS via "AUTH TLS" command. TLSImplicit means open the connection using
// TLS. Defaults to TLSExplicit.
TLSMode TLSMode
// This flag controls whether to use IPv6 addresses found when resolving
// hostnames. Defaults to false to prevent failures when your computer can't
// IPv6. If the hostname(s) only resolve to IPv6 addresses, Dial() will still
// try to use them as a last ditch effort. You can still directly give an
// IPv6 address to Dial() even with this flag off.
IPv6Lookup bool
// Logging destination for debugging messages. Set to os.Stderr to log to stderr.
// Password value will not be logged.
Logger io.Writer
// Time zone of the FTP server. Used when parsing mtime from "LIST" output if
// server does not support "MLST"/"MLSD". Defaults to UTC.
ServerLocation *time.Location
// Enable "active" FTP data connections where the server connects to the client to
// establish data connections (does not work if client is behind NAT). If TLSConfig
// is specified, it will be used when listening for active connections.
ActiveTransfers bool
// Set the host:port to listen on for active data connections. If the host and/or
// port is empty, the local address/port of the control connection will be used. A
// port of 0 will listen on a random port. If not specified, the default behavior is
// ":0", i.e. listen on the local control connection host and a random port.
ActiveListenAddr string
// Disables EPSV in favour of PASV. This is useful in cases where EPSV connections
// neither complete nor downgrade to PASV successfully by themselves, resulting in
// hung connections.
DisableEPSV bool
// For testing convenience.
stubResponses map[string]stubResponse
}
// Client maintains a connection pool to the FTP server(s), so you typically only
// need one Client object. Client methods are safe to call concurrently from
// different goroutines, but once you are using all ConnectionsPerHost connections
// per host, methods will block waiting for a free connection.
type Client struct {
config Config
hosts []string
freeConnCh chan *persistentConn
numConnsPerHost map[string]int
allCons map[int]*persistentConn
connIdx int
rawConnIdx int
mu sync.Mutex
t0 time.Time
closed bool
}
// Construct and return a new client Conn, setting default config
// values as necessary.
func newClient(config Config, hosts []string) *Client {
if config.ConnectionsPerHost <= 0 {
config.ConnectionsPerHost = 5
}
if config.Timeout <= 0 {
config.Timeout = 5 * time.Second
}
if config.User == "" {
config.User = "anonymous"
}
if config.Password == "" {
config.Password = "anonymous"
}
if config.ServerLocation == nil {
config.ServerLocation = time.UTC
}
if config.ActiveListenAddr == "" {
config.ActiveListenAddr = ":0"
}
return &Client{
config: config,
freeConnCh: make(chan *persistentConn, len(hosts)*config.ConnectionsPerHost),
t0: time.Now(),
hosts: hosts,
allCons: make(map[int]*persistentConn),
numConnsPerHost: make(map[string]int),
}
}
// Close closes all open server connections. Currently this does not attempt
// to do any kind of polite FTP connection termination. It will interrupt
// all transfers in progress.
func (c *Client) Close() error {
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return ftpError{err: errors.New("already closed")}
}
c.closed = true
var conns []*persistentConn
for _, conn := range c.allCons {
conns = append(conns, conn)
}
c.mu.Unlock()
for _, pconn := range conns {
c.removeConn(pconn)
}
return nil
}
// Log a debug message in the context of the client (i.e. not for a
// particular connection).
func (c *Client) debug(f string, args ...interface{}) {
if c.config.Logger == nil {
return
}
fmt.Fprintf(c.config.Logger, "goftp: %.3f %s\n",
time.Now().Sub(c.t0).Seconds(),
fmt.Sprintf(f, args...),
)
}
func (c *Client) numOpenConns() int {
var numOpen int
for _, num := range c.numConnsPerHost {
numOpen += int(num)
}
return numOpen
}
// Get an idle connection.
func (c *Client) getIdleConn() (*persistentConn, error) {
// First check for available connections in the channel.
Loop:
for {
select {
case pconn := <-c.freeConnCh:
if pconn.broken {
c.debug("#%d was ready (broken)", pconn.idx)
c.mu.Lock()
c.numConnsPerHost[pconn.host]--
c.mu.Unlock()
c.removeConn(pconn)
} else {
c.debug("#%d was ready", pconn.idx)
return pconn, nil
}
default:
break Loop
}
}
// No available connections. Loop until we can open a new one, or
// one becomes available.
for {
c.mu.Lock()
// can we open a connection to some host
if c.numOpenConns() < len(c.hosts)*c.config.ConnectionsPerHost {
c.connIdx++
idx := c.connIdx
// find the next host with less than ConnectionsPerHost connections
var host string
for i := idx; i < idx+len(c.hosts); i++ {
if c.numConnsPerHost[c.hosts[i%len(c.hosts)]] < c.config.ConnectionsPerHost {
host = c.hosts[i%len(c.hosts)]
break
}
}
if host == "" {
panic("this shouldn't be possible")
}
c.numConnsPerHost[host]++
c.mu.Unlock()
pconn, err := c.openConn(idx, host)
if err != nil {
c.mu.Lock()
c.numConnsPerHost[host]--
c.mu.Unlock()
c.debug("#%d error connecting: %s", idx, err)
}
return pconn, err
}
c.mu.Unlock()
// block waiting for a free connection
pconn := <-c.freeConnCh
if pconn.broken {
c.debug("waited and got #%d (broken)", pconn.idx)
c.mu.Lock()
c.numConnsPerHost[pconn.host]--
c.mu.Unlock()
c.removeConn(pconn)
} else {
c.debug("waited and got #%d", pconn.idx)
return pconn, nil
}
}
}
func (c *Client) removeConn(pconn *persistentConn) {
c.mu.Lock()
delete(c.allCons, pconn.idx)
c.mu.Unlock()
pconn.close()
}
func (c *Client) returnConn(pconn *persistentConn) {
c.freeConnCh <- pconn
}
// OpenRawConn opens a "raw" connection to the server which allows you to run any control
// or data command you want. See the RawConn interface for more details. The RawConn will
// not participate in the Client's pool (i.e. does not count against ConnectionsPerHost).
func (c *Client) OpenRawConn() (RawConn, error) {
c.mu.Lock()
idx := c.rawConnIdx
host := c.hosts[idx%len(c.hosts)]
c.rawConnIdx++
c.mu.Unlock()
return c.openConn(-(idx + 1), host)
}
// Open and set up a control connection.
func (c *Client) openConn(idx int, host string) (pconn *persistentConn, err error) {
pconn = &persistentConn{
idx: idx,
features: make(map[string]string),
config: c.config,
t0: c.t0,
currentType: "A",
host: host,
epsvNotSupported: c.config.DisableEPSV,
}
var conn net.Conn
if c.config.TLSConfig != nil && c.config.TLSMode == TLSImplicit {
pconn.debug("opening TLS control connection to %s", host)
dialer := &net.Dialer{
Timeout: c.config.Timeout,
}
conn, err = tls.DialWithDialer(dialer, "tcp", host, pconn.config.TLSConfig)
} else {
pconn.debug("opening control connection to %s", host)
conn, err = net.DialTimeout("tcp", host, c.config.Timeout)
}
var (
code int
msg string
)
if err != nil {
var isTemporary bool
if ne, ok := err.(net.Error); ok {
isTemporary = ne.Temporary()
}
err = ftpError{
err: err,
temporary: isTemporary,
}
goto Error
}
pconn.setControlConn(conn)
code, msg, err = pconn.readResponse()
if err != nil {
goto Error
}
if code != replyServiceReady {
err = ftpError{code: code, msg: msg}
goto Error
}
if c.config.TLSConfig != nil && c.config.TLSMode == TLSExplicit {
err = pconn.logInTLS()
} else {
err = pconn.logIn()
}
if err != nil {
goto Error
}
if err = pconn.fetchFeatures(); err != nil {
goto Error
}
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
err = ftpError{err: errors.New("client closed")}
goto Error
}
if idx >= 0 {
c.allCons[idx] = pconn
}
return pconn, nil
Error:
pconn.close()
return nil, err
}