# frozen_string_literal: true require "net/ftp" require "test/unit" require "ostruct" require "stringio" require "tempfile" require "tmpdir" class FTPTest < Test::Unit::TestCase SERVER_NAME = "localhost" SERVER_ADDR = begin Addrinfo.getaddrinfo(SERVER_NAME, 0, nil, :STREAM)[0].ip_address rescue SocketError "127.0.0.1" end CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) def setup @thread = nil @default_passive = Net::FTP.default_passive Net::FTP.default_passive = false end def teardown Net::FTP.default_passive = @default_passive if @thread @thread.join end end def test_not_connected ftp = Net::FTP.new assert_raise(Net::FTPConnectionError) do ftp.quit end end def test_closed_when_not_connected ftp = Net::FTP.new assert_equal(true, ftp.closed?) assert_nothing_raised(Net::FTPConnectionError) do ftp.close end end def test_connect_fail server = create_ftp_server { |sock| sock.print("421 Service not available, closing control connection.\r\n") } begin ftp = Net::FTP.new assert_raise(Net::FTPTempError){ ftp.connect(SERVER_ADDR, server.port) } ensure ftp.close if ftp server.close end end def test_parse227 ftp = Net::FTP.new host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)") assert_equal("192.168.0.1", host) assert_equal(3106, port) assert_raise(Net::FTPReplyError) do ftp.send(:parse227, "500 Syntax error") end assert_raise(Net::FTPProtoError) do ftp.send(:parse227, "227 Entering Passive Mode") end assert_raise(Net::FTPProtoError) do ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34,56)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse227, "227 ) foo bar (") end end def test_parse228 ftp = Net::FTP.new host, port = ftp.send(:parse228, "228 Entering Long Passive Mode (4,4,192,168,0,1,2,12,34)") assert_equal("192.168.0.1", host) assert_equal(3106, port) host, port = ftp.send(:parse228, "228 Entering Long Passive Mode (6,16,16,128,0,0,0,0,0,0,0,8,8,0,32,12,65,122,2,12,34)") assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) assert_equal(3106, port) assert_raise(Net::FTPReplyError) do ftp.send(:parse228, "500 Syntax error") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Passive Mode") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Long Passive Mode (6,4,192,168,0,1,2,12,34)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Long Passive Mode (4,4,192,168,0,1,3,12,34,56)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Long Passive Mode (4,16,16,128,0,0,0,0,0,0,0,8,8,0,32,12,65,122,2,12,34)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Long Passive Mode (6,16,16,128,0,0,0,0,0,0,0,8,8,0,32,12,65,122,3,12,34,56)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse228, "228 Entering Long Passive Mode (6,16,16,128,0,0,0,0,0,0,0,8,8,0,32,12,65,122,2,12,34,56)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse227, "227 ) foo bar (") end end def test_parse229 ftp = Net::FTP.new sock = OpenStruct.new sock.remote_address = OpenStruct.new sock.remote_address.ip_address = "1080:0000:0000:0000:0008:0800:200c:417a" ftp.instance_variable_set(:@bare_sock, sock) host, port = ftp.send(:parse229, "229 Entering Passive Mode (|||3106|)") assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) assert_equal(3106, port) host, port = ftp.send(:parse229, "229 Entering Passive Mode (!!!3106!)") assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) assert_equal(3106, port) host, port = ftp.send(:parse229, "229 Entering Passive Mode (~~~3106~)") assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) assert_equal(3106, port) assert_raise(Net::FTPReplyError) do ftp.send(:parse229, "500 Syntax error") end assert_raise(Net::FTPProtoError) do ftp.send(:parse229, "229 Entering Passive Mode") end assert_raise(Net::FTPProtoError) do ftp.send(:parse229, "229 Entering Passive Mode (|!!3106!)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse229, "229 Entering Passive Mode ( 3106 )") end assert_raise(Net::FTPProtoError) do ftp.send(:parse229, "229 Entering Passive Mode (\x7f\x7f\x7f3106\x7f)") end assert_raise(Net::FTPProtoError) do ftp.send(:parse229, "229 ) foo bar (") end end def test_parse_pasv_port ftp = Net::FTP.new assert_equal(12, ftp.send(:parse_pasv_port, "12")) assert_equal(3106, ftp.send(:parse_pasv_port, "12,34")) assert_equal(795192, ftp.send(:parse_pasv_port, "12,34,56")) assert_equal(203569230, ftp.send(:parse_pasv_port, "12,34,56,78")) end def test_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("230 Login successful.\r\n") commands.push(sock.gets) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, 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_login_fail1 commands = [] server = create_ftp_server { |sock| sock.print("220 (test_ftp).\r\n") commands.push(sock.gets) sock.print("502 Command not implemented.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) assert_raise(Net::FTPPermError){ ftp.login } ensure ftp.close if ftp end ensure server.close end end def test_login_fail2 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("530 Not logged in.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) assert_raise(Net::FTPPermError){ ftp.login } ensure ftp.close if ftp end ensure server.close 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, username: "foo", password: "bar", account: "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, username: "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 def test_s_new_timeout_options ftp = Net::FTP.new assert_equal(nil, ftp.open_timeout) assert_equal(60, ftp.read_timeout) ftp = Net::FTP.new(nil, open_timeout: 123, read_timeout: 234) assert_equal(123, ftp.open_timeout) assert_equal(234, ftp.read_timeout) end # TODO: How can we test open_timeout? sleep before accept cannot delay # connections. def _test_open_timeout_exceeded commands = [] server = create_ftp_server(0.2) { |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 begin ftp = Net::FTP.new ftp.open_timeout = 0.1 ftp.connect(SERVER_ADDR, server.port) assert_raise(Net::OpenTimeout) do ftp.login end assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_read_timeout_exceeded commands = [] server = create_ftp_server { |sock| sock.print("220 (test_ftp).\r\n") commands.push(sock.gets) sleep(0.1) sock.print("331 Please specify the password.\r\n") commands.push(sock.gets) sleep(2.0) # Net::ReadTimeout sock.print("230 Login successful.\r\n") commands.push(sock.gets) sleep(0.1) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 0.4 ftp.connect(SERVER_ADDR, server.port) assert_raise(Net::ReadTimeout) do ftp.login end assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_read_timeout_not_exceeded commands = [] server = create_ftp_server { |sock| sock.print("220 (test_ftp).\r\n") commands.push(sock.gets) sleep(0.1) sock.print("331 Please specify the password.\r\n") commands.push(sock.gets) sleep(0.1) sock.print("230 Login successful.\r\n") commands.push(sock.gets) sleep(0.1) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 1.0 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close assert_equal(1.0, ftp.read_timeout) end ensure server.close end end def test_list_read_timeout_exceeded commands = [] list_lines = [ "-rw-r--r-- 1 0 0 0 Mar 30 11:22 foo.txt", "-rw-r--r-- 1 0 0 0 Mar 30 11:22 bar.txt", "-rw-r--r-- 1 0 0 0 Mar 30 11:22 baz.txt" ] 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") commands.push(sock.gets) sock.print("200 Switching to ASCII mode.\r\n") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("150 Here comes the directory listing.\r\n") begin conn = TCPSocket.new(host, port) list_lines.each_with_index do |l, i| if i == 1 sleep(0.5) else sleep(0.1) end conn.print(l, "\r\n") end rescue Errno::EPIPE ensure assert_nil($!) conn.close end sock.print("226 Directory send OK.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 0.2 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_raise(Net::ReadTimeout) do ftp.list end assert_equal("TYPE A\r\n", commands.shift) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("LIST\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_list_read_timeout_not_exceeded commands = [] list_lines = [ "-rw-r--r-- 1 0 0 0 Mar 30 11:22 foo.txt", "-rw-r--r-- 1 0 0 0 Mar 30 11:22 bar.txt", "-rw-r--r-- 1 0 0 0 Mar 30 11:22 baz.txt" ] 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") commands.push(sock.gets) sock.print("200 Switching to ASCII mode.\r\n") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("150 Here comes the directory listing.\r\n") conn = TCPSocket.new(host, port) list_lines.each do |l| sleep(0.1) conn.print(l, "\r\n") end conn.close sock.print("226 Directory send OK.\r\n") commands.push(sock.gets) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 1.0 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_equal(list_lines, ftp.list) assert_equal("TYPE A\r\n", commands.shift) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("LIST\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_list_fail 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") commands.push(sock.gets) sock.print("200 Switching to ASCII mode.\r\n") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("553 Requested action not taken.\r\n") commands.push(sock.gets) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_raise(Net::FTPPermError){ ftp.list } assert_equal("TYPE A\r\n", commands.shift) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("LIST\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_open_data_port_fail_no_leak 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") commands.push(sock.gets) sock.print("200 Switching to ASCII mode.\r\n") line = sock.gets commands.push(line) sock.print("421 Service not available, closing control connection.\r\n") commands.push(sock.gets) sock.print("200 Switching to Binary mode.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_raise(Net::FTPTempError){ ftp.list } assert_equal("TYPE A\r\n", commands.shift) assert_match(/\A(PORT|EPRT) /, 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_retrbinary_read_timeout_exceeded commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) 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) sleep(0.1) conn.print(binary_data[0,1024]) sleep(1.0) conn.print(binary_data[1024, 1024]) rescue nil # may raise EPIPE or something conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 0.5 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) buf = String.new assert_raise(Net::ReadTimeout) do ftp.retrbinary("RETR foo", 1024) do |s| buf << s end end assert_equal(1024, buf.bytesize) assert_equal(binary_data[0, 1024], buf) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close unless ftp.closed? end ensure server.close end end def test_retrbinary_read_timeout_not_exceeded commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) 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| sleep(0.2) conn.print(s) end conn.shutdown(Socket::SHUT_WR) conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.read_timeout = 1.0 ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) buf = String.new ftp.retrbinary("RETR foo", 1024) do |s| buf << s end assert_equal(binary_data.bytesize, buf.bytesize) assert_equal(binary_data, buf) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_retrbinary_fail 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("550 Requested action not taken.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_raise(Net::FTPPermError){ ftp.retrbinary("RETR foo", 1024) } assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_getbinaryfile commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) 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.shutdown(Socket::SHUT_WR) conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) 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(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_getbinaryfile_empty commands = [] binary_data = "" 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) 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.shutdown(Socket::SHUT_WR) conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) 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(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_getbinaryfile_with_filename_and_block commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) 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.shutdown(Socket::SHUT_WR) conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) Tempfile.create("foo", external_encoding: "ASCII-8BIT") do |f| f.binmode buf = String.new res = ftp.getbinaryfile("foo", f.path) { |s| buf << s } assert_equal(nil, res) assert_equal(binary_data, buf) assert_equal(Encoding::ASCII_8BIT, buf.encoding) assert_equal(binary_data, f.read) end assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_storbinary commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 stored_data = nil 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("150 Opening BINARY mode data connection for foo\r\n") conn = TCPSocket.new(host, port) stored_data = conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) ftp.storbinary("STOR foo", StringIO.new(binary_data), 1024) assert_equal(binary_data, stored_data) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("STOR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_storbinary_fail commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("452 Requested file action aborted.\r\n") } begin begin ftp = Net::FTP.new ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) assert_raise(Net::FTPTempError){ ftp.storbinary("STOR foo", StringIO.new(binary_data), 1024) } assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("STOR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end ensure server.close end end def test_retrlines commands = [] text_data = < 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(SERVER_NAME, :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 = [] session_reused_for_data_connection = nil 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) host, port = process_port_or_eprt(sock, line) 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 session_reused_for_data_connection = conn.session_reused? 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(SERVER_NAME, 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(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h. # See https://github.com/openssl/openssl/pull/5967 for details. if OpenSSL::OPENSSL_LIBRARY_VERSION !~ /OpenSSL 1.1.0h/ assert_equal(true, session_reused_for_data_connection) end ensure ftp.close end end def test_passive_private_data_connection server = TCPServer.new(SERVER_ADDR, 0) port = server.addr[1] commands = [] session_reused_for_data_connection = nil 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 = create_data_connection_server(sock) 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 session_reused_for_data_connection = conn.session_reused? 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(SERVER_NAME, 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_match(/\A(PASV|EPSV)\r\n/, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h. if OpenSSL::OPENSSL_LIBRARY_VERSION !~ /OpenSSL 1.1.0h/ assert_equal(true, session_reused_for_data_connection) end 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) host, port = process_port_or_eprt(sock, line) 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(SERVER_NAME, 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(/\A(PORT|EPRT) /, 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 = create_data_connection_server(sock) 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(SERVER_NAME, 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_match(/\A(PASV|EPSV)\r\n/, commands.shift) assert_equal("RETR foo\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close end end def test_tls_connect_timeout server = TCPServer.new(SERVER_ADDR, 0) port = server.addr[1] commands = [] sock = nil @thread = Thread.start do sock = server.accept sock.print("220 (test_ftp).\r\n") commands.push(sock.gets) sock.print("234 AUTH success.\r\n") end begin assert_raise(Net::OpenTimeout) do Net::FTP.new(SERVER_NAME, port: port, ssl: { ca_file: CA_FILE }, ssl_handshake_timeout: 0.1) end @thread.join ensure sock.close if sock server.close end end end def test_abort_tls return unless defined?(OpenSSL) commands = [] server = create_ftp_server { |sock| 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 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) sock.print("225 No transfer to ABOR.\r\n") } begin begin ftp = Net::FTP.new(SERVER_NAME, port: server.port, ssl: { ca_file: CA_FILE }) 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) ftp.abort assert_equal("ABOR\r\n", commands.shift) assert_equal(nil, commands.shift) rescue RuntimeError, LoadError # skip (require openssl) ensure ftp.close if ftp end ensure server.close end end def test_getbinaryfile_command_injection skip "| is not allowed in filename on Windows" if windows? [false, true].each do |resume| commands = [] binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 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") line = sock.gets commands.push(line) host, port = process_port_or_eprt(sock, line) commands.push(sock.gets) sock.print("150 Opening BINARY mode data connection for |echo hello (#{binary_data.size} bytes)\r\n") conn = TCPSocket.new(host, port) binary_data.scan(/.{1,1024}/nm) do |s| conn.print(s) end conn.shutdown(Socket::SHUT_WR) conn.read conn.close sock.print("226 Transfer complete.\r\n") } begin chdir_to_tmpdir do begin ftp = Net::FTP.new ftp.resume = resume ftp.read_timeout = RubyVM::MJIT.enabled? ? 5 : 0.2 # use large timeout for --jit-wait ftp.connect(SERVER_ADDR, server.port) ftp.login assert_match(/\AUSER /, commands.shift) assert_match(/\APASS /, commands.shift) assert_equal("TYPE I\r\n", commands.shift) ftp.getbinaryfile("|echo hello") assert_equal(binary_data, File.binread("./|echo hello")) assert_match(/\A(PORT|EPRT) /, commands.shift) assert_equal("RETR |echo hello\r\n", commands.shift) assert_equal(nil, commands.shift) ensure ftp.close if ftp end end ensure server.close end end end def test_gettextfile_command_injection skip "| is not allowed in filename on Windows" if windows? commands = [] text_data = <