diff --git a/Gopkg.lock b/Gopkg.lock index 289e8a0ab..bb80c047a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,4 +1,4 @@ -memo = "28c7ca08636da6264c587a36b72ce4f3a45fdf5c32969ffd2092b98456c73685" +memo = "8903edaabf2c61a2bb9386fb033b3ff7710880ee02387c7a3f5eaaee82cd6e67" [[projects]] branch = "master" @@ -78,6 +78,12 @@ memo = "28c7ca08636da6264c587a36b72ce4f3a45fdf5c32969ffd2092b98456c73685" packages = ["."] revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" +[[projects]] + branch = "master" + name = "github.com/jlaffaye/ftp" + packages = ["."] + revision = "5c7b901224c7880b293e0b5486cb6ebf97bfca37" + [[projects]] name = "github.com/jmespath/go-jmespath" packages = ["."] diff --git a/vendor/github.com/jlaffaye/ftp/.travis.yml b/vendor/github.com/jlaffaye/ftp/.travis.yml new file mode 100644 index 000000000..1b58030ef --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/.travis.yml @@ -0,0 +1,15 @@ +language: go +dist: trusty +sudo: required +go: + - 1.7.5 + - 1.8.1 +env: + - FTP_SERVER=vsftpd + - FTP_SERVER=proftpd +before_install: +- sudo $TRAVIS_BUILD_DIR/.travis/prepare.sh "$FTP_SERVER" +- sudo sysctl net.ipv6.conf.lo.disable_ipv6=0 +- go get github.com/mattn/goveralls +script: +- goveralls -v diff --git a/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh b/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh new file mode 100755 index 000000000..40970d9ac --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/.travis/prepare.sh @@ -0,0 +1,18 @@ +#!/bin/sh -e + +case "$1" in + proftpd) + mkdir -p /etc/proftpd/conf.d/ + cp $TRAVIS_BUILD_DIR/.travis/proftpd.conf /etc/proftpd/conf.d/ + ;; + vsftpd) + cp $TRAVIS_BUILD_DIR/.travis/vsftpd.conf /etc/vsftpd.conf + ;; + *) + echo "unknown software: $1" + exit 1 +esac + +mkdir --mode 0777 -p /var/ftp/incoming + +apt-get install -qq "$1" diff --git a/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf b/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf new file mode 100644 index 000000000..342d08fe9 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/.travis/proftpd.conf @@ -0,0 +1,9 @@ + + User ftp + Group nogroup + MaxClients 2 + # We want clients to be able to login with "anonymous" as well as "ftp" + UserAlias anonymous ftp + + RequireValidShell off + diff --git a/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf b/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf new file mode 100644 index 000000000..f3d6dbfd3 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/.travis/vsftpd.conf @@ -0,0 +1,15 @@ +# Used by Travis CI + +listen=NO +listen_ipv6=YES + +write_enable=YES +dirmessage_enable=YES +secure_chroot_dir=/var/run/vsftpd/empty + +anonymous_enable=YES +anon_root=/var/ftp +anon_upload_enable=YES +anon_mkdir_write_enable=YES +anon_other_write_enable=YES +anon_umask=022 diff --git a/vendor/github.com/jlaffaye/ftp/LICENSE b/vendor/github.com/jlaffaye/ftp/LICENSE new file mode 100644 index 000000000..9ab085c51 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2011-2013, Julien Laffaye + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/jlaffaye/ftp/README.md b/vendor/github.com/jlaffaye/ftp/README.md new file mode 100644 index 000000000..b711be7ad --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/README.md @@ -0,0 +1,17 @@ +# goftp # + +[![Build Status](https://travis-ci.org/jlaffaye/ftp.svg?branch=master)](https://travis-ci.org/jlaffaye/ftp) +[![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master) +[![Go ReportCard](http://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp) + +A FTP client package for Go + +## Install ## + +``` +go get -u github.com/jlaffaye/ftp +``` + +## Documentation ## + +http://godoc.org/github.com/jlaffaye/ftp diff --git a/vendor/github.com/jlaffaye/ftp/client_multiline_test.go b/vendor/github.com/jlaffaye/ftp/client_multiline_test.go new file mode 100644 index 000000000..9d3b0ce1d --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/client_multiline_test.go @@ -0,0 +1,107 @@ +package ftp + +import ( + "net" + "net/textproto" + "reflect" + "strings" + "sync" + "testing" +) + +type ftpMock struct { + listener net.Listener + commands []string // list of received commands + sync.WaitGroup +} + +func newFtpMock(t *testing.T, addresss string) *ftpMock { + var err error + mock := &ftpMock{} + mock.listener, err = net.Listen("tcp", addresss) + if err != nil { + t.Fatal(err) + } + + go func() { + // Listen for an incoming connection. + conn, err := mock.listener.Accept() + if err != nil { + t.Fatal(err) + } + + mock.Add(1) + defer mock.Done() + defer conn.Close() + + proto := textproto.NewConn(conn) + proto.Writer.PrintfLine("220 FTP Server ready.") + + for { + command, _ := proto.ReadLine() + + // Strip the arguments + if i := strings.Index(command, " "); i > 0 { + command = command[:i] + } + + // Append to list of received commands + mock.commands = append(mock.commands, command) + + // At least one command must have a multiline response + switch command { + case "FEAT": + proto.Writer.PrintfLine("211-Features:\r\nFEAT\r\nPASV\r\nSIZE\r\n211 End") + case "USER": + proto.Writer.PrintfLine("331 Please send your password") + case "PASS": + proto.Writer.PrintfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted") + case "TYPE": + proto.Writer.PrintfLine("200 Type set ok") + case "QUIT": + proto.Writer.PrintfLine("221 Goodbye.") + return + default: + t.Fatal("unknown command:", command) + } + } + }() + + return mock +} + +// Closes the listening socket +func (mock *ftpMock) Close() { + mock.listener.Close() +} + +// ftp.mozilla.org uses multiline 220 response +func TestMultiline(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + address := "localhost:2121" + mock := newFtpMock(t, address) + defer mock.Close() + + c, err := Dial(address) + if err != nil { + t.Fatal(err) + } + + err = c.Login("anonymous", "anonymous") + if err != nil { + t.Fatal(err) + } + + c.Quit() + + // Wait for the connection to close + mock.Wait() + + expected := []string{"FEAT", "USER", "PASS", "TYPE", "QUIT"} + if !reflect.DeepEqual(mock.commands, expected) { + t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected) + } +} diff --git a/vendor/github.com/jlaffaye/ftp/client_test.go b/vendor/github.com/jlaffaye/ftp/client_test.go new file mode 100644 index 000000000..8297e49cf --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/client_test.go @@ -0,0 +1,270 @@ +package ftp + +import ( + "bytes" + "io/ioutil" + "net/textproto" + "strings" + "testing" + "time" +) + +const ( + testData = "Just some text" + testDir = "mydir" +) + +func TestConnPASV(t *testing.T) { + testConn(t, true) +} + +func TestConnEPSV(t *testing.T) { + testConn(t, false) +} + +func testConn(t *testing.T, disableEPSV bool) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + c, err := DialTimeout("localhost:21", 5*time.Second) + if err != nil { + t.Fatal(err) + } + + if disableEPSV { + delete(c.features, "EPSV") + c.DisableEPSV = true + } + + err = c.Login("anonymous", "anonymous") + if err != nil { + t.Fatal(err) + } + + err = c.NoOp() + if err != nil { + t.Error(err) + } + + err = c.ChangeDir("incoming") + if err != nil { + t.Error(err) + } + + data := bytes.NewBufferString(testData) + err = c.Stor("test", data) + if err != nil { + t.Error(err) + } + + _, err = c.List(".") + if err != nil { + t.Error(err) + } + + err = c.Rename("test", "tset") + if err != nil { + t.Error(err) + } + + // Read without deadline + r, err := c.Retr("tset") + if err != nil { + t.Error(err) + } else { + buf, err := ioutil.ReadAll(r) + if err != nil { + t.Error(err) + } + if string(buf) != testData { + t.Errorf("'%s'", buf) + } + r.Close() + r.Close() // test we can close two times + } + + // Read with deadline + r, err = c.Retr("tset") + if err != nil { + t.Error(err) + } else { + r.SetDeadline(time.Now()) + _, err := ioutil.ReadAll(r) + if err == nil { + t.Error("deadline should have caused error") + } else if !strings.HasSuffix(err.Error(), "i/o timeout") { + t.Error(err) + } + r.Close() + } + + // Read with offset + r, err = c.RetrFrom("tset", 5) + if err != nil { + t.Error(err) + } else { + buf, err := ioutil.ReadAll(r) + if err != nil { + t.Error(err) + } + expected := testData[5:] + if string(buf) != expected { + t.Errorf("read %q, expected %q", buf, expected) + } + r.Close() + } + + fileSize, err := c.FileSize("tset") + if err != nil { + t.Error(err) + } + if fileSize != 14 { + t.Errorf("file size %q, expected %q", fileSize, 14) + } + + data = bytes.NewBufferString("") + err = c.Stor("tset", data) + if err != nil { + t.Error(err) + } + + fileSize, err = c.FileSize("tset") + if err != nil { + t.Error(err) + } + if fileSize != 0 { + t.Errorf("file size %q, expected %q", fileSize, 0) + } + + _, err = c.FileSize("not-found") + if err == nil { + t.Fatal("expected error, got nil") + } + + err = c.Delete("tset") + if err != nil { + t.Error(err) + } + + err = c.MakeDir(testDir) + if err != nil { + t.Error(err) + } + + err = c.ChangeDir(testDir) + if err != nil { + t.Error(err) + } + + dir, err := c.CurrentDir() + if err != nil { + t.Error(err) + } else { + if dir != "/incoming/"+testDir { + t.Error("Wrong dir: " + dir) + } + } + + err = c.ChangeDirToParent() + if err != nil { + t.Error(err) + } + + entries, err := c.NameList("/") + if err != nil { + t.Error(err) + } + if len(entries) != 1 || entries[0] != "/incoming" { + t.Errorf("Unexpected entries: %v", entries) + } + + err = c.RemoveDir(testDir) + if err != nil { + t.Error(err) + } + + err = c.Logout() + if err != nil { + if protoErr := err.(*textproto.Error); protoErr != nil { + if protoErr.Code != StatusNotImplemented { + t.Error(err) + } + } else { + t.Error(err) + } + } + + c.Quit() + + err = c.NoOp() + if err == nil { + t.Error("Expected error") + } +} + +func TestConnIPv6(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + c, err := DialTimeout("[::1]:21", 5*time.Second) + if err != nil { + t.Fatal(err) + } + + err = c.Login("anonymous", "anonymous") + if err != nil { + t.Fatal(err) + } + + _, err = c.List(".") + if err != nil { + t.Error(err) + } + + c.Quit() +} + +// TestConnect tests the legacy Connect function +func TestConnect(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + c, err := Connect("localhost:21") + if err != nil { + t.Fatal(err) + } + + c.Quit() +} + +func TestTimeout(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + c, err := DialTimeout("localhost:2121", 1*time.Second) + if err == nil { + t.Fatal("expected timeout, got nil error") + c.Quit() + } +} + +func TestWrongLogin(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + c, err := DialTimeout("localhost:21", 5*time.Second) + if err != nil { + t.Fatal(err) + } + defer c.Quit() + + err = c.Login("zoo2Shia", "fei5Yix9") + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/vendor/github.com/jlaffaye/ftp/ftp.go b/vendor/github.com/jlaffaye/ftp/ftp.go new file mode 100644 index 000000000..c29cb3847 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/ftp.go @@ -0,0 +1,557 @@ +// Package ftp implements a FTP client as described in RFC 959. +// +// A textproto.Error is returned for errors at the protocol level. +package ftp + +import ( + "bufio" + "errors" + "io" + "net" + "net/textproto" + "strconv" + "strings" + "time" +) + +// EntryType describes the different types of an Entry. +type EntryType int + +// The differents types of an Entry +const ( + EntryTypeFile EntryType = iota + EntryTypeFolder + EntryTypeLink +) + +// ServerConn represents the connection to a remote FTP server. +// It should be protected from concurrent accesses. +type ServerConn struct { + // Do not use EPSV mode + DisableEPSV bool + + conn *textproto.Conn + host string + timeout time.Duration + features map[string]string + mlstSupported bool +} + +// Entry describes a file and is returned by List(). +type Entry struct { + Name string + Type EntryType + Size uint64 + Time time.Time +} + +// Response represents a data-connection +type Response struct { + conn net.Conn + c *ServerConn + closed bool +} + +// Connect is an alias to Dial, for backward compatibility +func Connect(addr string) (*ServerConn, error) { + return Dial(addr) +} + +// Dial is like DialTimeout with no timeout +func Dial(addr string) (*ServerConn, error) { + return DialTimeout(addr, 0) +} + +// DialTimeout initializes the connection to the specified ftp server address. +// +// It is generally followed by a call to Login() as most FTP commands require +// an authenticated user. +func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) { + tconn, err := net.DialTimeout("tcp", addr, timeout) + if err != nil { + return nil, err + } + + // Use the resolved IP address in case addr contains a domain name + // If we use the domain name, we might not resolve to the same IP. + remoteAddr := tconn.RemoteAddr().String() + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + return nil, err + } + + conn := textproto.NewConn(tconn) + + c := &ServerConn{ + conn: conn, + host: host, + timeout: timeout, + features: make(map[string]string), + } + + _, _, err = c.conn.ReadResponse(StatusReady) + if err != nil { + c.Quit() + return nil, err + } + + err = c.feat() + if err != nil { + c.Quit() + return nil, err + } + + if _, mlstSupported := c.features["MLST"]; mlstSupported { + c.mlstSupported = true + } + + return c, nil +} + +// Login authenticates the client with specified user and password. +// +// "anonymous"/"anonymous" is a common user/password scheme for FTP servers +// that allows anonymous read-only accounts. +func (c *ServerConn) Login(user, password string) error { + code, message, err := c.cmd(-1, "USER %s", user) + if err != nil { + return err + } + + switch code { + case StatusLoggedIn: + case StatusUserOK: + _, _, err = c.cmd(StatusLoggedIn, "PASS %s", password) + if err != nil { + return err + } + default: + return errors.New(message) + } + + // Switch to binary mode + if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil { + return err + } + + // Switch to UTF-8 + if err := c.setUTF8(); err != nil { + return err + } + + return nil +} + +// feat issues a FEAT FTP command to list the additional commands supported by +// the remote FTP server. +// FEAT is described in RFC 2389 +func (c *ServerConn) feat() error { + code, message, err := c.cmd(-1, "FEAT") + if err != nil { + return err + } + + if code != StatusSystem { + // The server does not support the FEAT command. This is not an + // error: we consider that there is no additional feature. + return nil + } + + lines := strings.Split(message, "\n") + for _, line := range lines { + if !strings.HasPrefix(line, " ") { + continue + } + + line = strings.TrimSpace(line) + featureElements := strings.SplitN(line, " ", 2) + + command := featureElements[0] + + var commandDesc string + if len(featureElements) == 2 { + commandDesc = featureElements[1] + } + + c.features[command] = commandDesc + } + + return nil +} + +// setUTF8 issues an "OPTS UTF8 ON" command. +func (c *ServerConn) setUTF8() error { + if _, ok := c.features["UTF8"]; !ok { + return nil + } + + code, message, err := c.cmd(-1, "OPTS UTF8 ON") + if err != nil { + return err + } + + // The ftpd "filezilla-server" has FEAT support for UTF8, but always returns + // "202 UTF8 mode is always enabled. No need to send this command." when + // trying to use it. That's OK + if code == StatusCommandNotImplemented { + return nil + } + + if code != StatusCommandOK { + return errors.New(message) + } + + return nil +} + +// epsv issues an "EPSV" command to get a port number for a data connection. +func (c *ServerConn) epsv() (port int, err error) { + _, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV") + if err != nil { + return + } + + start := strings.Index(line, "|||") + end := strings.LastIndex(line, "|") + if start == -1 || end == -1 { + err = errors.New("Invalid EPSV response format") + return + } + port, err = strconv.Atoi(line[start+3 : end]) + return +} + +// pasv issues a "PASV" command to get a port number for a data connection. +func (c *ServerConn) pasv() (port int, err error) { + _, line, err := c.cmd(StatusPassiveMode, "PASV") + if err != nil { + return + } + + // PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). + start := strings.Index(line, "(") + end := strings.LastIndex(line, ")") + if start == -1 || end == -1 { + return 0, errors.New("Invalid PASV response format") + } + + // We have to split the response string + pasvData := strings.Split(line[start+1:end], ",") + + if len(pasvData) < 6 { + return 0, errors.New("Invalid PASV response format") + } + + // Let's compute the port number + portPart1, err1 := strconv.Atoi(pasvData[4]) + if err1 != nil { + err = err1 + return + } + + portPart2, err2 := strconv.Atoi(pasvData[5]) + if err2 != nil { + err = err2 + return + } + + // Recompose port + port = portPart1*256 + portPart2 + return +} + +// getDataConnPort returns a port for a new data connection +// it uses the best available method to do so +func (c *ServerConn) getDataConnPort() (int, error) { + if !c.DisableEPSV { + if port, err := c.epsv(); err == nil { + return port, nil + } + + // if there is an error, disable EPSV for the next attempts + c.DisableEPSV = true + } + + return c.pasv() +} + +// openDataConn creates a new FTP data connection. +func (c *ServerConn) openDataConn() (net.Conn, error) { + port, err := c.getDataConnPort() + if err != nil { + return nil, err + } + + return net.DialTimeout("tcp", net.JoinHostPort(c.host, strconv.Itoa(port)), c.timeout) +} + +// cmd is a helper function to execute a command and check for the expected FTP +// return code +func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) { + _, err := c.conn.Cmd(format, args...) + if err != nil { + return 0, "", err + } + + return c.conn.ReadResponse(expected) +} + +// cmdDataConnFrom executes a command which require a FTP data connection. +// Issues a REST FTP command to specify the number of bytes to skip for the transfer. +func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) { + conn, err := c.openDataConn() + if err != nil { + return nil, err + } + + if offset != 0 { + _, _, err := c.cmd(StatusRequestFilePending, "REST %d", offset) + if err != nil { + conn.Close() + return nil, err + } + } + + _, err = c.conn.Cmd(format, args...) + if err != nil { + conn.Close() + return nil, err + } + + code, msg, err := c.conn.ReadResponse(-1) + if err != nil { + conn.Close() + return nil, err + } + if code != StatusAlreadyOpen && code != StatusAboutToSend { + conn.Close() + return nil, &textproto.Error{Code: code, Msg: msg} + } + + return conn, nil +} + +// NameList issues an NLST FTP command. +func (c *ServerConn) NameList(path string) (entries []string, err error) { + conn, err := c.cmdDataConnFrom(0, "NLST %s", path) + if err != nil { + return + } + + r := &Response{conn: conn, c: c} + defer r.Close() + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + entries = append(entries, scanner.Text()) + } + if err = scanner.Err(); err != nil { + return entries, err + } + return +} + +// List issues a LIST FTP command. +func (c *ServerConn) List(path string) (entries []*Entry, err error) { + var cmd string + var parseFunc func(string) (*Entry, error) + + if c.mlstSupported { + cmd = "MLSD" + parseFunc = parseRFC3659ListLine + } else { + cmd = "LIST" + parseFunc = parseListLine + } + + conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path) + if err != nil { + return + } + + r := &Response{conn: conn, c: c} + defer r.Close() + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + entry, err := parseFunc(scanner.Text()) + if err == nil { + entries = append(entries, entry) + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return +} + +// ChangeDir issues a CWD FTP command, which changes the current directory to +// the specified path. +func (c *ServerConn) ChangeDir(path string) error { + _, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path) + return err +} + +// ChangeDirToParent issues a CDUP FTP command, which changes the current +// directory to the parent directory. This is similar to a call to ChangeDir +// with a path set to "..". +func (c *ServerConn) ChangeDirToParent() error { + _, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP") + return err +} + +// CurrentDir issues a PWD FTP command, which Returns the path of the current +// directory. +func (c *ServerConn) CurrentDir() (string, error) { + _, msg, err := c.cmd(StatusPathCreated, "PWD") + if err != nil { + return "", err + } + + start := strings.Index(msg, "\"") + end := strings.LastIndex(msg, "\"") + + if start == -1 || end == -1 { + return "", errors.New("Unsuported PWD response format") + } + + return msg[start+1 : end], nil +} + +// FileSize issues a SIZE FTP command, which Returns the size of the file +func (c *ServerConn) FileSize(path string) (int64, error) { + _, msg, err := c.cmd(StatusFile, "SIZE %s", path) + if err != nil { + return 0, err + } + + return strconv.ParseInt(msg, 10, 64) +} + +// Retr issues a RETR FTP command to fetch the specified file from the remote +// FTP server. +// +// The returned ReadCloser must be closed to cleanup the FTP data connection. +func (c *ServerConn) Retr(path string) (*Response, error) { + return c.RetrFrom(path, 0) +} + +// RetrFrom issues a RETR FTP command to fetch the specified file from the remote +// FTP server, the server will not send the offset first bytes of the file. +// +// The returned ReadCloser must be closed to cleanup the FTP data connection. +func (c *ServerConn) RetrFrom(path string, offset uint64) (*Response, error) { + conn, err := c.cmdDataConnFrom(offset, "RETR %s", path) + if err != nil { + return nil, err + } + + return &Response{conn: conn, c: c}, nil +} + +// Stor issues a STOR FTP command to store a file to the remote FTP server. +// Stor creates the specified file with the content of the io.Reader. +// +// Hint: io.Pipe() can be used if an io.Writer is required. +func (c *ServerConn) Stor(path string, r io.Reader) error { + return c.StorFrom(path, r, 0) +} + +// StorFrom issues a STOR FTP command to store a file to the remote FTP server. +// Stor creates the specified file with the content of the io.Reader, writing +// on the server will start at the given file offset. +// +// Hint: io.Pipe() can be used if an io.Writer is required. +func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error { + conn, err := c.cmdDataConnFrom(offset, "STOR %s", path) + if err != nil { + return err + } + + _, err = io.Copy(conn, r) + conn.Close() + if err != nil { + return err + } + + _, _, err = c.conn.ReadResponse(StatusClosingDataConnection) + return err +} + +// Rename renames a file on the remote FTP server. +func (c *ServerConn) Rename(from, to string) error { + _, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from) + if err != nil { + return err + } + + _, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to) + return err +} + +// Delete issues a DELE FTP command to delete the specified file from the +// remote FTP server. +func (c *ServerConn) Delete(path string) error { + _, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path) + return err +} + +// MakeDir issues a MKD FTP command to create the specified directory on the +// remote FTP server. +func (c *ServerConn) MakeDir(path string) error { + _, _, err := c.cmd(StatusPathCreated, "MKD %s", path) + return err +} + +// RemoveDir issues a RMD FTP command to remove the specified directory from +// the remote FTP server. +func (c *ServerConn) RemoveDir(path string) error { + _, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path) + return err +} + +// NoOp issues a NOOP FTP command. +// NOOP has no effects and is usually used to prevent the remote FTP server to +// close the otherwise idle connection. +func (c *ServerConn) NoOp() error { + _, _, err := c.cmd(StatusCommandOK, "NOOP") + return err +} + +// Logout issues a REIN FTP command to logout the current user. +func (c *ServerConn) Logout() error { + _, _, err := c.cmd(StatusReady, "REIN") + return err +} + +// Quit issues a QUIT FTP command to properly close the connection from the +// remote FTP server. +func (c *ServerConn) Quit() error { + c.conn.Cmd("QUIT") + return c.conn.Close() +} + +// Read implements the io.Reader interface on a FTP data connection. +func (r *Response) Read(buf []byte) (int, error) { + return r.conn.Read(buf) +} + +// Close implements the io.Closer interface on a FTP data connection. +// After the first call, Close will do nothing and return nil. +func (r *Response) Close() error { + if r.closed { + return nil + } + err := r.conn.Close() + _, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection) + if err2 != nil { + err = err2 + } + r.closed = true + return err +} + +// SetDeadline sets the deadlines associated with the connection. +func (r *Response) SetDeadline(t time.Time) error { + return r.conn.SetDeadline(t) +} diff --git a/vendor/github.com/jlaffaye/ftp/parse.go b/vendor/github.com/jlaffaye/ftp/parse.go new file mode 100644 index 000000000..076062a48 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/parse.go @@ -0,0 +1,213 @@ +package ftp + +import ( + "errors" + "strconv" + "strings" + "time" +) + +var errUnsupportedListLine = errors.New("Unsupported LIST line") + +var listLineParsers = []func(line string) (*Entry, error){ + parseRFC3659ListLine, + parseLsListLine, + parseDirListLine, +} + +var dirTimeFormats = []string{ + "01-02-06 03:04PM", + "2006-01-02 15:04", +} + +// parseRFC3659ListLine parses the style of directory line defined in RFC 3659. +func parseRFC3659ListLine(line string) (*Entry, error) { + iSemicolon := strings.Index(line, ";") + iWhitespace := strings.Index(line, " ") + + if iSemicolon < 0 || iSemicolon > iWhitespace { + return nil, errUnsupportedListLine + } + + e := &Entry{ + Name: line[iWhitespace+1:], + } + + for _, field := range strings.Split(line[:iWhitespace-1], ";") { + i := strings.Index(field, "=") + if i < 1 { + return nil, errUnsupportedListLine + } + + key := field[:i] + value := field[i+1:] + + switch key { + case "modify": + var err error + e.Time, err = time.Parse("20060102150405", value) + if err != nil { + return nil, err + } + case "type": + switch value { + case "dir", "cdir", "pdir": + e.Type = EntryTypeFolder + case "file": + e.Type = EntryTypeFile + } + case "size": + e.setSize(value) + } + } + return e, nil +} + +// parseLsListLine parses a directory line in a format based on the output of +// the UNIX ls command. +func parseLsListLine(line string) (*Entry, error) { + + // Has the first field a length of 10 bytes? + if strings.IndexByte(line, ' ') != 10 { + return nil, errUnsupportedListLine + } + + scanner := newScanner(line) + fields := scanner.NextFields(6) + + if len(fields) < 6 { + return nil, errUnsupportedListLine + } + + if fields[1] == "folder" && fields[2] == "0" { + e := &Entry{ + Type: EntryTypeFolder, + Name: scanner.Remaining(), + } + if err := e.setTime(fields[3:6]); err != nil { + return nil, err + } + + return e, nil + } + + if fields[1] == "0" { + fields = append(fields, scanner.Next()) + e := &Entry{ + Type: EntryTypeFile, + Name: scanner.Remaining(), + } + + if err := e.setSize(fields[2]); err != nil { + return nil, err + } + if err := e.setTime(fields[4:7]); err != nil { + return nil, err + } + + return e, nil + } + + // Read two more fields + fields = append(fields, scanner.NextFields(2)...) + if len(fields) < 8 { + return nil, errUnsupportedListLine + } + + e := &Entry{ + Name: scanner.Remaining(), + } + switch fields[0][0] { + case '-': + e.Type = EntryTypeFile + if err := e.setSize(fields[4]); err != nil { + return nil, err + } + case 'd': + e.Type = EntryTypeFolder + case 'l': + e.Type = EntryTypeLink + default: + return nil, errors.New("Unknown entry type") + } + + if err := e.setTime(fields[5:8]); err != nil { + return nil, err + } + + return e, nil +} + +// parseDirListLine parses a directory line in a format based on the output of +// the MS-DOS DIR command. +func parseDirListLine(line string) (*Entry, error) { + e := &Entry{} + var err error + + // Try various time formats that DIR might use, and stop when one works. + for _, format := range dirTimeFormats { + if len(line) > len(format) { + e.Time, err = time.Parse(format, line[:len(format)]) + if err == nil { + line = line[len(format):] + break + } + } + } + if err != nil { + // None of the time formats worked. + return nil, errUnsupportedListLine + } + + line = strings.TrimLeft(line, " ") + if strings.HasPrefix(line, "") { + e.Type = EntryTypeFolder + line = strings.TrimPrefix(line, "") + } else { + space := strings.Index(line, " ") + if space == -1 { + return nil, errUnsupportedListLine + } + e.Size, err = strconv.ParseUint(line[:space], 10, 64) + if err != nil { + return nil, errUnsupportedListLine + } + e.Type = EntryTypeFile + line = line[space:] + } + + e.Name = strings.TrimLeft(line, " ") + return e, nil +} + +// parseListLine parses the various non-standard format returned by the LIST +// FTP command. +func parseListLine(line string) (*Entry, error) { + for _, f := range listLineParsers { + e, err := f(line) + if err != errUnsupportedListLine { + return e, err + } + } + return nil, errUnsupportedListLine +} + +func (e *Entry) setSize(str string) (err error) { + e.Size, err = strconv.ParseUint(str, 0, 64) + return +} + +func (e *Entry) setTime(fields []string) (err error) { + var timeStr string + if strings.Contains(fields[2], ":") { // this year + thisYear, _, _ := time.Now().Date() + timeStr = fields[1] + " " + fields[0] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[2] + " GMT" + } else { // not this year + if len(fields[2]) != 4 { + return errors.New("Invalid year format in time string") + } + timeStr = fields[1] + " " + fields[0] + " " + fields[2][2:4] + " 00:00 GMT" + } + e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr) + return +} diff --git a/vendor/github.com/jlaffaye/ftp/parse_test.go b/vendor/github.com/jlaffaye/ftp/parse_test.go new file mode 100644 index 000000000..ab44c3848 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/parse_test.go @@ -0,0 +1,104 @@ +package ftp + +import ( + "testing" + "time" +) + +var thisYear, _, _ = time.Now().Date() + +type line struct { + line string + name string + size uint64 + entryType EntryType + time time.Time +} + +type unsupportedLine struct { + line string + err string +} + +var listTests = []line{ + // UNIX ls -l style + {"drwxr-xr-x 3 110 1002 3 Dec 02 2009 pub", "pub", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, + {"drwxr-xr-x 3 110 1002 3 Dec 02 2009 p u b", "p u b", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, + {"-rw-r--r-- 1 marketwired marketwired 12016 Mar 16 2016 2016031611G087802-001.newsml", "2016031611G087802-001.newsml", 12016, EntryTypeFile, time.Date(2016, time.March, 16, 0, 0, 0, 0, time.UTC)}, + + {"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 fileName", "fileName", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, + {"lrwxrwxrwx 1 root other 7 Jan 25 00:17 bin -> usr/bin", "bin -> usr/bin", 0, EntryTypeLink, time.Date(thisYear, time.January, 25, 0, 17, 0, 0, time.UTC)}, + + // Another ls style + {"drwxr-xr-x folder 0 Aug 15 05:49 !!!-Tipp des Haus!", "!!!-Tipp des Haus!", 0, EntryTypeFolder, time.Date(thisYear, time.August, 15, 5, 49, 0, 0, time.UTC)}, + {"drwxrwxrwx folder 0 Aug 11 20:32 P0RN", "P0RN", 0, EntryTypeFolder, time.Date(thisYear, time.August, 11, 20, 32, 0, 0, time.UTC)}, + {"-rw-r--r-- 0 18446744073709551615 18446744073709551615 Nov 16 2006 VIDEO_TS.VOB", "VIDEO_TS.VOB", 18446744073709551615, EntryTypeFile, time.Date(2006, time.November, 16, 0, 0, 0, 0, time.UTC)}, + + // Microsoft's FTP servers for Windows + {"---------- 1 owner group 1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", 1803128, EntryTypeFile, time.Date(thisYear, time.July, 10, 10, 18, 0, 0, time.UTC)}, + {"d--------- 1 owner group 0 May 9 19:45 Softlib", "Softlib", 0, EntryTypeFolder, time.Date(thisYear, time.May, 9, 19, 45, 0, 0, time.UTC)}, + + // WFTPD for MSDOS + {"-rwxrwxrwx 1 noone nogroup 322 Aug 19 1996 message.ftp", "message.ftp", 322, EntryTypeFile, time.Date(1996, time.August, 19, 0, 0, 0, 0, time.UTC)}, + + // RFC3659 format: https://tools.ietf.org/html/rfc3659#section-7 + {"modify=20150813224845;perm=fle;type=cdir;unique=119FBB87U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; .", ".", 0, EntryTypeFolder, time.Date(2015, time.August, 13, 22, 48, 45, 0, time.UTC)}, + {"modify=20150813224845;perm=fle;type=pdir;unique=119FBB87U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; ..", "..", 0, EntryTypeFolder, time.Date(2015, time.August, 13, 22, 48, 45, 0, time.UTC)}, + {"modify=20150806235817;perm=fle;type=dir;unique=1B20F360U4;UNIX.group=0;UNIX.mode=0755;UNIX.owner=0; movies", "movies", 0, EntryTypeFolder, time.Date(2015, time.August, 6, 23, 58, 17, 0, time.UTC)}, + {"modify=20150814172949;perm=flcdmpe;type=dir;unique=85A0C168U4;UNIX.group=0;UNIX.mode=0777;UNIX.owner=0; _upload", "_upload", 0, EntryTypeFolder, time.Date(2015, time.August, 14, 17, 29, 49, 0, time.UTC)}, + {"modify=20150813175250;perm=adfr;size=951;type=file;unique=119FBB87UE;UNIX.group=0;UNIX.mode=0644;UNIX.owner=0; welcome.msg", "welcome.msg", 951, EntryTypeFile, time.Date(2015, time.August, 13, 17, 52, 50, 0, time.UTC)}, + + // DOS DIR command output + {"08-07-15 07:50PM 718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, time.Date(2015, time.August, 7, 19, 50, 0, 0, time.UTC)}, + {"08-10-15 02:04PM Billing", "Billing", 0, EntryTypeFolder, time.Date(2015, time.August, 10, 14, 4, 0, 0, time.UTC)}, + + // dir and file names that contain multiple spaces + {"drwxr-xr-x 3 110 1002 3 Dec 02 2009 spaces dir name", "spaces dir name", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, + {"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 file name", "file name", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, + {"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 foo bar ", " foo bar ", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)}, +} + +// Not supported, we expect a specific error message +var listTestsFail = []unsupportedLine{ + {"d [R----F--] supervisor 512 Jan 16 18:53 login", "Unsupported LIST line"}, + {"- [R----F--] rhesus 214059 Oct 20 15:27 cx.exe", "Unsupported LIST line"}, + {"drwxr-xr-x 3 110 1002 3 Dec 02 209 pub", "Invalid year format in time string"}, + {"modify=20150806235817;invalid;UNIX.owner=0; movies", "Unsupported LIST line"}, + {"Zrwxrwxrwx 1 root other 7 Jan 25 00:17 bin -> usr/bin", "Unknown entry type"}, + {"total 1", "Unsupported LIST line"}, + {"", "Unsupported LIST line"}, +} + +func TestParseValidListLine(t *testing.T) { + for _, lt := range listTests { + entry, err := parseListLine(lt.line) + if err != nil { + t.Errorf("parseListLine(%v) returned err = %v", lt.line, err) + continue + } + if entry.Name != lt.name { + t.Errorf("parseListLine(%v).Name = '%v', want '%v'", lt.line, entry.Name, lt.name) + } + if entry.Type != lt.entryType { + t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.Type, lt.entryType) + } + if entry.Size != lt.size { + t.Errorf("parseListLine(%v).Size = %v, want %v", lt.line, entry.Size, lt.size) + } + if entry.Time.Unix() != lt.time.Unix() { + t.Errorf("parseListLine(%v).Time = %v, want %v", lt.line, entry.Time, lt.time) + } + } +} + +func TestParseUnsupportedListLine(t *testing.T) { + for _, lt := range listTestsFail { + _, err := parseListLine(lt.line) + if err == nil { + t.Errorf("parseListLine(%v) expected to fail", lt.line) + } + if err.Error() != lt.err { + t.Errorf("parseListLine(%v) expected to fail with error: '%s'; was: '%s'", lt.line, lt.err, err.Error()) + } + } +} diff --git a/vendor/github.com/jlaffaye/ftp/scanner.go b/vendor/github.com/jlaffaye/ftp/scanner.go new file mode 100644 index 000000000..0dcc8ae64 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/scanner.go @@ -0,0 +1,58 @@ +package ftp + +// A scanner for fields delimited by one or more whitespace characters +type scanner struct { + bytes []byte + position int +} + +// newScanner creates a new scanner +func newScanner(str string) *scanner { + return &scanner{ + bytes: []byte(str), + } +} + +// NextFields returns the next `count` fields +func (s *scanner) NextFields(count int) []string { + fields := make([]string, 0, count) + for i := 0; i < count; i++ { + if field := s.Next(); field != "" { + fields = append(fields, field) + } else { + break + } + } + return fields +} + +// Next returns the next field +func (s *scanner) Next() string { + sLen := len(s.bytes) + + // skip trailing whitespace + for s.position < sLen { + if s.bytes[s.position] != ' ' { + break + } + s.position++ + } + + start := s.position + + // skip non-whitespace + for s.position < sLen { + if s.bytes[s.position] == ' ' { + s.position++ + return string(s.bytes[start : s.position-1]) + } + s.position++ + } + + return string(s.bytes[start:s.position]) +} + +// Remaining returns the remaining string +func (s *scanner) Remaining() string { + return string(s.bytes[s.position:len(s.bytes)]) +} diff --git a/vendor/github.com/jlaffaye/ftp/scanner_test.go b/vendor/github.com/jlaffaye/ftp/scanner_test.go new file mode 100644 index 000000000..86ff4bf86 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/scanner_test.go @@ -0,0 +1,28 @@ +package ftp + +import "testing" +import "github.com/stretchr/testify/assert" + +func TestScanner(t *testing.T) { + assert := assert.New(t) + + s := newScanner("foo bar x y") + assert.Equal("foo", s.Next()) + assert.Equal(" bar x y", s.Remaining()) + assert.Equal("bar", s.Next()) + assert.Equal("x y", s.Remaining()) + assert.Equal("x", s.Next()) + assert.Equal(" y", s.Remaining()) + assert.Equal("y", s.Next()) + assert.Equal("", s.Next()) + assert.Equal("", s.Remaining()) +} + +func TestScannerEmpty(t *testing.T) { + assert := assert.New(t) + + s := newScanner("") + assert.Equal("", s.Next()) + assert.Equal("", s.Next()) + assert.Equal("", s.Remaining()) +} diff --git a/vendor/github.com/jlaffaye/ftp/status.go b/vendor/github.com/jlaffaye/ftp/status.go new file mode 100644 index 000000000..e90ca6211 --- /dev/null +++ b/vendor/github.com/jlaffaye/ftp/status.go @@ -0,0 +1,106 @@ +package ftp + +// FTP status codes, defined in RFC 959 +const ( + StatusInitiating = 100 + StatusRestartMarker = 110 + StatusReadyMinute = 120 + StatusAlreadyOpen = 125 + StatusAboutToSend = 150 + + StatusCommandOK = 200 + StatusCommandNotImplemented = 202 + StatusSystem = 211 + StatusDirectory = 212 + StatusFile = 213 + StatusHelp = 214 + StatusName = 215 + StatusReady = 220 + StatusClosing = 221 + StatusDataConnectionOpen = 225 + StatusClosingDataConnection = 226 + StatusPassiveMode = 227 + StatusLongPassiveMode = 228 + StatusExtendedPassiveMode = 229 + StatusLoggedIn = 230 + StatusLoggedOut = 231 + StatusLogoutAck = 232 + StatusRequestedFileActionOK = 250 + StatusPathCreated = 257 + + StatusUserOK = 331 + StatusLoginNeedAccount = 332 + StatusRequestFilePending = 350 + + StatusNotAvailable = 421 + StatusCanNotOpenDataConnection = 425 + StatusTransfertAborted = 426 + StatusInvalidCredentials = 430 + StatusHostUnavailable = 434 + StatusFileActionIgnored = 450 + StatusActionAborted = 451 + Status452 = 452 + + StatusBadCommand = 500 + StatusBadArguments = 501 + StatusNotImplemented = 502 + StatusBadSequence = 503 + StatusNotImplementedParameter = 504 + StatusNotLoggedIn = 530 + StatusStorNeedAccount = 532 + StatusFileUnavailable = 550 + StatusPageTypeUnknown = 551 + StatusExceededStorage = 552 + StatusBadFileName = 553 +) + +var statusText = map[int]string{ + // 200 + StatusCommandOK: "Command okay.", + StatusCommandNotImplemented: "Command not implemented, superfluous at this site.", + StatusSystem: "System status, or system help reply.", + StatusDirectory: "Directory status.", + StatusFile: "File status.", + StatusHelp: "Help message.", + StatusName: "", + StatusReady: "Service ready for new user.", + StatusClosing: "Service closing control connection.", + StatusDataConnectionOpen: "Data connection open; no transfer in progress.", + StatusClosingDataConnection: "Closing data connection. Requested file action successful.", + StatusPassiveMode: "Entering Passive Mode.", + StatusLongPassiveMode: "Entering Long Passive Mode.", + StatusExtendedPassiveMode: "Entering Extended Passive Mode.", + StatusLoggedIn: "User logged in, proceed.", + StatusLoggedOut: "User logged out; service terminated.", + StatusLogoutAck: "Logout command noted, will complete when transfer done.", + StatusRequestedFileActionOK: "Requested file action okay, completed.", + StatusPathCreated: "Path created.", + + // 300 + StatusUserOK: "User name okay, need password.", + StatusLoginNeedAccount: "Need account for login.", + StatusRequestFilePending: "Requested file action pending further information.", + + // 400 + StatusNotAvailable: "Service not available, closing control connection.", + StatusCanNotOpenDataConnection: "Can't open data connection.", + StatusTransfertAborted: "Connection closed; transfer aborted.", + StatusInvalidCredentials: "Invalid username or password.", + StatusHostUnavailable: "Requested host unavailable.", + StatusFileActionIgnored: "Requested file action not taken.", + StatusActionAborted: "Requested action aborted. Local error in processing.", + Status452: "Insufficient storage space in system.", + + // 500 + StatusBadCommand: "Command unrecognized.", + StatusBadArguments: "Syntax error in parameters or arguments.", + StatusNotImplemented: "Command not implemented.", + StatusBadSequence: "Bad sequence of commands.", + StatusNotImplementedParameter: "Command not implemented for that parameter.", + StatusNotLoggedIn: "Not logged in.", + StatusStorNeedAccount: "Need account for storing files.", + StatusFileUnavailable: "File unavailable.", + StatusPageTypeUnknown: "Page type unknown.", + StatusExceededStorage: "Exceeded storage allocation.", + StatusBadFileName: "File name not allowed.", +}