costan-rtunnel 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +21 -0
  3. data/Manifest +49 -0
  4. data/README.markdown +84 -0
  5. data/Rakefile +45 -0
  6. data/bin/rtunnel_client +4 -0
  7. data/bin/rtunnel_server +4 -0
  8. data/lib/rtunnel.rb +20 -0
  9. data/lib/rtunnel/client.rb +308 -0
  10. data/lib/rtunnel/command_processor.rb +62 -0
  11. data/lib/rtunnel/command_protocol.rb +50 -0
  12. data/lib/rtunnel/commands.rb +233 -0
  13. data/lib/rtunnel/connection_id.rb +24 -0
  14. data/lib/rtunnel/core.rb +58 -0
  15. data/lib/rtunnel/crypto.rb +106 -0
  16. data/lib/rtunnel/frame_protocol.rb +34 -0
  17. data/lib/rtunnel/io_extensions.rb +54 -0
  18. data/lib/rtunnel/leak.rb +35 -0
  19. data/lib/rtunnel/rtunnel_client_cmd.rb +41 -0
  20. data/lib/rtunnel/rtunnel_server_cmd.rb +32 -0
  21. data/lib/rtunnel/server.rb +351 -0
  22. data/lib/rtunnel/socket_factory.rb +119 -0
  23. data/spec/client_spec.rb +47 -0
  24. data/spec/cmds_spec.rb +127 -0
  25. data/spec/integration_spec.rb +105 -0
  26. data/spec/server_spec.rb +21 -0
  27. data/spec/spec_helper.rb +3 -0
  28. data/test/command_stubs.rb +77 -0
  29. data/test/protocol_mocks.rb +43 -0
  30. data/test/scenario_connection.rb +109 -0
  31. data/test/test_client.rb +48 -0
  32. data/test/test_command_protocol.rb +82 -0
  33. data/test/test_commands.rb +49 -0
  34. data/test/test_connection_id.rb +30 -0
  35. data/test/test_crypto.rb +127 -0
  36. data/test/test_frame_protocol.rb +109 -0
  37. data/test/test_io_extensions.rb +70 -0
  38. data/test/test_server.rb +70 -0
  39. data/test/test_socket_factory.rb +42 -0
  40. data/test/test_tunnel.rb +186 -0
  41. data/test_data/authorized_keys2 +4 -0
  42. data/test_data/known_hosts +4 -0
  43. data/test_data/random_rsa_key +27 -0
  44. data/test_data/ssh_host_dsa_key +12 -0
  45. data/test_data/ssh_host_rsa_key +27 -0
  46. data/tests/_ab_test.rb +16 -0
  47. data/tests/_stress_test.rb +96 -0
  48. data/tests/lo_http_server.rb +55 -0
  49. metadata +121 -0
