• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jlaffaye / ftp / 8113579643

01 Mar 2024 03:23PM UTC coverage: 72.431%. Remained the same
8113579643

Pull #365

github

web-flow
Bump github.com/stretchr/testify from 1.8.4 to 1.9.0

Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #365: Bump github.com/stretchr/testify from 1.8.4 to 1.9.0

712 of 983 relevant lines covered (72.43%)

18.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.84
/ftp.go
1
// Package ftp implements a FTP client as described in RFC 959.
2
//
3
// A textproto.Error is returned for errors at the protocol level.
4
package ftp
5

6
import (
7
        "bufio"
8
        "context"
9
        "crypto/tls"
10
        "errors"
11
        "io"
12
        "net"
13
        "net/textproto"
14
        "strconv"
15
        "strings"
16
        "time"
17

18
        "github.com/hashicorp/go-multierror"
19
)
20

21
const (
22
        // 30 seconds was chosen as it's the
23
        // same duration as http.DefaultTransport's timeout.
24
        DefaultDialTimeout = 30 * time.Second
25
)
26

27
// EntryType describes the different types of an Entry.
28
type EntryType int
29

30
// The differents types of an Entry
31
const (
32
        EntryTypeFile EntryType = iota
33
        EntryTypeFolder
34
        EntryTypeLink
35
)
36

37
// TransferType denotes the formats for transferring Entries.
38
type TransferType string
39

40
// The different transfer types
41
const (
42
        TransferTypeBinary = TransferType("I")
43
        TransferTypeASCII  = TransferType("A")
44
)
45

46
// Time format used by the MDTM and MFMT commands
47
const timeFormat = "20060102150405"
48

49
// ServerConn represents the connection to a remote FTP server.
50
// A single connection only supports one in-flight data connection.
51
// It is not safe to be called concurrently.
52
type ServerConn struct {
53
        options *dialOptions
54
        conn    *textproto.Conn // connection wrapper for text protocol
55
        netConn net.Conn        // underlying network connection
56
        host    string
57

58
        // Server capabilities discovered at runtime
59
        features      map[string]string
60
        skipEPSV      bool
61
        mlstSupported bool
62
        mfmtSupported bool
63
        mdtmSupported bool
64
        mdtmCanWrite  bool
65
        usePRET       bool
66
}
67

68
// DialOption represents an option to start a new connection with Dial
69
type DialOption struct {
70
        setup func(do *dialOptions)
71
}
72

73
// dialOptions contains all the options set by DialOption.setup
74
type dialOptions struct {
75
        context         context.Context
76
        dialer          net.Dialer
77
        tlsConfig       *tls.Config
78
        explicitTLS     bool
79
        disableEPSV     bool
80
        disableUTF8     bool
81
        disableMLSD     bool
82
        writingMDTM     bool
83
        forceListHidden bool
84
        location        *time.Location
85
        debugOutput     io.Writer
86
        dialFunc        func(network, address string) (net.Conn, error)
87
        shutTimeout     time.Duration // time to wait for data connection closing status
88
}
89

90
// Entry describes a file and is returned by List().
91
type Entry struct {
92
        Name   string
93
        Target string // target of symbolic link
94
        Type   EntryType
95
        Size   uint64
96
        Time   time.Time
97
}
98

99
// Response represents a data-connection
100
type Response struct {
101
        conn   net.Conn
102
        c      *ServerConn
103
        closed bool
104
}
105

106
// Dial connects to the specified address with optional options
107
func Dial(addr string, options ...DialOption) (*ServerConn, error) {
22✔
108
        do := &dialOptions{}
22✔
109
        for _, option := range options {
34✔
110
                option.setup(do)
12✔
111
        }
12✔
112

113
        if do.location == nil {
44✔
114
                do.location = time.UTC
22✔
115
        }
22✔
116

117
        dialFunc := do.dialFunc
22✔
118

22✔
119
        if dialFunc == nil {
43✔
120
                ctx := do.context
21✔
121

21✔
122
                if ctx == nil {
42✔
123
                        ctx = context.Background()
21✔
124
                }
21✔
125
                if _, ok := ctx.Deadline(); !ok {
42✔
126
                        var cancel context.CancelFunc
21✔
127
                        ctx, cancel = context.WithTimeout(ctx, DefaultDialTimeout)
21✔
128
                        defer cancel()
21✔
129
                }
21✔
130

131
                if do.tlsConfig != nil && !do.explicitTLS {
21✔
132
                        dialFunc = func(network, address string) (net.Conn, error) {
×
133
                                tlsDialer := &tls.Dialer{
×
134
                                        NetDialer: &do.dialer,
×
135
                                        Config:    do.tlsConfig,
×
136
                                }
×
137
                                return tlsDialer.DialContext(ctx, network, addr)
×
138
                        }
×
139
                } else {
21✔
140

21✔
141
                        dialFunc = func(network, address string) (net.Conn, error) {
42✔
142
                                return do.dialer.DialContext(ctx, network, addr)
21✔
143
                        }
21✔
144
                }
145
        }
146

147
        tconn, err := dialFunc("tcp", addr)
22✔
148
        if err != nil {
24✔
149
                return nil, err
2✔
150
        }
2✔
151

152
        // Use the resolved IP address in case addr contains a domain name
153
        // If we use the domain name, we might not resolve to the same IP.
154
        remoteAddr := tconn.RemoteAddr().(*net.TCPAddr)
20✔
155

20✔
156
        c := &ServerConn{
20✔
157
                options:  do,
20✔
158
                features: make(map[string]string),
20✔
159
                conn:     textproto.NewConn(do.wrapConn(tconn)),
20✔
160
                netConn:  tconn,
20✔
161
                host:     remoteAddr.IP.String(),
20✔
162
        }
20✔
163

20✔
164
        _, _, err = c.conn.ReadResponse(StatusReady)
20✔
165
        if err != nil {
20✔
166
                _ = c.Quit()
×
167
                return nil, err
×
168
        }
×
169

170
        if do.explicitTLS {
20✔
171
                if err := c.authTLS(); err != nil {
×
172
                        _ = c.Quit()
×
173
                        return nil, err
×
174
                }
×
175
                tconn = tls.Client(tconn, do.tlsConfig)
×
176
                c.conn = textproto.NewConn(do.wrapConn(tconn))
×
177
        }
178

179
        return c, nil
20✔
180
}
181

