Codebase list golang-github-jlaffaye-ftp / bf77ec5
New upstream snapshot. Debian Janitor 2 years ago
14 changed file(s) with 518 addition(s) and 122 deletion(s). Raw diff Collapse all Expand all
0 name: golangci-lint
1 on: [push, pull_request]
2
3 jobs:
4 golangci-lint:
5 name: lint
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v2
9 - name: golangci-lint
10 uses: golangci/golangci-lint-action@v2
11 with:
12 only-new-issues: true
0 name: Units tests
1 on: [push, pull_request]
2 jobs:
3 checks:
4 name: test
5 runs-on: ubuntu-latest
6 steps:
7 - uses: actions/checkout@master
8 - name: Setup go
9 uses: actions/setup-go@v2
10 with:
11 go-version: 1.17
12 - uses: actions/cache@v2
13 with:
14 path: |
15 ~/go/pkg/mod
16 ~/.cache/go-build
17 key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
18 restore-keys: |
19 ${{ runner.os }}-go-
20 - name: Run tests
21 run: go test -v -covermode=count -coverprofile=coverage.out
22 - name: Convert coverage to lcov
23 uses: jandelgado/gcov2lcov-action@v1.0.8
24 - name: Coveralls
25 uses: coverallsapp/github-action@v1.1.2
26 with:
27 github-token: ${{ secrets.github_token }}
28 path-to-lcov: coverage.lcov
+0
-14
.travis.yml less more
0 language: go
1 sudo: required
2 dist: xenial
3 go:
4 - 1.11.x
5 - 1.12.x
6 - 1.13.x
7 before_install:
8 - sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
9 - go get github.com/mattn/goveralls
10 - go get golang.org/x/lint/golint
11 script:
12 - goveralls -v
13 - golint -set_exit_status $(go list ./...)
00 # goftp #
11
2 [![Build Status](https://travis-ci.org/jlaffaye/ftp.svg?branch=master)](https://travis-ci.org/jlaffaye/ftp)
2 [![Units tests](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml)
33 [![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master)
4 [![Go ReportCard](http://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp)
5 [![godoc.org](https://godoc.org/github.com/jlaffaye/ftp?status.svg)](http://godoc.org/github.com/jlaffaye/ftp)
4 [![Go ReportCard](https://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp)
5 [![Go Reference](https://pkg.go.dev/badge/github.com/jlaffaye/ftp.svg)](https://pkg.go.dev/github.com/jlaffaye/ftp)
66
77 A FTP client package for Go
88
1414
1515 ## Documentation ##
1616
17 https://pkg.go.dev/github.com/jlaffaye/ftp?tab=doc
17 https://pkg.go.dev/github.com/jlaffaye/ftp
1818
1919 ## Example ##
2020
66 "strings"
77 "testing"
88 "time"
9
10 "github.com/stretchr/testify/assert"
911 )
1012
1113 const (
7072 if err != nil {
7173 t.Error(err)
7274 } else {
73 buf, err := ioutil.ReadAll(r)
75 buf, errRead := ioutil.ReadAll(r)
7476 if err != nil {
75 t.Error(err)
77 t.Error(errRead)
7678 }
7779 if string(buf) != testData {
7880 t.Errorf("'%s'", buf)
8789 t.Error(err)
8890 } else {
8991 r.SetDeadline(time.Now())
90 _, err := ioutil.ReadAll(r)
92 _, err = ioutil.ReadAll(r)
9193 if err == nil {
9294 t.Error("deadline should have caused error")
9395 } else if !strings.HasSuffix(err.Error(), "i/o timeout") {
101103 if err != nil {
102104 t.Error(err)
103105 } else {
104 buf, err := ioutil.ReadAll(r)
105 if err != nil {
106 t.Error(err)
106 buf, errRead := ioutil.ReadAll(r)
107 if errRead != nil {
108 t.Error(errRead)
107109 }
108110 expected := testData[5:]
109111 if string(buf) != expected {
123125 if err != nil {
124126 t.Error(err)
125127 } else {
126 buf, err := ioutil.ReadAll(r)
128 buf, errRead := ioutil.ReadAll(r)
127129 if err != nil {
128 t.Error(err)
130 t.Error(errRead)
129131 }
130132 if string(buf) != testData+testData {
131133 t.Errorf("'%s'", buf)
190192 }
191193 }
192194
193 if err := c.Quit(); err != nil {
195 if err = c.Quit(); err != nil {
194196 t.Fatal(err)
195197 }
196198
227229 t.Skip("skipping test in short mode.")
228230 }
229231
230 c, err := DialTimeout("localhost:2121", 1*time.Second)
231 if err == nil {
232 if c, err := DialTimeout("localhost:2121", 1*time.Second); err == nil {
233 c.Quit()
232234 t.Fatal("expected timeout, got nil error")
233 c.Quit()
234235 }
235236 }
236237
300301 // Wait for the connection to close
301302 mock.Wait()
302303 }
304
305 func TestListCurrentDir(t *testing.T) {
306 mock, c := openConn(t, "127.0.0.1")
307
308 _, err := c.List("")
309 assert.NoError(t, err)
310 assert.Equal(t, "LIST", mock.lastFull, "LIST must not have a trailing whitespace")
311
312 _, err = c.NameList("")
313 assert.NoError(t, err)
314 assert.Equal(t, "NLST", mock.lastFull, "NLST must not have a trailing whitespace")
315
316 err = c.Quit()
317 assert.NoError(t, err)
318
319 mock.Wait()
320 }
321
322 func TestTimeUnsupported(t *testing.T) {
323 mock, c := openConnExt(t, "127.0.0.1", "no-time")
324
325 assert.False(t, c.mdtmSupported, "MDTM must NOT be supported")
326 assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
327
328 assert.False(t, c.IsGetTimeSupported(), "GetTime must NOT be supported")
329 assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
330
331 _, err := c.GetTime("file1")
332 assert.NotNil(t, err)
333
334 err = c.SetTime("file1", time.Now())
335 assert.NotNil(t, err)
336
337 assert.NoError(t, c.Quit())
338 mock.Wait()
339 }
340
341 func TestTimeStandard(t *testing.T) {
342 mock, c := openConnExt(t, "127.0.0.1", "std-time")
343
344 assert.True(t, c.mdtmSupported, "MDTM must be supported")
345 assert.True(t, c.mfmtSupported, "MFMT must be supported")
346
347 assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
348 assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
349
350 tm, err := c.GetTime("file1")
351 assert.NoError(t, err)
352 assert.False(t, tm.IsZero(), "GetTime must return valid time")
353
354 err = c.SetTime("file1", time.Now())
355 assert.NoError(t, err)
356
357 assert.NoError(t, c.Quit())
358 mock.Wait()
359 }
360
361 func TestTimeVsftpdPartial(t *testing.T) {
362 mock, c := openConnExt(t, "127.0.0.1", "vsftpd")
363
364 assert.True(t, c.mdtmSupported, "MDTM must be supported")
365 assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
366
367 assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
368 assert.False(t, c.IsSetTimeSupported(), "SetTime must NOT be supported")
369
370 tm, err := c.GetTime("file1")
371 assert.NoError(t, err)
372 assert.False(t, tm.IsZero(), "GetTime must return valid time")
373
374 err = c.SetTime("file1", time.Now())
375 assert.NotNil(t, err)
376
377 assert.NoError(t, c.Quit())
378 mock.Wait()
379 }
380
381 func TestTimeVsftpdFull(t *testing.T) {
382 mock, c := openConnExt(t, "127.0.0.1", "vsftpd", DialWithWritingMDTM(true))
383
384 assert.True(t, c.mdtmSupported, "MDTM must be supported")
385 assert.False(t, c.mfmtSupported, "MFMT must NOT be supported")
386
387 assert.True(t, c.IsGetTimeSupported(), "GetTime must be supported")
388 assert.True(t, c.IsSetTimeSupported(), "SetTime must be supported")
389
390 tm, err := c.GetTime("file1")
391 assert.NoError(t, err)
392 assert.False(t, tm.IsZero(), "GetTime must return valid time")
393
394 err = c.SetTime("file1", time.Now())
395 assert.NoError(t, err)
396
397 assert.NoError(t, c.Quit())
398 mock.Wait()
399 }
1010 "strings"
1111 "sync"
1212 "testing"
13 "time"
1314 )
1415
1516 type ftpMock struct {
1617 address string
18 modtime string // no-time, std-time, vsftpd
1719 listener *net.TCPListener
1820 proto *textproto.Conn
1921 commands []string // list of received commands
22 lastFull string // full last command
2023 rest int
2124 fileCont *bytes.Buffer
2225 dataConn *mockDataConn
2629 // newFtpMock returns a mock implementation of a FTP server
2730 // For simplication, a mock instance only accepts a signle connection and terminates afer
2831 func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
32 return newFtpMockExt(t, address, "no-time")
33 }
34
35 func newFtpMockExt(t *testing.T, address, modtime string) (*ftpMock, error) {
2936 var err error
30 mock := &ftpMock{address: address}
37 mock := &ftpMock{
38 address: address,
39 modtime: modtime,
40 }
3141
3242 l, err := net.Listen("tcp", address+":0")
3343 if err != nil {
6575
6676 for {
6777 fullCommand, _ := mock.proto.ReadLine()
78 mock.lastFull = fullCommand
6879
6980 cmdParts := strings.Split(fullCommand, " ")
7081
7485 // At least one command must have a multiline response
7586 switch cmdParts[0] {
7687 case "FEAT":
77 mock.proto.Writer.PrintfLine("211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n211 End")
88 features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n"
89 switch mock.modtime {
90 case "std-time":
91 features += " MDTM\r\n MFMT\r\n"
92 case "vsftpd":
93 features += " MDTM\r\n"
94 }
95 features += "211 End"
96 _ = mock.proto.Writer.PrintfLine(features)
7897 case "USER":
7998 if cmdParts[1] == "anonymous" {
8099 mock.proto.Writer.PrintfLine("331 Please send your password")
193212 }
194213 mock.rest = rest
195214 mock.proto.Writer.PrintfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
215 case "MDTM":
216 var answer string
217 switch {
218 case mock.modtime == "no-time":
219 answer = "500 Unknown command MDTM"
220 case len(cmdParts) == 3 && mock.modtime == "vsftpd":
221 answer = "213 UTIME OK"
222 _, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
223 if err != nil {
224 answer = "501 Can't get a time stamp"
225 }
226 case len(cmdParts) == 2:
227 answer = "213 20201213202400"
228 default:
229 answer = "500 wrong number of arguments"
230 }
231 _ = mock.proto.Writer.PrintfLine(answer)
232 case "MFMT":
233 var answer string
234 switch {
235 case mock.modtime == "std-time" && len(cmdParts) == 3:
236 answer = "213 UTIME OK"
237 _, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
238 if err != nil {
239 answer = "501 Can't get a time stamp"
240 }
241 default:
242 answer = "500 Unknown command MFMT"
243 }
244 _ = mock.proto.Writer.PrintfLine(answer)
196245 case "NOOP":
197246 mock.proto.Writer.PrintfLine("200 NOOP ok.")
198247 case "OPTS":
202251 }
203252 if (strings.Join(cmdParts[1:], " ")) == "UTF8 ON" {
204253 mock.proto.Writer.PrintfLine("200 OK, UTF-8 enabled")
205 break
206254 }
207255 case "REIN":
208256 mock.proto.Writer.PrintfLine("220 Logged out")
305353
306354 // Helper to return a client connected to a mock server
307355 func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *ServerConn) {
308 mock, err := newFtpMock(t, addr)
356 return openConnExt(t, addr, "no-time", options...)
357 }
358
359 func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
360 mock, err := newFtpMockExt(t, addr, modtime)
309361 if err != nil {
310362 t.Fatal(err)
311363 }
327379
328380 // Helper to close a client connected to a mock server
329381 func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
330 expected := []string{"FEAT", "USER", "PASS", "TYPE", "OPTS"}
382 expected := []string{"USER", "PASS", "FEAT", "TYPE", "OPTS"}
331383 expected = append(expected, commands...)
332384 expected = append(expected, "QUIT")
333385
0 golang-github-jlaffaye-ftp (0.0~git20211117.1182040-1) UNRELEASED; urgency=low
1
2 * New upstream snapshot.
3
4 -- Debian Janitor <janitor@jelmer.uk> Sat, 29 Jan 2022 23:10:50 -0000
5
06 golang-github-jlaffaye-ftp (0.0~git20200812.39e3779-1) unstable; urgency=medium
17
28 * Team upload.
1818 func (w *debugWrapper) Close() error {
1919 return w.conn.Close()
2020 }
21
22 type streamDebugWrapper struct {
23 io.Reader
24 closer io.ReadCloser
25 }
26
27 func newStreamDebugWrapper(rd io.ReadCloser, w io.Writer) io.ReadCloser {
28 return &streamDebugWrapper{
29 Reader: io.TeeReader(rd, w),
30 closer: rd,
31 }
32 }
33
34 func (w *streamDebugWrapper) Close() error {
35 return w.closer.Close()
36 }
+260
-72
ftp.go less more
77 "context"
88 "crypto/tls"
99 "errors"
10 "fmt"
1011 "io"
1112 "net"
1213 "net/textproto"
2526 EntryTypeLink
2627 )
2728
29 // Time format used by the MDTM and MFMT commands
30 const timeFormat = "20060102150405"
31
2832 // ServerConn represents the connection to a remote FTP server.
2933 // A single connection only supports one in-flight data connection.
3034 // It is not safe to be called concurrently.
3135 type ServerConn struct {
3236 options *dialOptions
33 conn *textproto.Conn
37 conn *textproto.Conn // connection wrapper for text protocol
38 netConn net.Conn // underlying network connection
3439 host string
3540
3641 // Server capabilities discovered at runtime
3742 features map[string]string
3843 skipEPSV bool
3944 mlstSupported bool
45 mfmtSupported bool
46 mdtmSupported bool
47 mdtmCanWrite bool
48 usePRET bool
4049 }
4150
4251 // DialOption represents an option to start a new connection with Dial
5362 conn net.Conn
5463 disableEPSV bool
5564 disableUTF8 bool
65 disableMLSD bool
66 writingMDTM bool
5667 location *time.Location
5768 debugOutput io.Writer
5869 dialFunc func(network, address string) (net.Conn, error)
70 shutTimeout time.Duration // time to wait for data connection closing status
5971 }
6072
6173 // Entry describes a file and is returned by List().
116128 options: do,
117129 features: make(map[string]string),
118130 conn: textproto.NewConn(do.wrapConn(tconn)),
131 netConn: tconn,
119132 host: remoteAddr.IP.String(),
120133 }
121134
122135 _, _, err := c.conn.ReadResponse(StatusReady)
123136 if err != nil {
124 c.Quit()
137 _ = c.Quit()
125138 return nil, err
126139 }
127140
134147 c.conn = textproto.NewConn(do.wrapConn(tconn))
135148 }
136149
137 err = c.feat()
138 if err != nil {
139 c.Quit()
140 return nil, err
141 }
142
143 if _, mlstSupported := c.features["MLST"]; mlstSupported {
144 c.mlstSupported = true
145 }
146
147150 return c, nil
148151 }
149152
151154 func DialWithTimeout(timeout time.Duration) DialOption {
152155 return DialOption{func(do *dialOptions) {
153156 do.dialer.Timeout = timeout
157 }}
158 }
159
160 // DialWithShutTimeout returns a DialOption that configures the ServerConn with
161 // maximum time to wait for the data closing status on control connection
162 // and nudging the control connection deadline before reading status.
163 func DialWithShutTimeout(shutTimeout time.Duration) DialOption {
164 return DialOption{func(do *dialOptions) {
165 do.shutTimeout = shutTimeout
154166 }}
155167 }
156168
180192 func DialWithDisabledUTF8(disabled bool) DialOption {
181193 return DialOption{func(do *dialOptions) {
182194 do.disableUTF8 = disabled
195 }}
196 }
197
198 // DialWithDisabledMLSD returns a DialOption that configures the ServerConn with MLSD option disabled
199 //
200 // This is useful for servers which advertise MLSD (eg some versions
201 // of Serv-U) but don't support it properly.
202 func DialWithDisabledMLSD(disabled bool) DialOption {
203 return DialOption{func(do *dialOptions) {
204 do.disableMLSD = disabled
205 }}
206 }
207
208 // DialWithWritingMDTM returns a DialOption making ServerConn use MDTM to set file time
209 //
210 // This option addresses a quirk in the VsFtpd server which doesn't support
211 // the MFMT command for setting file time like other servers but by default
212 // uses the MDTM command with non-standard arguments for that.
213 // See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
214 func DialWithWritingMDTM(enabled bool) DialOption {
215 return DialOption{func(do *dialOptions) {
216 do.writingMDTM = enabled
183217 }}
184218 }
185219
245279 }
246280
247281 return newDebugWrapper(netConn, o.debugOutput)
282 }
283
284 func (o *dialOptions) wrapStream(rd io.ReadCloser) io.ReadCloser {
285 if o.debugOutput == nil {
286 return rd
287 }
288
289 return newStreamDebugWrapper(rd, o.debugOutput)
248290 }
249291
250292 // Connect is an alias to Dial, for backward compatibility
281323 return errors.New(message)
282324 }
283325
326 // Probe features
327 err = c.feat()
328 if err != nil {
329 return err
330 }
331 if _, mlstSupported := c.features["MLST"]; mlstSupported && !c.options.disableMLSD {
332 c.mlstSupported = true
333 }
334 _, c.usePRET = c.features["PRET"]
335
336 _, c.mfmtSupported = c.features["MFMT"]
337 _, c.mdtmSupported = c.features["MDTM"]
338 c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
339
284340 // Switch to binary mode
285341 if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil {
286342 return err
293349
294350 // If using implicit TLS, make data connections also use TLS
295351 if c.options.tlsConfig != nil {
296 c.cmd(StatusCommandOK, "PBSZ 0")
297 c.cmd(StatusCommandOK, "PROT P")
352 if _, _, err = c.cmd(StatusCommandOK, "PBSZ 0"); err != nil {
353 return err
354 }
355 if _, _, err = c.cmd(StatusCommandOK, "PROT P"); err != nil {
356 return err
357 }
298358 }
299359
300360 return err
377437 func (c *ServerConn) epsv() (port int, err error) {
378438 _, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
379439 if err != nil {
380 return
440 return 0, err
381441 }
382442
383443 start := strings.Index(line, "|||")
384444 end := strings.LastIndex(line, "|")
385445 if start == -1 || end == -1 {
386 err = errors.New("invalid EPSV response format")
387 return
446 return 0, errors.New("invalid EPSV response format")
388447 }
389448 port, err = strconv.Atoi(line[start+3 : end])
390 return
449 return port, err
391450 }
392451
393452 // pasv issues a "PASV" command to get a port number for a data connection.
394453 func (c *ServerConn) pasv() (host string, port int, err error) {
395454 _, line, err := c.cmd(StatusPassiveMode, "PASV")
396455 if err != nil {
397 return
456 return "", 0, err
398457 }
399458
400459 // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
401460 start := strings.Index(line, "(")
402461 end := strings.LastIndex(line, ")")
403462 if start == -1 || end == -1 {
404 err = errors.New("invalid PASV response format")
405 return
463 return "", 0, errors.New("invalid PASV response format")
406464 }
407465
408466 // We have to split the response string
409467 pasvData := strings.Split(line[start+1:end], ",")
410468
411469 if len(pasvData) < 6 {
412 err = errors.New("invalid PASV response format")
413 return
470 return "", 0, errors.New("invalid PASV response format")
414471 }
415472
416473 // Let's compute the port number
417 portPart1, err1 := strconv.Atoi(pasvData[4])
418 if err1 != nil {
419 err = err1
420 return
421 }
422
423 portPart2, err2 := strconv.Atoi(pasvData[5])
424 if err2 != nil {
425 err = err2
426 return
474 portPart1, err := strconv.Atoi(pasvData[4])
475 if err != nil {
476 return "", 0, err
477 }
478
479 portPart2, err := strconv.Atoi(pasvData[5])
480 if err != nil {
481 return "", 0, err
427482 }
428483
429484 // Recompose port
431486
432487 // Make the IP address to connect to
433488 host = strings.Join(pasvData[0:4], ".")
434 return
489 return host, port, nil
435490 }
436491
437492 // getDataConnPort returns a host, port for a new data connection
486541 // cmdDataConnFrom executes a command which require a FTP data connection.
487542 // Issues a REST FTP command to specify the number of bytes to skip for the transfer.
488543 func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) {
544 // If server requires PRET send the PRET command to warm it up
545 // See: https://tools.ietf.org/html/draft-dd-pret-00
546 if c.usePRET {
547 _, _, err := c.cmd(-1, "PRET "+format, args...)
548 if err != nil {
549 return nil, err
550 }
551 }
552
489553 conn, err := c.openDataConn()
490554 if err != nil {
491555 return nil, err
492556 }
493557
494558 if offset != 0 {
495 _, _, err := c.cmd(StatusRequestFilePending, "REST %d", offset)
559 _, _, err = c.cmd(StatusRequestFilePending, "REST %d", offset)
496560 if err != nil {
497 conn.Close()
561 _ = conn.Close()
498562 return nil, err
499563 }
500564 }
501565
502566 _, err = c.conn.Cmd(format, args...)
503567 if err != nil {
504 conn.Close()
568 _ = conn.Close()
505569 return nil, err
506570 }
507571
508572 code, msg, err := c.conn.ReadResponse(-1)
509573 if err != nil {
510 conn.Close()
574 _ = conn.Close()
511575 return nil, err
512576 }
513577 if code != StatusAlreadyOpen && code != StatusAboutToSend {
514 conn.Close()
578 _ = conn.Close()
515579 return nil, &textproto.Error{Code: code, Msg: msg}
516580 }
517581
520584
521585 // NameList issues an NLST FTP command.
522586 func (c *ServerConn) NameList(path string) (entries []string, err error) {
523 conn, err := c.cmdDataConnFrom(0, "NLST %s", path)
524 if err != nil {
525 return
587 space := " "
588 if path == "" {
589 space = ""
590 }
591 conn, err := c.cmdDataConnFrom(0, "NLST%s%s", space, path)
592 if err != nil {
593 return nil, err
526594 }
527595
528596 r := &Response{conn: conn, c: c}
529 defer r.Close()
530
531 scanner := bufio.NewScanner(r)
597 defer func() {
598 errClose := r.Close()
599 if err == nil {
600 err = errClose
601 }
602 }()
603
604 scanner := bufio.NewScanner(c.options.wrapStream(r))
532605 for scanner.Scan() {
533606 entries = append(entries, scanner.Text())
534607 }
535 if err = scanner.Err(); err != nil {
536 return entries, err
537 }
538 return
608
609 err = scanner.Err()
610 return entries, err
539611 }
540612
541613 // List issues a LIST FTP command.
551623 parser = parseListLine
552624 }
553625
554 conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path)
555 if err != nil {
556 return
626 space := " "
627 if path == "" {
628 space = ""
629 }
630 conn, err := c.cmdDataConnFrom(0, "%s%s%s", cmd, space, path)
631 if err != nil {
632 return nil, err
557633 }
558634
559635 r := &Response{conn: conn, c: c}
560 defer r.Close()
561
562 scanner := bufio.NewScanner(r)
636 defer func() {
637 errClose := r.Close()
638 if err == nil {
639 err = errClose
640 }
641 }()
642
643 scanner := bufio.NewScanner(c.options.wrapStream(r))
563644 now := time.Now()
564645 for scanner.Scan() {
565 entry, err := parser(scanner.Text(), now, c.options.location)
566 if err == nil {
646 entry, errParse := parser(scanner.Text(), now, c.options.location)
647 if errParse == nil {
567648 entries = append(entries, entry)
568649 }
569650 }
570 if err := scanner.Err(); err != nil {
571 return nil, err
572 }
573 return
651
652 err = scanner.Err()
653 return entries, err
654 }
655
656 // IsTimePreciseInList returns true if client and server support the MLSD
657 // command so List can return time with 1-second precision for all files.
658 func (c *ServerConn) IsTimePreciseInList() bool {
659 return c.mlstSupported
574660 }
575661
576662 // ChangeDir issues a CWD FTP command, which changes the current directory to
616702 return strconv.ParseInt(msg, 10, 64)
617703 }
618704
705 // GetTime issues the MDTM FTP command to obtain the file modification time.
706 // It returns a UTC time.
707 func (c *ServerConn) GetTime(path string) (time.Time, error) {
708 var t time.Time
709 if !c.mdtmSupported {
710 return t, errors.New("GetTime is not supported")
711 }
712 _, msg, err := c.cmd(StatusFile, "MDTM %s", path)
713 if err != nil {
714 return t, err
715 }
716 return time.ParseInLocation(timeFormat, msg, time.UTC)
717 }
718
719 // IsGetTimeSupported allows library callers to check in advance that they
720 // can use GetTime to get file time.
721 func (c *ServerConn) IsGetTimeSupported() bool {
722 return c.mdtmSupported
723 }
724
725 // SetTime issues the MFMT FTP command to set the file modification time.
726 // Also it can use a non-standard form of the MDTM command supported by
727 // the VsFtpd server instead of MFMT for the same purpose.
728 // See "mdtm_write" in https://security.appspot.com/vsftpd/vsftpd_conf.html
729 func (c *ServerConn) SetTime(path string, t time.Time) (err error) {
730 utime := t.In(time.UTC).Format(timeFormat)
731 switch {
732 case c.mfmtSupported:
733 _, _, err = c.cmd(StatusFile, "MFMT %s %s", utime, path)
734 case c.mdtmCanWrite:
735 _, _, err = c.cmd(StatusFile, "MDTM %s %s", utime, path)
736 default:
737 err = errors.New("SetTime is not supported")
738 }
739 return
740 }
741
742 // IsSetTimeSupported allows library callers to check in advance that they
743 // can use SetTime to set file time.
744 func (c *ServerConn) IsSetTimeSupported() bool {
745 return c.mfmtSupported || c.mdtmCanWrite
746 }
747
619748 // Retr issues a RETR FTP command to fetch the specified file from the remote
620749 // FTP server.
621750 //
643772 // Hint: io.Pipe() can be used if an io.Writer is required.
644773 func (c *ServerConn) Stor(path string, r io.Reader) error {
645774 return c.StorFrom(path, r, 0)
775 }
776
777 // checkDataShut reads the "closing data connection" status from the
778 // control connection. It is called after transferring a piece of data
779 // on the data connection during which the control connection was idle.
780 // This may result in the idle timeout triggering on the control connection
781 // right when we try to read the response.
782 // The ShutTimeout dial option will rescue here. It will nudge the control
783 // connection deadline right before checking the data closing status.
784 func (c *ServerConn) checkDataShut() error {
785 if c.options.shutTimeout != 0 {
786 shutDeadline := time.Now().Add(c.options.shutTimeout)
787 if err := c.netConn.SetDeadline(shutDeadline); err != nil {
788 return err
789 }
790 }
791 _, _, err := c.conn.ReadResponse(StatusClosingDataConnection)
792 return err
646793 }
647794
648795 // StorFrom issues a STOR FTP command to store a file to the remote FTP server.
662809 // the response and we cannot use the connection to send other commands.
663810 // So we don't check io.Copy error and we return the error from
664811 // ReadResponse so the user can see the real error
665 io.Copy(conn, r)
666 conn.Close()
667
668 _, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
812 var n int64
813 n, err = io.Copy(conn, r)
814
815 // If we wrote no bytes but got no error, make sure we call
816 // tls.Handshake on the connection as it won't get called
817 // unless Write() is called.
818 //
819 // ProFTP doesn't like this and returns "Unable to build data
820 // connection: Operation not permitted" when trying to upload
821 // an empty file without this.
822 if n == 0 && err == nil {
823 if do, ok := conn.(interface{ Handshake() error }); ok {
824 err = do.Handshake()
825 }
826 }
827
828 // Use io.Copy or Handshake error in preference to this one
829 closeErr := conn.Close()
830 if err == nil {
831 err = closeErr
832 }
833
834 // Read the response and use this error in preference to
835 // previous errors
836 respErr := c.checkDataShut()
837 if respErr != nil {
838 err = respErr
839 }
669840 return err
670841 }
671842
681852 }
682853
683854 // see the comment for StorFrom above
684 io.Copy(conn, r)
685 conn.Close()
686
687 _, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
855 _, err = io.Copy(conn, r)
856 errClose := conn.Close()
857
858 respErr := c.checkDataShut()
859 if respErr != nil {
860 err = respErr
861 }
862
863 if err == nil {
864 err = errClose
865 }
866
688867 return err
689868 }
690869
792971 // Quit issues a QUIT FTP command to properly close the connection from the
793972 // remote FTP server.
794973 func (c *ServerConn) Quit() error {
795 c.conn.Cmd("QUIT")
796 return c.conn.Close()
974 _, errQuit := c.conn.Cmd("QUIT")
975 err := c.conn.Close()
976
977 if errQuit != nil {
978 if err != nil {
979 return fmt.Errorf("error while quitting: %s: %w", errQuit, err)
980 }
981 return errQuit
982 }
983
984 return err
797985 }
798986
799987 // Read implements the io.Reader interface on a FTP data connection.
808996 return nil
809997 }
810998 err := r.conn.Close()
811 _, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection)
999 err2 := r.c.checkDataShut()
8121000 if err2 != nil {
8131001 err = err2
8141002 }
6262 e.Type = EntryTypeFile
6363 }
6464 case "size":
65 e.setSize(value)
65 if err := e.setSize(value); err != nil {
66 return nil, err
67 }
6668 }
6769 }
6870 return e, nil
159159 for _, test := range tests {
160160 t.Run(test.line, func(t *testing.T) {
161161 entry := &Entry{}
162 entry.setTime(strings.Fields(test.line), now, time.UTC)
162 if err := entry.setTime(strings.Fields(test.line), now, time.UTC); err != nil {
163 t.Fatal(err)
164 }
163165
164166 assert.Equal(t, test.expected, entry.Time)
165167 })
00 package ftp
11
2 import "testing"
3 import "github.com/stretchr/testify/assert"
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
47
58 func TestScanner(t *testing.T) {
69 assert := assert.New(t)
3535 if w.descend && w.cur.entry.Type == EntryTypeFolder {
3636 entries, err := w.serverConn.List(w.cur.path)
3737
38 // an error occured, drop out and stop walking
38 // an error occurred, drop out and stop walking
3939 if err != nil {
4040 w.cur.err = err
4141 return false
103103 assert, require := assert.New(t), require.New(t)
104104
105105 mock, err := newFtpMock(t, "127.0.0.1")
106 require.NotNil(err)
107 defer mock.Close()
108
109 c, cErr := Connect(mock.Addr())
110 require.NotNil(cErr)
106 require.Nil(err)
107 defer mock.Close()
108
109 c, cErr := Connect(mock.Addr())
110 require.Nil(cErr)
111111
112112 w := c.Walk("/root")
113113
135135 assert, require := assert.New(t), require.New(t)
136136
137137 mock, err := newFtpMock(t, "127.0.0.1")
138 require.NotNil(err)
139 defer mock.Close()
140
141 c, cErr := Connect(mock.Addr())
142 require.NotNil(cErr)
138 require.Nil(err)
139 defer mock.Close()
140
141 c, cErr := Connect(mock.Addr())
142 require.Nil(cErr)
143143
144144 w := c.Walk("/root")
145145 w.cur = &item{
177177 }
178178
179179 result := w.Next()
180 assert.Equal(true, result, "Result should return true")
181
180182 result = w.Next()
181183
182184 assert.Equal(true, result, "Result should return true")