require 'xdr'

module Msf

###
#
# This module exposes methods for accessing NDMP services using a class-based wrapper
# around normal sockets, allowing the use of more than one NDMP connection at once.
#
###
module Exploit::Remote::NDMPSocket

module NDMP

#
# This class represents a NDMP message, including its header and body.
#
class Message
  class Header < XDR::Struct
    attribute :sequence_num,       XDR::Int
    attribute :timestamp,          XDR::Int
    attribute :is_response,        XDR::Bool
    attribute :type,               XDR::Int
    attribute :reply_sequence_num, XDR::Int
    attribute :error,              XDR::Int
  end

  CONFIG_GET_HOST_INFO   = 0x100
  CONFIG_GET_BUTYPE_ATTR = 0x101
  CONFIG_GET_SERVER_INFO = 0x108
  NOTIFY_CONNECTED       = 0x502
  CONNECT_OPEN           = 0x900
  CONNECT_CLIENT_AUTH    = 0x901

  def self.new_request(type, body='')
    header = Header.new(
      :sequence_num       => nil,
      :timestamp          => nil,
      :is_response        => false,
      :type               => type,
      :reply_sequence_num => 0,
      :error              => 0
    )
    new(header, body)
  end

  attr_accessor :header, :body

  def initialize(header, body)
    @header = header
    @body = body
  end
end

#
# This class wraps a normal socket with NDMP functionality, such as NDMP message reading
# and writing.
#
class Socket
  def initialize(sock)
    @sock = sock
    @next_sequence_num = 1
  end

  def raw_recv(*args)
    @sock.recv(*args)
  end

  def raw_recvall(n, *args)
    r = ''
    while r.length < n
      s = raw_recv(n - r.length, *args)
      return nil if s.to_s.empty?
      r << s
    end
    r
  end

  def raw_send(*args)
    @sock.send(*args)
  end

  def raw_sendall(s, *args)
    n = 0
    while n < s.length
      i = raw_send(s[n..s.length], *args)
      return false if i <= 0
      n += i
    end

    true
  end

  def close
    @sock.close
  end

  #
  # Read a single NDMP message. If require_ok_type is given, the message must be of the
  # given type and have no error indicated.
  #
  def read_ndmp_msg(require_ok_type=nil)
    frags = read_ndmp_frags
    return nil if frags.nil?
    header = Message::Header.from_xdr(frags.slice!(0...(4 * 6)))

    return nil if require_ok_type && (require_ok_type != header.type || header.error != 0)

    Message.new(header, frags)
  end

  #
  # Prepare a NDMP message for sending and send it. Can send messages multiple times.
  #
  # If all_but_all_char is true, then the last character will be held back and will be
  # returned so that it can be sent at a later point elsewhere. This is sometimes
  # necessary for exploiting e.g. race conditions.
  #
  def prepare_and_write_ndmp_msg(msg, all_but_last_char=false, times=1, flags=0)
    msg.header.sequence_num = @next_sequence_num
    @next_sequence_num += 1
    msg.header.timestamp = Time.now.to_i

    frag = msg.header.to_xdr + msg.body
    write_ndmp_frag(frag, all_but_last_char, times, flags)
  end

  #
  # Send and receive a pair of NDMP messages.
  #
  def do_request_response(msg, *args)
    return nil unless prepare_and_write_ndmp_msg(msg, *args)
    read_ndmp_msg(msg.header.type)
  end

  #
  # Establish a SSL session on the socket. Raw socket reading/writing functions are
  # replaced with their SSL equivalents.
  #
  def wrap_with_ssl(ssl_context)
    @sock = OpenSSL::SSL::SSLSocket.new(@sock, ssl_context)
    @sock.connect

    def self.raw_recv(n, *_args)
      @sock.sysread(n)
    end

    def self.raw_send(b, *_args)
      @sock.syswrite(b)
    end
  end

  private

  #
  # Read and reassemble a group of NDMP fragments. Usually NDMP messages are sent in a
  # single NDMP fragment, but this is not guaranteed by the standard.
  #
  def read_ndmp_frags
    result = ''

    loop do
      buf = raw_recvall(4)
      return nil if buf.nil?
      n = buf.unpack('N')[0]
      len = n & 0x7fffffff
      last = (n & 0x80000000) != 0

      buf = raw_recvall(len)
      return nil if buf.nil?
      result << buf

      break if last
    end

    result
  end

  #
  # Write a NDMP fragment (i.e. containing a NDMP packet).
  #
  # Can hold back on sending the last character; see prepare_and_write_ndmp_msg.
  #
  def write_ndmp_frag(buf, all_but_last_char, times, flags)
    buf = ([buf.length | 0x80000000].pack('N') + buf) * times

    return false unless raw_sendall(all_but_last_char ? buf[0...-1] : buf, flags)

    all_but_last_char ? buf[-1] : true
  end

end

end

end

end