182
// DialWithTimeout returns a DialOption that configures the ServerConn with specified timeout
183
func DialWithTimeout(timeout time.Duration) DialOption {
4✔
184
        return DialOption{func(do *dialOptions) {
8✔
185
                do.dialer.Timeout = timeout
4✔
186
        }}
4✔
187
}
188

189
// DialWithShutTimeout returns a DialOption that configures the ServerConn with
190
// maximum time to wait for the data closing status on control connection
191
// and nudging the control connection deadline before reading status.
192
func DialWithShutTimeout(shutTimeout time.Duration) DialOption {
×
193
        return DialOption{func(do *dialOptions) {
×
194
                do.shutTimeout = shutTimeout
×
195
        }}
×
196
}
197

198
// DialWithDialer returns a DialOption that configures the ServerConn with specified net.Dialer
199
func DialWithDialer(dialer net.Dialer) DialOption {
1✔
200
        return DialOption{func(do *dialOptions) {
2✔
201
                do.dialer = dialer
1✔
202
        }}
1✔
203
}
204

205
// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
206
//
207
// Deprecated: Use [DialWithDialFunc] instead
208
func DialWithNetConn(conn net.Conn) DialOption {
×
209
        return DialWithDialFunc(func(network, address string) (net.Conn, error) {
×
210
                return conn, nil
×
211
        })
×
212
}
213

214
// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
215
// Note that EPSV is only used when advertised in the server features.
216
func DialWithDisabledEPSV(disabled bool) DialOption {
2✔
217
        return DialOption{func(do *dialOptions) {
4✔
218
                do.disableEPSV = disabled
2✔
219
        }}
2✔
220
}
221

222
// DialWithDisabledUTF8 returns a DialOption that configures the ServerConn with UTF8 option disabled
223
func DialWithDisabledUTF8(disabled bool) DialOption {
×
224
        return DialOption{func(do *dialOptions) {
×
225
                do.disableUTF8 = disabled
×
226
        }}
×
227
}
228

229
// DialWithDisabledMLSD returns a DialOption that configures the ServerConn with MLSD option disabled
230
//
231
// This is useful for servers which advertise MLSD (eg some versions
232
// of Serv-U) but don't support it properly.
233
func DialWithDisabledMLSD(disabled bool) DialOption {
2✔
234
        return DialOption{func(do *dialOptions) {
4✔
235
                do.disableMLSD = disabled
2✔
236
        }}
2✔
237
}
238

239
// DialWithWritingMDTM returns a DialOption making ServerConn use MDTM to set file time
240
//
241
// This option addresses a quirk in the VsFtpd server which doesn't support
242
// the MFMT command for setting file time like other servers but by default
243
// uses the MDTM command with non-standard arguments for that.
244
// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
245
func DialWithWritingMDTM(enabled bool) DialOption {
1✔
246
        return DialOption{func(do *dialOptions) {
2✔
247
                do.writingMDTM = enabled
1✔
248
        }}
1✔
249
}
250

251
// DialWithForceListHidden returns a DialOption making ServerConn use LIST -a to include hidden files and folders in directory listings
252
//
253
// This is useful for servers that do not do this by default, but it forces the use of the LIST command
254
// even if the server supports MLST.
255
func DialWithForceListHidden(enabled bool) DialOption {
1✔
256
        return DialOption{func(do *dialOptions) {
2✔
257
                do.forceListHidden = enabled
1✔
258
        }}
1✔
259
}
260

261
// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
262
// The location is used to parse the dates sent by the server which are in server's timezone
263
func DialWithLocation(location *time.Location) DialOption {
×
264
        return DialOption{func(do *dialOptions) {
×
265
                do.location = location
×
266
        }}
×
267
}
268

269
// DialWithContext returns a DialOption that configures the ServerConn with specified context
270
// The context will be used for the initial connection setup
271
func DialWithContext(ctx context.Context) DialOption {
×
272
        return DialOption{func(do *dialOptions) {
×
273
                do.context = ctx
×
274
        }}
×
275
}
276

277
// DialWithTLS returns a DialOption that configures the ServerConn with specified TLS config
278
//
279
// If called together with the DialWithDialFunc option, the DialWithDialFunc function
280
// will be used when dialing new connections but regardless of the function,
281
// the connection will be treated as a TLS connection.
282
func DialWithTLS(tlsConfig *tls.Config) DialOption {
×
283
        return DialOption{func(do *dialOptions) {
×
284
                do.tlsConfig = tlsConfig
×
285
        }}
×
286
}
287

288
// DialWithExplicitTLS returns a DialOption that configures the ServerConn to be upgraded to TLS
289
// See DialWithTLS for general TLS documentation
290
func DialWithExplicitTLS(tlsConfig *tls.Config) DialOption {
×
291
        return DialOption{func(do *dialOptions) {
×
292
                do.explicitTLS = true
×
293
                do.tlsConfig = tlsConfig
×
294
        }}
×
295
}
296

