226 lines
9.1 KiB
Ruby
226 lines
9.1 KiB
Ruby
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 |