costan-rtunnel 0.4.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.
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