297
// DialWithDebugOutput returns a DialOption that configures the ServerConn to write to the Writer
298
// everything it reads from the server
299
func DialWithDebugOutput(w io.Writer) DialOption {
×
300
        return DialOption{func(do *dialOptions) {
×
301
                do.debugOutput = w
×
302
        }}
×
303
}
304

305
// DialWithDialFunc returns a DialOption that configures the ServerConn to use the
306
// specified function to establish both control and data connections
307
//
308
// If used together with the DialWithNetConn option, the DialWithNetConn
309
// takes precedence for the control connection, while data connections will
310
// be established using function specified with the DialWithDialFunc option
311
func DialWithDialFunc(f func(network, address string) (net.Conn, error)) DialOption {
1✔
312
        return DialOption{func(do *dialOptions) {
2✔
313
                do.dialFunc = f
1✔
314
        }}
1✔
315
}
316

317
func (o *dialOptions) wrapConn(netConn net.Conn) io.ReadWriteCloser {
20✔
318
        if o.debugOutput == nil {
40✔
319
                return netConn
20✔
320
        }
20✔
321

322
        return newDebugWrapper(netConn, o.debugOutput)
×
323
}
324

325
func (o *dialOptions) wrapStream(rd io.ReadCloser) io.ReadCloser {
9✔
326
        if o.debugOutput == nil {
18✔
327
                return rd
9✔
328
        }
9✔
329

330
        return newStreamDebugWrapper(rd, o.debugOutput)
×
331
}
332

333
// Connect is an alias to Dial, for backward compatibility
334
//
335
// Deprecated: Use [Dial] instead
336
func Connect(addr string) (*ServerConn, error) {
6✔
337
        return Dial(addr)
6✔
338
}
6✔
339

340
// DialTimeout initializes the connection to the specified ftp server address.
341
//
342
// Deprecated: Use [Dial] with [DialWithTimeout] option instead
343
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
2✔
344
        return Dial(addr, DialWithTimeout(timeout))
2✔
345
}
2✔
346

347
// Login authenticates the client with specified user and password.
348
//
349
// "anonymous"/"anonymous" is a common user/password scheme for FTP servers
350
// that allows anonymous read-only accounts.
351
func (c *ServerConn) Login(user, password string) error {
15✔
352
        code, message, err := c.cmd(-1, "USER %s", user)
15✔
353
        if err != nil {
15✔
354
                return err
×
355
        }
×
356

357
        switch code {
15✔
358
        case StatusLoggedIn:
×
359
        case StatusUserOK:
14✔
360
                _, _, err = c.cmd(StatusLoggedIn, "PASS %s", password)
14✔
361
                if err != nil {
14✔
362
                        return err
×
363
                }
×
364
        default:
1✔
365
                return errors.New(message)
1✔
366
        }
367

368
        // Probe features
369
        err = c.feat()
14✔
370
        if err != nil {
14✔
371
                return err
×
372
        }
×
373
        if _, mlstSupported := c.features["MLST"]; mlstSupported && !c.options.disableMLSD {
26✔
374
                c.mlstSupported = true
12✔
375
        }
12✔
376
        _, c.usePRET = c.features["PRET"]
14✔
377

14✔
378
        _, c.mfmtSupported = c.features["MFMT"]
14✔
379
        _, c.mdtmSupported = c.features["MDTM"]
14✔
380
        c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
14✔
381

14✔
382
        // Switch to binary mode
14✔
383
        if err = c.Type(TransferTypeBinary); err != nil {
14✔
384
                return err
×
385
        }
×
386

387
        // Switch to UTF-8
388
        if !c.options.disableUTF8 {
28✔
389
                err = c.setUTF8()
14✔
390
        }
14✔
391

392
        // If using implicit TLS, make data connections also use TLS
393
        if c.options.tlsConfig != nil {
14✔
394
                if _, _, err = c.cmd(StatusCommandOK, "PBSZ 0"); err != nil {
×
395
                        return err
×
396
                }
×
397
                if _, _, err = c.cmd(StatusCommandOK, "PROT P"); err != nil {
×
398
                        return err
×
399
                }
×
400
        }
401

402
        return err
14✔
403
}
404

405
// authTLS upgrades the connection to use TLS
406
func (c *ServerConn) authTLS() error {
×
407
        _, _, err := c.cmd(StatusAuthOK, "AUTH TLS")
×
408
        return err
×
409
}
×
410

411
// feat issues a FEAT FTP command to list the additional commands supported by
412
// the remote FTP server.
413
// FEAT is described in RFC 2389
414
func (c *ServerConn) feat() error {
14✔
415
        code, message, err := c.cmd(-1, "FEAT")
14✔
416
        if err != nil {
14✔
417
                return err
×
418
        }
×
419

420
        if code != StatusSystem {
14✔
421
                // The server does not support the FEAT command. This is not an
×
422
                // error: we consider that there is no additional feature.
×
423
                return nil
×
424
        }
×
425

426
        lines := strings.Split(message, "\n")
14✔
427
        for _, line := range lines {
130✔
428
                if !strings.HasPrefix(line, " ") {
144✔
429
                        continue
28✔
430
                }
431

432
                line = strings.TrimSpace(line)
88✔
433
                featureElements := strings.SplitN(line, " ", 2)
88✔
434

88✔
435
                command := featureElements[0]
88✔
436

88✔
437
                var commandDesc string
88✔
438
                if len(featureElements) == 2 {
88✔
439
                        commandDesc = featureElements[1]
×
440
                }
×
441

442
                c.features[command] = commandDesc
88✔
443
        }
444

445
        return nil
14✔
446
}
447

