require 'em-http-request'
require 'json'


module Msf::RPC::JSON

  # Represents a JSON-RPC request. This is an EM::Deferrable class and instances
  # respond to #callback and #errback to store callback actions.
  class Request
    include EM::Deferrable

    JSON_MEDIA_TYPE = 'application/json'
    JSON_RPC_VERSION = '2.0'
    JSON_RPC_RESPONSE_REQUIRED_MEMBERS = %i(jsonrpc id)
    JSON_RPC_RESPONSE_MEMBER_TYPES = {
        # A String specifying the version of the JSON-RPC protocol.
        jsonrpc: [String],
        # An identifier established by the Client that MUST contain a String,
        # Number, or NULL value if included. If it is not included it is assumed
        # to be a notification. The value SHOULD normally not be Null [1] and
        # Numbers SHOULD NOT contain fractional parts [2]
        id: [Integer, String, NilClass],
    }
    JSON_RPC_ERROR_RESPONSE_REQUIRED_MEMBERS = %i(code message)
    JSON_RPC_ERROR_RESPONSE_MEMBER_TYPES = {
        # A Number that indicates the error type that occurred.
        # This MUST be an integer.
        code: [Integer],
        # A String providing a short description of the error.
        # The message SHOULD be limited to a concise single sentence.
        message: [String]
    }

    # Instantiate a Request.
    # @param uri [URI::HTTP] the JSON-RPC service URI
    # @param api_token [String] the API token. Default: nil
    # @param method [String] the JSON-RPC method name.
    # @param params [Array, Hash] the JSON-RPC method parameters. Default: nil
    # @param namespace [String] the namespace for the JSON-RPC method. The namespace will
    #   be prepended to the method name with a period separator. Default: nil
    # @param symbolize_names [Boolean] If true, symbols are used for the names (keys) when
    #   processing JSON objects; otherwise, strings are used. Default: true
    # @param is_notification [Boolean] If true, the request is created as a notification;
    #   otherwise, a standard request. Default: false
    # @param private_key_file [String] the SSL private key file used for the HTTPS request. Default: nil
    # @param cert_chain_file [String] the SSL cert chain file used for the HTTPS request. Default: nil
    # @param verify_peer [Boolean] indicates whether a server should request a certificate
    #   from a peer, to be verified by user code. Default: nil
    def initialize(uri, api_token: nil, method:, params: nil, namespace: nil,
                   symbolize_names: true, is_notification: false,
                   private_key_file: nil, cert_chain_file: nil, verify_peer: nil)
      @uri = uri
      @api_token = api_token
      @namespace = namespace
      @symbolize_names = symbolize_names
      @is_notification = is_notification
      @headers = {
          Accept: JSON_MEDIA_TYPE,
          'Content-Type': JSON_MEDIA_TYPE,
          Authorization: "Bearer #{@api_token}"
      }

      absolute_method_name = @namespace.nil? ? method : "#{@namespace}.#{method}"
      request_msg = {
          jsonrpc: JSON_RPC_VERSION,
          method: absolute_method_name
      }
      request_msg[:id] = Request.generate_id unless is_notification
      request_msg[:params] = params unless params.nil?

      @request_options = {
          head: @headers,
          body: request_msg.to_json
      }

      # add SSL options if specified
      if !private_key_file.nil? || !cert_chain_file.nil? || verify_peer.is_a?(TrueClass) ||
          verify_peer.is_a?(FalseClass)
        ssl_options = {}
        ssl_options[:private_key_file] = private_key_file unless private_key_file.nil?
        ssl_options[:cert_chain_file] = cert_chain_file unless cert_chain_file.nil?
        ssl_options[:verify_peer] = verify_peer if verify_peer.is_a?(TrueClass) || verify_peer.is_a?(FalseClass)
        @request_options[:ssl] = ssl_options
      end
    end

    # Sends the JSON-RPC request using an EM::HttpRequest object, then validates and processes
    # the JSON-RPC response.
    def send
      http = EM::HttpRequest.new(@uri).post(@request_options)

      http.callback do
        process(http.response)
      end

      http.errback do
        fail(http.error)
      end
    end

    private

    # Process the JSON-RPC response.
    # @param source [String] the JSON-RPC response
    def process(source)
      begin
        response = JSON.parse(source, symbolize_names: @symbolize_names)
        if response.is_a?(Array)
          # process batch response
          # TODO: implement batch response processing
          fail("#{self.class.name}##{__method__} is not implemented for batch response")
        else
          process_response(response)
        end
      rescue JSON::ParserError
        fail(JSONParseError.new(response: source))
      end
    end


    # Validate and process the JSON-RPC response.
    # @param response [Hash] the JSON-RPC response
    def process_response(response)
      if !valid_rpc_response?(response)
        fail(InvalidResponse.new(response: response))
        return
      end

      error_key = @symbolize_names ? :error : :error.to_s
      if response.key?(error_key)
        # process error response
        fail(ErrorResponse.parse(response, symbolize_names: @symbolize_names))
      else
        # process successful response
        succeed(Response.parse(response, symbolize_names: @symbolize_names))
      end
    end

    # Validate the JSON-RPC response.
    # @param response [Hash] the JSON-RPC response
    # @return [Boolean] true if the JSON-RPC response is valid; otherwise, false.
    def valid_rpc_response?(response)
      # validate response is an object
      return false unless response.is_a?(Hash)

      JSON_RPC_RESPONSE_REQUIRED_MEMBERS.each do |member|
        tmp_member = @symbolize_names ? member : member.to_s
        return false unless response.key?(tmp_member)
      end

      # validate response members are correct types
      response.each do |member, value|
        tmp_member = @symbolize_names ? member : member.to_sym
        return false if JSON_RPC_RESPONSE_MEMBER_TYPES.key?(tmp_member) &&
            !JSON_RPC_RESPONSE_MEMBER_TYPES[tmp_member].one? { |type| value.is_a?(type) }
      end

      return false if response[:jsonrpc] != JSON_RPC_VERSION

      result_key = @symbolize_names ? :result : :result.to_s
      error_key = @symbolize_names ? :error : :error.to_s

      return false if response.key?(result_key) && response.key?(error_key)

      if response.key?(error_key)
        error_response = response[error_key]
        # validate error response is an object
        return false unless error_response.is_a?(Hash)

        JSON_RPC_ERROR_RESPONSE_REQUIRED_MEMBERS.each do |member|
          tmp_member = @symbolize_names ? member : member.to_s
          return false unless error_response.key?(tmp_member)
        end

        # validate error response members are correct types
        error_response.each do |member, value|
          tmp_member = @symbolize_names ? member : member.to_sym
          return false if JSON_RPC_ERROR_RESPONSE_MEMBER_TYPES.key?(tmp_member) &&
              !JSON_RPC_ERROR_RESPONSE_MEMBER_TYPES[tmp_member].one? { |type| value.is_a?(type) }
        end
      end

      true
    end

    # Generates a random id.
    # @param n [Integer] Upper boundary for the random id.
    # @return [Integer] A random id. If a positive integer is given for n,
    #   returns an integer: 0 <= id < n.
    def self.generate_id(n = (2**(0.size * 8 - 1))-1)
      SecureRandom.random_number(n)
    end
  end

  # Represents a JSON-RPC Notification. This is an EM::Deferrable class and
  # instances respond to #callback and #errback to store callback actions.
  class Notification < Request
    # Instantiate a Notification.
    # @param uri [URI::HTTP] the JSON-RPC service URI
    # @param api_token [String] the API token. Default: nil
    # @param method [String] the JSON-RPC method name.
    # @param params [Array, Hash] the JSON-RPC method parameters. Default: nil
    # @param namespace [String] the namespace for the JSON-RPC method. The namespace will
    #   be prepended to the method name with a period separator. Default: nil
    # @param symbolize_names [Boolean] If true, symbols are used for the names (keys) when
    #   processing JSON objects; otherwise, strings are used. Default: true
    # @param private_key_file [String] the SSL private key file used for the HTTPS request. Default: nil
    # @param cert_chain_file [String] the SSL cert chain file used for the HTTPS request. Default: nil
    # @param verify_peer [Boolean] indicates whether a server should request a certificate
    #   from a peer, to be verified by user code. Default: nil
    def initialize(uri, api_token: nil, method:, params: nil, namespace: nil,
                   symbolize_names: true, private_key_file: nil,
                   cert_chain_file: nil, verify_peer: nil)
      super(uri,
            api_token: api_token,
            method: method,
            params: params,
            namespace: namespace,
            symbolize_names: symbolize_names,
            is_notification: true,
            private_key_file: private_key_file,
            cert_chain_file: cert_chain_file,
            verify_peer: verify_peer)
    end
  end
end