class Net::SSH::Buffer
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.
Attributes
exposes the raw content of the buffer
the current position of the pointer in the buffer
Public Class Methods
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.
# 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
Creates a new buffer, initialized to the given content. The position is initialized to the beginning of the buffer.
# File lib/net/ssh/buffer.rb, line 73 def initialize(content = String.new) @content = content.to_s @position = 0 end
Public Instance Methods
Compares the contents of the two buffers, returning true
only if they are identical in size and content.
# File lib/net/ssh/buffer.rb, line 96 def ==(buffer) to_s == buffer.to_s end
Appends the given text to the end of the buffer. Does not alter the read position. Returns the buffer object itself.
# File lib/net/ssh/buffer.rb, line 145 def append(text) @content << text self end
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).
# File lib/net/ssh/buffer.rb, line 85 def available length - position end
Resets the buffer, making it empty. Also, resets the read position to 0.
# File lib/net/ssh/buffer.rb, line 119 def clear! @content = String.new @position = 0 end
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.
# 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
Returns true
if the buffer contains no data (e.g., it is of zero length).
# File lib/net/ssh/buffer.rb, line 101 def empty? @content.empty? end
Returns true if the pointer is at the end of the buffer. Subsequent reads will return nil, in this case.
# File lib/net/ssh/buffer.rb, line 113 def eof? @position >= length end
Returns the length of the buffer’s content.
# File lib/net/ssh/buffer.rb, line 79 def length @content.length end
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.
# 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
Calls block(self) until the buffer is empty, and returns all results.
# File lib/net/ssh/buffer.rb, line 190 def read_all(&block) Enumerator.new { |e| e << yield(self) until eof? }.to_a end
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.
# File lib/net/ssh/buffer.rb, line 236 def read_bignum data = read_string return unless data OpenSSL::BN.new(data, 2) end
Read a single byte and convert it into a boolean, using ‘C’ rules (i.e., zero is false, non-zero is true).
# File lib/net/ssh/buffer.rb, line 228 def read_bool b = read_byte or return nil b != 0 end
Reads the next string from the buffer, and returns a new Buffer
object that wraps it.
# File lib/net/ssh/buffer.rb, line 350 def read_buffer Buffer.new(read_string) end
Read and return the next byte in the buffer. Returns nil if called at the end of the buffer.
# File lib/net/ssh/buffer.rb, line 213 def read_byte b = read(1) or return nil b.getbyte(0) end
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.
# 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 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.
# File lib/net/ssh/buffer.rb, line 246 def read_key type = read_string return (type ? read_keyblob(type) : nil) end
Read a keyblob of the given type from the buffer, and return it as a key. Only RSA, DSA, and ECDSA keys are supported.
# 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
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.
# File lib/net/ssh/buffer.rb, line 206 def read_long b = read(4) or return nil b.unpack("N").first end
# 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 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.
# File lib/net/ssh/buffer.rb, line 221 def read_string length = read_long or return nil read(length) end
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.
# 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
Returns all text from the current pointer to the end of the buffer as a new Net::SSH::Buffer
object.
# File lib/net/ssh/buffer.rb, line 152 def remainder_as_buffer Buffer.new(@content[@position..-1]) end
Resets the pointer to the start of the buffer. Subsequent reads will begin at position 0.
# File lib/net/ssh/buffer.rb, line 107 def reset! @position = 0 end
Returns a copy of the buffer’s content.
# File lib/net/ssh/buffer.rb, line 90 def to_s (@content || "").dup end
Writes the given data literally into the string. Does not alter the read position. Returns the buffer object.
# File lib/net/ssh/buffer.rb, line 356 def write(*data) data.each { |datum| @content << datum.dup.force_encoding('BINARY') } self end
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.
# File lib/net/ssh/buffer.rb, line 436 def write_bignum(*n) @content << n.map { |b| b.to_ssh }.join self end
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.
# File lib/net/ssh/buffer.rb, line 428 def write_bool(*b) b.each { |v| @content << (v ? "\1" : "\0") } self end
Writes each argument to the buffer as a byte. Does not alter the read position. Returns the buffer object.
# File lib/net/ssh/buffer.rb, line 395 def write_byte(*n) n.each { |b| @content << b.chr } self end
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.
# 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
Writes the given arguments to the buffer as SSH2-encoded keys. Does not alter the read position. Returns the buffer object.
# File lib/net/ssh/buffer.rb, line 443 def write_key(*key) key.each { |k| append(k.to_blob) } self end
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.
# File lib/net/ssh/buffer.rb, line 388 def write_long(*n) @content << n.pack("N*") self end
Optimized version of write where the caller gives up ownership of string to the method. This way we can mutate the string.
# 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
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
# 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
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.
# 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