ftpd 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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