File: channel.rb

package info (click to toggle)
libnet-ssh-ruby 1.1.2-1
  • links: PTS
  • area: main
  • in suites: lenny
  • size: 3,472 kB
  • ctags: 2,465
  • sloc: ruby: 10,848; makefile: 17
file content (504 lines) | stat: -rw-r--r-- 17,922 bytes parent folder | download | duplicates (2)
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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
#--
# =============================================================================
# Copyright (c) 2004,2005 Jamis Buck (jamis@37signals.com)
# All rights reserved.
#
# This source file is distributed as part of the Net::SSH Secure Shell Client
# library for Ruby. This file (and the library as a whole) may be used only as
# allowed by either the BSD license, or the Ruby license (or, by association
# with the Ruby license, the GPL). See the "doc" subdirectory of the Net::SSH
# distribution for the texts of these licenses.
# -----------------------------------------------------------------------------
# net-ssh website : http://net-ssh.rubyforge.org
# project website: http://rubyforge.org/projects/net-ssh
# =============================================================================
#++

require 'net/ssh/connection/constants'
require 'net/ssh/connection/term'

module Net
  module SSH
    module Connection

      class Channel
        include Constants

        #--
        # ====================================================================
        # ATTRIBUTES
        # ====================================================================
        #++

        # The channel's local id (assigned by the connection)
        attr_reader :local_id

        # The channel's remote id (assigned by the remote server)
        attr_reader :remote_id

        # The connection driver instance that owns this channel
        attr_reader :connection

        # The type of this channel
        attr_reader :type
        
        # The maximum packet size that may be sent over this channel
        attr_reader :maximum_packet_size

        # The maximum data window size for this channel
        attr_reader :window_size

        # The maximum packet size that may be sent over this channel
        attr_reader :local_maximum_packet_size

        # The maximum data window size for this channel
        attr_reader :local_window_size

        #--
        # ====================================================================
        # FACTORY METHODS
        # ====================================================================
        #++

        # Requests that a new channel be opened on the remote host.
        # This will return immediately, but the +on_confirm_open+ callback
        # will be invoked when the remote host confirms that the channel has
        # been successfully opened.
        def self.open( connection, log, buffers, type, data=nil )
          channel = new( connection, log, buffers, type )

          msg = buffers.writer

          msg.write_byte CHANNEL_OPEN
          msg.write_string type
          msg.write_long channel.local_id
          msg.write_long channel.local_window_size
          msg.write_long channel.local_maximum_packet_size
          msg.write data.to_s if data

          connection.send_message msg

          channel
        end

        # Creates a new channel object with the given internal
        # information. The channel is assumed to already be
        # connected to a remote host.
        def self.create( connection, log, buffers, type, remote_id,
          window_size, packet_size )
        # begin
          channel = new( connection, log, buffers, type )
          channel.do_confirm_open remote_id, window_size, packet_size
          channel
        end

        private_class_method :new

        #--
        # ====================================================================
        # CONSTRUCTOR
        # ====================================================================
        #++

        # Create a new channel object on the given connection, and of the given
        # type.
        def initialize( connection, log, buffers, type )
          @connection = connection
          @log = log
          @buffers = buffers
          @type = type
          @local_id = @connection.allocate_channel_id
          @local_window_size = 0x20000
          @local_maximum_packet_size = 0x10000
        end

        #--
        # ====================================================================
        # CALLBACK HOOKS
        # ====================================================================
        #++

        # Set the callback to use when the channel has been confirmed
        # to be open.
        def on_confirm_open( &block )
          @on_confirm_open = block
        end

        # Set the callback to use when the channel could not be opened
        # for some reason.
        def on_confirm_failed( &block )
          @on_confirm_failed = block
        end

        # Set the callback to be invoked when the server requests
        # that the window size be adjusted.
        def on_window_adjust( &block )
          @on_window_adjust = block
        end

        # Set the callback to be invoked when the server sends a
        # data packet over the channel.
        def on_data( &block )
          @on_data = block
        end

        # Set the callback to be invoked when the server sends an
        # extended data packet over the channel.
        def on_extended_data( &block )
          @on_extended_data = block
        end

        # Set the callback to be invoked when the server sends an EOF
        # packet.
        def on_eof( &block )
          @on_eof = block
        end

        # Set the callback to be invoked when the server sends a
        # request packet.
        def on_request( &block )
          @on_request = block
        end

        # Set the callback to invoked when the server sends
        # confirmation of a successful operation.
        def on_success( &block )
          @on_success = block
        end

        # Set the callback to invoked when the server sends
        # notification of a failed operation.
        def on_failure( &block )
          @on_failure = block
        end

        # Set the callback to be invoked when the channel is closed.
        def on_close( &block )
          @on_close = block
        end

        #--
        # ====================================================================
        # CHANNEL STATE ACCESSORS
        # ====================================================================
        #++

        def valid?
          not @local_id.nil?
        end

        # Retrieved a named property of the channel.
        def property( name )
          ( @properties ||= Hash.new )[ name ]
        end

        # Set a named property on the channel.
        def set_property( name, value )
          ( @properties ||= Hash.new )[ name ] = value
        end

        alias :[]  :property
        alias :[]= :set_property

        #--
        # ====================================================================
        # CHANNEL AFFECTORS
        # ====================================================================
        #++

        # Closes the channel.
        def close( client_initiated=true )
          unless defined?(@already_closed) && @already_closed
            msg = @buffers.writer
            msg.write_byte CHANNEL_CLOSE
            msg.write_long @remote_id
            @connection.send_message msg
            @already_closed = true
          end

          unless client_initiated
            @connection.remove_channel( self )
            callback :close, self
          end

          self
        end

        # Send an EOF across the channel. No data should be sent from the client
        # to the server over this channel after this, although packets may still
        # be received from the server.
        def send_eof
          msg = @buffers.writer
          msg.write_byte CHANNEL_EOF
          msg.write_long @remote_id
          @connection.send_message msg
          self
        end

        # Send the given signal to process on the other side of the channel. The
        # parameter should be one of the Channel::SIGxxx constants.
        def send_signal( sig, want_reply=false )
          send_request_string "signal", sig, want_reply
          self
        end

        # Send a channel request with the given name. It will have one data
        # item, which will be interpreted as a string.
        def send_request_string( request_name, data, want_reply=false )
          msg = @buffers.writer
          msg.write_string data.to_s
          send_request request_name, msg, want_reply
        end

        # Send a generic channel request with the given name. The data item will
        # be written directly into the request (after converting it to a string,
        # as necessary).
        def send_request( request_name, data, want_reply=false )
          msg = @buffers.writer
          msg.write_byte CHANNEL_REQUEST
          msg.write_long @remote_id
          msg.write_string request_name
          msg.write_bool want_reply
          msg.write data.to_s
          @connection.send_message msg
          self
        end

        # Send a "window adjust" message to the server for this channel,
        # informing it that it may send this many more bytes over the
        # channel.
        def send_window_adjust( size )
          msg = @buffers.writer
          msg.write_byte CHANNEL_WINDOW_ADJUST
          msg.write_long @remote_id
          msg.write_long size
          @connection.send_message msg
        end

        # Send a data packet to the server, over the channel.
        def send_data( data )
          @connection.register_data_request( self, data )
        end

        # Send an extended data packet to the server, over the channel.
        # Extended data always has a numeric type associated with it. The
        # only predefined type is 1, whic corresponds to +stderr+ data.
        def send_extended_data( type, data )
          @connection.register_data_request( self, data, type )
        end

        # Splits the given data so that it will fit in a data packet, taking
        # into consideration the current window size and maximum packet size.
        # The +overhead+ parameter is the number of additional bytes added by
        # the packet itself.
        #
        # This returns a tuple, <tt>[data,data_to_return]</tt>, where the first
        # element is the data to include in the packet, and the second element
        # is the data remaining that would not fit in the packet.
        def split_data_for_packet( data, overhead )
          data_to_return = nil

          if data.length > window_size 
            data_to_return = data[window_size..-1]
            data = data[0,window_size]
          end

          max_size_less_overhead = maximum_packet_size - overhead
          if data.length > max_size_less_overhead
            data_to_return = data[max_size_less_overhead..-1] + ( data_to_return || "" )
            data = data[0,max_size_less_overhead]
          end

          [ data, data_to_return ]
        end
        private :split_data_for_packet

        # Send a data packet to the server, over the channel. Only sends as
        # much of that data as the channel is currently capable of sending
        # (based on window size and maximum packet size), and returns any
        # data that could not be sent. Returns +nil+ if all the data that
        # was requested to be sent, was sent.
        def send_data_packet( data )
          # overhead is ( byte.length + id.length + strlen.length ) = 9
          data, data_to_return = split_data_for_packet( data.to_s, 9 )
          @window_size -= data.length

          msg = @buffers.writer
          msg.write_byte CHANNEL_DATA
          msg.write_long @remote_id
          msg.write_string data
          @connection.send_message msg

          data_to_return
        end

        # Send an extended data packet to the server, over the channel.
        # Extended data always has a numeric type associated with it. The
        # only predefined type is 1, whic corresponds to +stderr+ data.
        def send_extended_data_packet( type, data )
          # overhead is
          #   ( byte.length + id.length + type.length + strlen.length ) = 13
          data, data_to_return = split_data_for_packet( data.to_s, 13 )
          @window_size -= data.length

          msg = @buffers.writer
          msg.write_byte CHANNEL_EXTENDED_DATA
          msg.write_long @remote_id
          msg.write_long type
          msg.write_string data
          @connection.send_message msg

          data_to_return
        end

        VALID_PTY_OPTIONS = { :term=>"xterm",
                              :chars_wide=>80,
                              :chars_high=>24,
                              :pixels_wide=>640,
                              :pixels_high=>480,
                              :modes=>{},
                              :want_reply=>false }

        # Request that a pty be opened for this channel. Valid options are
        # :term, :chars_wide, :chars_high, :pixels_wide, :pixels_high, :modes,
        # and :want_reply. :modes is a Hash, where the keys are constants from
        # Net::SSH::Service::Term, and values are integers describing the
        # corresponding key.
        def request_pty( opts = {} )
          invalid_opts = opts.keys - VALID_PTY_OPTIONS.keys
          unless invalid_opts.empty?
            raise ArgumentError,
              "invalid option(s) to request_pty: #{invalid_opts.inspect}"
          end

          opts = VALID_PTY_OPTIONS.merge( opts )

          msg = @buffers.writer
          msg.write_string opts[ :term ]
          msg.write_long opts[ :chars_wide ]
          msg.write_long opts[ :chars_high ]
          msg.write_long opts[ :pixels_wide ]
          msg.write_long opts[ :pixels_high ]

          modes = @buffers.writer
          opts[ :modes ].each do |mode, data|
            modes.write_byte mode
            modes.write_long data
          end
          modes.write_byte Term::TTY_OP_END

          msg.write_string modes.to_s

          send_request "pty-req", msg, opts[:want_reply]
        end

        # Execute the given remote command over the channel. This should be
        # invoked in the "on_confirm" callback of a channel. This method will
        # return immediately.
        def exec( command, want_reply=false )
          send_request_string "exec", command, want_reply
        end

        # Request the given subsystem. This method will return immediately.
        def subsystem( subsystem, want_reply=true )
          send_request_string "subsystem", subsystem, want_reply
        end

        #--
        # ====================================================================
        # CHANNEL EVENTS
        # ====================================================================
        #++

        # A convenience method for defining new event callbacks.
        def self.event( event, *parameters )
          define_method "do_#{event}" do |*args|
            callback event, self, *args
            self
          end
        end

        # Invoked when the server confirms the opening of a channel.
        def do_confirm_open( remote_id, window_size, packet_size )
          @remote_id = remote_id
          @window_size = window_size
          @maximum_packet_size = packet_size
          callback :confirm_open, self
        end

        # Invoked when the server failed to confirm the opening of a channel.
        def do_confirm_failed( reason_code, description, language )
          @local_id = nil
          @connection = nil
          callback :confirm_failed, self, reason_code, description, language
        end

        # Invoked when the server asks to adjust the window size. This in turn
        # calls the "on_window_adjust" callback.
        def do_window_adjust( bytes_to_add )
          @window_size += bytes_to_add
          callback :window_adjust, self, bytes_to_add
        end

        # Invoked when the server sends a data packet. This in turn calls the
        # "on_data" callback.
        def do_data( data )
          update_local_window_size data
          callback :data, self, data
        end

        # Invoked when the server sends an extended data packet. This in turn
        # calls the "on_extended_data" callback.
        def do_extended_data( type, data )
          update_local_window_size data
          callback :extended_data, self, type, data
        end

        # Invoked when the server sends an EOF packet. This in turn calls the
        # "on_eof" callback.
        event :eof

        # Invoked when the server sends a request packet. This in turn calls
        # the "on_request" callback.
        event :request, :type, :want_reply, :data

        # Invoked when the server sends confirmation of a successful operation.
        # This in turn invokes the "on_success" callback, if set.
        event :success

        # Invoked when the server sends notification of a failed operation.
        # This in turn invokes the "on_failure" callback, if set.
        event :failure

        #--
        # ====================================================================
        # PRIVATE UTILITIES
        # ====================================================================
        #++

        # Updates the window size for this channel based on the size of the
        # data that was receieved. If no more space in the window is left,
        # a message is sent to the server indicating that the window size
        # is increased.
        def update_local_window_size( data )
          @local_window_size -= data.length
          if @local_window_size < 4096
            @local_window_size += 0x20000
            send_window_adjust 0x20000
          end
        end
        private :update_local_window_size

        # A convenience utility method for invoking a named callback with a
        # set of arguments.
        def callback( which, *args )
          block = instance_variable_get( "@on_#{which.to_s}" )
          block.call( *args ) if block
        end
        private :callback

      end

    end
  end
end