Landing #10884 - Add JSON-RPC Client
commit
97ee965c6e
11
Gemfile.lock
11
Gemfile.lock
|
@ -12,6 +12,7 @@ PATH
|
|||
concurrent-ruby (= 1.0.5)
|
||||
dnsruby
|
||||
ed25519
|
||||
em-http-request
|
||||
faker
|
||||
filesize
|
||||
jsobfu
|
||||
|
@ -119,6 +120,7 @@ GEM
|
|||
builder (3.2.3)
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
cookiejar (0.3.3)
|
||||
crass (1.0.4)
|
||||
daemons (1.2.6)
|
||||
diff-lcs (1.3)
|
||||
|
@ -126,6 +128,14 @@ GEM
|
|||
addressable (~> 2.5)
|
||||
docile (1.3.1)
|
||||
ed25519 (1.2.4)
|
||||
em-http-request (1.1.5)
|
||||
addressable (>= 2.3.4)
|
||||
cookiejar (!= 0.3.1)
|
||||
em-socksify (>= 0.3)
|
||||
eventmachine (>= 1.0.3)
|
||||
http_parser.rb (>= 0.6.0)
|
||||
em-socksify (0.3.2)
|
||||
eventmachine (>= 1.0.0.beta.4)
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.2.7)
|
||||
factory_bot (4.11.1)
|
||||
|
@ -140,6 +150,7 @@ GEM
|
|||
filesize (0.2.0)
|
||||
fivemat (1.3.7)
|
||||
hashery (2.1.2)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jsobfu (0.4.2)
|
||||
|
|
|
@ -17,12 +17,16 @@ module Msf::RPC
|
|||
|
||||
|
||||
module JSON
|
||||
autoload :Client, 'msf/core/rpc/json/client'
|
||||
autoload :Dispatcher, 'msf/core/rpc/json/dispatcher'
|
||||
autoload :DispatcherHelper, 'msf/core/rpc/json/dispatcher_helper'
|
||||
autoload :Request, 'msf/core/rpc/json/request'
|
||||
autoload :Response, 'msf/core/rpc/json/response'
|
||||
autoload :RpcCommand, 'msf/core/rpc/json/rpc_command'
|
||||
autoload :RpcCommandFactory, 'msf/core/rpc/json/rpc_command_factory'
|
||||
|
||||
# exception classes
|
||||
# server
|
||||
autoload :Error, 'msf/core/rpc/json/error'
|
||||
autoload :ParseError, 'msf/core/rpc/json/error'
|
||||
autoload :InvalidRequest, 'msf/core/rpc/json/error'
|
||||
|
@ -31,5 +35,11 @@ module Msf::RPC
|
|||
autoload :InternalError, 'msf/core/rpc/json/error'
|
||||
autoload :ServerError, 'msf/core/rpc/json/error'
|
||||
autoload :ApplicationServerError, 'msf/core/rpc/json/error'
|
||||
# client
|
||||
autoload :ClientError, 'msf/core/rpc/json/error'
|
||||
autoload :InvalidResponse, 'msf/core/rpc/json/error'
|
||||
autoload :JSONParseError, 'msf/core/rpc/json/error'
|
||||
autoload :ErrorResponse, 'msf/core/rpc/json/error'
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
require 'json'
|
||||
require 'uri'
|
||||
|
||||
require 'msf/core/rpc'
|
||||
|
||||
module Msf::RPC::JSON
|
||||
# JSON-RPC Client
|
||||
# All client method call requests must be dispatched from within an
|
||||
# EventMachine (reactor) run loop.
|
||||
class Client
|
||||
attr_reader :uri
|
||||
attr_reader :api_token
|
||||
attr_reader :symbolize_names
|
||||
attr_accessor :namespace
|
||||
|
||||
# Instantiate a Client.
|
||||
# @param uri [String] the JSON-RPC service URI
|
||||
# @param api_token [String] the API token. 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, namespace: nil, symbolize_names: true,
|
||||
private_key_file: nil, cert_chain_file: nil, verify_peer: nil)
|
||||
@uri = URI.parse(uri)
|
||||
@api_token = api_token
|
||||
@namespace = namespace
|
||||
@symbolize_names = symbolize_names
|
||||
@private_key_file = private_key_file
|
||||
@cert_chain_file = cert_chain_file
|
||||
@verify_peer = verify_peer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Invoked by Ruby when obj is sent a message it cannot handle, then processes
|
||||
# the call as an RPC method invocation.
|
||||
# @param symbol [Symbol] the symbol for the method called
|
||||
# @param args [Array] any positional arguments passed to the method
|
||||
# @param keyword_args [Hash] any keyword arguments passed to the method
|
||||
# @returns [Msf::RPC::JSON::Request] an EM::Deferrable for the RPC method invocation.
|
||||
def method_missing(symbol, *args, **keyword_args, &block)
|
||||
# assemble method parameters
|
||||
if !args.empty? && !keyword_args.empty?
|
||||
params = args << keyword_args
|
||||
elsif !args.empty?
|
||||
params = args
|
||||
elsif !keyword_args.empty?
|
||||
params = keyword_args
|
||||
else
|
||||
params = nil
|
||||
end
|
||||
|
||||
process_call_async(symbol, params)
|
||||
end
|
||||
|
||||
# Asynchronously processes the RPC method invocation.
|
||||
# @param method [Symbol] the method
|
||||
# @param params [Array, Hash] any arguments passed to the method
|
||||
# @returns [Msf::RPC::JSON::Request] an EM::Deferrable for the RPC method invocation.
|
||||
def process_call_async(method, params)
|
||||
req = Request.new(@uri,
|
||||
api_token: @api_token,
|
||||
method: method,
|
||||
params: params,
|
||||
namespace: @namespace,
|
||||
symbolize_names: @symbolize_names,
|
||||
private_key_file: @private_key_file,
|
||||
cert_chain_file: @cert_chain_file,
|
||||
verify_peer: @verify_peer)
|
||||
req.send
|
||||
|
||||
req
|
||||
end
|
||||
end
|
||||
end
|
|
@ -120,7 +120,7 @@ module Msf::RPC::JSON
|
|||
|
||||
# Validate the JSON-RPC request.
|
||||
# @param request [Hash] the JSON-RPC request
|
||||
# @returns [Boolean] true if the JSON-RPC request is a valid; otherwise, false.
|
||||
# @returns [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)
|
||||
|
|
|
@ -133,4 +133,94 @@ module Msf::RPC::JSON
|
|||
super(APPLICATION_SERVER_ERROR, ERROR_MESSAGES[APPLICATION_SERVER_ERROR] % {msg: message}, data: data)
|
||||
end
|
||||
end
|
||||
|
||||
# Base class for all Msf::RPC::JSON client exceptions.
|
||||
class ClientError < StandardError
|
||||
attr_reader :response
|
||||
|
||||
# Instantiate a ClientError object.
|
||||
#
|
||||
# @param message [String] A String providing a short description of the error.
|
||||
# @param response [Hash] A response hash. The default value is nil.
|
||||
def initialize(message = nil, response: nil)
|
||||
super(message)
|
||||
@response = response
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidResponse < ClientError
|
||||
# Instantiate an InvalidResponse object.
|
||||
#
|
||||
# @param message [String] A String providing a short description of the error.
|
||||
# @param response [Hash] A response hash. The default value is nil.
|
||||
def initialize(message = 'Invalid response from server', response: nil)
|
||||
super(message, response: response)
|
||||
end
|
||||
end
|
||||
|
||||
class JSONParseError < ClientError
|
||||
# Instantiate an JSONParseError object.
|
||||
#
|
||||
# @param message [String] A String providing a short description of the error.
|
||||
# @param response [Hash] A response hash. The default value is nil.
|
||||
def initialize(message = 'Invalid JSON was received from the server', response: nil)
|
||||
super(message, response: response)
|
||||
end
|
||||
end
|
||||
|
||||
class ErrorResponse < ClientError
|
||||
attr_reader :id
|
||||
attr_reader :code
|
||||
attr_reader :message
|
||||
attr_reader :data
|
||||
|
||||
# Parse response and return a new ErrorResponse instance.
|
||||
# @param response [Hash] A response hash.
|
||||
# @param symbolize_names [Boolean] If true, symbols are used for the names (keys) when
|
||||
# processing JSON objects; otherwise, strings are used. Default: true
|
||||
# @returns [ErrorResponse] ErrorResponse object that represents the response hash.
|
||||
def self.parse(response, symbolize_names: true)
|
||||
id_key = symbolize_names ? :id : :id.to_s
|
||||
error_key = symbolize_names ? :error : :error.to_s
|
||||
code_key = symbolize_names ? :code : :code.to_s
|
||||
message_key = symbolize_names ? :message : :message.to_s
|
||||
data_key = symbolize_names ? :data : :data.to_s
|
||||
|
||||
id = response[id_key]
|
||||
error = response[error_key]
|
||||
|
||||
if !error.nil?
|
||||
code = error[code_key]
|
||||
message = error[message_key]
|
||||
data = error[data_key]
|
||||
else
|
||||
code = nil
|
||||
message = nil
|
||||
data = nil
|
||||
end
|
||||
|
||||
ErrorResponse.new(id: id, code: code, message: message, data: data, response: response)
|
||||
end
|
||||
|
||||
# Instantiate an ErrorResponse object.
|
||||
#
|
||||
# @param id [Integer, String, NilClass] It MUST be the same as the value of the
|
||||
# id member in the Request Object. If there was an error in detecting the id
|
||||
# in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.
|
||||
# @param code [Integer] A Number that indicates the error type that occurred.
|
||||
# @param message [String] A String providing a short description of the error.
|
||||
# The message SHOULD be limited to a concise single sentence.
|
||||
# @param data [Object] A Primitive or Structured value that contains additional
|
||||
# information about the error. This may be omitted. The value of this member is
|
||||
# defined by the Server (e.g. detailed error information, nested errors etc.).
|
||||
# The default value is nil.
|
||||
# @param response [Hash] A response hash. The default value is nil.
|
||||
def initialize(id:, code:, message:, data: nil, response: nil)
|
||||
super(message, response: response)
|
||||
@id = id
|
||||
@code = code
|
||||
@message = message
|
||||
@data = data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
require 'em-http-request'
|
||||
require 'json'
|
||||
|
||||
require 'msf/core/rpc'
|
||||
|
||||
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
|
||||
# @returns [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
|
|
@ -0,0 +1,37 @@
|
|||
module Msf::RPC::JSON
|
||||
|
||||
# Represents a JSON-RPC response.
|
||||
class Response
|
||||
attr_reader :response
|
||||
attr_reader :id
|
||||
attr_reader :result
|
||||
|
||||
# Parse response and return a new Response instance.
|
||||
# @param response [Hash] A response hash.
|
||||
# @param symbolize_names [Boolean] If true, symbols are used for the names (keys) when
|
||||
# processing JSON objects; otherwise, strings are used. Default: true
|
||||
# @returns [Response] Response object that represents the response hash.
|
||||
def self.parse(response, symbolize_names: true)
|
||||
id_key = symbolize_names ? :id : :id.to_s
|
||||
result_key = symbolize_names ? :result : :result.to_s
|
||||
|
||||
id = response[id_key]
|
||||
result = response[result_key]
|
||||
|
||||
Response.new(id: id, result: result, response: response)
|
||||
end
|
||||
|
||||
# Instantiate a Response object.
|
||||
#
|
||||
# @param id [Integer, String, NilClass] It MUST be the same as the value of the
|
||||
# id member in the Request Object. If there was an error in detecting the id
|
||||
# in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.
|
||||
# @param result [Integer, String, Array, Hash, NilClass] Result of the method.
|
||||
# @param response [Hash] A response hash. The default value is nil.
|
||||
def initialize(id:, result:, response: nil)
|
||||
@id = id
|
||||
@result = result
|
||||
@response = response
|
||||
end
|
||||
end
|
||||
end
|
|
@ -105,6 +105,8 @@ Gem::Specification.new do |spec|
|
|||
spec.add_runtime_dependency 'sinatra'
|
||||
spec.add_runtime_dependency 'sysrandom'
|
||||
spec.add_runtime_dependency 'warden'
|
||||
# Required for JSON-RPC client
|
||||
spec.add_runtime_dependency 'em-http-request'
|
||||
# TimeZone info
|
||||
spec.add_runtime_dependency 'tzinfo-data'
|
||||
# Gem for dealing with SSHKeys
|
||||
|
|
Loading…
Reference in New Issue