# -*- coding: binary -*-

module Rex
module Proto
module Ssh
###
#
# Runtime extension of the SSH clients that connect to the server.
#
###

module ServerClient
  #
  # Initialize a new connection instance.
  #
  def init_cli(server, do_not_start = false)
    # Ssh relies on PTY not available on Windows, limiting the `require` here
    # ensures eager_load patterns from zeitwerk will not attempt to load `hrr_rb_ssh`
    # during startup.
    require 'rex/proto/ssh/connection'
    @server          = server
    @connection      = Rex::Proto::Ssh::Connection.new(
      self, server.server_options.merge(ssh_server: server), server.context
    )
    @connection_thread = Rex::ThreadFactory.spawn("SshConnectionMonitor-#{self}", false) {
      self.connection.start
    } unless do_not_start
  rescue LoadError => e
    wlog(e)
  end

  def close
    @connection_thread.kill if @connection_thread and @connection_thread.alive?
    super
  end

  attr_reader :connection, :server
end

###
#
# Acts as an SSH server, accepting clients and extending them with Connections
#
###
class Server

  include Proto
  #
  # Initializes an SSH server as listening on the provided port and
  # hostname.
  #
  def initialize(port = 22, listen_host = '0.0.0.0', context = {}, comm = nil,
    ssh_opts = default_options, cc_cb = nil, cd_cb = nil)

    self.listen_host            = listen_host
    self.listen_port            = port
    self.context                = context
    self.comm                   = comm
    self.listener               = nil
    self.server_options         = ssh_opts
    self.on_client_connect_proc = cc_cb
    self.on_client_data_proc    = cd_cb
  end

  # More readable inspect that only shows the url and resources
  # @return [String]
  def inspect
    "#<#{self.class} ssh://#{listen_host}:#{listen_port}>"
  end

  #
  # Returns the hardcore alias for the SSH service
  #
  def self.hardcore_alias(*args)
    "#{(args[0])}-#{(args[1])}-#{args[4] || ''}"
  end

  #
  # SSH server.
  #
  def alias
    super || "SSH Server"
  end


  #
  # Listens on the defined port and host and starts monitoring for clients.
  #
  def start(srvsock = nil)

    self.listener = srvsock.is_a?(Rex::Socket::TcpServer) ? srvsock : Rex::Socket::TcpServer.create(
      'LocalHost' => self.listen_host,
      'LocalPort' => self.listen_port,
      'Context'   => self.context,
      'Comm'      => self.comm
    )

    # Register callbacks
    self.listener.on_client_connect_proc = Proc.new { |cli|
      on_client_connect(cli)
    }
    # self.listener.on_client_data_proc = Proc.new { |cli|
    #   on_client_data(cli)
    # }
    self.clients         = []
    self.monitor_thread  = Rex::ThreadFactory.spawn("SshServerClientMonitor", false) {
      monitor_clients
    }
    self.listener.start
  end

  #
  # Terminates the monitor thread and turns off the listener.
  #
  def stop
    self.listener.stop
    self.listener.close
    self.clients = []
  end


  #
  # Waits for the SSH service to terminate
  #
  def wait
    self.listener.wait if self.listener
  end

  #
  # Closes the supplied client, if valid.
  #
  def close_client(cli)
    clients.delete(cli)
    listener.close_client(cli.parent)
  end


  attr_accessor :listen_port, :listen_host, :context, :comm, :clients, :monitor_thread
  attr_accessor :listener, :server_options, :on_client_connect_proc, :on_client_data_proc

protected

  #
  # Extends new clients with the ServerClient module and initializes them.
  #
  def on_client_connect(cli)
    cli.extend(ServerClient)

    cli.init_cli(self)
    if self.on_client_connect_proc
      self.on_client_connect_proc.call(cli)
    else
      enqueue_client(cli)
    end
  end

  #
  # Watches FD channel abstractions, removes closed instances,
  # checks for read data on clients if client data callback is defined,
  # invokes the callback if possible, sleeps otherwise.
  #
  def monitor_clients
    loop do
      self.clients.delete_if {|c| c.closed? }
      if self.on_client_data_proc
        if clients.any? { |cli|
          cli.has_read_data? and self.on_client_data_proc.call(cli)}
          next
        else
          sleep 0.05
        end
      else
        sleep 0.5
      end
    end
  rescue => e
    wlog(e)
  end

  #
  # Waits for SSH client to "grow a pair" of FDs and adds
  # a ChannelFD object derived from the client's Connection
  # Channel's FDs to the Ssh::Server's clients array
  #
  # @param cli [Rex::Proto::Ssh::ServerClient] SSH client
  #
  def enqueue_client(cli)
    Rex::ThreadFactory.spawn("ChannelFDWaiter", false) do
      begin
        Timeout::timeout(15) do
          while cli.connection.open_channel_keys.empty? do
            sleep 0.02
          end
          self.clients.push(Ssh::ChannelFD.new(cli))
        end
      rescue Timeout::Error
        elog("Unable to find channel FDs for client #{cli}")
      end
    end
  end

  private

  # Ssh relies on PTY not available on Windows, limiting the `require` here
  # ensures eager_load patterns from zeitwerk will not attempt to load `hrr_rb_ssh`
  # during startup.
  def default_options
    require 'rex/proto/ssh/connection'
    Ssh::Connection.default_options
  rescue LoadError => e
    wlog(e)
  end
end

end
end
end
