ftpd 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ module Ftpd
2
+
3
+ # A logger that does not log.
4
+ # Quacks enough like a Logger to fool Ftpd.
5
+
6
+ class NullLogger
7
+
8
+ def self.stub(method_name)
9
+ define_method method_name do |*args|
10
+ end
11
+ end
12
+
13
+ stub :unknown
14
+ stub :fatal
15
+ stub :error
16
+ stub :warn
17
+ stub :info
18
+ stub :debug
19
+
20
+ end
21
+
22
+ end
@@ -72,9 +72,9 @@ module Ftpd
72
72
  def start_session_thread(socket)
73
73
  Thread.new do
74
74
  begin
75
- session(socket)
75
+ session socket
76
76
  ensure
77
- socket.close
77
+ close_socket socket
78
78
  end
79
79
  end
80
80
  end
@@ -83,5 +83,14 @@ module Ftpd
83
83
  @server_socket.accept
84
84
  end
85
85
 
86
+ def close_socket(socket)
87
+ if socket.respond_to?(:shutdown)
88
+ socket.shutdown
89
+ socket.read
90
+ end
91
+ ensure
92
+ socket.close
93
+ end
94
+
86
95
  end
87
96
  end
@@ -6,6 +6,10 @@ module Ftpd
6
6
  include Error
7
7
 
8
8
  def initialize(opts)
9
+ @log = opts[:log] || NullLogger.new
10
+ @allow_low_data_ports = opts[:allow_low_data_ports]
11
+ @server_name = opts[:server_name]
12
+ @server_version = opts[:server_version]
9
13
  @driver = opts[:driver]
10
14
  @auth_level = opts[:auth_level]
11
15
  @socket = opts[:socket]
@@ -14,8 +18,6 @@ module Ftpd
14
18
  @socket.encrypt
15
19
  end
16
20
  @name_prefix = '/'
17
- @debug_path = opts[:debug_path]
18
- @debug = opts[:debug]
19
21
  @list_formatter = opts[:list_formatter]
20
22
  @data_type = 'A'
21
23
  @mode = 'S'
@@ -23,7 +25,9 @@ module Ftpd
23
25
  @response_delay = opts[:response_delay]
24
26
  @data_channel_protection_level = :clear
25
27
  @command_sequence_checker = init_command_sequence_checker
28
+ @session_timeout = opts[:session_timeout]
26
29
  @logged_in = false
30
+ set_socket_options
27
31
  end
28
32
 
29
33
  def run
@@ -32,6 +36,7 @@ module Ftpd
32
36
  loop do
33
37
  begin
34
38
  s = get_command
39
+ s = process_telnet_sequences(s)
35
40
  syntax_error unless s =~ /^(\w+)(?: (.*))?$/
36
41
  command, argument = $1.downcase, $2
37
42
  method = 'cmd_' + command
@@ -111,8 +116,13 @@ module Ftpd
111
116
  syntax_error unless (0..255) === i
112
117
  i
113
118
  end
114
- @data_hostname = pieces[0..3].join('.')
115
- @data_port = pieces[4] << 8 | pieces[5]
119
+ hostname = pieces[0..3].join('.')
120
+ port = pieces[4] << 8 | pieces[5]
121
+ if port < 1024 && !@allow_low_data_ports
122
+ error "504 Command not implemented for that parameter"
123
+ end
124
+ @data_hostname = hostname
125
+ @data_port = port
116
126
  reply "200 PORT command successful"
117
127
  end
118
128
 
@@ -452,6 +462,12 @@ module Ftpd
452
462
  end
453
463
  end
454
464
 
465
+ def cmd_stat(argument)
466
+ ensure_logged_in
467
+ syntax_error if argument
468
+ reply "211 #{@server_name} #{@server_version}"
469
+ end
470
+
455
471
  def self.unimplemented(command)
456
472
  method_name = "cmd_#{command}"