448
// setUTF8 issues an "OPTS UTF8 ON" command.
449
func (c *ServerConn) setUTF8() error {
14✔
450
        if _, ok := c.features["UTF8"]; !ok {
14✔
451
                return nil
×
452
        }
×
453

454
        code, message, err := c.cmd(-1, "OPTS UTF8 ON")
14✔
455
        if err != nil {
14✔
456
                return err
×
457
        }
×
458

459
        // Workaround for FTP servers, that does not support this option.
460
        if code == StatusBadArguments || code == StatusNotImplementedParameter {
14✔
461
                return nil
×
462
        }
×
463

464
        // The ftpd "filezilla-server" has FEAT support for UTF8, but always returns
465
        // "202 UTF8 mode is always enabled. No need to send this command." when
466
        // trying to use it. That's OK
467
        if code == StatusCommandNotImplemented {
14✔
468
                return nil
×
469
        }
×
470

471
        if code != StatusCommandOK {
14✔
472
                return errors.New(message)
×
473
        }
×
474

475
        return nil
14✔
476
}
477

478
// epsv issues an "EPSV" command to get a port number for a data connection.
479
func (c *ServerConn) epsv() (port int, err error) {
13✔
480
        _, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
13✔
481
        if err != nil {
13✔
482
                return 0, err
×
483
        }
×
484

485
        start := strings.Index(line, "|||")
13✔
486
        end := strings.LastIndex(line, "|")
13✔
487
        if start == -1 || end == -1 {
13✔
488
                return 0, errors.New("invalid EPSV response format")
×
489
        }
×
490
        port, err = strconv.Atoi(line[start+3 : end])
13✔
491
        return port, err
13✔
492
}
493

494
// pasv issues a "PASV" command to get a port number for a data connection.
495
func (c *ServerConn) pasv() (host string, port int, err error) {
8✔
496
        _, line, err := c.cmd(StatusPassiveMode, "PASV")
8✔
497
        if err != nil {
8✔
498
                return "", 0, err
×
499
        }
×
500

501
        // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
502
        start := strings.Index(line, "(")
8✔
503
        end := strings.LastIndex(line, ")")
8✔
504
        if start == -1 || end == -1 {
8✔
505
                return "", 0, errors.New("invalid PASV response format")
×
506
        }
×
507

508
        // We have to split the response string
509
        pasvData := strings.Split(line[start+1:end], ",")
8✔
510

8✔
511
        if len(pasvData) < 6 {
8✔
512
                return "", 0, errors.New("invalid PASV response format")
×
513
        }
×
514

515
        // Let's compute the port number
516
        portPart1, err := strconv.Atoi(pasvData[4])
8✔
517
        if err != nil {
8✔
518
                return "", 0, err
×
519
        }
×
520

521
        portPart2, err := strconv.Atoi(pasvData[5])
8✔
522
        if err != nil {
8✔
523
                return "", 0, err
×
524
        }
×
525

526
        // Recompose port
527
        port = portPart1*256 + portPart2
8✔
528

8✔
529
        // Make the IP address to connect to
8✔
530
        host = strings.Join(pasvData[0:4], ".")
8✔
531

8✔
532
        if c.host != host {
8✔
533
                if cmdIP := net.ParseIP(c.host); cmdIP != nil {
×
534
                        if dataIP := net.ParseIP(host); dataIP != nil {
×
535
                                if isBogusDataIP(cmdIP, dataIP) {
×
536
                                        return c.host, port, nil
×
537
                                }
×
538
                        }
539
                }
540
        }
541
        return host, port, nil
8✔
542
}
543

544
func isBogusDataIP(cmdIP, dataIP net.IP) bool {
4✔
545
        // Logic stolen from lftp (https://github.com/lavv17/lftp/blob/d67fc14d085849a6b0418bb3e912fea2e94c18d1/src/ftpclass.cc#L769)
4✔
546
        return dataIP.IsMulticast() ||
4✔
547
                cmdIP.IsPrivate() != dataIP.IsPrivate() ||
4✔
548
                cmdIP.IsLoopback() != dataIP.IsLoopback()
4✔
549
}
4✔
550

551
// getDataConnPort returns a host, port for a new data connection
552
// it uses the best available method to do so
553
func (c *ServerConn) getDataConnPort() (string, int, error) {
21✔
554
        if !c.options.disableEPSV && !c.skipEPSV {
34✔
555
                if port, err := c.epsv(); err == nil {
26✔
556
                        return c.host, port, nil
13✔
557
                }
13✔
558

559
                // if there is an error, skip EPSV for the next attempts
560
                c.skipEPSV = true
×
561
        }
562

563
        return c.pasv()
8✔
564
}
565

566
// openDataConn creates a new FTP data connection.
567
func (c *ServerConn) openDataConn() (net.Conn, error) {
21✔
568
        host, port, err := c.getDataConnPort()
21✔
569
        if err != nil {
21✔
570
                return nil, err
×
571
        }
×
572

573
        addr := net.JoinHostPort(host, strconv.Itoa(port))
21✔
574
        if c.options.dialFunc != nil {
21✔
575
                return c.options.dialFunc("tcp", addr)
×
576
        }
×
577

578
        if c.options.tlsConfig != nil {
21✔
579
                // We don't use tls.DialWithDialer here (which does Dial, create
×
580
                // the Client and then do the Handshake) because it seems to
×
581
                // hang with some FTP servers, namely proftpd and pureftpd.
×
582
                //
×
583
                // Instead we do Dial, create the Client and wait for the first
×
584
                // Read or Write to trigger the Handshake.
×
585
                //
×
586
                // This means that if we are uploading a zero sized file, we
×
587
                // need to make sure we do the Handshake explicitly as Write
×
588
                // won't have been called. This is done in StorFrom().
×
589
                //
×
590
                // See: https://github.com/jlaffaye/ftp/issues/282
×
591
                conn, err := c.options.dialer.Dial("tcp", addr)
×
592
                if err != nil {
×
593
                        return nil, err
×
594
                }
×
595
                tlsConn := tls.Client(conn, c.options.tlsConfig)
×
596
                return tlsConn, nil
×
597
        }
598

599
        return c.options.dialer.Dial("tcp", addr)
21✔
600
}
601

