class Net::SSH::Buffer
class can be quite handy.
are ever implementing a protocol on top of SSH (e.g. SFTP), this buffer
with these buffer objects directly, but it could happen. Also, if you
As a consumer of the Net::SSH library, you will rarely come into contact
taking up where the last left off.
byte of the buffer and increments the read cursor, with subsequent reads
read cursor is. Reading, on the other hand, always begins at the first
Writing to a buffer always appends to the end, regardless of where the
for building binary packets given a signature.
reading data items from the buffer, as well as a useful helper method
data packets. It provides a stream-like interface for sequentially
Net::SSH::Buffer is a flexible class for building and parsing binary
def self.from(*args)
Any of these, except for :raw, accepts an Array argument, to make it
* :key => write an SSH-encoded key value (#write_key)
* :bignum => write an SSH-encoded bignum (#write_bignum)
* :bool => write a single byte, interpreted as a boolean (#write_bool)
* :mstring => same as string, but caller cannot resuse the string, avoids potential duplication (#write_moved)
* :string => write a 4-byte length followed by character data (#write_string)
* :byte => write a single byte (#write_byte)
* :long => write a 4-byte integer (#write_long)
* :int64 => write an 8-byte integer (#write_int64)
* :raw => write the next value verbatim (#write)
The supported data types are:
#-> "\1\0\0\0\5hello\1\2\3\4"
b = Buffer.from(:byte, 1, :string, "hello", :raw, "\1\2\3\4")
to the hash.
data that follows. If the type is :raw, the value is written directly
first of each pair of arguments being a symbol naming the type of the
from a single command. The arguments must be even in length, with the
This is a convenience method for creating and populating a new buffer
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
def ==(buffer)
Compares the contents of the two buffers, returning +true+ only if they
def ==(buffer) to_s == buffer.to_s end
def append(text)
Appends the given text to the end of the buffer. Does not alter the
def append(text) @content << text self end
def available
Returns the number of bytes available to be read (e.g., how many bytes
def available length - position end
def clear!
Resets the buffer, making it empty. Also, resets the read position to
def clear! @content = String.new @position = 0 end
def consume!(n = position)
would otherwise tend to grow without bound.
to be appended. It helps to keep the size of buffers down when they
buffer that has previously been read, when you are expecting more data
unless otherwise specified. This is useful for removing data from the
Consumes n bytes from the buffer, where n is the current position
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
def empty?
def empty? @content.empty? end
def eof?
Returns true if the pointer is at the end of the buffer. Subsequent
def eof? @position >= length end
def initialize(content = String.new)
Creates a new buffer, initialized to the given content. The position
def initialize(content = String.new) @content = content.to_s @position = 0 end
def length
def length @content.length end
def read(count = nil)
the read position. If +count+ is +nil+, this will return all remaining
Reads and returns the next +count+ bytes from the buffer, starting from
def read(count = nil) count ||= length count = length - @position if @position + count > length @position += count @content[@position - count, count] end
def read!(count = nil)
Reads (as #read) and returns the given number of bytes from the buffer,
def read!(count = nil) data = read(count) consume! data end
def read_all(&block)
def read_all(&block) Enumerator.new { |e| e << yield(self) until eof? }.to_a end
def read_bignum
essentially just a string, which is reinterpreted to be a bignum in
Read a bignum (OpenSSL::BN) from the buffer, in SSH2 format. It is
def read_bignum data = read_string return unless data OpenSSL::BN.new(data, 2) end
def read_bool
Read a single byte and convert it into a boolean, using 'C' rules
def read_bool b = read_byte or return nil b != 0 end
def read_buffer
Reads the next string from the buffer, and returns a new Buffer
def read_buffer Buffer.new(read_string) end
def read_byte
Read and return the next byte in the buffer. Returns nil if called at
def read_byte b = read(1) or return nil b.getbyte(0) end
def read_int64
Returns nil if there are less than 8 bytes remaining to be read in the
Return the next 8 bytes as a 64-bit integer (in network byte order).
def read_int64 hi = read_long or return nil lo = read_long or return nil return (hi << 32) + lo end
def read_key
describing its type. The remainder of the key is defined by the
Read a key from the buffer. The key will start with a string
def read_key type = read_string return (type ? read_keyblob(type) : nil) end
def read_keyblob(type)
Read a keyblob of the given type from the buffer, and return it as
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
def read_long
Returns nil if there are less than 4 bytes remaining to be read in the
Return the next four bytes as a long integer (in network byte order).
def read_long b = read(4) or return nil b.unpack("N").first end
def read_private_keyblob(type)
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
def read_string
integer that describes the number of bytes remaining in the string.
Read and return an SSH2-encoded string. The string starts with a long
def read_string length = read_long or return nil read(length) end
def read_to(pattern)
immediately after the pattern, if it does match. Returns all data up to
does. Returns nil if nothing matches. Increments the position to point
String, Fixnum, or Regexp and is interpreted exactly as String#index
Reads all data up to and including the given pattern, which may be a
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
def remainder_as_buffer
Returns all text from the current pointer to the end of the buffer as
def remainder_as_buffer Buffer.new(@content[@position..-1]) end
def reset!
Resets the pointer to the start of the buffer. Subsequent reads will
def reset! @position = 0 end
def to_s
def to_s (@content || "").dup end
def write(*data)
Writes the given data literally into the string. Does not alter the
def write(*data) data.each { |datum| @content << datum.dup.force_encoding('BINARY') } self end
def write_bignum(*n)
checking is done to ensure that the arguments are, in fact, bignums.
Writes each argument to the buffer as a bignum (SSH2-style). No
def write_bignum(*n) @content << n.map { |b| b.to_ssh }.join self end
def write_bool(*b)
meaning true, and 0 meaning false. Does not alter the read position.
Writes each argument to the buffer as a (C-style) boolean, with 1
def write_bool(*b) b.each { |v| @content << (v ? "\1" : "\0") } self end
def write_byte(*n)
Writes each argument to the buffer as a byte. Does not alter the read
def write_byte(*n) n.each { |b| @content << b.chr } self end
def write_int64(*n)
64-bit integer (8 bytes). Does not alter the read position. Returns the
Writes each argument to the buffer as a network-byte-order-encoded
def write_int64(*n) n.each do |i| hi = (i >> 32) & 0xFFFFFFFF lo = i & 0xFFFFFFFF @content << [hi, lo].pack("N2") end self end
def write_key(*key)
Writes the given arguments to the buffer as SSH2-encoded keys. Does not
def write_key(*key) key.each { |k| append(k.to_blob) } self end
def write_long(*n)
long (4-byte) integer. Does not alter the read position. Returns the
Writes each argument to the buffer as a network-byte-order-encoded
def write_long(*n) @content << n.pack("N*") self end
def write_moved(string)
Optimized version of write where the caller gives up ownership of string
def write_moved(string) @content << if string.frozen? string.dup.force_encoding('BINARY') else string.force_encoding('BINARY') end self end
def write_mstring(*text)
Does not alter the read position. Returns the buffer object.
string is prefixed by its length, encoded as a 4-byte long integer.
Writes each argument to the buffer as an SSH2-encoded string. Each
def write_mstring(*text) text.each do |string| s = string.to_s write_long(s.bytesize) write_moved(s) end self end
def write_string(*text)
string is prefixed by its length, encoded as a 4-byte long integer.
Writes each argument to the buffer as an SSH2-encoded string. Each
def write_string(*text) text.each do |string| s = string.to_s write_long(s.bytesize) write(s) end self end