# -*- coding: binary -*-
require 'rex/proto/ssh/hrr_rb_ssh'

module Rex
module Proto
module Ssh

##
# Whitelist-based access control scaffold
##
module AccessControlList

  #
  # Add permitted access control entry to access control list
  # Create ACL if it does not yet exist
  #
  # @param host [String] Host/hostname for which to grant access
  # @param port [Integer] Port for which to grant access
  # @param bind [TrueClass,FalseClass] Whether this ACE is for servers
  #
  def permit=(host, port, bind = false)
    @acl ||= { bind:[], connect:[] }
    unless permit?(host, port, bind)
      @acl[ bind ? :bind : :connect ] << "#{host}:#{port}"
    end
  end

  #
  # Delete permitted access control entry from access control list
  #
  # @param host [String] Host/hostname for which to grant access
  # @param port [Integer] Port for which to grant access
  # @param bind [TrueClass,FalseClass] Whether this ACE is for servers
  #
  def deny=(host, port, bind = false)
    @acl[ bind ? :bind : :connect ].select! do |ent|
      ent != "#{host}:#{port}"
    end if @acl
  end

  #
  # Check if access control entry exists in access control list
  #
  # @param host [String] Host/hostname for which to check access
  # @param port [Integer] Port for which to check access
  # @param bind [TrueClass,FalseClass] Whether this ACE is for servers
  #
  # @return [TrueClass,FalseClass] Permission boolean for access
  def permit?(host, port, bind = false)
    @acl and ["#{host}:#{port}", "*:*", "#{host}:*", "*:#{port}"].any? do |m|
      @acl[ bind ? :bind : :connect ].include?(m)
    end
  end
end

##
# Encapsulation of Connection constructor for Rex use
# Provides ACLs for port forwarding and client (io) access hooks
##
class Connection < ::HrrRbSsh::Connection
  include AccessControlList
  def self.default_options
    noneauth = HrrRbSsh::Authentication::Authenticator.new { |context| true }
    return {
      'authentication_none_authenticator' => noneauth,
      'authentication_password_authenticator' => noneauth,
      'authentication_publickey_authenticator' => noneauth,
      'authentication_keyboard_interactive_authenticator' => noneauth,
      'local_version' => 'SSH-2.0-RexProtoSsh'
    }
  end
  #
  # Create new Connection from an IO and options set, pull trans
  # and auth from options if present, create from options set otherwise.
  #
  # Creates a default empty handler set for channel requests.
  #
  # @param io [IO] Socket, FD, or abstraction on which to build Connection
  # @param options [Hash] Options for constructing Connection components
  #
  # @return [Rex::Proto::Ssh::Connection] a new connection object
  def initialize(io = nil, options = self.default_options, context = {})
    @context = context
    @logger = Logger.new self.class.name
    @server = options.delete(:ssh_server)
    @mode = options.delete(:ssh_mode) || HrrRbSsh::Mode::SERVER
    # Take a pre-built transport from the options or build one on the fly
    @transport = options.delete(:ssh_transport) || HrrRbSsh::Transport.new(
      io,
      @mode,
      options
    )
    # Take a pre-built authentication from the options or build one on the fly
    @authentication = options.delete(:ssh_authentication) ||
      HrrRbSsh::Authentication.new(@transport, @mode, options)
    @global_request_handler = GlobalRequestHandler.new(self)
    # Retain remaining options for later use
    @options = options

    @channels = Hash.new
    @username = nil
    @closed = nil
  end

  #
  # Provide keys of explicitly not closed channels
  #
  # @param ctype [String] Channel type to select, nil for all
  #
  # @return [Array] Array of integers indexing open channels
  def open_channel_keys(ctype = 'session')
    channels.keys.sort.select do |cn|
      channels[cn].closed? === false and (
        ctype.nil? or channels[cn].channel_type == ctype
      )
    end
  end

  #
  # Provide IO from which to read remote-end inputs
  #
  # @param fd [Integer] Desired descriptor from which to read
  # @param cn [Integer] Desired channel from which to take fd
  #
  # @return [IO] File descriptor for reading
  def reader(fd = 0, cn = open_channel_keys.first)
    channels[cn].io[fd]
  end

  #
  # Provide IO into which writes to the remote end can be sent
  #
  # @param fd [Integer] Desired descriptor to which to write
  # @param cn [Integer] Desired channel from which to take fd
  #
  # @return [IO] File descriptor for writing
  def writer(fd = 1, cn = open_channel_keys.first)
    channels[cn].io[fd]
  end

  #
  # Close the connection and underlying socket
  #
  def close
    super
    @transport.io.close if @transport and !@transport.io.closed?
  end

  attr_accessor :transport, :authentication, :channels, :global_request_handler
  attr_reader :server, :context
