[前][次][番号順一覧][スレッド一覧]

ruby-changes:44761

From: shugo <ko1@a...>
Date: Sat, 19 Nov 2016 11:29:27 +0900 (JST)
Subject: [ruby-changes:44761] shugo:r56834 (trunk): Support TLS and hash styles options for Net::FTP.new.

shugo	2016-11-19 11:29:23 +0900 (Sat, 19 Nov 2016)

  New Revision: 56834

  https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=56834

  Log:
    Support TLS and hash styles options for Net::FTP.new.
    
    If the :ssl options is specified, the control connection is protected with
    TLS in the manner described in RFC 4217.  Data connections are also
    protected with TLS unless the :private_data_connection is set to false.

  Modified files:
    trunk/NEWS
    trunk/lib/net/ftp.rb
    trunk/test/net/ftp/test_ftp.rb
Index: test/net/ftp/test_ftp.rb
===================================================================
--- test/net/ftp/test_ftp.rb	(revision 56833)
+++ test/net/ftp/test_ftp.rb	(revision 56834)
@@ -8,6 +8,9 @@ require "tempfile" https://github.com/ruby/ruby/blob/trunk/test/net/ftp/test_ftp.rb#L8
 
 class FTPTest < Test::Unit::TestCase
   SERVER_ADDR = "127.0.0.1"
+  CA_FILE = File.expand_path("../imap/cacert.pem", __dir__)
+  SERVER_KEY = File.expand_path("../imap/server.key", __dir__)
+  SERVER_CERT = File.expand_path("../imap/server.crt", __dir__)
 
   def setup
     @thread = nil
@@ -219,6 +222,62 @@ class FTPTest < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/net/ftp/test_ftp.rb#L222
     end
   end
 