457
473
  define_method method_name do |arguments|
@@ -465,7 +481,6 @@ module Ftpd
465
481
  unimplemented :rest
466
482
  unimplemented :site
467
483
  unimplemented :smnt
468
- unimplemented :stat
469
484
 
470
485
  def supported_commands
471
486
  private_methods.map do |method|
@@ -520,21 +535,31 @@ module Ftpd
520
535
  def transmit_file(contents, data_type = @data_type)
521
536
  open_data_connection do |data_socket|
522
537
  contents = unix_to_nvt_ascii(contents) if data_type == 'A'
523
- data_socket.write(contents)
524
- debug("Sent #{contents.size} bytes")
538
+ handle_data_disconnect do
539
+ data_socket.write(contents)
540
+ end
541
+ @log.debug "Sent #{contents.size} bytes"
525
542
  reply "226 Transfer complete"
526
543
  end
527
544
  end
528
545
 
529
546
  def receive_file(path_to_advertise = nil)
530
547
  open_data_connection(path_to_advertise) do |data_socket|
531
- contents = data_socket.read
548
+ contents = handle_data_disconnect do
549
+ data_socket.read
550
+ end
532
551
  contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
533
- debug("Received #{contents.size} bytes")
552
+ @log.debug "Received #{contents.size} bytes"
534
553
  contents
535
554
  end
536
555
  end
537
556
 
557
+ def handle_data_disconnect
558
+ return yield
559
+ rescue Errno::ECONNRESET, Errno::EPIPE
560
+ reply "426 Connection closed; transfer aborted."
561
+ end
562
+
538
563
  def unix_to_nvt_ascii(s)
539
564
  return s if s =~ /\r\n/
540
565
  s.gsub(/\n/, "\r\n")
@@ -642,20 +667,31 @@ module Ftpd
642
667
  end
643
668
 
644
669
  def get_command
645
- s = @socket.gets
670
+ s = gets_with_timeout(@socket)
646
671
  throw :done if s.nil?
647
672
  s = s.chomp
648
- debug(s)
673
+ @log.debug s
649
674
  s
650
675
  end
651
676
 
677
+ def gets_with_timeout(socket)
678
+ ready = IO.select([@socket], nil, nil, @session_timeout)
679
+ timeout if ready.nil?
680
+ ready[0].first.gets
681
+ end
682
+
683
+ def timeout
684
+ reply '421 Control connection timed out.'
685
+ throw :done
686
+ end
687
+
652
688
  def reply(s)
653
689
  if @response_delay.to_i != 0
654
- debug "#{@response_delay} second delay before replying"
690
+ @log.warn "#{@response_delay} second delay before replying"
655
691
  sleep @response_delay
656
692
  end
657
- debug(s)
658
- @socket.puts(s)
693
+ @log.debug s
694
+ @socket.write s + "\r\n"
659
695
  end
660
696
 
661
697
  def unique_path(path)
@@ -677,17 +713,6 @@ module Ftpd
677
713
  end.join
678
714
  end
679
715
 
680
- def debug(*s)
681
- return unless debug?
682
- File.open(@debug_path, 'a') do |file|
683
- file.puts(*s)
684
- end
685
- end
686
-
687
- def debug?
688
- @debug || ENV['FTPD_DEBUG'].to_i != 0
689
- end
690
-
691
716
  def init_command_sequence_checker
692
717
  checker = CommandSequenceChecker.new
693
718
  checker.must_expect 'acct'
@@ -736,5 +761,26 @@ module Ftpd
736
761
  @logged_in = true
737
762
  end
738
763
 
764
+ def set_socket_options
765
+ disable_nagle @socket
766
+ receive_oob_data_inline @socket
767
+ end
768
+
769
+ def disable_nagle(socket)
770
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
771
+ end
772
+
773
+ def receive_oob_data_inline(socket)
774
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_OOBINLINE, 1)
775
+ end
776
+
777
+ def process_telnet_sequences(s)
778
+ telnet = Telnet.new(s)
779
+ unless telnet.reply.empty?
780
+ @socket.write telnet.reply
781
+ end
782
+ telnet.plain
783
+ end
784
+
739
785
  end