end

##
# A modified Rex::IO::Stream for separate file descriptors
# consumers are responsible for relevant initialization and
# fd_rd+fd_wr methods to expose selectable R/W IOs.
##
module IOMergeAbstraction
  def inspect
    "#{self.class}(#{fd_rd.inspect}|#{fd_wr.inspect})"
  end

  def write(buf, opts = {})
    total_sent   = 0
    total_length = buf.length
    block_size   = 32768

    begin
      while( total_sent < total_length )
        s = Rex::ThreadSafe.select( nil, [ fd_wr ], nil, 0.2 )
        if( s == nil || s[0] == nil )
          next
        end
        data = buf[total_sent, block_size]
        sent = fd_wr.write_nonblock( data )
        if sent > 0
          total_sent += sent
        end
      end
    rescue ::Errno::EAGAIN, ::Errno::EWOULDBLOCK
      # Sleep for a half a second, or until we can write again
      Rex::ThreadSafe.select( nil, [ fd_wr ], nil, 0.5 )
      # Decrement the block size to handle full sendQs better
      block_size = 1024
      # Try to write the data again
      retry
    rescue ::IOError, ::Errno::EPIPE
      return nil
    end

    total_sent
  end

  #
  # This method reads data of the supplied length from the stream.
  #
  def read(length = nil, opts = {})

    begin
      return fd_rd.read_nonblock( length )
    rescue ::Errno::EAGAIN, ::Errno::EWOULDBLOCK
      # Sleep for a half a second, or until we can read again
      Rex::ThreadSafe.select( [ fd_rd ], nil, nil, 0.5 )
      # Decrement the block size to handle full sendQs better
      retry
    rescue ::IOError, ::Errno::EPIPE
      return nil
    end
  end

  #
  # Polls the stream to see if there is any read data available.  Returns
  # true if data is available for reading, otherwise false is returned.
  #
  def has_read_data?(timeout = nil)

    # Allow a timeout of "0" that waits almost indefinitely for input, this
    # mimics the behavior of Rex::ThreadSafe.select() and fixes some corner
    # cases of unintentional no-wait timeouts.
    timeout = 3600 if (timeout and timeout == 0)

    begin
      if ((rv = ::IO.select([ fd_rd ], nil, nil, timeout)) and
          (rv[0]) and
          (rv[0][0] == fd_rd))
        true
      else
        false
      end
    rescue ::Errno::EBADF, ::Errno::ENOTSOCK
      raise ::EOFError
    rescue StreamClosedError, ::IOError, ::EOFError, ::Errno::EPIPE
      #  Return false if the socket is dead
      return false
    end
  end

  def close
    fd_rd.close if (fd_rd and !fd_rd.closed?)
    fd_wr.close if (fd_wr and !fd_wr.closed?)
  end

  def closed?
    (fd_rd.nil? or fd_rd.closed?) and (fd_wr.nil? or fd_wr.closed?)
  end
end

##
# Emulate a single bidirectional IO using the clients Connections Channels IOs
##
class ChannelFD
  include Rex::IO::Stream
  include IOMergeAbstraction
  def initialize(parent, chan_id = nil)
    @parent = parent
  end

  def inspect
    "#{super}/#{@parent.inspect}"
  end

  def close
    super
    @parent.close unless @parent.closed?
  end

  def closed?
    super and @parent.closed?
  end

  def cid
    if @cid.nil?
      @cid = @parent.connection.open_channel_keys.first
    end
    @cid
  end

  def cid=(chan_id)
    if @parent.connection.open_channel_keys.include?(chan_id)
      @cid = chan_id
    else
      raise "Invalid Channel ID passed to #{self.inspect}"
    end
  end
  attr_reader :parent

# private

  #
  # Provide a selectable filedescriptor open for reading
  #
  # @return [IO] Descriptor for reading
  def fd_rd
    begin
      channel.io[0]
    rescue => e
      elog(e)
    end
  end

  #
  # Provide a selectable filedescriptor open for writing
  #
  # @param fd [Symbol] Output FD type, anything but :stderr uses 1 (STDOUT)
  #
  # @return [IO] Descriptor for writing
  def fd_wr(fd = :stdout)
    begin
      channel.io[(fd == :stderr ? 2 : 1)]
    rescue => e
      elog(e)
    end
  end

  #
  # Expose a Channel from the Connection
  #
  # @return [HrrRbSsh::Connection::Channel] Channel object
  def channel
    @parent.connection.channels[cid]
  end
end

end
end
end