+  def test_implicit_login
+    commands = []
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("332 Need account for login.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      begin
+        ftp = Net::FTP.new(SERVER_ADDR,
+                           port: server.port,
+                           user: "foo",
+                           passwd: "bar",
+                           acct: "baz")
+        assert_equal("USER foo\r\n", commands.shift)
+        assert_equal("PASS bar\r\n", commands.shift)
+        assert_equal("ACCT baz\r\n", commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close if ftp
+      end
+    ensure
+      server.close
+    end
+  end
+
+  def test_s_open
+    commands = []
+    server = create_ftp_server { |sock|
+      sock.print("220 (test_ftp).\r\n")
+      commands.push(sock.gets)
+      sock.print("331 Please specify the password.\r\n")
+      commands.push(sock.gets)
+      sock.print("230 Login successful.\r\n")
+      commands.push(sock.gets)
+      sock.print("200 Switching to Binary mode.\r\n")
+    }
+    begin
+      Net::FTP.open(SERVER_ADDR, port: server.port, user: "anonymous") do
+      end
+      assert_equal("USER anonymous\r\n", commands.shift)
+      assert_equal("PASS anonymous@\r\n", commands.shift)
+      assert_equal("TYPE I\r\n", commands.shift)
+      assert_equal(nil, commands.shift)
+    ensure
+      server.close
+    end
+  end
+
   # TODO: How can we test open_timeout?  sleep before accept cannot delay
   # connections.
   def _test_open_timeout_exceeded
@@ -1644,6 +1703,362 @@ EOF https://github.com/ruby/ruby/blob/trunk/test/net/ftp/test_ftp.rb#L1703
     end
   end
 
+  if defined?(OpenSSL::SSL)
+    def test_tls_unknown_ca
+      assert_raise(OpenSSL::SSL::SSLError) do
+        tls_test do |port|
+          begin
+            Net::FTP.new("localhost",
+                         :port => port,
+                         :ssl => true)
+          rescue SystemCallError
+            skip $!
+          end
+        end
+      end
+    end
+
+    def test_tls_with_ca_file
+      assert_nothing_raised do
+        tls_test do |port|
+          begin
+            Net::FTP.new("localhost",
+                         :port => port,
+                         :ssl => { :ca_file => CA_FILE })
+          rescue SystemCallError
+            skip $!
+          end
+        end
+      end
+    end
+
+    def test_tls_verify_none
+      assert_nothing_raised do
+        tls_test do |port|
+          Net::FTP.new(SERVER_ADDR,
+                       :port => port,
+                       :ssl => { :verify_mode => OpenSSL::SSL::VERIFY_NONE })
+        end
+      end
+    end
+
+    def test_tls_post_connection_check
+      assert_raise(OpenSSL::SSL::SSLError) do
+        tls_test do |port|
+          # SERVER_ADDR is different from the hostname in the certificate,
+          # so the following code should raise a SSLError.
+          Net::FTP.new(SERVER_ADDR,
+                       :port => port,
+                       :ssl => { :ca_file => CA_FILE })
+        end
+      end
+    end
+
+    def test_active_private_data_connection
+      server = TCPServer.new(SERVER_ADDR, 0)
+      port = server.addr[1]
+      commands = []
+      binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+      @thread = Thread.start do
+        sock = server.accept
+        begin
+          sock.print("220 (test_ftp).\r\n")
+          commands.push(sock.gets)
+          sock.print("234 AUTH success.\r\n")
+          ctx = OpenSSL::SSL::SSLContext.new
+          ctx.ca_file = CA_FILE
+          ctx.key = File.open(SERVER_KEY) { |f|
+            OpenSSL::PKey::RSA.new(f)
+          }
+          ctx.cert = File.open(SERVER_CERT) { |f|
+            OpenSSL::X509::Certificate.new(f)
+          }
+          sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
+          sock.sync_close = true
+          begin
+            sock.accept
+            commands.push(sock.gets)
+            sock.print("200 PSBZ success.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 PROT success.\r\n")
+            commands.push(sock.gets)
+            sock.print("331 Please specify the password.\r\n")
+            commands.push(sock.gets)
+            sock.print("230 Login successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 Switching to Binary mode.\r\n")
+            line = sock.gets
+            commands.push(line)
+            port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+            host = port_args[0, 4].join(".")
+            port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+            sock.print("200 PORT command successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+            conn = TCPSocket.new(host, port)
+            conn = OpenSSL::SSL::SSLSocket.new(conn, ctx)
+            conn.sync_close = true
+            conn.accept
+            binary_data.scan(/.{1,1024}/nm) do |s|
+              conn.print(s)
+            end
+            conn.close
+            sock.print("226 Transfer complete.\r\n")
+          rescue OpenSSL::SSL::SSLError
+          end
+        ensure
+          sock.close
+          server.close
+        end
+      end
+      ftp = Net::FTP.new("localhost",
+                         port: port,
+                         ssl: { ca_file: CA_FILE },
+                         passive: false)
+      begin
+        assert_equal("AUTH TLS\r\n", commands.shift)
+        assert_equal("PBSZ 0\r\n", commands.shift)
+        assert_equal("PROT P\r\n", commands.shift)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ftp.getbinaryfile("foo", nil)
+        assert_equal(binary_data, buf)
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close
+      end
+    end
+
+    def test_passive_private_data_connection
+      server = TCPServer.new(SERVER_ADDR, 0)
+      port = server.addr[1]
+      commands = []
+      binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+      @thread = Thread.start do
+        sock = server.accept
+        begin
+          sock.print("220 (test_ftp).\r\n")
+          commands.push(sock.gets)
+          sock.print("234 AUTH success.\r\n")
+          ctx = OpenSSL::SSL::SSLContext.new
+          ctx.ca_file = CA_FILE
+          ctx.key = File.open(SERVER_KEY) { |f|
+            OpenSSL::PKey::RSA.new(f)
+          }
+          ctx.cert = File.open(SERVER_CERT) { |f|
+            OpenSSL::X509::Certificate.new(f)
+          }
+          sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
+          sock.sync_close = true
+          begin
+            sock.accept
+            commands.push(sock.gets)
+            sock.print("200 PSBZ success.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 PROT success.\r\n")
+            commands.push(sock.gets)
+            sock.print("331 Please specify the password.\r\n")
+            commands.push(sock.gets)
+            sock.print("230 Login successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 Switching to Binary mode.\r\n")
+            commands.push(sock.gets)
+            data_server = TCPServer.new(SERVER_ADDR, 0)
+            port = data_server.local_address.ip_port
+            sock.printf("227 Entering Passive Mode (127,0,0,1,%s).\r\n",
+                        port.divmod(256).join(","))
+            commands.push(sock.gets)
+            sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+            conn = data_server.accept
+            conn = OpenSSL::SSL::SSLSocket.new(conn, ctx)
+            conn.sync_close = true
+            conn.accept
+            binary_data.scan(/.{1,1024}/nm) do |s|
+              conn.print(s)
+            end
+            conn.close
+            data_server.close
+            sock.print("226 Transfer complete.\r\n")
+          rescue OpenSSL::SSL::SSLError
+          end
+        ensure
+          sock.close
+          server.close
+        end
+      end
+      ftp = Net::FTP.new("localhost",
+                         port: port,
+                         ssl: { ca_file: CA_FILE },
+                         passive: true)
+      begin
+        assert_equal("AUTH TLS\r\n", commands.shift)
+        assert_equal("PBSZ 0\r\n", commands.shift)
+        assert_equal("PROT P\r\n", commands.shift)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ftp.getbinaryfile("foo", nil)
+        assert_equal(binary_data, buf)
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
+        assert_equal("PASV\r\n", commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close
+      end
+    end
+
+    def test_active_clear_data_connection
+      server = TCPServer.new(SERVER_ADDR, 0)
+      port = server.addr[1]
+      commands = []
+      binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+      @thread = Thread.start do
+        sock = server.accept
+        begin
+          sock.print("220 (test_ftp).\r\n")
+          commands.push(sock.gets)
+          sock.print("234 AUTH success.\r\n")
+          ctx = OpenSSL::SSL::SSLContext.new
+          ctx.ca_file = CA_FILE
+          ctx.key = File.open(SERVER_KEY) { |f|
+            OpenSSL::PKey::RSA.new(f)
+          }
+          ctx.cert = File.open(SERVER_CERT) { |f|
+            OpenSSL::X509::Certificate.new(f)
+          }
+          sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
+          sock.sync_close = true
+          begin
+            sock.accept
+            commands.push(sock.gets)
+            sock.print("331 Please specify the password.\r\n")
+            commands.push(sock.gets)
+            sock.print("230 Login successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 Switching to Binary mode.\r\n")
+            line = sock.gets
+            commands.push(line)
+            port_args = line.slice(/\APORT (.*)/, 1).split(/,/)
+            host = port_args[0, 4].join(".")
+            port = port_args[4, 2].map(&:to_i).inject {|x, y| (x << 8) + y}
+            sock.print("200 PORT command successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+            conn = TCPSocket.new(host, port)
+            binary_data.scan(/.{1,1024}/nm) do |s|
+              conn.print(s)
+            end
+            conn.close
+            sock.print("226 Transfer complete.\r\n")
+          rescue OpenSSL::SSL::SSLError
+          end
+        ensure
+          sock.close
+          server.close
+        end
+      end
+      ftp = Net::FTP.new("localhost",
+                         port: port,
+                         ssl: { ca_file: CA_FILE },
+                         private_data_connection: false,
+                         passive: false)
+      begin
+        assert_equal("AUTH TLS\r\n", commands.shift)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ftp.getbinaryfile("foo", nil)
+        assert_equal(binary_data, buf)
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
+        assert_match(/\APORT /, commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close
+      end
+    end
+
+    def test_passive_clear_data_connection
+      server = TCPServer.new(SERVER_ADDR, 0)
+      port = server.addr[1]
+      commands = []
+      binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3
+      @thread = Thread.start do
+        sock = server.accept
+        begin
+          sock.print("220 (test_ftp).\r\n")
+          commands.push(sock.gets)
+          sock.print("234 AUTH success.\r\n")
+          ctx = OpenSSL::SSL::SSLContext.new
+          ctx.ca_file = CA_FILE
+          ctx.key = File.open(SERVER_KEY) { |f|
+            OpenSSL::PKey::RSA.new(f)
+          }
+          ctx.cert = File.open(SERVER_CERT) { |f|
+            OpenSSL::X509::Certificate.new(f)
+          }
+          sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
+          sock.sync_close = true
+          begin
+            sock.accept
+            commands.push(sock.gets)
+            sock.print("331 Please specify the password.\r\n")
+            commands.push(sock.gets)
+            sock.print("230 Login successful.\r\n")
+            commands.push(sock.gets)
+            sock.print("200 Switching to Binary mode.\r\n")
+            commands.push(sock.gets)
+            data_server = TCPServer.new(SERVER_ADDR, 0)
+            port = data_server.local_address.ip_port
+            sock.printf("227 Entering Passive Mode (127,0,0,1,%s).\r\n",
+                        port.divmod(256).join(","))
+            commands.push(sock.gets)
+            sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n")
+            conn = data_server.accept
+            binary_data.scan(/.{1,1024}/nm) do |s|
+              conn.print(s)
+            end
+            conn.close
+            data_server.close
+            sock.print("226 Transfer complete.\r\n")
+          rescue OpenSSL::SSL::SSLError
+          end
+        ensure
+          sock.close
+          server.close
+        end
+      end
+      ftp = Net::FTP.new("localhost",
+                         port: port,
+                         ssl: { ca_file: CA_FILE },
+                         private_data_connection: false,
+                         passive: true)
+      begin
+        assert_equal("AUTH TLS\r\n", commands.shift)
+        ftp.login
+        assert_match(/\AUSER /, commands.shift)
+        assert_match(/\APASS /, commands.shift)
+        assert_equal("TYPE I\r\n", commands.shift)
+        buf = ftp.getbinaryfile("foo", nil)
+        assert_equal(binary_data, buf)
+        assert_equal(Encoding::ASCII_8BIT, buf.encoding)
+        assert_equal("PASV\r\n", commands.shift)
+        assert_equal("RETR foo\r\n", commands.shift)
+        assert_equal(nil, commands.shift)
+      ensure
+        ftp.close
+      end
+    end
+  end
+
   private
 
   def create_ftp_server(sleep_time = nil)
@@ -1667,4 +2082,45 @@ EOF https://github.com/ruby/ruby/blob/trunk/test/net/ftp/test_ftp.rb#L2082
     end
     return server
   end
+
+  def tls_test
+    server = TCPServer.new(SERVER_ADDR, 0)
+    port = server.addr[1]
+    commands = []
+    @thread = Thread.start do
+      sock = server.accept
+      begin
+        sock.print("220 (test_ftp).\r\n")
+        commands.push(sock.gets)
+        sock.print("234 AUTH success.\r\n")
+        ctx = OpenSSL::SSL::SSLContext.new
+        ctx.ca_file = CA_FILE
+        ctx.key = File.open(SERVER_KEY) { |f|
+          OpenSSL::PKey::RSA.new(f)
+        }
+        ctx.cert = File.open(SERVER_CERT) { |f|
+          OpenSSL::X509::Certificate.new(f)
+        }
+        sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
+        sock.sync_close = true
+        begin
+          sock.accept
+          commands.push(sock.gets)
+          sock.print("200 PSBZ success.\r\n")
+          commands.push(sock.gets)
+          sock.print("200 PROT success.\r\n")
+        rescue OpenSSL::SSL::SSLError
+        end
+      ensure
+        sock.close
+        server.close
+      end
+    end
+    ftp = yield(port)
+    ftp.close
+
+    assert_equal("AUTH TLS\r\n", commands.shift)
+    assert_equal("PBSZ 0\r\n", commands.shift)
+    assert_equal("PROT P\r\n", commands.shift)
+  end
 end
Index: lib/net/ftp.rb
===================================================================
--- lib/net/ftp.rb	(revision 56833)
+++ lib/net/ftp.rb	(revision 56834)
@@ -19,6 +19,10 @@ require "socket" https://github.com/ruby/ruby/blob/trunk/lib/net/ftp.rb#L19
 require "monitor"
 require "net/protocol"
 require "time"
+begin
+  require "openssl"
+rescue LoadError
+end
 
 module Net
 
@@ -75,6 +79,10 @@ module Net https://github.com/ruby/ruby/blob/trunk/lib/net/ftp.rb#L79
   #
   class FTP
     include MonitorMixin
+    if defined?(OpenSSL::SSL)
+      include OpenSSL
+      include SSL
+    end
 
     # :stopdoc:
     FTP_PORT = 21
@@ -143,38 +151,108 @@ module Net https://github.com/ruby/ruby/blob/trunk/lib/net/ftp.rb#L151
     # If a block is given, it is passed the +FTP+ object, which will be closed
     # when the block finishes, or when an exception is raised.
     #
-    def FTP.open(host, user = nil, passwd = nil, acct = nil)
+    def FTP.open(host, *args)
       if block_given?
-        ftp = new(host, user, passwd, acct)
+        ftp = new(host, *args)
         begin
           yield ftp
         ensure
           ftp.close
         end
       else
-        new(host, user, passwd, acct)
+        new(host, *args)
       end
     end
 
+    # :call-seq:
+    #    Net::FTP.new(host = nil, options = {})
     #
     # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
-    # is made. Additionally, if the +user+ is given, the given user name,
-    # password, and (optionally) account are used to log in.  See #login.
+    # is made.
+    #
+    # +options+ is an option hash, each key of which is a symbol.
+    #
+    # The available options are:
+    #
+    # port::    Port number (default value is 21)
+    # ssl::     If options[:ssl] is true, then an attempt will be made
+    #           to use SSL (now TLS) to connect to the server.  For this to
+    #           work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions
+    #           need to be installed.  If options[:ssl] is a hash, it's
+    #           passed to OpenSSL::SSL::SSLContext#set_params as parameters.
+    # private_data_connection::  If true, TLS is used for data connections.
+    #                            Default: +true+ when options[:ssl] is true.
+    # user::    Username for login.  If options[:user] is the string
+    #           "anonymous" and the options[:password] is +nil+,
+    #           "anonymous@" is used as a password.  If options[:user] is
+    #           +nil+, 
+    # passwd::  Password for login.
+    # acct::    Account information for ACCT.
+    # passive:: When +true+, the connection is in passive mode. Default: +true+.
+    # debug_mode::  When +true+, all tra (... truncated)

--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]