740
786
  end
@@ -0,0 +1,119 @@
1
+ # -*- ruby encoding: us-ascii -*-
2
+
3
+ module Ftpd
4
+
5
+ # Handle the limited processing of Telnet sequences required by the
6
+ # FTP RFCs.
7
+ #
8
+ # Telnet option processing is quite complex, but we need do only a
9
+ # simple subset of it, since we can disagree with any request by the
10
+ # client to turn on an option (RFC-1123 4.1.2.12). Adhering to
11
+ # RFC-1143 ("The Q Method of Implementing TELNET Option Negiation"),
12
+ # and supporting only what's needed to keep all options turned off:
13
+ #
14
+ # * Reply to WILL sequence with DONT sequence
15
+ # * Reply to DO sequence with WONT sequence
16
+ # * Ignore WONT sequence
17
+ # * Ignore DONT sequence
18
+ #
19
+ # We also handle the "interrupt process" and "data mark" sequences,
20
+ # which the client sends before the ABORT command, by ignoring them.
21
+ #
22
+ # All Telnet sequence start with an IAC, followed by at least one
23
+ # character. Here are the sequences we care about:
24
+ #
25
+ # SEQUENCE CODES
26
+ # ----------------- --------------------
27
+ # WILL IAC WILL option-code
28
+ # WONT IAC WONT option-code
29
+ # DO IAC DO option-code
30
+ # DONT IAC DONT option-code
31
+ # escaped 255 IAC IAC
32
+ # interrupt process IAC IP
33
+ # data mark IAC DM
34
+ #
35
+ # Any pathalogical sequence (e.g. IAC + \x01), or any sequence we
36
+ # don't recognize, we pass through.
37
+
38
+ class Telnet
39
+
40
+ # The command with recognized Telnet sequences removed
41
+
42
+ attr_reader :plain
43
+
44
+ # Any Telnet sequences to send
45
+
46
+ attr_reader :reply
47
+
48
+ # Create a new instance with a command that may contain Telnet
49
+ # sequences.
50
+ # @param command [String]
51
+
52
+ def initialize(command)
53
+ telnet_state_machine command
54
+ end
55
+
56
+ private
57
+
58
+ module Codes
59
+ IAC = 255.chr # 0xff
60
+ DONT = 254.chr # 0xfe
61
+ DO = 253.chr # 0xfd
62
+ WONT = 252.chr # 0xfc
63
+ WILL = 251.chr # 0xfb
64
+ IP = 244.chr # 0xf4
65
+ DM = 242.chr # 0xf2
66
+ end
67
+ include Codes
68
+
69
+ def telnet_state_machine (command)
70
+ @plain = ''
71
+ @reply = ''
72
+ state = :idle
73
+ command.each_char do |c|
74
+ case state
75
+ when :idle
76
+ if c == IAC
77
+ state = :iac
78
+ else
79
+ @plain << c
80
+ end
81
+ when :iac
82
+ case c
83
+ when IAC
84
+ @plain << c
85
+ state = :idle
86
+ when WILL
87
+ state = :will
88
+ when WONT
89
+ state = :wont
90
+ when DO
91
+ state = :do
92
+ when DONT
93
+ state = :dont
94
+ when IP
95
+ state = :idle
96
+ when DM
97
+ state = :idle
98
+ else
99
+ @plain << IAC + c
100
+ state = :idle
101
+ end
102
+ when :will
103
+ @reply << IAC + DONT + c
104
+ state = :idle
105
+ when :wont
106
+ state = :idle
107
+ when :do
108
+ @reply << IAC + WONT + c
109
+ state = :idle
110
+ when :dont
111
+ state = :idle
112
+ else
113
+ raise "Unknown state #{state.inspect}"
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+ end
@@ -2,7 +2,6 @@ require 'cucumber/rake/task'
2
2
 