602
// cmd is a helper function to execute a command and check for the expected FTP
603
// return code
604
func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) {
137✔
605
        _, err := c.conn.Cmd(format, args...)
137✔
606
        if err != nil {
139✔
607
                return 0, "", err
2✔
608
        }
2✔
609

610
        return c.conn.ReadResponse(expected)
135✔
611
}
612

613
// cmdDataConnFrom executes a command which require a FTP data connection.
614
// Issues a REST FTP command to specify the number of bytes to skip for the transfer.
615
func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) {
21✔
616
        // If server requires PRET send the PRET command to warm it up
21✔
617
        // See: https://tools.ietf.org/html/draft-dd-pret-00
21✔
618
        if c.usePRET {
21✔
619
                _, _, err := c.cmd(-1, "PRET "+format, args...)
×
620
                if err != nil {
×
621
                        return nil, err
×
622
                }
×
623
        }
624

625
        conn, err := c.openDataConn()
21✔
626
        if err != nil {
21✔
627
                return nil, err
×
628
        }
×
629

630
        if offset != 0 {
23✔
631
                _, _, err = c.cmd(StatusRequestFilePending, "REST %d", offset)
2✔
632
                if err != nil {
2✔
633
                        _ = conn.Close()
×
634
                        return nil, err
×
635
                }
×
636
        }
637

638
        _, err = c.conn.Cmd(format, args...)
21✔
639
        if err != nil {
21✔
640
                _ = conn.Close()
×
641
                return nil, err
×
642
        }
×
643

644
        code, msg, err := c.conn.ReadResponse(-1)
21✔
645
        if err != nil {
21✔
646
                _ = conn.Close()
×
647
                return nil, err
×
648
        }
×
649
        if code != StatusAlreadyOpen && code != StatusAboutToSend {
21✔
650
                _ = conn.Close()
×
651
                return nil, &textproto.Error{Code: code, Msg: msg}
×
652
        }
×
653

654
        return conn, nil
21✔
655
}
656

657
// Type switches the transfer mode for the connection.
658
func (c *ServerConn) Type(transferType TransferType) (err error) {
14✔
659
        _, _, err = c.cmd(StatusCommandOK, "TYPE "+string(transferType))
14✔
660
        return err
14✔
661
}
14✔
662

663
// NameList issues an NLST FTP command.
664
func (c *ServerConn) NameList(path string) (entries []string, err error) {
3✔
665
        space := " "
3✔
666
        if path == "" {
4✔
667
                space = ""
1✔
668
        }
1✔
669
        conn, err := c.cmdDataConnFrom(0, "NLST%s%s", space, path)
3✔
670
        if err != nil {
3✔
671
                return nil, err
×
672
        }
×
673

674
        var errs *multierror.Error
3✔
675

3✔
676
        r := &Response{conn: conn, c: c}
3✔
677

3✔
678
        scanner := bufio.NewScanner(c.options.wrapStream(r))
3✔
679
        for scanner.Scan() {
6✔
680
                entries = append(entries, scanner.Text())
3✔
681
        }
3✔
682

683
        if err := scanner.Err(); err != nil {
3✔
684
                errs = multierror.Append(errs, err)
×
685
        }
×
686
        if err := r.Close(); err != nil {
3✔
687
                errs = multierror.Append(errs, err)
×
688
        }
×
689

690
        return entries, errs.ErrorOrNil()
3✔
691
}
692

693
// List issues a LIST FTP command.
694
func (c *ServerConn) List(path string) (entries []*Entry, err error) {
6✔
695
        var cmd string
6✔
696
        var parser parseFunc
6✔
697

6✔
698
        if c.mlstSupported && !c.options.forceListHidden {
9✔
699
                cmd = "MLSD"
3✔
700
                parser = parseRFC3659ListLine
3✔
701
        } else {
6✔
702
                cmd = "LIST"
3✔
703
                if c.options.forceListHidden {
4✔
704
                        cmd += " -a"
1✔
705
                }
1✔
706
                parser = parseListLine
3✔
707
        }
708

709
        space := " "
6✔
710
        if path == "" {
8✔
711
                space = ""
2✔
712
        }
2✔
713
        conn, err := c.cmdDataConnFrom(0, "%s%s%s", cmd, space, path)
6✔
714
        if err != nil {
6✔
715
                return nil, err
×
716
        }
×
717

718
        var errs *multierror.Error
6✔
719

6✔
720
        r := &Response{conn: conn, c: c}
6✔
721

6✔
722
        scanner := bufio.NewScanner(c.options.wrapStream(r))
6✔
723
        now := time.Now()
6✔
724
        for scanner.Scan() {
15✔
725
                entry, errParse := parser(scanner.Text(), now, c.options.location)
9✔
726
                if errParse == nil {
15✔
727
                        entries = append(entries, entry)
6✔
728
                }
6✔
729
        }
730

731
        if err := scanner.Err(); err != nil {
6✔
732
                errs = multierror.Append(errs, err)
×
733
        }
×
734
        if err := r.Close(); err != nil {
6✔
735
                errs = multierror.Append(errs, err)
×
736
        }
×
737

738
        return entries, errs.ErrorOrNil()
6✔
739
}
740

