require 'socket'
require 'timeout'
require 'ssh_scan/constants'
require 'ssh_scan/protocol'
require 'ssh_scan/banner'
require 'ssh_scan/error'

module SSHScan
  class Client
    def initialize(ip, port, timeout = 3)
      @ip = ip
      @timeout = timeout

      @port = port.to_i
      @client_banner = SSHScan::Constants::DEFAULT_CLIENT_BANNER
      @server_banner = nil
      @kex_init_raw = SSHScan::Constants::DEFAULT_KEY_INIT.to_binary_s
    end

    def ip
      @ip
    end

    def port
      @port
    end

    def banner
      @server_banner
    end

    def close()
      begin
        unless @sock.nil?
          @sock.close
        end
      rescue
        @sock = nil
      end
      return true
    end

    def connect()
      @error = nil

      begin
        Timeout::timeout(@timeout) {
          @sock = Socket.tcp(@ip, @port, connect_timeout: @timeout)
          @raw_server_banner = @sock.gets
        }
      rescue SocketError => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      rescue Errno::ETIMEDOUT,
             Timeout::Error => e
        @error = SSHScan::Error::ConnectTimeout.new(e.message)
        @sock = nil
      rescue Errno::ECONNREFUSED => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      rescue Errno::ENETUNREACH => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
       rescue Errno::ECONNRESET => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      rescue Errno::EACCES => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      rescue Errno::EHOSTUNREACH => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      rescue Errno::ENOPROTOOPT => e
        @error = SSHScan::Error::ConnectionRefused.new(e.message)
        @sock = nil
      else
        if @raw_server_banner.nil?
          @error = SSHScan::Error::NoBanner.new(
            "service did not respond with an SSH banner"
          )
          @sock = nil
        else
          @raw_server_banner = @raw_server_banner.chomp
          @server_banner = SSHScan::Banner.read(@raw_server_banner)
          @sock.puts(@client_banner.to_s)
        end
      end
    end

    def error?
      !@error.nil?
    end

    def error
      @error
    end

    def get_kex_result(kex_init_raw = @kex_init_raw)
      if !@sock
        @error = "Socket is no longer valid"
        return nil
      end

      kex_exchange_init = nil

      begin
        Timeout::timeout(@timeout) {
          @sock.write(kex_init_raw)
          resp = @sock.read(4)

          if resp.nil?
            @error = SSHScan::Error::NoKexResponse.new(
              "service did not respond to our kex init request"
            )
            @sock = nil
            return nil
          end

          resp += @sock.read(resp.unpack("N").first)
          @sock.close

          kex_exchange_init = SSHScan::KeyExchangeInit.read(resp)
        }
      rescue Errno::ETIMEDOUT,
             Timeout::Error => e
        @error = SSHScan::Error::ConnectTimeout.new(e.message)
        @sock = nil
        return nil
      rescue Errno::ECONNREFUSED,
             Errno::ENETUNREACH,
             Errno::ECONNRESET,
             Errno::EACCES,
             Errno::EHOSTUNREACH
        @error = SSHScan::Error::NoKexResponse.new(
          "service did not respond to our kex init request"
        )
        @sock = nil
        return nil
      end

      return kex_exchange_init.to_hash
    end
  end
end
