# -*- coding: binary -*-

require 'rex/socket'

module Rex
module Proto
module Nuuo
# This class is a representation of a nuuo client
class Client
  # @!attribute host
  #   @return [String] The nuuo server host
  attr_accessor :host
  # @!attribute port
  #   @return [Integer] The nuuo server port
  attr_accessor :port
  # @!attribute timeout
  #   @return [Integer] The connect/read timeout
  attr_accessor :timeout
  # @!attribute connection
  #   @return [IO] The connection established through Rex sockets
  attr_accessor :connection
  # @!attribute context
  #   @return [Hash] The Msf context where the connection belongs to
  attr_accessor :context
  # @!attribute ncs_version
  #   @return [String] NCS version used in session
  attr_accessor :ncs_version
  # @!attribute username
  #   @return [String] Username for NCS
  attr_accessor :username
  # @!attribute password
  #   @return [String] Password for NCS user
  attr_accessor :password
  # @!attribute user_session
  #   @return [String] ID for the user session
  attr_accessor :user_session
  # @!attribute config
  #   @return [Hash] ClientRequest configuration options
  attr_accessor :config

  def initialize(opts = {})
    self.host         = opts[:host]
    self.port         = opts[:port] || 5180
    self.timeout      = opts[:timeout] || 10
    self.context      = opts[:context] || {}
    self.username     = opts[:username]
    self.password     = opts[:password]
    self.user_session = opts[:user_session]

    self.config = Nuuo::ClientRequest::DefaultConfig
  end

  # Creates a connection through a Rex socket
  #
  # @return [Rex::Socket::Tcp]
  def connect(temp: false)
    return connection if connection && !temp
    return create_tcp_connection(temp: temp)
  end

  # Closes the connection
  def close
    if connection
      connection.shutdown
      connection.close unless connection.closed?
    end

    self.connection = nil
  end

  def send_recv(req, conn=nil, t=-1)
    send_request(req, conn)
    read_response(conn, t)
  end

  def send_request(req, conn=nil)
    conn ? conn.put(req.to_s) : connect.put(req.to_s)
  end

  def read_response(conn=nil, t=-1)
    res = Response.new
    conn = connection unless conn

    return res if not t
    Timeout.timeout((t < 0) ? nil : t) do
      parse_status = nil
      while (!conn.closed? &&
              parse_status != Response::ParseCode::Completed &&
              parse_status != Response::ParseCode::Error
      )
        begin
          buff = conn.get_once
          parse_status = res.parse(buff || '')
        rescue ::Errno::EPIPE, ::EOFError, ::IOError
          case res.state
          when Response::ParseState::ProcessingHeader
            res = nil
          when Response::ParseState::ProcessingBody
            res.error = :truncated
          end
          break
        end
      end
    end

    res
  end

  def user_session_header(opts)
    val = nil
    if opts['user_session']
      val = opts['user_session']
    elsif self.user_session
      val = self.user_session
    end
  end

  def request_ping(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'PING'
    session = user_session_header(opts)
    opts['headers']['User-Session-No'] = session if session

    ClientRequest.new(opts)
  end

  def request_sendlicfile(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'SENDLICFILE'

    session = user_session_header(opts)
    opts['headers']['User-Session-No'] = session if session
    opts['data'] = '' unless opts['data']

    opts['headers']['FileName'] = opts['file_name']
    opts['headers']['Content-Length'] = opts['data'].length

    ClientRequest.new(opts)
  end

  # GETCONFIG
  # FileName:
  # FileType: 1
  # User-Session-No: <session-no>
  # @return [ClientRequest]
  def request_getconfig(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'GETCONFIG'

    opts['headers']['FileName'] = opts['file_name']
    opts['headers']['FileType'] = opts['file_type'] || 1
    session = user_session_header(opts)
    opts['headers']['User-Session-No'] = session if session

    ClientRequest.new(opts)
  end

  # COMMITCONFIG
  # FileName:
  # FileType: 1
  # Content-Length
  # User-Session-No: <session-no>
  #
  # <data> filedata
  # @return [ClientRequest]
  def request_commitconfig(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'COMMITCONFIG'

    opts['headers']['FileName'] = opts['file_name']
    opts['headers']['FileType'] = opts['file_type'] || 1

    session = user_session_header(opts)
    opts['headers']['User-Session-No'] = session if session

    opts['data'] = '' unless opts['data']
    opts['headers']['Content-Length'] = opts['data'].length

    ClientRequest.new(opts)
  end

  # USERLOGIN
  # Version:
  # Username:
  # Password-Length:
  # TimeZone-Length: 0
  #
  # <data> password
  # @return [ClientRequest]
  def request_userlogin(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'USERLOGIN'

    # Account for version...
    opts['headers']['Version'] = opts['server_version']

    username = nil
    if opts['username'] && opts['username'] != ''
      username = opts['username']
    elsif self.username && self.username != ''
      username = self.username
    end

    opts['headers']['Username'] = username

    password = ''
    if opts['password'] && opts['password'] != ''
      password = opts['password']
    elsif self.password && self.password != ''
      password = self.password
    end
    opts['data'] = password
    opts['headers']['Password-Length'] = password.length

    # Need to verify if this is needed
    opts['headers']['TimeZone-Length'] = '0'

    ClientRequest.new(opts)
  end

  # GETOPENALARM NUCM/1.0
  # DeviceID: <number>
  # SourceServer: <server-id>
  # LastOne: <number>
  # @return [ClientRequest]
  def request_getopenalarm(opts={})
    opts = self.config.merge(opts)
    opts['headers'] ||= {}
    opts['method'] = 'GETOPENALARM'

    opts['headers']['DeviceID'] = opts['device_id'] || 1
    opts['headers']['SourceServer'] = opts['source_server'] || 1
    opts['headers']['LastOne'] = opts['last_one'] || 1

    ClientRequest.new(opts)
  end


  private

  # Creates a TCP connection using Rex::Socket::Tcp
  #
  # @return [Rex::Socket::Tcp]
  def create_tcp_connection(temp: false)
    tcp_connection = Rex::Socket::Tcp.create(
      'PeerHost'  => host,
      'PeerPort'  => port.to_i,
      'Context'   => context,
      'Timeout'   => timeout
    )
    self.connection = tcp_connection unless temp
    tcp_connection
  end

end
end
end
end
