summaryrefslogtreecommitdiff
path: root/test/transport/test_session.rb
blob: 117fb89cb9572a6321aac3e3a2a94c0df31fa5c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
require_relative '../common'
require 'net/ssh/transport/session'
require 'net/ssh/proxy/http'
require 'logger'

# mocha adds #verify to Object, which throws off the host-key-verifier part of
# these tests.

# can't use .include? because ruby18 uses strings and ruby19 uses symbols :/
Object.send(:undef_method, :verify) if Object.instance_methods.any? { |v| v.to_sym == :verify }

module Transport
  class TestSession < NetSSHTest
    include Net::SSH::Transport::Constants

    TEST_HOST = "net.ssh.test"
    TEST_PORT = 22

    def test_constructor_defaults
      assert_equal TEST_HOST, session.host
      assert_equal TEST_PORT, session.port
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNewOrLocalTunnel,
        session.host_key_verifier
      )
    end

    def test_verify_host_key_true_uses_accept_new_or_local_tunnel_verifier
      Kernel.expects(:warn).with(
        'verify_host_key: true is deprecated, use :accept_new_or_local_tunnel'
      )
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNewOrLocalTunnel,
        session(verify_host_key: true).host_key_verifier
      )
    end

    def test_verify_host_key_accept_new_or_local_tunnel_uses_accept_new_or_local_tunnel_verifier
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNewOrLocalTunnel,
        session(verify_host_key: :accept_new_or_local_tunnel).host_key_verifier
      )
    end

    def test_verify_host_key_nil_uses_accept_new_or_local_tunnel_verifier
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNewOrLocalTunnel,
        session(verify_host_key: nil).host_key_verifier
      )
    end

    def test_verify_host_key_very_uses_accept_new_verifier
      Kernel.expects(:warn).with('verify_host_key: :very is deprecated, use :accept_new')
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNew,
        session(verify_host_key: :very).host_key_verifier
      )
    end

    def test_verify_host_key_accept_new_uses_accept_new_verifier
      assert_instance_of(
        Net::SSH::Verifiers::AcceptNew,
        session(verify_host_key: :accept_new).host_key_verifier
      )
    end

    def test_verify_host_key_secure_uses_always_verifier
      Kernel.expects(:warn).with('verify_host_key: :secure is deprecated, use :always')
      assert_instance_of(
        Net::SSH::Verifiers::Always,
        session(verify_host_key: :secure).host_key_verifier
      )
    end

    def test_verify_host_key_false_uses_never_verifier
      Kernel.expects(:warn).with('verify_host_key: false is deprecated, use :never')
      assert_instance_of(
        Net::SSH::Verifiers::Never,
        session(verify_host_key: false).host_key_verifier
      )
    end

    def test_verify_host_key_null_uses_never_verifier
      assert_instance_of(
        Net::SSH::Verifiers::Never,
        session(verify_host_key: :never).host_key_verifier
      )
    end

    def test_unknown_verify_host_key_value_raises_exception_if_value_does_not_respond_to_verify
      assert_raises(ArgumentError) { session(verify_host_key: :bogus).host_key_verifier }
    end

    def test_verify_host_key_value_responding_to_verify_should_pass_muster
      object = stub("thingy", verify: true, verify_signature: true)
      assert_equal object, session(verify_host_key: object).host_key_verifier
    end

    def test_deprecated_host_key_verifier
      Kernel.expects(:warn).with('Warning: verifier without :verify_signature is deprecated')

      object = stub("thingy", verify: true)
      assert_not_nil session(verify_host_key: object).host_key_verifier
    end

    def test_host_as_string_should_return_host_and_ip_when_port_is_default
      session!
      socket.stubs(:peer_ip).returns("1.2.3.4")
      assert_equal "#{TEST_HOST},1.2.3.4", session.host_as_string
    end

    def test_host_as_string_should_return_host_and_ip_with_port_when_port_is_not_default
      session(port: 1234) # force session to be instantiated
      socket.stubs(:peer_ip).returns("1.2.3.4")
      assert_equal "[#{TEST_HOST}]:1234,[1.2.3.4]:1234", session.host_as_string
    end

    def test_host_as_string_should_return_only_host_when_host_is_ip
      session!(host: "1.2.3.4")
      socket.stubs(:peer_ip).returns("1.2.3.4")
      assert_equal "1.2.3.4", session.host_as_string
    end

    def test_host_as_string_should_return_only_host_and_port_when_host_is_ip_and_port_is_not_default
      session!(host: "1.2.3.4", port: 1234)
      socket.stubs(:peer_ip).returns("1.2.3.4")
      assert_equal "[1.2.3.4]:1234", session.host_as_string
    end

    def test_host_as_string_should_return_only_host_when_proxy_command_is_set
      session!(host: "1.2.3.4")
      socket.stubs(:peer_ip).returns(Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP)
      assert_equal "1.2.3.4", session.host_as_string
    end

    def test_host_as_string_should_return_only_host_and_port_when_host_is_ip_and_port_is_not_default_and_proxy_command_is_set
      session!(host: "1.2.3.4", port: 1234)
      socket.stubs(:peer_ip).returns(Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP)
      assert_equal "[1.2.3.4]:1234", session.host_as_string
    end

    def test_close_should_cleanup_and_close_socket
      session!
      socket.expects(:cleanup)
      socket.expects(:close)
      session.close
    end

    def test_service_request_should_return_buffer
      assert_equal "\005\000\000\000\004sftp", session.service_request('sftp').to_s
    end

    def test_rekey_when_kex_is_pending_should_do_nothing
      algorithms.stubs(pending?: true)
      algorithms.expects(:rekey!).never
      session.rekey!
    end

    def test_rekey_when_no_kex_is_pending_should_initiate_rekey_and_block_until_it_completes
      algorithms.stubs(pending?: false)
      algorithms.expects(:rekey!)
      session.expects(:wait).yields
      algorithms.expects(:initialized?).returns(true)
      session.rekey!
    end

    def test_rekey_as_needed_when_kex_is_pending_should_do_nothing
      session!
      algorithms.stubs(pending?: true)
      socket.expects(:if_needs_rekey?).never
      session.rekey_as_needed
    end

    def test_rekey_as_needed_when_no_kex_is_pending_and_no_rekey_is_needed_should_do_nothing
      session!
      algorithms.stubs(pending?: false)
      socket.stubs(if_needs_rekey?: false)
      session.expects(:rekey!).never
      session.rekey_as_needed
    end

    def test_rekey_as_needed_when_no_kex_is_pending_and_rekey_is_needed_should_initiate_rekey_and_block
      session!
      algorithms.stubs(pending?: false)
      socket.expects(:if_needs_rekey?).yields
      session.expects(:rekey!)
      session.rekey_as_needed
    end

    def test_peer_should_return_hash_of_info_about_peer
      session!
      socket.stubs(peer_ip: "1.2.3.4")
      assert_equal({ ip: "1.2.3.4", port: TEST_PORT, host: TEST_HOST, canonized: "net.ssh.test,1.2.3.4" }, session.peer)
    end

    def test_next_message_should_block_until_next_message_is_available
      session.expects(:poll_message).with(:block)
      session.next_message
    end

    def test_poll_message_should_query_next_packet_using_the_given_blocking_parameter
      session!
      socket.expects(:next_packet).with(:blocking_parameter, nil).returns(nil)
      session.poll_message(:blocking_parameter)
    end

    def test_poll_message_should_query_next_packet_using_the_timeout_option
      session!(timeout: 7)
      socket.expects(:next_packet).with(:nonblock, 7).returns(nil)
      session.poll_message
    end

    def test_poll_message_should_default_to_non_blocking
      session!
      socket.expects(:next_packet).with(:nonblock, nil).returns(nil)
      session.poll_message
    end

    def test_poll_message_should_silently_handle_disconnect_packets
      session!
      socket.expects(:next_packet).returns(P(:byte, DISCONNECT, :long, 1, :string, "testing", :string, ""))
      assert_raises(Net::SSH::Disconnect) { session.poll_message }
    end

    def test_poll_message_should_silently_handle_ignore_packets
      session!
      socket.expects(:next_packet).times(2).returns(P(:byte, IGNORE, :string, "test"), nil)
      assert_nil session.poll_message
    end

    def test_poll_message_should_silently_handle_unimplemented_packets
      session!
      socket.expects(:next_packet).times(2).returns(P(:byte, UNIMPLEMENTED, :long, 15), nil)
      assert_nil session.poll_message
    end

    def test_poll_message_should_silently_handle_debug_packets_with_always_display
      session!
      socket.expects(:next_packet).times(2).returns(P(:byte, DEBUG, :bool, true, :string, "testing", :string, ""), nil)
      assert_nil session.poll_message
    end

    def test_poll_message_should_silently_handle_debug_packets_without_always_display
      session!
      socket.expects(:next_packet).times(2).returns(P(:byte, DEBUG, :bool, false, :string, "testing", :string, ""), nil)
      assert_nil session.poll_message
    end

    def test_poll_message_should_silently_handle_kexinit_packets
      session!
      packet = P(:byte, KEXINIT, :raw, "lasdfalksdjfa;slkdfja;slkfjsdfaklsjdfa;df")
      socket.expects(:next_packet).times(2).returns(packet, nil)
      algorithms.expects(:accept_kexinit).with(packet)
      assert_nil session.poll_message
    end

    def test_poll_message_should_return_other_packets
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      socket.expects(:next_packet).returns(packet)
      assert_equal packet, session.poll_message
    end

    def test_poll_message_should_enqueue_packets_when_algorithm_disallows_packet
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      algorithms.stubs(:allow?).with(packet).returns(false)
      socket.expects(:next_packet).times(2).returns(packet, nil)
      assert_nil session.poll_message
      assert_equal [packet], session.queue
    end

    def test_poll_message_should_read_from_queue_when_next_in_queue_is_allowed_and_consume_queue_is_true
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      session.push(packet)
      socket.expects(:next_packet).never
      assert_equal packet, session.poll_message
      assert session.queue.empty?
    end

    def test_poll_message_should_not_read_from_queue_when_next_in_queue_is_not_allowed
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      algorithms.stubs(:allow?).with(packet).returns(false)
      session.push(packet)
      socket.expects(:next_packet).returns(nil)
      assert_nil session.poll_message
      assert_equal [packet], session.queue
    end

    def test_poll_message_should_not_read_from_queue_when_consume_queue_is_false
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      session.push(packet)
      socket.expects(:next_packet).returns(nil)
      assert_nil session.poll_message(:nonblock, false)
      assert_equal [packet], session.queue
    end

    def test_wait_with_block_should_return_immediately_if_block_returns_truth
      session.expects(:poll_message).never
      session.wait { true }
    end

    def test_wait_should_not_consume_queue_on_reads
      n = 0
      session.expects(:poll_message).with(:nonblock, false).returns(nil)
      session.wait { (n += 1) > 1 }
    end

    def test_wait_without_block_should_return_after_first_read
      session.expects(:poll_message).returns(nil)
      session.wait
    end

    def test_wait_should_enqueue_packets
      session!

      p1 = P(:byte, SERVICE_REQUEST, :string, "test")
      p2 = P(:byte, SERVICE_ACCEPT, :string, "test")
      socket.expects(:next_packet).times(2).returns(p1, p2)

      n = 0
      session.wait { (n += 1) > 2 }
      assert_equal [p1, p2], session.queue
    end

    def test_push_should_enqueue_packet
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      session.push(packet)
      assert_equal [packet], session.queue
    end

    def test_send_message_should_delegate_to_socket
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      socket.expects(:send_packet).with(packet)
      session.send_message(packet)
    end

    def test_enqueue_message_should_delegate_to_socket
      session!
      packet = P(:byte, SERVICE_ACCEPT, :string, "test")
      socket.expects(:enqueue_packet).with(packet)
      session.enqueue_message(packet)
    end

    def test_configure_client_should_pass_options_to_socket_client_state
      session.configure_client compression: :standard
      assert_equal :standard, socket.client.compression
    end

    def test_configure_server_should_pass_options_to_socket_server_state
      session.configure_server compression: :standard
      assert_equal :standard, socket.server.compression
    end

    def test_hint_should_set_hint_on_socket
      assert !socket.hints[:authenticated]
      session.hint :authenticated
      assert socket.hints[:authenticated]
    end

    class TestLogger < Logger
      def initialize
        @strio = StringIO.new
        super(@strio)
      end

      def messages
        @strio.string
      end
    end

    def test_log_correct_debug_with_proxy
      logger = TestLogger.new
      proxy = Net::SSH::Proxy::HTTP.new("")

      session!(logger: logger, proxy: proxy)
      assert_match "establishing connection to #{TEST_HOST}:#{TEST_PORT} through proxy", logger.messages
    end

    def test_log_correct_debug_without_proxy
      logger = TestLogger.new

      session!(logger: logger)
      assert_match "establishing connection to #{TEST_HOST}:#{TEST_PORT}", logger.messages
    end

    private

    def socket
      @socket ||= stub("socket", hints: {})
    end

    def server_version
      @server_version ||= stub("server_version")
    end

    def algorithms
      @algorithms ||= stub("algorithms", initialized?: true, allow?: true, start: true)
    end

    def session(options={})
      @session ||= begin
        host = options.delete(:host) || TEST_HOST
        if (proxy = options[:proxy])
          proxy.stubs("open").returns(socket)
        else
          Socket.stubs(:tcp).with(host, options[:port] || TEST_PORT, nil, nil, { connect_timeout: options[:timeout] }).returns(socket)
        end
        Net::SSH::Transport::ServerVersion.stubs(:new).returns(server_version)
        Net::SSH::Transport::Algorithms.stubs(:new).returns(algorithms)

        Net::SSH::Transport::Session.new(host, options)
      end
    end
    # a simple alias to make the tests more self-documenting. the bang
    # version makes it look more like the session is being instantiated
    alias session! session
  end
end