741
// GetEntry issues a MLST FTP command which retrieves one single Entry using the
742
// control connection. The returnedEntry will describe the current directory
743
// when no path is given.
744
func (c *ServerConn) GetEntry(path string) (entry *Entry, err error) {
4✔
745
        if !c.mlstSupported {
4✔
746
                return nil, &textproto.Error{Code: StatusNotImplemented, Msg: StatusText(StatusNotImplemented)}
×
747
        }
×
748
        space := " "
4✔
749
        if path == "" {
4✔
750
                space = ""
×
751
        }
×
752
        _, msg, err := c.cmd(StatusRequestedFileActionOK, "%s%s%s", "MLST", space, path)
4✔
753
        if err != nil {
4✔
754
                return nil, err
×
755
        }
×
756

757
        // The expected reply will look something like:
758
        //
759
        //    250-File details
760
        //     Type=file;Size=1024;Modify=20220813133357; path
761
        //    250 End
762
        //
763
        // Multiple lines are allowed though, so it can also be in the form:
764
        //
765
        //    250-File details
766
        //     Type=file;Size=1024; path
767
        //     Modify=20220813133357; path
768
        //    250 End
769
        lines := strings.Split(msg, "\n")
4✔
770
        lc := len(lines)
4✔
771

4✔
772
        // lines must be a multi-line message with a length of 3 or more, and we
4✔
773
        // don't care about the first and last line
4✔
774
        if lc < 3 {
4✔
775
                return nil, errors.New("invalid response")
×
776
        }
×
777

778
        e := &Entry{}
4✔
779
        for _, l := range lines[1 : lc-1] {
12✔
780
                // According to RFC 3659, the entry lines must start with a space when passed over the
8✔
781
                // control connection. Some servers don't seem to add that space though. Both forms are
8✔
782
                // accepted here.
8✔
783
                if len(l) > 0 && l[0] == ' ' {
16✔
784
                        l = l[1:]
8✔
785
                }
8✔
786
                // Some severs seem to send a blank line at the end which we ignore
787
                if l == "" {
10✔
788
                        continue
2✔
789
                }
790
                if e, err = parseNextRFC3659ListLine(l, c.options.location, e); err != nil {
6✔
791
                        return nil, err
×
792
                }
×
793
        }
794
        return e, nil
4✔
795
}
796

797
// IsTimePreciseInList returns true if client and server support the MLSD
798
// command so List can return time with 1-second precision for all files.
799
func (c *ServerConn) IsTimePreciseInList() bool {
×
800
        return c.mlstSupported
×
801
}
×
802

803
// ChangeDir issues a CWD FTP command, which changes the current directory to
804
// the specified path.
805
func (c *ServerConn) ChangeDir(path string) error {
6✔
806
        _, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path)
6✔
807
        return err
6✔
808
}
6✔
809

810
// ChangeDirToParent issues a CDUP FTP command, which changes the current
811
// directory to the parent directory.  This is similar to a call to ChangeDir
812
// with a path set to "..".
813
func (c *ServerConn) ChangeDirToParent() error {
3✔
814
        _, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP")
3✔
815
        return err
3✔
816
}
3✔
817

818
// CurrentDir issues a PWD FTP command, which Returns the path of the current
819
// directory.
820
func (c *ServerConn) CurrentDir() (string, error) {
3✔
821
        _, msg, err := c.cmd(StatusPathCreated, "PWD")
3✔
822
        if err != nil {
3✔
823
                return "", err
×
824
        }
×
825

826
        start := strings.Index(msg, "\"")
3✔
827
        end := strings.LastIndex(msg, "\"")
3✔
828

3✔
829
        if start == -1 || end == -1 {
3✔
830
                return "", errors.New("unsuported PWD response format")
×
831
        }
×
832

833
        return msg[start+1 : end], nil
3✔
834
}
835

836
// FileSize issues a SIZE FTP command, which Returns the size of the file
837
func (c *ServerConn) FileSize(path string) (int64, error) {
4✔
838
        _, msg, err := c.cmd(StatusFile, "SIZE %s", path)
4✔
839
        if err != nil {
6✔
840
                return 0, err
2✔
841
        }
2✔
842

843
        return strconv.ParseInt(msg, 10, 64)
2✔
844
}
845

846
// GetTime issues the MDTM FTP command to obtain the file modification time.
847
// It returns a UTC time.
848
func (c *ServerConn) GetTime(path string) (time.Time, error) {
4✔
849
        var t time.Time
4✔
850
        if !c.mdtmSupported {
5✔
851
                return t, errors.New("GetTime is not supported")
1✔
852
        }
1✔
853
        _, msg, err := c.cmd(StatusFile, "MDTM %s", path)
3✔
854
        if err != nil {
3✔
855
                return t, err
×
856
        }
×
857
        return time.ParseInLocation(timeFormat, msg, time.UTC)
3✔
858
}
859

860
// IsGetTimeSupported allows library callers to check in advance that they
861
// can use GetTime to get file time.
862
func (c *ServerConn) IsGetTimeSupported() bool {
4✔
863
        return c.mdtmSupported
4✔
864
}
4✔
865

866
// SetTime issues the MFMT FTP command to set the file modification time.
867
// Also it can use a non-standard form of the MDTM command supported by
868
// the VsFtpd server instead of MFMT for the same purpose.
869
// See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
870
func (c *ServerConn) SetTime(path string, t time.Time) (err error) {
4✔
871
        utime := t.In(time.UTC).Format(timeFormat)
4✔
872
        switch {
4✔
873
        case c.mfmtSupported:
1✔
874
                _, _, err = c.cmd(StatusFile, "MFMT %s %s", utime, path)
1✔
875
        case c.mdtmCanWrite:
1✔
876
                _, _, err = c.cmd(StatusFile, "MDTM %s %s", utime, path)
1✔
877
        default:
2✔
878
                err = errors.New("SetTime is not supported")
2✔
879
        }
880
        return
4✔
881
}
882