@@ -0,0 +1,43 @@
1
+ # Mocks the sending end of an EventMachine connection.
2
+ # The sent data is concatenated in a string available by calling #string.
3
+ class EmSendMock
4
+ attr_reader :string
5
+
6
+ def initialize
7
+ @string = ''
8
+ end
9
+
10
+ def send_data(data)
11
+ @string << data
12
+ end
13
+ end
14
+
15
+ # Mocks the receiving end of an EventMachine connection.
16
+ # The data to be received is passed as an array of strings to the constructor.
17
+ # Calling #replay mocks receiving the data.
18
+ class EmReceiveMock
19
+ attr_accessor :strings
20
+ attr_accessor :objects
21
+
22
+ def initialize(strings = [''])
23
+ @strings = strings
24
+ @objects = []
25
+ end
26
+
27
+ def replay
28
+ @strings.each { |str| receive_data str }
29
+ self
30
+ end
31
+
32
+ def receive_object(object)
33
+ @objects << object
34
+ end
35
+
36
+ # Declares the name of the object to be received. For instance, a frame
37
+ # protocol would use :frame for name. This generates a receive_frame method,
38
+ # and a frames accessor.
39
+ def self.object_name(name)
40
+ alias_method "receive_#{name}".to_sym, :receive_object
41
+ alias_method "#{name}s".to_sym, :objects
42
+ end
43
+ end
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+
4
+ # Event Machine connection that runs a fixed send-expect scenario.
5
+ class ScenarioConnection < EventMachine::Connection
6
+ def initialize(test_case, scenario = nil)
7
+ super()
8
+
9
+ @test_case = test_case
10
+ @scenario = scenario || yield
11
+ @ignore_unbind = false
12
+ next_step
13
+ end
14
+
15
+ def post_init
16
+ scenario_can_send
17
+ end
18
+
19
+ def receive_data(data)
20
+ scenario_received data
21
+ end
22
+
23
+ def unbind
24
+ return if @ignore_unbind
25
+
26
+ unless @step and @step.first == :unbind
27
+ scenario_fail "Received unexpected unbind\n"
28
+ end
29
+ next_step
30
+ while @step and @step.first == :proc
31
+ @step.last.call
32
+ next_step
33
+ end
34
+ if @step and @step.first == :stop
35
+ scenario_stop @step.last
36
+ end
37
+ end
38
+
39
+ # Called when data is received. Plays the connection scenario.
40
+ def scenario_received(data)
41
+ unless @step and @step.first == :recv
42
+ scenario_fail "Received unexpected data: #{data}\n"
43
+ end
44
+
45
+ @test_case.send :assert_equal, @step.last, data
46
+ next_step
47
+ scenario_can_send
48
+ end
49
+
50
+ # Called when data can be sent. Plays the connection scenario.
51
+ def scenario_can_send
52
+ while @step
53
+ case @step.first
54
+ when :proc
55
+ @step.last.call
56
+ when :send
57
+ send_data @step.last
58
+ else
59
+ break
60
+ end
61
+ next_step
62
+ end
63
+
64
+ unless @step
65
+ # EM might stifle this exception and reraise
66
+ msg = "Scenario completed prematurely"
67
+ $stderr.puts msg
68
+ fail msg
69
+ end
70
+
71
+ case @step.first
72
+ when :receive
73
+ # wait to receive
74
+ return
75
+ when :unbind
76
+ # wait for unbind
77
+ return
78
+ when :close
79
+ @ignore_unbind = true
80
+ close_connection_after_writing
81
+ next_step
82
+ end
83
+ end
84
+
85
+ def scenario_fail(fail_message)
86
+ if @step
87
+ fail_message << "Expected to #{@step.inspect}"
88
+ else
89
+ fail_message << "Scenario was completed\n"
90
+ end
91
+ @test_case.send :fail, fail_message
92
+ end
93
+
94
+ def scenario_stop(stop_proc)
95
+ if stop_proc.kind_of? Proc
96
+ # call the proc, then give em time to stop all its servers
97
+ stop_proc.call
98
+ EventMachine.add_timer 0.1 do
99
+ EventMachine.stop_event_loop
100
+ end
101
+ else
102
+ EventMachine.stop_event_loop
103
+ end
104
+ end
105
+
106
+ def next_step
107
+ @step = @scenario.shift
108
+ end
109
+ end
@@ -0,0 +1,48 @@
1
+ require 'rtunnel'
2
+
3
+ require 'openssl'
4
+ require 'resolv'
5
+ require 'test/unit'
6
+
7
+ class ClientTest < Test::Unit::TestCase
8
+ def setup
9
+ @client = RTunnel::Client.new(:control_address => 'localhost',
10
+ :remote_listen_address => '9199',
11
+ :tunnel_to_address => '4444',
12
+ :private_key => 'test_data/ssh_host_rsa_key',
13
+ :tunnel_timeout => 5)
14
+ @localhost_addr = Resolv.getaddress 'localhost'
15
+ end
16
+
17
+ def test_options
18
+ client = RTunnel::Client
19
+ assert_equal "18.241.3.100:#{RTunnel::DEFAULT_CONTROL_PORT}",
20
+ client.extract_control_address('18.241.3.100')
21
+ assert_equal "18.241.3.100:9199",
22
+ client.extract_control_address('18.241.3.100:9199')
23
+ assert_equal "#{@localhost_addr}:#{RTunnel::DEFAULT_CONTROL_PORT}",
24
+ client.extract_control_address('localhost')
25
+ assert_equal "#{@localhost_addr}:9199",
26
+ client.extract_control_address('localhost:9199')
27
+
28
+ assert_equal "0.0.0.0:9199",
29
+ client.extract_remote_listen_address('9199')
30
+ assert_equal "18.241.3.100:9199",
31
+ client.extract_remote_listen_address('18.241.3.100:9199')
32
+ assert_equal "#{@localhost_addr}:9199",
33
+ client.extract_remote_listen_address('localhost:9199')
34
+
35
+ assert_equal "18.241.3.100:9199",
36
+ client.extract_tunnel_to_address('18.241.3.100:9199')
37
+ assert_equal "#{@localhost_addr}:9199",
38
+ client.extract_tunnel_to_address('9199')
39
+
40
+ assert_equal RTunnel::TUNNEL_TIMEOUT, client.extract_tunnel_timeout(nil)
41
+ assert_equal 29, client.extract_tunnel_timeout(29)
42
+
43
+ assert_equal nil, client.extract_private_key(nil)
44
+ key = client.extract_private_key 'test_data/ssh_host_rsa_key'
45
+ assert_equal OpenSSL::PKey::RSA, key.class
46
+ assert_equal File.read('test_data/ssh_host_rsa_key'), key.to_pem
47
+ end
48
+ end
@@ -0,0 +1,82 @@
1
+ require 'rtunnel'
2
+
3
+ require 'test/unit'
4
+
5
+ require 'test/command_stubs.rb'
6
+ require 'test/protocol_mocks.rb'
7
+
8
+ # Send mock for commands.
9
+ class EmSendCommandsMock < EmSendMock
10
+ include RTunnel::CommandProtocol
11
+ end
12
+
13
+ # Receive mock for commands.
14
+ class EmReceiveCommandsMock < EmReceiveMock
15
+ include RTunnel::CommandProtocol
16
+ object_name :command
17
+ end
18
+
19
+ class CommandProtocolTest < Test::Unit::TestCase
20
+ include CommandStubs
21
+ C = RTunnel::Crypto
22
+
23
+ def setup
24
+ super
25
+ @send_mock = EmSendCommandsMock.new
26
+ end
27
+
28
+ def teardown
29
+ super
30
+ end
31
+
32
+ def commandset_test(names)
33
+ @send_mock.outgoing_command_hasher = @hasher if @hasher
34
+ names.each do |name|
35
+ command = self.send "generate_#{name}".to_sym
36
+ @send_mock.send_command command
37
+ end
38
+ receive_mock = EmReceiveCommandsMock.new([@send_mock.string])
39
+ receive_mock.incoming_command_hasher = C::Hasher.new(@hasher.key) if @hasher
40
+ o_commands = receive_mock.replay.commands
41
+ self.assert_equal names.length, o_commands.length
42
+ names.each_index do |i|
43
+ self.send "verify_#{names[i]}", o_commands[i]
44
+ end
45
+ end
46
+
47
+ CommandStubs.command_names.each do |name|
48
+ define_method "test_#{name}".to_sym do
49
+ commandset_test [name]
50
+ end
51
+
52
+ define_method "test_signed_#{name}".to_sym do
53
+ @hasher = C::Hasher.new
54
+ commandset_test [name]
55
+ end
56
+
57
+ define_method "test_signed_#{name}_has_signature".to_sym do
58
+ sig_send_mock = EmSendCommandsMock.new
59
+ sig_send_mock.outgoing_command_hasher = C::Hasher.new
60
+ outputs = [@send_mock, sig_send_mock].map do |mock|
61
+ mock.send_command self.send("generate_#{name}".to_sym)
62
+ mock.string
63
+ end
64
+ assert outputs.first.length < outputs.last.length,
65
+ "No signature generated"
66
+ end
67
+
68
+ define_method "test_signed_#{name}_enforces_signature".to_sym do
69
+ @send_mock.outgoing_command_hasher = hasher = C::Hasher.new
70
+ @send_mock.send_command self.send("generate_#{name}".to_sym)
71
+ signed_str = @send_mock.string
72
+
73
+ 0.upto(signed_str.length - 1) do |i|
74
+ bad_str = signed_str.dup
75
+ bad_str[i] ^= 0x01
76
+ recv_mock = EmReceiveCommandsMock.new([bad_str])
77
+ recv_mock.incoming_command_hasher = C::Hasher.new hasher.key
78
+ assert_equal [], recv_mock.replay.commands
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,49 @@
1
+ require 'rtunnel'
2
+
3
+ require 'stringio'
4
+ require 'test/unit'
5
+
6
+ require 'test/command_stubs.rb'
7
+
8
+ class CommandsTest < Test::Unit::TestCase
9
+ include CommandStubs
10
+
11
+ def setup
12
+ super
13
+ @str = StringIO.new
14
+ end
15
+
16
+ CommandStubs.command_names.each do |cmd|
17
+ define_method "test_#{cmd}_encode" do
18
+ command = self.send "generate_#{cmd}"
19
+ command.encode @str
20
+ @str.rewind
21
+ decoded_command = RTunnel::Command.decode @str
22
+ self.send "verify_#{cmd}", decoded_command
23
+ assert_equal "", @str.read, "Command #{cmd} did not consume its entire outpt"
24
+ end
25
+
26
+ define_method "test_#{cmd}_to_encoded_str" do
27
+ command = self.send "generate_#{cmd}"
28
+ command.encode @str
29
+ @str.rewind
30
+ assert_equal @str.read, command.to_encoded_str
31
+ end
32
+ end
33
+
34
+ def test_all_encodes
35
+ sequence = CommandStubs.command_test_sequence
36
+ sequence.each { |cmd| self.send("generate_#{cmd}").encode @str }
37
+ @str.rewind
38
+ sequence.each do |cmd|
39
+ command = RTunnel::Command.decode(@str)
40
+ self.send "verify_#{cmd}", command
41
+ end
42
+ assert_equal "", @str.read
43
+ end
44
+
45
+ def test_codes
46
+ # TODO(not_me): it'd be nice to have more than a smoke test here
47
+ assert_equal String, RTunnel::Command.printable_codes.class
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ require 'rtunnel'
2
+
3
+ require 'test/unit'
4
+
5
+ class ConnectionIdTest < Test::Unit::TestCase
6
+ class CidWrapper
7
+ include RTunnel::ConnectionId
8
+ end
9
+
10
+ def setup
11
+ @server = CidWrapper.new
12
+ end
13
+
14
+ def test_ids_are_unique
15
+ n_ids = 1024
16
+ ids = (0...n_ids).map { @server.new_connection_id }
17
+ assert_equal n_ids, ids.uniq.length
18
+ end
19
+
20
+ def test_id_sequences_are_not_trivial
21
+ n_servers = 256
22
+ n_ids = 16
23
+ sequences = (0...n_servers).map { CidWrapper.new }.map do |server|
24
+ (0...n_ids).map { server.new_connection_id }
25
+ end
26
+ 0.upto(n_ids - 1) do |i|
27
+ assert_equal n_servers, sequences.map { |seq| seq[i] }.uniq.length
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,127 @@
1
+ require 'rtunnel'
2
+
3
+ require 'openssl'
4
+ require 'test/unit'
5
+
6
+ class CryptoTest < Test::Unit::TestCase
7
+ C = RTunnel::Crypto
8
+
9
+ @@rsa_key_path = 'test_data/ssh_host_rsa_key'
10
+ @@dsa_key_path = 'test_data/ssh_host_dsa_key'
11
+ @@known_hosts_path = 'test_data/known_hosts'
12
+ @@authorized_keys_path = 'test_data/authorized_keys2'
13
+
14
+ def test_read_private_key
15
+ key = C.read_private_key @@rsa_key_path
16
+ assert_equal File.read(@@rsa_key_path), key.to_pem
17
+ assert_equal OpenSSL::PKey::RSA, key.class
18
+
19
+ key = C.read_private_key @@dsa_key_path
20
+ assert_equal File.read(@@dsa_key_path), key.to_pem
21
+ assert_equal OpenSSL::PKey::DSA, key.class
22
+ end
23
+
24
+ def test_read_known_hosts
25
+ keys = C.read_authorized_keys @@known_hosts_path
26
+ verify_authorized_keys keys
27
+ end
28
+
29
+ def test_read_authorized_keys
30
+ keys = C.read_authorized_keys @@authorized_keys_path
31
+ verify_authorized_keys keys
32
+ end
33
+
34
+ def verify_authorized_keys(keys)
35
+ assert_equal 4, keys.length
36
+ assert_equal [OpenSSL::PKey::RSA] * 3 + [OpenSSL::PKey::DSA],
37
+ keys.map { |k| k.class }
38
+ assert_equal C.read_private_key(@@rsa_key_path).public_key.to_pem,
39
+ keys[1].to_pem
40
+ assert_equal C.read_private_key(@@dsa_key_path).public_key.to_pem,
41
+ keys[3].to_pem
42
+ end
43
+
44
+ def test_key_fingerprint
45
+ keys = C.read_authorized_keys @@known_hosts_path
46
+
47
+ assert_equal 4, keys.map { |k| C.key_fingerprint k }.uniq.length
48
+ keys.each { |k| assert_equal C.key_fingerprint(k), C.key_fingerprint(k) }
49
+ end
50
+
51
+ def test_load_public_keys
52
+ keyset = C.load_public_keys @@known_hosts_path
53
+ rsa_key = C.read_private_key @@rsa_key_path
54
+ dsa_key = C.read_private_key @@dsa_key_path
55
+
56
+ assert_equal rsa_key.public_key.to_pem,
57
+ keyset[C.key_fingerprint(rsa_key)].to_pem
58
+ assert_equal dsa_key.public_key.to_pem,
59
+ keyset[C.key_fingerprint(dsa_key)].to_pem
60
+ end
61
+
62
+ def test_key_encryption
63
+ test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
64
+ rsa_key = C.read_private_key @@rsa_key_path
65
+ dsa_key = C.read_private_key @@rsa_key_path
66
+
67
+ [rsa_key, dsa_key].each do |key|
68
+ encrypted_data = C.encrypt_with_key key.public_key, test_data
69
+ decrypted_data = C.decrypt_with_key key, encrypted_data
70
+
71
+ assert_equal test_data, decrypted_data
72
+ 0.upto(test_data.length - 4) do |i|
73
+ assert !encrypted_data.index(test_data[i, 4]),
74
+ 'Encryption did not wipe the original pattern'
75
+ end
76
+ end
77
+ end
78
+
79
+ def test_encryption_depends_on_key
80
+ num_keys = 16
81
+ test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
82
+ keys = (0...num_keys).map { OpenSSL::PKey::RSA.generate 1024, 35 }
83
+ assert_equal num_keys, keys.map { |k| C.encrypt_with_key k, test_data }.
84
+ uniq.length
85
+ end
86
+
87
+ def test_hasher_random_keys_are_random
88
+ num_keys = 1024
89
+ assert_equal num_keys, (0...num_keys).map { C::Hasher.random_key }.
90
+ uniq.length
91
+ end
92
+
93
+ def test_hasher_random_keys_yield_random_results
94
+ num_keys = 1024
95
+ test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
96
+ assert_equal num_keys, (0...num_keys).map { C::Hasher.new.hash test_data }.
97
+ uniq.length
98
+ end
99
+
100
+ def test_hasher_hashes_are_finite
101
+ num_blocks = 64
102
+ block = 'qwertyuiopasdfghjklzxcvbnm'
103
+
104
+ hasher = C::Hasher.new
105
+ hash_length = hasher.hash('').length
106
+ 1.upto(num_blocks) { |n| assert_equal hash_length, hasher.hash(block * n).
107
+ length }
108
+ end
109
+
110
+ def test_hasher_has_state
111
+ num_blocks = 1024
112
+ block = 'qwertyuiopasdfghjklzxcvbnm'
113
+ hasher = C::Hasher.new
114
+
115
+ assert_equal num_blocks, (0...num_blocks).map { hasher.hash block}.uniq.
116
+ length
117
+ end
118
+
119
+ def test_hasher_is_reproducible
120
+ hasher = C::Hasher.new
121
+ hasher2 = C::Hasher.new hasher.key
122
+
123
+ num_blocks = 128
124
+ block = 'qwertyuiopasdfghjklzxcvbnm'
125
+ 1.upto(num_blocks) { assert_equal hasher.hash(block), hasher2.hash(block) }
126
+ end
127
+ end