lib/msf/core/rpc/json/dispatcher.rb
require 'json'
module Msf::RPC::JSON
class Dispatcher
JSON_RPC_VERSION = '2.0'
JSON_RPC_REQUIRED_MEMBERS = %i(jsonrpc method)
JSON_RPC_MEMBER_TYPES = {
# A String specifying the version of the JSON-RPC protocol.
jsonrpc: [String],
# A String containing the name of the method to be invoked.
method: [String],
# If present, parameters for the rpc call MUST be provided as a Structured
# value. Either by-position through an Array or by-name through an Object.
# * by-position: params MUST be an Array, containing the values in the
# Server expected order.
# * by-name: params MUST be an Object, with member names that match the
# Server expected parameter names. The absence of expected names MAY
# result in an error being generated. The names MUST match exactly,
# including case, to the method's expected parameters.
params: [Array, Hash],
# 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]
}
attr_reader :framework
attr_reader :command
# Instantiate a Dispatcher.
# @param framework [Msf::Simple::Framework] Framework wrapper instance
def initialize(framework)
@framework = framework
@command = nil
end
# Set the command.
# @param command [RpcCommand] the command used by the Dispatcher.
def set_command(command)
@command = command
end
# Process the JSON-RPC request.
# @param source [String] the JSON-RPC request
# @return [String] JSON-RPC response that encapsulates the RPC result
# if successful; otherwise, a JSON-RPC error response.
def process(source)
begin
request = parse_json_request(source)
if request.is_a?(Array)
# If the batch rpc call itself fails to be recognized as an valid
# JSON or as an Array with at least one value, the response from
# the Server MUST be a single Response object.
raise InvalidRequest.new if request.empty?
# process batch request
response = request.map { |r| process_request(r) }
# A Response object SHOULD exist for each Request object, except that
# there SHOULD NOT be any Response objects for notifications.
# Remove nil responses from response array
response.compact!
else
response = process_request(request)
end
rescue ParseError, InvalidRequest => e
# If there was an error in detecting the id in the Request object
# (e.g. Parse error/Invalid Request), then the id member MUST be
# Null. Don't pass request obj when building the error response.
response = self.class.create_error_response(e)
rescue RpcError => e
# other JSON-RPC errors should include the id from the Request object
response = self.class.create_error_response(e, request)
rescue => e
response = self.class.create_error_response(ApplicationServerError.new(e), request)
end
# When a rpc call is made, the Server MUST reply with a Response, except
# for in the case of Notifications. The Response is expressed as a single
# JSON Object.
self.class.to_json(response)
end
# Validate and execute the JSON-RPC request.
# @param request [Hash] the JSON-RPC request
# @raise [InvalidParams] ArgumentError occurred during execution.
# @raise [ApplicationServerError] General server-error wrapper around an Msf::RPC::Exception that occurred during execution.
# @return [Hash, nil] JSON-RPC response that encapsulates the RPC result, or Nil if a Notification request was sent.
def process_request(request)
begin
if !validate_rpc_request(request)
response = self.class.create_error_response(InvalidRequest.new)
return response
end
# dispatch method execution to command
result = @command.execute(request[:method], request[:params])
# A Notification is a Request object without an "id" member. A Request
# object that is a Notification signifies the Client's lack of interest
# in the corresponding Response object, and as such no Response object
# needs to be returned to the client. The Server MUST NOT reply to a
# Notification, including those that are within a batch request.
if request.key?(:id)
response = self.class.create_success_response(result, request)
else
response = nil
end
response
rescue Msf::OptionValidateError => e
raise InvalidParams.new(data: { options: e.options, message: e.message })
rescue ::NoMethodError => e
raise MethodNotFound.new(e.name, data: { method: e.name, message: e.message })
rescue ArgumentError
raise InvalidParams.new
rescue Msf::RPC::Exception => e
raise ApplicationServerError.new(e.message, data: { code: e.code })
end
end
# Validate the JSON-RPC request.
# @param request [Hash] the JSON-RPC request
# @return [Boolean] true if the JSON-RPC request is valid; otherwise, false.
def validate_rpc_request(request)
# validate request is an object
return false unless request.is_a?(Hash)
# validate request contains required members
JSON_RPC_REQUIRED_MEMBERS.each { |member| return false unless request.key?(member) }
return false if request[:jsonrpc] != JSON_RPC_VERSION
# validate request members are correct types
request.each do |member, value|
return false if JSON_RPC_MEMBER_TYPES.key?(member) &&
!JSON_RPC_MEMBER_TYPES[member].one? { |type| value.is_a?(type) }
end
true
end
# Parse the JSON document source into a Hash or Array with symbols for the names (keys).
# @param source [String] the JSON source
# @raise [ParseError] Invalid JSON was received by the server.
# An error occurred on the server while parsing the JSON text.
# @return [Hash or Array] Hash or Array representation of source
def parse_json_request(source)
begin
JSON.parse(source, symbolize_names: true)
rescue
raise ParseError.new
end
end
# Serialize data as JSON string.
# @param data [Hash] data
# @return [String] data serialized JSON string if data not nil; otherwise, nil.
def self.to_json(data)
return nil if data.nil?
json = data.to_json
return json.to_s
end
# Create a JSON-RPC success response.
# @param result [Object] the RPC method's return value
# @param request [Hash] the JSON-RPC request
# @return [Hash] JSON-RPC success response.
def self.create_success_response(result, request = nil)
response = {
# A String specifying the version of the JSON-RPC protocol.
jsonrpc: JSON_RPC_VERSION,
# This member is REQUIRED on success.
# This member MUST NOT exist if there was an error invoking the method.
# The value of this member is determined by the method invoked on the Server.
result: result
}
self.add_response_id_member(response, request)
response
end
# Create a JSON-RPC error response.
# @param error [RpcError] a RpcError instance
# @param request [Hash] the JSON-RPC request
# @return [Hash] JSON-RPC error response.
def self.create_error_response(error, request = nil)
response = {
# A String specifying the version of the JSON-RPC protocol.
jsonrpc: JSON_RPC_VERSION,
# This member is REQUIRED on error.
# This member MUST NOT exist if there was no error triggered during invocation.
# The value for this member MUST be an Object as defined in section 5.1.
error: error.to_h
}
self.add_response_id_member(response, request)
response
end
# Adds response id based on request id.
# @param response [Hash] the JSON-RPC response
# @param request [Hash] the JSON-RPC request
def self.add_response_id_member(response, request)
if !request.nil? && request.key?(:id)
response[:id] = request[:id]
else
response[:id] = nil
end
end
end
end