883
// IsSetTimeSupported allows library callers to check in advance that they
884
// can use SetTime to set file time.
885
func (c *ServerConn) IsSetTimeSupported() bool {
4✔
886
        return c.mfmtSupported || c.mdtmCanWrite
4✔
887
}
4✔
888

889
// Retr issues a RETR FTP command to fetch the specified file from the remote
890
// FTP server.
891
//
892
// The returned ReadCloser must be closed to cleanup the FTP data connection.
893
func (c *ServerConn) Retr(path string) (*Response, error) {
6✔
894
        return c.RetrFrom(path, 0)
6✔
895
}
6✔
896

897
// RetrFrom issues a RETR FTP command to fetch the specified file from the remote
898
// FTP server, the server will not send the offset first bytes of the file.
899
//
900
// The returned ReadCloser must be closed to cleanup the FTP data connection.
901
func (c *ServerConn) RetrFrom(path string, offset uint64) (*Response, error) {
8✔
902
        conn, err := c.cmdDataConnFrom(offset, "RETR %s", path)
8✔
903
        if err != nil {
8✔
904
                return nil, err
×
905
        }
×
906

907
        return &Response{conn: conn, c: c}, nil
8✔
908
}
909

910
// Stor issues a STOR FTP command to store a file to the remote FTP server.
911
// Stor creates the specified file with the content of the io.Reader.
912
//
913
// Hint: io.Pipe() can be used if an io.Writer is required.
914
func (c *ServerConn) Stor(path string, r io.Reader) error {
2✔
915
        return c.StorFrom(path, r, 0)
2✔
916
}
2✔
917

918
// checkDataShut reads the "closing data connection" status from the
919
// control connection. It is called after transferring a piece of data
920
// on the data connection during which the control connection was idle.
921
// This may result in the idle timeout triggering on the control connection
922
// right when we try to read the response.
923
// The ShutTimeout dial option will rescue here. It will nudge the control
924
// connection deadline right before checking the data closing status.
925
func (c *ServerConn) checkDataShut() error {
21✔
926
        if c.options.shutTimeout != 0 {
21✔
927
                shutDeadline := time.Now().Add(c.options.shutTimeout)
×
928
                if err := c.netConn.SetDeadline(shutDeadline); err != nil {
×
929
                        return err
×
930
                }
×
931
        }
932
        _, _, err := c.conn.ReadResponse(StatusClosingDataConnection)
21✔
933
        return err
21✔
934
}
935

936
// StorFrom issues a STOR FTP command to store a file to the remote FTP server.
937
// Stor creates the specified file with the content of the io.Reader, writing
938
// on the server will start at the given file offset.
939
//
940
// Hint: io.Pipe() can be used if an io.Writer is required.
941
func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
2✔
942
        conn, err := c.cmdDataConnFrom(offset, "STOR %s", path)
2✔
943
        if err != nil {
2✔
944
                return err
×
945
        }
×
946

947
        var errs *multierror.Error
2✔
948

2✔
949
        // if the upload fails we still need to try to read the server
2✔
950
        // response otherwise if the failure is not due to a connection problem,
2✔
951
        // for example the server denied the upload for quota limits, we miss
2✔
952
        // the response and we cannot use the connection to send other commands.
2✔
953
        if n, err := io.Copy(conn, r); err != nil {
2✔
954
                errs = multierror.Append(errs, err)
×
955
        } else if n == 0 {
2✔
956
                // If we wrote no bytes and got no error, make sure we call
×
957
                // tls.Handshake on the connection as it won't get called
×
958
                // unless Write() is called. (See comment in openDataConn()).
×
959
                //
×
960
                // ProFTP doesn't like this and returns "Unable to build data
×
961
                // connection: Operation not permitted" when trying to upload
×
962
                // an empty file without this.
×
963
                if do, ok := conn.(interface{ Handshake() error }); ok {
×
964
                        if err := do.Handshake(); err != nil {
×
965
                                errs = multierror.Append(errs, err)
×
966
                        }
×
967
                }
968
        }
969

970
        if err := conn.Close(); err != nil {
2✔
971
                errs = multierror.Append(errs, err)
×
972
        }
×
973

974
        if err := c.checkDataShut(); err != nil {
2✔
975
                errs = multierror.Append(errs, err)
×
976
        }
×
977

978
        return errs.ErrorOrNil()
2✔
979
}
980

981
// Append issues a APPE FTP command to store a file to the remote FTP server.
982
// If a file already exists with the given path, then the content of the
983
// io.Reader is appended. Otherwise, a new file is created with that content.
984
//
985
// Hint: io.Pipe() can be used if an io.Writer is required.
986
func (c *ServerConn) Append(path string, r io.Reader) error {
2✔
987
        conn, err := c.cmdDataConnFrom(0, "APPE %s", path)
2✔
988
        if err != nil {
2✔
989
                return err
×
990
        }
×
991

992
        var errs *multierror.Error
2✔
993

2✔
994
        if _, err := io.Copy(conn, r); err != nil {
2✔
995
                errs = multierror.Append(errs, err)
×
996
        }
×
997

998
        if err := conn.Close(); err != nil {
2✔
999
                errs = multierror.Append(errs, err)
×
1000
        }
×
1001

1002
        if err := c.checkDataShut(); err != nil {
2✔
1003
                errs = multierror.Append(errs, err)
×
1004
        }
×
1005

1006
        return errs.ErrorOrNil()
2✔
1007
}
1008

1009
// Rename renames a file on the remote FTP server.
1010
func (c *ServerConn) Rename(from, to string) error {
2✔
1011
        _, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
2✔
1012
        if err != nil {
2✔
1013
                return err
×
1014
        }
×
1015

1016
        _, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to)
