diff --git a/.travis.yml b/.travis.yml index 1b58030..4c8cec9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ go: - 1.8.1 env: - FTP_SERVER=vsftpd + - FTP_SERVER=vsftpd_implicit_tls - FTP_SERVER=proftpd before_install: - sudo $TRAVIS_BUILD_DIR/.travis/prepare.sh "$FTP_SERVER" diff --git a/.travis/prepare.sh b/.travis/prepare.sh index 40970d9..08445bf 100755 --- a/.travis/prepare.sh +++ b/.travis/prepare.sh @@ -1,18 +1,31 @@ #!/bin/sh -e +mkdir --mode 0777 -p /var/ftp/incoming + case "$1" in proftpd) mkdir -p /etc/proftpd/conf.d/ cp $TRAVIS_BUILD_DIR/.travis/proftpd.conf /etc/proftpd/conf.d/ + apt-get install -qq "$1" ;; vsftpd) cp $TRAVIS_BUILD_DIR/.travis/vsftpd.conf /etc/vsftpd.conf + apt-get install -qq "$1" + ;; + vsftpd_implicit_tls) + openssl req \ + -new \ + -newkey rsa:1024 \ + -days 365 \ + -nodes \ + -x509 \ + -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" \ + -keyout /etc/ssl/certs/vsftpd.pem \ + -out /etc/ssl/certs/vsftpd.pem + cp $TRAVIS_BUILD_DIR/.travis/vsftpd_implicit_tls.conf /etc/vsftpd.conf + apt-get install -qq vsftpd ;; *) echo "unknown software: $1" exit 1 esac - -mkdir --mode 0777 -p /var/ftp/incoming - -apt-get install -qq "$1" diff --git a/.travis/vsftpd_implicit_tls.conf b/.travis/vsftpd_implicit_tls.conf new file mode 100644 index 0000000..c940e98 --- /dev/null +++ b/.travis/vsftpd_implicit_tls.conf @@ -0,0 +1,33 @@ +# 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 + +force_local_logins_ssl=YES +force_local_data_ssl=YES +ssl_enable=YES +implicit_ssl=YES +listen_port=21 +ssl_tlsv1=YES +ssl_sslv2=NO +ssl_sslv3=NO +rsa_cert_file=/etc/ssl/certs/vsftpd.pem +rsa_private_key_file=/etc/ssl/certs/vsftpd.pem +require_ssl_reuse=NO +ssl_ciphers=HIGH +xferlog_enable=yes +log_ftp_protocol=true +xferlog_file=/var/log/vsftpd.log +debug_ssl=true +allow_anon_ssl=true \ No newline at end of file diff --git a/client_test.go b/client_test.go index ed1ecaa..865be12 100644 --- a/client_test.go +++ b/client_test.go @@ -2,8 +2,10 @@ package ftp import ( "bytes" + "crypto/tls" "io/ioutil" "net/textproto" + "os" "strings" "testing" "time" @@ -14,20 +16,39 @@ const ( testDir = "mydir" ) +func isTLSServer() bool { + return os.Getenv("FTP_SERVER") == "vsftpd_implicit_tls" +} + +func getConnection() (*ServerConn, error) { + if isTLSServer() { + return DialImplicitTLS("localhost:21", &tls.Config{InsecureSkipVerify: true}) + } else { + return DialTimeout("localhost:21", 5*time.Second) + } +} + func TestConnPASV(t *testing.T) { - testConn(t, true) + testConn(t, true, false) } func TestConnEPSV(t *testing.T) { - testConn(t, false) + testConn(t, false, false) } -func testConn(t *testing.T, disableEPSV bool) { +func TestConnTLS(t *testing.T) { + if !isTLSServer() { + t.Skip("skipping test in non TLS server env.") + } + testConn(t, false, true) +} + +func testConn(t *testing.T, disableEPSV bool, implicitTLS bool) { if testing.Short() { t.Skip("skipping test in short mode.") } + c, err := getConnection() - c, err := DialTimeout("localhost:21", 5*time.Second) if err != nil { t.Fatal(err) } @@ -207,6 +228,9 @@ func TestConnIPv6(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } + if isTLSServer() { + t.Skip("skipping test in TLS mode.") + } c, err := DialTimeout("[::1]:21", 5*time.Second) if err != nil { @@ -228,7 +252,7 @@ func TestConnIPv6(t *testing.T) { // TestConnect tests the legacy Connect function func TestConnect(t *testing.T) { - if testing.Short() { + if testing.Short() || isTLSServer() { t.Skip("skipping test in short mode.") } @@ -244,6 +268,9 @@ func TestTimeout(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } + if isTLSServer() { + t.Skip("skipping test in TLS mode.") + } c, err := DialTimeout("localhost:2121", 1*time.Second) if err == nil { @@ -251,13 +278,12 @@ func TestTimeout(t *testing.T) { 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) + c, err := getConnection() if err != nil { t.Fatal(err) } @@ -273,7 +299,7 @@ func TestDeleteDirRecur(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } - c, err := DialTimeout("localhost:21", 5*time.Second) + c, err := getConnection() if err != nil { t.Fatal(err) } @@ -355,7 +381,7 @@ func TestFileDeleteDirRecur(t *testing.T) { t.Skip("skipping test in short mode.") } - c, err := DialTimeout("localhost:21", 5*time.Second) + c, err := getConnection() if err != nil { t.Fatal(err) } @@ -414,7 +440,7 @@ func TestMissingFolderDeleteDirRecur(t *testing.T) { t.Skip("skipping test in short mode.") } - c, err := DialTimeout("localhost:21", 5*time.Second) + c, err := getConnection() if err != nil { t.Fatal(err) } diff --git a/ftp.go b/ftp.go index 1803423..a1417af 100644 --- a/ftp.go +++ b/ftp.go @@ -5,6 +5,7 @@ package ftp import ( "bufio" + "crypto/tls" "errors" "io" "net" @@ -62,6 +63,15 @@ func Dial(addr string) (*ServerConn, error) { return DialTimeout(addr, 0) } +// Dial a ftps server with implicit TLS +func DialImplicitTLS(addr string, config *tls.Config) (*ServerConn, error) { + tconn, err := tls.Dial("tcp", addr, config) + if err != nil { + return nil, err + } + return dialServer(tconn, 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 @@ -71,7 +81,10 @@ func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) { if err != nil { return nil, err } + return dialServer(tconn, timeout) +} +func dialServer(tconn net.Conn, timeout time.Duration) (*ServerConn, error) { // 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()