3
3
  Cucumber::Rake::Task.new 'test:features' do |t|
4
4
  t.fork = true
5
- t.cucumber_opts = '--format progress'
6
5
  end
7
6
 
8
7
  task 'test:cucumber' => ['test:features']
@@ -0,0 +1,24 @@
1
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
2
+
3
+ module Ftpd
4
+ describe NullLogger do
5
+
6
+ subject {NullLogger.new}
7
+
8
+ def self.should_stub(method)
9
+ describe "#{method}" do
10
+ specify do
11
+ subject.should respond_to method
12
+ end
13
+ end
14
+ end
15
+
16
+ should_stub :unknown
17
+ should_stub :fatal
18
+ should_stub :error
19
+ should_stub :warn
20
+ should_stub :info
21
+ should_stub :debug
22
+
23
+ end
24
+ end
@@ -0,0 +1,75 @@
1
+ # -*- ruby encoding: us-ascii -*-
2
+
3
+ require File.expand_path('spec_helper', File.dirname(__FILE__))
4
+
5
+ module Ftpd
6
+ describe Telnet do
7
+
8
+ IAC = 255.chr # 0xff
9
+ DONT = 254.chr # 0xfe
10
+ DO = 253.chr # 0xfd
11
+ WONT = 252.chr # 0xfc
12
+ WILL = 251.chr # 0xfb
13
+ IP = 244.chr # 0xf4
14
+ DM = 242.chr # 0xf2
15
+
16
+ subject {Telnet.new(command)}
17
+ let(:plain_command) {"NOOP\r\n"}
18
+ let(:command) {codes + plain_command}
19
+
20
+ context '(plain command)' do
21
+ let(:codes) {''}
22
+ its(:reply) {should == ''}
23
+ its(:plain) {should == plain_command}
24
+ end
25
+
26
+ context '(escaped IAC)' do
27
+ let(:codes) {"#{IAC}#{IAC}"}
28
+ its(:reply) {should == ''}
29
+ its(:plain) {should == "#{IAC}" + plain_command}
30
+ end
31
+
32
+ context '(IAC + unknown code)' do
33
+ let(:codes) {"#{IAC}\x01"}
34
+ its(:reply) {should == ''}
35
+ its(:plain) {should == codes + plain_command}
36
+ end
37
+
38
+ context '(WILL)' do
39
+ let(:codes) {"#{IAC}#{WILL}\x01"}
40
+ its(:reply) {should == "#{IAC}#{DONT}\x01"}
41
+ its(:plain) {should == plain_command}
42
+ end
43
+
44
+ context '(WONT)' do
45
+ let(:codes) {"#{IAC}#{WONT}\x01"}
46
+ its(:reply) {should == ''}
47
+ its(:plain) {should == plain_command}
48
+ end
49
+
50
+ context '(DO)' do
51
+ let(:codes) {"#{IAC}#{DO}\x01"}
52
+ its(:reply) {should == "#{IAC}#{WONT}\x01"}
53
+ its(:plain) {should == plain_command}
54
+ end
55
+
56
+ context '(DONT)' do
57
+ let(:codes) {"#{IAC}#{DONT}\x01"}
58
+ its(:reply) {should == ''}
59
+ its(:plain) {should == plain_command}
60
+ end
61
+
62
+ context '(interrupt process)' do
63
+ let(:codes) {"#{IAC}#{IP}"}
64
+ its(:reply) {should == ''}
65
+ its(:plain) {should == plain_command}
66
+ end
67
+
68
+ context '(data mark)' do
69
+ let(:codes) {"#{IAC}#{DM}"}
70
+ its(:reply) {should == ''}
71
+ its(:plain) {should == plain_command}
72
+ end
73
+
74
+ end
75
+ end