2✔
1017
        return err
2✔
1018
}
1019

1020
// Delete issues a DELE FTP command to delete the specified file from the
1021
// remote FTP server.
1022
func (c *ServerConn) Delete(path string) error {
3✔
1023
        _, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path)
3✔
1024
        return err
3✔
1025
}
3✔
1026

1027
// RemoveDirRecur deletes a non-empty folder recursively using
1028
// RemoveDir and Delete
1029
func (c *ServerConn) RemoveDirRecur(path string) error {
2✔
1030
        err := c.ChangeDir(path)
2✔
1031
        if err != nil {
3✔
1032
                return err
1✔
1033
        }
1✔
1034
        currentDir, err := c.CurrentDir()
1✔
1035
        if err != nil {
1✔
1036
                return err
×
1037
        }
×
1038

1039
        entries, err := c.List(currentDir)
1✔
1040
        if err != nil {
1✔
1041
                return err
×
1042
        }
×
1043

1044
        for _, entry := range entries {
2✔
1045
                if entry.Name != ".." && entry.Name != "." {
2✔
1046
                        if entry.Type == EntryTypeFolder {
1✔
1047
                                err = c.RemoveDirRecur(currentDir + "/" + entry.Name)
×
1048
                                if err != nil {
×
1049
                                        return err
×
1050
                                }
×
1051
                        } else {
1✔
1052
                                err = c.Delete(entry.Name)
1✔
1053
                                if err != nil {
1✔
1054
                                        return err
×
1055
                                }
×
1056
                        }
1057
                }
1058
        }
1059
        err = c.ChangeDirToParent()
1✔
1060
        if err != nil {
1✔
1061
                return err
×
1062
        }
×
1063
        err = c.RemoveDir(currentDir)
1✔
1064
        return err
1✔
1065
}
1066

1067
// MakeDir issues a MKD FTP command to create the specified directory on the
1068
// remote FTP server.
1069
func (c *ServerConn) MakeDir(path string) error {
2✔
1070
        _, _, err := c.cmd(StatusPathCreated, "MKD %s", path)
2✔
1071
        return err
2✔
1072
}
2✔
1073

1074
// RemoveDir issues a RMD FTP command to remove the specified directory from
1075
// the remote FTP server.
1076
func (c *ServerConn) RemoveDir(path string) error {
3✔
1077
        _, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path)
3✔
1078
        return err
3✔
1079
}
3✔
1080

1081
// Walk prepares the internal walk function so that the caller can begin traversing the directory
1082
func (c *ServerConn) Walk(root string) *Walker {
5✔
1083
        w := new(Walker)
5✔
1084
        w.serverConn = c
5✔
1085

5✔
1086
        if !strings.HasSuffix(root, "/") {
10✔
1087
                root += "/"
5✔
1088
        }
5✔
1089

1090
        w.root = root
5✔
1091
        w.descend = true
5✔
1092

5✔
1093
        return w
5✔
1094
}
1095

1096
// NoOp issues a NOOP FTP command.
1097
// NOOP has no effects and is usually used to prevent the remote FTP server to
1098
// close the otherwise idle connection.
1099
func (c *ServerConn) NoOp() error {
4✔
1100
        _, _, err := c.cmd(StatusCommandOK, "NOOP")
4✔
1101
        return err
4✔
1102
}
4✔
1103

1104
// Logout issues a REIN FTP command to logout the current user.
1105
func (c *ServerConn) Logout() error {
2✔
1106
        _, _, err := c.cmd(StatusReady, "REIN")
2✔
1107
        return err
2✔
1108
}
2✔
1109

1110
// Quit issues a QUIT FTP command to properly close the connection from the
1111
// remote FTP server.
1112
func (c *ServerConn) Quit() error {
15✔
1113
        var errs *multierror.Error
15✔
1114

15✔
1115
        if _, err := c.conn.Cmd("QUIT"); err != nil {
15✔
1116
                errs = multierror.Append(errs, err)
×
1117
        }
×
1118

1119
        if err := c.conn.Close(); err != nil {
15✔
1120
                errs = multierror.Append(errs, err)
×
1121
        }
×
1122

1123
        return errs.ErrorOrNil()
15✔
1124
}
1125

1126
// Read implements the io.Reader interface on a FTP data connection.
1127
func (r *Response) Read(buf []byte) (int, error) {
32✔
1128
        return r.conn.Read(buf)
32✔
1129
}
32✔
1130

1131
// Close implements the io.Closer interface on a FTP data connection.
1132
// After the first call, Close will do nothing and return nil.
1133
func (r *Response) Close() error {
19✔
1134
        if r.closed {
21✔
1135
                return nil
2✔
1136
        }
2✔
1137

1138
        var errs *multierror.Error
17✔
1139

17✔
1140
        if err := r.conn.Close(); err != nil {
17✔
1141
                errs = multierror.Append(errs, err)
×
1142
        }
×
1143

1144
        if err := r.c.checkDataShut(); err != nil {
17✔
1145
                errs = multierror.Append(errs, err)
×
1146
        }
×
1147

1148
        r.closed = true
17✔
1149
        return errs.ErrorOrNil()
17✔
1150
}
1151

1152
// SetDeadline sets the deadlines associated with the connection.
1153
func (r *Response) SetDeadline(t time.Time) error {
2✔
1154
        return r.conn.SetDeadline(t)
2✔
1155
}
2✔
1156

1157
// String returns the string representation of EntryType t.
1158
func (t EntryType) String() string {
3✔
1159
        return [...]string{"file", "folder", "link"}[t]
3✔
1160
}
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc