Class: Net::SSH::Buffer

Inherits:
Object
  • Object
show all
Defined in:
lib/net/ssh/buffer.rb

Overview

Net::SSH::Buffer is a flexible class for building and parsing binary data packets. It provides a stream-like interface for sequentially reading data items from the buffer, as well as a useful helper method for building binary packets given a signature.

Writing to a buffer always appends to the end, regardless of where the read cursor is. Reading, on the other hand, always begins at the first byte of the buffer and increments the read cursor, with subsequent reads taking up where the last left off.

As a consumer of the Net::SSH library, you will rarely come into contact with these buffer objects directly, but it could happen. Also, if you are ever implementing a protocol on top of SSH (e.g. SFTP), this buffer class can be quite handy.

Direct Known Subclasses

Packet

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content = String.new) ⇒ Buffer

Creates a new buffer, initialized to the given content. The position is initialized to the beginning of the buffer.



73
74
75
76
# File 'lib/net/ssh/buffer.rb', line 73

def initialize(content = String.new)
  @content = content.to_s
  @position = 0
end

Instance Attribute Details

#contentObject (readonly)

exposes the raw content of the buffer



66
67
68
# File 'lib/net/ssh/buffer.rb', line 66

def content
  @content
end

#positionObject

the current position of the pointer in the buffer



69
70
71
# File 'lib/net/ssh/buffer.rb', line 69

def position
  @position
end

Class Method Details

.from(*args) ⇒ Object

This is a convenience method for creating and populating a new buffer from a single command. The arguments must be even in length, with the first of each pair of arguments being a symbol naming the type of the data that follows. If the type is :raw, the value is written directly to the hash.

b = Buffer.from(:byte, 1, :string, "hello", :raw, "\1\2\3\4")
#-> "\1\0\0\0\5hello\1\2\3\4"

The supported data types are:

  • :raw => write the next value verbatim (#write)

  • :int64 => write an 8-byte integer (#write_int64)

  • :long => write a 4-byte integer (#write_long)

  • :byte => write a single byte (#write_byte)

  • :string => write a 4-byte length followed by character data (#write_string)

  • :mstring => same as string, but caller cannot resuse the string, avoids potential duplication (#write_moved)

  • :bool => write a single byte, interpreted as a boolean (#write_bool)

  • :bignum => write an SSH-encoded bignum (#write_bignum)

  • :key => write an SSH-encoded key value (#write_key)

Any of these, except for :raw, accepts an Array argument, to make it easier to write multiple values of the same type in a briefer manner.

Raises:

  • (ArgumentError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/net/ssh/buffer.rb', line 46

def self.from(*args)
  raise ArgumentError, "odd number of arguments given" unless args.length % 2 == 0

  buffer = new
  0.step(args.length - 1, 2) do |index|
    type = args[index]
    value = args[index + 1]
    if type == :raw
      buffer.append(value.to_s)
    elsif Array === value
      buffer.send("write_#{type}", *value)
    else
      buffer.send("write_#{type}", value)
    end
  end

  buffer
end

Instance Method Details

#==(buffer) ⇒ Object

Compares the contents of the two buffers, returning true only if they are identical in size and content.



96
97
98
# File 'lib/net/ssh/buffer.rb', line 96

def ==(buffer)
  to_s == buffer.to_s
end

#append(text) ⇒ Object

Appends the given text to the end of the buffer. Does not alter the read position. Returns the buffer object itself.



145
146
147
148
# File 'lib/net/ssh/buffer.rb', line 145

def append(text)
  @content << text
  self
end

#availableObject

Returns the number of bytes available to be read (e.g., how many bytes remain between the current position and the end of the buffer).



85
86
87
# File 'lib/net/ssh/buffer.rb', line 85

def available
  length - position
end

#clear!Object

Resets the buffer, making it empty. Also, resets the read position to 0.



119
120
121
122
# File 'lib/net/ssh/buffer.rb', line 119

def clear!
  @content = String.new
  @position = 0
end

#consume!(n = position) ⇒ Object

Consumes n bytes from the buffer, where n is the current position unless otherwise specified. This is useful for removing data from the buffer that has previously been read, when you are expecting more data to be appended. It helps to keep the size of buffers down when they would otherwise tend to grow without bound.

Returns the buffer object itself.



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/net/ssh/buffer.rb', line 131

def consume!(n = position)
  if n >= length
    # optimize for a fairly common case
    clear!
  elsif n > 0
    @content = @content[n..-1] || String.new
    @position -= n
    @position = 0 if @position < 0
  end
  self
end

#empty?Boolean

Returns true if the buffer contains no data (e.g., it is of zero length).

Returns:

  • (Boolean)


101
102
103
# File 'lib/net/ssh/buffer.rb', line 101

def empty?
  @content.empty?
end

#eof?Boolean

Returns true if the pointer is at the end of the buffer. Subsequent reads will return nil, in this case.

Returns:

  • (Boolean)


113
114
115
# File 'lib/net/ssh/buffer.rb', line 113

def eof?
  @position >= length
end

#lengthObject

Returns the length of the buffer’s content.



79
80
81
# File 'lib/net/ssh/buffer.rb', line 79

def length
  @content.length
end

#read(count = nil) ⇒ Object

Reads and returns the next count bytes from the buffer, starting from the read position. If count is nil, this will return all remaining text in the buffer. This method will increment the pointer.



174
175
176
177
178
179
# File 'lib/net/ssh/buffer.rb', line 174

def read(count = nil)
  count ||= length
  count = length - @position if @position + count > length
  @position += count
  @content[@position - count, count]
end

#read!(count = nil) ⇒ Object

Reads (as #read) and returns the given number of bytes from the buffer, and then consumes (as #consume!) all data up to the new read position.



183
184
185
186
187
# File 'lib/net/ssh/buffer.rb', line 183

def read!(count = nil)
  data = read(count)
  consume!
  data
end

#read_all(&block) ⇒ Object

Calls block(self) until the buffer is empty, and returns all results.



190
191
192
# File 'lib/net/ssh/buffer.rb', line 190

def read_all(&block)
  Enumerator.new { |e| e << yield(self) until eof? }.to_a
end

#read_bignumObject

Read a bignum (OpenSSL::BN) from the buffer, in SSH2 format. It is essentially just a string, which is reinterpreted to be a bignum in binary format.



236
237
238
239
240
241
# File 'lib/net/ssh/buffer.rb', line 236

def read_bignum
  data = read_string
  return unless data

  OpenSSL::BN.new(data, 2)
end

#read_boolObject

Read a single byte and convert it into a boolean, using ‘C’ rules (i.e., zero is false, non-zero is true).



228
229
230
231
# File 'lib/net/ssh/buffer.rb', line 228

def read_bool
  b = read_byte or return nil
  b != 0
end

#read_bufferObject

Reads the next string from the buffer, and returns a new Buffer object that wraps it.



350
351
352
# File 'lib/net/ssh/buffer.rb', line 350

def read_buffer
  Buffer.new(read_string)
end

#read_byteObject

Read and return the next byte in the buffer. Returns nil if called at the end of the buffer.



213
214
215
216
# File 'lib/net/ssh/buffer.rb', line 213

def read_byte
  b = read(1) or return nil
  b.getbyte(0)
end

#read_int64Object

Return the next 8 bytes as a 64-bit integer (in network byte order). Returns nil if there are less than 8 bytes remaining to be read in the buffer.



197
198
199
200
201
# File 'lib/net/ssh/buffer.rb', line 197

def read_int64
  hi = read_long or return nil
  lo = read_long or return nil
  return (hi << 32) + lo
end

#read_keyObject

Read a key from the buffer. The key will start with a string describing its type. The remainder of the key is defined by the type that was read.



246
247
248
249
# File 'lib/net/ssh/buffer.rb', line 246

def read_key
  type = read_string
  return (type ? read_keyblob(type) : nil)
end

#read_keyblob(type) ⇒ Object

Read a keyblob of the given type from the buffer, and return it as a key. Only RSA, DSA, and ECDSA keys are supported.



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
# File 'lib/net/ssh/buffer.rb', line 295

def read_keyblob(type)
  case type
  when /^(.*)-cert-v01@openssh\.com$/
    key = Net::SSH::Authentication::Certificate.read_certblob(self, $1)
  when /^ssh-dss$/
    p = read_bignum
    q = read_bignum
    g = read_bignum
    pub_key = read_bignum

    asn1 = OpenSSL::ASN1::Sequence.new(
      [
        OpenSSL::ASN1::Sequence.new(
          [
            OpenSSL::ASN1::ObjectId.new('DSA'),
            OpenSSL::ASN1::Sequence.new(
              [
                OpenSSL::ASN1::Integer.new(p),
                OpenSSL::ASN1::Integer.new(q),
                OpenSSL::ASN1::Integer.new(g)
              ]
            )
          ]
        ),
        OpenSSL::ASN1::BitString.new(OpenSSL::ASN1::Integer.new(pub_key).to_der)
      ]
    )

    key = OpenSSL::PKey::DSA.new(asn1.to_der)
  when /^ssh-rsa$/
    e = read_bignum
    n = read_bignum

    asn1 = OpenSSL::ASN1::Sequence(
      [
        OpenSSL::ASN1::Integer(n),
        OpenSSL::ASN1::Integer(e)
      ]
    )

    key = OpenSSL::PKey::RSA.new(asn1.to_der)
  when /^ssh-ed25519$/
    Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("unsupported key type `#{type}'")
    key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self)
  when /^ecdsa\-sha2\-(\w*)$/
    key = OpenSSL::PKey::EC.read_keyblob($1, self)
  else
    raise NotImplementedError, "unsupported key type `#{type}'"
  end

  return key
end

#read_longObject

Return the next four bytes as a long integer (in network byte order). Returns nil if there are less than 4 bytes remaining to be read in the buffer.



206
207
208
209
# File 'lib/net/ssh/buffer.rb', line 206

def read_long
  b = read(4) or return nil
  b.unpack("N").first
end

#read_private_keyblob(type) ⇒ Object



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
# File 'lib/net/ssh/buffer.rb', line 251

def read_private_keyblob(type)
  case type
  when /^ssh-rsa$/
    n = read_bignum
    e = read_bignum
    d = read_bignum
    iqmp = read_bignum
    p = read_bignum
    q = read_bignum
    _unkown1 = read_bignum
    _unkown2 = read_bignum
    dmp1 = d % (p - 1)
    dmq1 = d % (q - 1)
    # Public key
    data_sequence = OpenSSL::ASN1::Sequence([
                                              OpenSSL::ASN1::Integer(n),
                                              OpenSSL::ASN1::Integer(e),
                                            ])

    if d && p && q && dmp1 && dmq1 && iqmp
      data_sequence = OpenSSL::ASN1::Sequence([
                                                OpenSSL::ASN1::Integer(0),
                                                OpenSSL::ASN1::Integer(n),
                                                OpenSSL::ASN1::Integer(e),
                                                OpenSSL::ASN1::Integer(d),
                                                OpenSSL::ASN1::Integer(p),
                                                OpenSSL::ASN1::Integer(q),
                                                OpenSSL::ASN1::Integer(dmp1),
                                                OpenSSL::ASN1::Integer(dmq1),
                                                OpenSSL::ASN1::Integer(iqmp),
                                              ])
    end

    asn1 = OpenSSL::ASN1::Sequence(data_sequence)
    OpenSSL::PKey::RSA.new(asn1.to_der)
  when /^ecdsa\-sha2\-(\w*)$/
    OpenSSL::PKey::EC.read_keyblob($1, self)
  else
    raise Exception, "Cannot decode private key of type #{type}"
  end
end

#read_stringObject

Read and return an SSH2-encoded string. The string starts with a long integer that describes the number of bytes remaining in the string. Returns nil if there are not enough bytes to satisfy the request.



221
222
223
224
# File 'lib/net/ssh/buffer.rb', line 221

def read_string
  length = read_long or return nil
  read(length)
end

#read_to(pattern) ⇒ Object

Reads all data up to and including the given pattern, which may be a String, Fixnum, or Regexp and is interpreted exactly as String#index does. Returns nil if nothing matches. Increments the position to point immediately after the pattern, if it does match. Returns all data up to and including the text that matched the pattern.



161
162
163
164
165
166
167
168
169
# File 'lib/net/ssh/buffer.rb', line 161

def read_to(pattern)
  index = @content.index(pattern, @position) or return nil
  length = case pattern
           when String then pattern.length
           when Integer then 1
           when Regexp then $&.length
           end
  index && read(index + length)
end

#remainder_as_bufferObject

Returns all text from the current pointer to the end of the buffer as a new Net::SSH::Buffer object.



152
153
154
# File 'lib/net/ssh/buffer.rb', line 152

def remainder_as_buffer
  Buffer.new(@content[@position..-1])
end

#reset!Object

Resets the pointer to the start of the buffer. Subsequent reads will begin at position 0.



107
108
109
# File 'lib/net/ssh/buffer.rb', line 107

def reset!
  @position = 0
end

#to_sObject

Returns a copy of the buffer’s content.



90
91
92
# File 'lib/net/ssh/buffer.rb', line 90

def to_s
  (@content || "").dup
end

#write(*data) ⇒ Object

Writes the given data literally into the string. Does not alter the read position. Returns the buffer object.



356
357
358
359
# File 'lib/net/ssh/buffer.rb', line 356

def write(*data)
  data.each { |datum| @content << datum.dup.force_encoding('BINARY') }
  self
end

#write_bignum(*n) ⇒ Object

Writes each argument to the buffer as a bignum (SSH2-style). No checking is done to ensure that the arguments are, in fact, bignums. Does not alter the read position. Returns the buffer object.



436
437
438
439
# File 'lib/net/ssh/buffer.rb', line 436

def write_bignum(*n)
  @content << n.map { |b| b.to_ssh }.join
  self
end

#write_bool(*b) ⇒ Object

Writes each argument to the buffer as a (C-style) boolean, with 1 meaning true, and 0 meaning false. Does not alter the read position. Returns the buffer object.



428
429
430
431
# File 'lib/net/ssh/buffer.rb', line 428

def write_bool(*b)
  b.each { |v| @content << (v ? "\1" : "\0") }
  self
end

#write_byte(*n) ⇒ Object

Writes each argument to the buffer as a byte. Does not alter the read position. Returns the buffer object.



395
396
397
398
# File 'lib/net/ssh/buffer.rb', line 395

def write_byte(*n)
  n.each { |b| @content << b.chr }
  self
end

#write_int64(*n) ⇒ Object

Writes each argument to the buffer as a network-byte-order-encoded 64-bit integer (8 bytes). Does not alter the read position. Returns the buffer object.



376
377
378
379
380
381
382
383
# File 'lib/net/ssh/buffer.rb', line 376

def write_int64(*n)
  n.each do |i|
    hi = (i >> 32) & 0xFFFFFFFF
    lo = i & 0xFFFFFFFF
    @content << [hi, lo].pack("N2")
  end
  self
end

#write_key(*key) ⇒ Object

Writes the given arguments to the buffer as SSH2-encoded keys. Does not alter the read position. Returns the buffer object.



443
444
445
446
# File 'lib/net/ssh/buffer.rb', line 443

def write_key(*key)
  key.each { |k| append(k.to_blob) }
  self
end

#write_long(*n) ⇒ Object

Writes each argument to the buffer as a network-byte-order-encoded long (4-byte) integer. Does not alter the read position. Returns the buffer object.



388
389
390
391
# File 'lib/net/ssh/buffer.rb', line 388

def write_long(*n)
  @content << n.pack("N*")
  self
end

#write_moved(string) ⇒ Object

Optimized version of write where the caller gives up ownership of string to the method. This way we can mutate the string.



363
364
365
366
367
368
369
370
371
# File 'lib/net/ssh/buffer.rb', line 363

def write_moved(string)
  @content <<
    if string.frozen?
      string.dup.force_encoding('BINARY')
    else
      string.force_encoding('BINARY')
    end
  self
end

#write_mstring(*text) ⇒ Object

Writes each argument to the buffer as an SSH2-encoded string. Each string is prefixed by its length, encoded as a 4-byte long integer. Does not alter the read position. Returns the buffer object. Might alter arguments see write_moved



416
417
418
419
420
421
422
423
# File 'lib/net/ssh/buffer.rb', line 416

def write_mstring(*text)
  text.each do |string|
    s = string.to_s
    write_long(s.bytesize)
    write_moved(s)
  end
  self
end

#write_string(*text) ⇒ Object

Writes each argument to the buffer as an SSH2-encoded string. Each string is prefixed by its length, encoded as a 4-byte long integer. Does not alter the read position. Returns the buffer object.



403
404
405
406
407
408
409
410
# File 'lib/net/ssh/buffer.rb', line 403

def write_string(*text)
  text.each do |string|
    s = string.to_s
    write_long(s.bytesize)
    write(s)
  end
  self
end