Land #10682, add JSON RPC framework and msfrpc v1.0 API endpoints
commit
572d430429
|
@ -19,6 +19,11 @@ module ServletHelper
|
|||
set_json_data_response(response: '')
|
||||
end
|
||||
|
||||
def set_raw_response(data, code: 200)
|
||||
headers = { 'Content-Type' => 'application/json' }
|
||||
[code, headers, data]
|
||||
end
|
||||
|
||||
def set_json_response(data, includes = nil, code = 200)
|
||||
headers = { 'Content-Type' => 'application/json' }
|
||||
[code, headers, to_json(data, includes)]
|
||||
|
|
|
@ -1,14 +1,35 @@
|
|||
# -*- coding: binary -*-
|
||||
require "msf/core/rpc/service"
|
||||
require "msf/core/rpc/client"
|
||||
module Msf::RPC
|
||||
require 'msf/core/rpc/v10/constants'
|
||||
|
||||
require "msf/core/rpc/base"
|
||||
require "msf/core/rpc/auth"
|
||||
require "msf/core/rpc/core"
|
||||
require "msf/core/rpc/session"
|
||||
require "msf/core/rpc/module"
|
||||
require "msf/core/rpc/job"
|
||||
require "msf/core/rpc/console"
|
||||
require "msf/core/rpc/db"
|
||||
require "msf/core/rpc/plugin"
|
||||
require 'msf/core/rpc/v10/service'
|
||||
require 'msf/core/rpc/v10/client'
|
||||
|
||||
require 'msf/core/rpc/v10/rpc_auth'
|
||||
require 'msf/core/rpc/v10/rpc_base'
|
||||
require 'msf/core/rpc/v10/rpc_console'
|
||||
require 'msf/core/rpc/v10/rpc_core'
|
||||
require 'msf/core/rpc/v10/rpc_db'
|
||||
require 'msf/core/rpc/v10/rpc_job'
|
||||
require 'msf/core/rpc/v10/rpc_module'
|
||||
require 'msf/core/rpc/v10/rpc_plugin'
|
||||
require 'msf/core/rpc/v10/rpc_session'
|
||||
|
||||
|
||||
module JSON
|
||||
autoload :Dispatcher, 'msf/core/rpc/json/dispatcher'
|
||||
autoload :DispatcherHelper, 'msf/core/rpc/json/dispatcher_helper'
|
||||
autoload :RpcCommand, 'msf/core/rpc/json/rpc_command'
|
||||
autoload :RpcCommandFactory, 'msf/core/rpc/json/rpc_command_factory'
|
||||
|
||||
# exception classes
|
||||
autoload :Error, 'msf/core/rpc/json/error'
|
||||
autoload :ParseError, 'msf/core/rpc/json/error'
|
||||
autoload :InvalidRequest, 'msf/core/rpc/json/error'
|
||||
autoload :MethodNotFound, 'msf/core/rpc/json/error'
|
||||
autoload :InvalidParams, 'msf/core/rpc/json/error'
|
||||
autoload :InternalError, 'msf/core/rpc/json/error'
|
||||
autoload :ServerError, 'msf/core/rpc/json/error'
|
||||
autoload :ApplicationServerError, 'msf/core/rpc/json/error'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
require 'json'
|
||||
require 'msf/core/rpc'
|
||||
|
||||
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
|
||||
# @returns [RpcCommand] an RpcCommand for the specified version
|
||||
# @raise [InvalidParams] ArgumentError occurred during execution.
|
||||
# @raise [ApplicationServerError] General server-error wrapper around an
|
||||
# Msf::RPC::Exception that occurred during execution.
|
||||
# @returns [Hash] JSON-RPC response that encapsulates the RPC result
|
||||
# if successful; otherwise, a JSON-RPC error response.
|
||||
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 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
|
||||
# @returns [Boolean] true if the JSON-RPC request is a 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
|
||||
# @returns [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
|
||||
# @returns [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
|
|
@ -0,0 +1,31 @@
|
|||
require 'msf/core/rpc'
|
||||
|
||||
module Msf::RPC::JSON
|
||||
module DispatcherHelper
|
||||
# Get an RPC Dispatcher for the RPC version. Creates a new instance and stores
|
||||
# it in the dispatchers hash if one does not already exist for the version.
|
||||
# @param dispatchers [Hash] hash of version Symbol - Msf::RPC::JSON::Dispatcher object pairs
|
||||
# @param version [Symbol] the RPC version
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @returns [Msf::RPC::JSON::Dispatcher] an RPC Dispatcher for the specified version
|
||||
def get_dispatcher(dispatchers, version, framework)
|
||||
unless dispatchers.key?(version)
|
||||
dispatchers[version] = create_dispatcher(version, framework)
|
||||
end
|
||||
|
||||
dispatchers[version]
|
||||
end
|
||||
|
||||
# Create an RPC Dispatcher composed of an RpcCommand for the provided version.
|
||||
# @param version [Symbol] the RPC version
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @returns [Msf::RPC::JSON::Dispatcher] an RPC Dispatcher for the specified version
|
||||
def create_dispatcher(version, framework)
|
||||
command = RpcCommandFactory.create(version, framework)
|
||||
dispatcher = Dispatcher.new(framework)
|
||||
dispatcher.set_command(command)
|
||||
|
||||
dispatcher
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,136 @@
|
|||
module Msf::RPC::JSON
|
||||
|
||||
# JSON-RPC 2.0 Error Codes
|
||||
## Specification errors:
|
||||
PARSE_ERROR = -32700
|
||||
INVALID_REQUEST = -32600
|
||||
METHOD_NOT_FOUND = -32601
|
||||
INVALID_PARAMS = -32602
|
||||
INTERNAL_ERROR = -32603
|
||||
## Implementation-defined server-errors:
|
||||
SERVER_ERROR_MAX = -32000
|
||||
SERVER_ERROR_MIN = -32099
|
||||
APPLICATION_SERVER_ERROR = -32000
|
||||
|
||||
# JSON-RPC 2.0 Error Messages
|
||||
ERROR_MESSAGES = {
|
||||
# Specification errors:
|
||||
PARSE_ERROR => 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.',
|
||||
INVALID_REQUEST => 'The JSON sent is not a valid Request object.',
|
||||
METHOD_NOT_FOUND => 'The method %<name>s does not exist.',
|
||||
INVALID_PARAMS => 'Invalid method parameter(s).',
|
||||
INTERNAL_ERROR => 'Internal JSON-RPC error',
|
||||
# Implementation-defined server-errors:
|
||||
APPLICATION_SERVER_ERROR => 'Application server error: %<msg>s',
|
||||
}
|
||||
|
||||
# Base class for all Msf::RPC::JSON exceptions.
|
||||
class RpcError < StandardError
|
||||
# Code Message Meaning
|
||||
# -32700 Parse error Invalid JSON was received by the server. An error
|
||||
# occurred on the server while parsing the JSON text.
|
||||
# -32600 Invalid Request The JSON sent is not a valid Request object.
|
||||
# -32601 Method not found The method does not exist / is not available.
|
||||
# -32602 Invalid params Invalid method parameter(s).
|
||||
# -32603 Internal error Internal JSON-RPC error.
|
||||
# -32000 to -32099 Server error Reserved for implementation-defined server-errors.
|
||||
|
||||
attr_reader :code
|
||||
attr_reader :message
|
||||
attr_reader :data
|
||||
|
||||
# Instantiate an RpcError object.
|
||||
#
|
||||
# @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.
|
||||
def initialize(code, message, data: nil)
|
||||
super(message)
|
||||
@code = code
|
||||
@message = message
|
||||
@data = data
|
||||
end
|
||||
|
||||
def to_h
|
||||
hash = {
|
||||
code: @code,
|
||||
message: @message
|
||||
}
|
||||
|
||||
# process data member
|
||||
unless @data.nil?
|
||||
if @data.is_a?(String) || @data.kind_of?(Numeric) || @data.is_a?(Array) || @data.is_a?(Hash)
|
||||
hash[:data] = @data
|
||||
elsif @data.respond_to?(:to_h)
|
||||
hash[:data] = @data.to_h
|
||||
else
|
||||
hash[:data] = @data.to_s
|
||||
end
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
class ParseError < RpcError
|
||||
def initialize(data: nil)
|
||||
super(PARSE_ERROR, ERROR_MESSAGES[PARSE_ERROR], data: data)
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidRequest < RpcError
|
||||
def initialize(data: nil)
|
||||
super(INVALID_REQUEST, ERROR_MESSAGES[INVALID_REQUEST], data: data)
|
||||
end
|
||||
end
|
||||
|
||||
class MethodNotFound < RpcError
|
||||
def initialize(method, data: nil)
|
||||
super(METHOD_NOT_FOUND, ERROR_MESSAGES[METHOD_NOT_FOUND] % {name: method}, data: data)
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidParams < RpcError
|
||||
def initialize(data: nil)
|
||||
super(INVALID_PARAMS, ERROR_MESSAGES[INVALID_PARAMS], data: data)
|
||||
end
|
||||
end
|
||||
|
||||
class InternalError < RpcError
|
||||
def initialize(e, data: nil)
|
||||
super(INTERNAL_ERROR, "#{ERROR_MESSAGES[INTERNAL_ERROR]}: #{e}", data: data)
|
||||
end
|
||||
end
|
||||
|
||||
# Class is reserved for implementation-defined server-error exceptions.
|
||||
class ServerError < RpcError
|
||||
|
||||
# Instantiate a ServerError object.
|
||||
#
|
||||
# @param code [Integer] A Number that indicates the error type that occurred.
|
||||
# The code must be between -32000 and -32099.
|
||||
# @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.
|
||||
# @raise [ArgumentError] Module not found (either the wrong type or name).
|
||||
def initialize(code, message, data: nil)
|
||||
if code < SERVER_ERROR_MIN || code > SERVER_ERROR_MAX
|
||||
raise ArgumentError.new("invalid code #{code}, must be between #{SERVER_ERROR_MAX} and #{SERVER_ERROR_MIN}")
|
||||
end
|
||||
super(code, message, data: data)
|
||||
end
|
||||
end
|
||||
|
||||
class ApplicationServerError < ServerError
|
||||
def initialize(message, data: nil)
|
||||
super(APPLICATION_SERVER_ERROR, ERROR_MESSAGES[APPLICATION_SERVER_ERROR] % {msg: message}, data: data)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
module Msf::RPC::JSON
|
||||
class RpcCommand
|
||||
attr_reader :framework
|
||||
attr_accessor :execute_timeout
|
||||
|
||||
# Instantiate an RpcCommand.
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @param execute_timeout [Integer] execute timeout duration in seconds
|
||||
def initialize(framework, execute_timeout: 7200)
|
||||
@framework = framework
|
||||
@execute_timeout = execute_timeout
|
||||
@methods = {}
|
||||
end
|
||||
|
||||
# Add a method to the RPC Command
|
||||
# @param method [Method] the Method
|
||||
# @param name [String] the name the method is register under. The method name is used if nil.
|
||||
# @returns [Method] the Method.
|
||||
def register_method(method, name: nil)
|
||||
if name.nil?
|
||||
if method.is_a?(Method)
|
||||
name = method.name.to_s
|
||||
else
|
||||
name = method.to_s
|
||||
end
|
||||
end
|
||||
@methods[name] = method
|
||||
end
|
||||
|
||||
# Invokes the method on the receiver object with the specified params,
|
||||
# returning the method's return value.
|
||||
# @param method [String] the RPC method name
|
||||
# @param params [Array, Hash] parameters for the RPC call
|
||||
# @raise [MethodNotFound] The method does not exist
|
||||
# @raise [Timeout::Error] The method failed to terminate in @execute_timeout seconds
|
||||
# @returns [Object] the method's return value.
|
||||
def execute(method, params)
|
||||
unless @methods.key?(method)
|
||||
raise MethodNotFound.new(method)
|
||||
end
|
||||
|
||||
::Timeout.timeout(@execute_timeout) do
|
||||
if params.nil?
|
||||
return @methods[method].call()
|
||||
elsif params.is_a?(Array)
|
||||
return @methods[method].call(*params)
|
||||
else
|
||||
return @methods[method].call(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
require 'msf/core/rpc'
|
||||
require 'msf/core/rpc/json/v1_0/rpc_command'
|
||||
require 'msf/core/rpc/json/v2_0/rpc_test'
|
||||
|
||||
module Msf::RPC::JSON
|
||||
class RpcCommandFactory
|
||||
# Create an RpcCommand for the provided version.
|
||||
# @param version [Symbol] the RPC version
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @raise [ArgumentError] invalid RPC version
|
||||
# @returns [RpcCommand] an RpcCommand for the specified version
|
||||
def self.create(version, framework)
|
||||
case version
|
||||
when :v1, :v1_0, :v10
|
||||
return Msf::RPC::JSON::V1_0::RpcCommand.new(framework)
|
||||
when :v2, :v2_0
|
||||
return RpcCommandFactory.create_rpc_command_v2_0(framework)
|
||||
else
|
||||
raise ArgumentError.new("invalid RPC version #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
# Creates an RpcCommand for a demonstration RPC version 2.0.
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @returns [RpcCommand] an RpcCommand for a demonstration RPC version 2.0
|
||||
def self.create_rpc_command_v2_0(framework)
|
||||
# TODO: does belong in some sort of loader class for an RPC version?
|
||||
# instantiate receiver
|
||||
rpc_test = Msf::RPC::JSON::V2_0::RpcTest.new()
|
||||
|
||||
command = Msf::RPC::JSON::RpcCommand.new(framework)
|
||||
|
||||
# Add class methods
|
||||
command.register_method(Msf::RPC::JSON::V2_0::RpcTest.method(:add))
|
||||
command.register_method(Msf::RPC::JSON::V2_0::RpcTest.method(:add), name: 'add_alias')
|
||||
# Add instance methods
|
||||
command.register_method(rpc_test.method(:get_instance_rand_num))
|
||||
command.register_method(rpc_test.method(:add_instance_rand_num))
|
||||
|
||||
command
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,152 @@
|
|||
require 'base64'
|
||||
require 'msf/core/rpc'
|
||||
|
||||
module Msf::RPC::JSON
|
||||
module V1_0
|
||||
class RpcCommand < ::Msf::RPC::JSON::RpcCommand
|
||||
METHOD_GROUP_SEPARATOR = '.'
|
||||
|
||||
MODULE_EXECUTE_KEY = 'module.execute'
|
||||
PAYLOAD_MODULE_TYPE_KEY = 'payload'
|
||||
PAYLOAD_KEY = 'payload'
|
||||
|
||||
# Instantiate an RpcCommand.
|
||||
# @param framework [Msf::Simple::Framework] Framework wrapper instance
|
||||
# @param execute_timeout [Integer] execute timeout duration in seconds
|
||||
def initialize(framework, execute_timeout: 7200)
|
||||
super(framework, execute_timeout: execute_timeout)
|
||||
|
||||
# The legacy Msf::RPC::Service will not be started, however, it will be used to proxy
|
||||
# requests to existing handlers. This frees the command from having to act as the
|
||||
# service to RPC_Base subclasses and expose accessors for tokens and users.
|
||||
@legacy_rpc_service = ::Msf::RPC::Service.new(@framework, {
|
||||
execute_timeout: @execute_timeout
|
||||
})
|
||||
end
|
||||
|
||||
# @raise [RuntimeError] The method is not implemented
|
||||
def register_method(method, name: nil)
|
||||
raise "#{self.class.name}##{__method__} is not implemented"
|
||||
end
|
||||
|
||||
# Invokes the method on the receiver object with the specified params,
|
||||
# returning the method's return value.
|
||||
# @param method [String] the RPC method name
|
||||
# @param params [Array, Hash] parameters for the RPC call
|
||||
# @returns [Object] the method's return value.
|
||||
def execute(method, params)
|
||||
result = execute_internal(method, params)
|
||||
result = post_process_result(result, method, params)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Internal method that invokes the method on the receiver object with
|
||||
# the specified params, returning the method's return value.
|
||||
# @param method [String] the RPC method name
|
||||
# @param params [Array, Hash] parameters for the RPC call
|
||||
# @raise [MethodNotFound] The method does not exist
|
||||
# @raise [Timeout::Error] The method failed to terminate in @execute_timeout seconds
|
||||
# @returns [Object] the method's return value.
|
||||
def execute_internal(method, params)
|
||||
group, base_method = parse_method_group(method)
|
||||
|
||||
method_name = "rpc_#{base_method}"
|
||||
method_name_noauth = "rpc_#{base_method}_noauth"
|
||||
|
||||
handler = (find_handler(@legacy_rpc_service.handlers, group, method_name) || find_handler(@legacy_rpc_service.handlers, group, method_name_noauth))
|
||||
if handler.nil?
|
||||
raise MethodNotFound.new(method)
|
||||
end
|
||||
|
||||
if handler.respond_to?(method_name_noauth)
|
||||
method_name = method_name_noauth
|
||||
end
|
||||
|
||||
::Timeout.timeout(@execute_timeout) do
|
||||
params = prepare_params(params)
|
||||
if params.nil?
|
||||
return handler.send(method_name)
|
||||
elsif params.is_a?(Array)
|
||||
return handler.send(method_name, *params)
|
||||
else
|
||||
return handler.send(method_name, **params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Parse method string in the format "group.base_method_name".
|
||||
# @param method [String] the RPC method name
|
||||
# @returns [Array] Tuple of strings, group and base_method
|
||||
def parse_method_group(method)
|
||||
idx = method.rindex(METHOD_GROUP_SEPARATOR)
|
||||
if idx.nil?
|
||||
group = nil
|
||||
base_method = method
|
||||
else
|
||||
group = method[0..idx - 1]
|
||||
base_method = method[idx + 1..-1]
|
||||
end
|
||||
return group, base_method
|
||||
end
|
||||
|
||||
# Find the concrete Msf::RPC::RPC_Base handler for the group and method name.
|
||||
# @param handlers [Hash] hash of group String - Msf::RPC::RPC_Base object pairs
|
||||
# @param group [String] the RPC group
|
||||
# @param method_name [String] the RPC method name
|
||||
# @returns [Msf::RPC::RPC_Base] concrete Msf::RPC::RPC_Base instance if one exists; otherwise, nil.
|
||||
def find_handler(handlers, group, method_name)
|
||||
handler = nil
|
||||
if !handlers[group].nil? && handlers[group].respond_to?(method_name)
|
||||
handler = handlers[group]
|
||||
end
|
||||
|
||||
handler
|
||||
end
|
||||
|
||||
# Prepare params for use by RPC methods by converting all hashes
|
||||
# inside of Arrays to use strings for their names (keys).
|
||||
# @param params [Object] parameters for the RPC call
|
||||
# @returns [Object] If params is an Array all hashes it contains will be
|
||||
# modified; otherwise, the object will simply pass-through.
|
||||
def prepare_params(params)
|
||||
clean_params = params
|
||||
if params.is_a?(Array)
|
||||
clean_params = params.map do |p|
|
||||
if p.is_a?(Hash)
|
||||
stringify_names(p)
|
||||
else
|
||||
p
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
clean_params
|
||||
end
|
||||
|
||||
# Stringify the names (keys) in hash.
|
||||
# @param hash [Hash] input hash
|
||||
# @returns [Hash] a new hash with strings for the keys.
|
||||
def stringify_names(hash)
|
||||
JSON.parse(JSON.dump(hash), symbolize_names: false)
|
||||
end
|
||||
|
||||
# Perform custom post processing of the execute result data.
|
||||
# @param result [Object] the method's return value
|
||||
# @param method [String] the RPC method name
|
||||
# @param params [Array, Hash] parameters for the RPC call
|
||||
# @returns [Object] processed method's return value
|
||||
def post_process_result(result, method, params)
|
||||
# post-process payload module result for JSON output
|
||||
if method == MODULE_EXECUTE_KEY && params.size >= 2 &&
|
||||
params[0] == PAYLOAD_MODULE_TYPE_KEY && result.key?(PAYLOAD_KEY)
|
||||
result[PAYLOAD_KEY] = Base64.strict_encode64(result[PAYLOAD_KEY])
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
module Msf::RPC::JSON::V2_0
|
||||
# Receiver class for demonstration RPC version 2.0.
|
||||
class RpcTest
|
||||
|
||||
def initialize
|
||||
r = Random.new
|
||||
@rand_num = r.rand(0..100)
|
||||
end
|
||||
|
||||
def self.add(x, y)
|
||||
x + y
|
||||
end
|
||||
|
||||
def get_instance_rand_num
|
||||
@rand_num
|
||||
end
|
||||
|
||||
def add_instance_rand_num(x)
|
||||
@rand_num = @rand_num + x
|
||||
|
||||
@rand_num
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
require 'sinatra/base'
|
||||
require 'swagger/blocks'
|
||||
require 'sysrandom/securerandom'
|
||||
require 'warden'
|
||||
require 'msf/core/rpc'
|
||||
require 'msf/core/db_manager/http/authentication'
|
||||
require 'msf/core/db_manager/http/servlet_helper'
|
||||
require 'msf/core/db_manager/http/servlet/auth_servlet'
|
||||
require 'msf/core/db_manager/http/servlet/user_servlet'
|
||||
require 'msf/core/web_services/servlet/json_rpc_servlet'
|
||||
|
||||
class JsonRpcApp < Sinatra::Base
|
||||
helpers ServletHelper
|
||||
helpers Msf::RPC::JSON::DispatcherHelper
|
||||
|
||||
# Servlet registration
|
||||
register AuthServlet
|
||||
register UserServlet
|
||||
register JsonRpcServlet
|
||||
|
||||
set :framework, Msf::Simple::Framework.create({})
|
||||
set :dispatchers, {}
|
||||
|
||||
configure do
|
||||
set :sessions, {key: 'msf-ws.session', expire_after: 300}
|
||||
set :session_secret, ENV.fetch('MSF_WS_SESSION_SECRET') { SecureRandom.hex(16) }
|
||||
end
|
||||
|
||||
before do
|
||||
# store DBManager in request environment so that it is available to Warden
|
||||
request.env['msf.db_manager'] = get_db
|
||||
# store flag indicating whether authentication is initialized in the request environment
|
||||
@@auth_initialized ||= get_db.users({}).count > 0
|
||||
request.env['msf.auth_initialized'] = @@auth_initialized
|
||||
end
|
||||
|
||||
use Warden::Manager do |config|
|
||||
# failed authentication is handled by this application
|
||||
config.failure_app = self
|
||||
# don't intercept 401 responses since the app will provide custom failure messages
|
||||
config.intercept_401 = false
|
||||
config.default_scope = :api
|
||||
|
||||
config.scope_defaults :user,
|
||||
# whether to persist the result in the session or not
|
||||
store: true,
|
||||
# list of strategies to use
|
||||
strategies: [:password],
|
||||
# action (route) of the failure application
|
||||
action: "#{AuthServlet.api_unauthenticated_path}/user"
|
||||
|
||||
config.scope_defaults :api,
|
||||
# whether to persist the result in the session or not
|
||||
store: false,
|
||||
# list of strategies to use
|
||||
strategies: [:api_token],
|
||||
# action (route) of the failure application
|
||||
action: AuthServlet.api_unauthenticated_path
|
||||
|
||||
config.scope_defaults :admin_api,
|
||||
# whether to persist the result in the session or not
|
||||
store: false,
|
||||
# list of strategies to use
|
||||
strategies: [:admin_api_token],
|
||||
# action (route) of the failure application
|
||||
action: AuthServlet.api_unauthenticated_path
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
require 'msf/core/rpc'
|
||||
|
||||
module JsonRpcServlet
|
||||
|
||||
def self.api_path
|
||||
'/api/:version/json-rpc'
|
||||
end
|
||||
|
||||
def self.registered(app)
|
||||
app.post JsonRpcServlet.api_path, &post_rpc
|
||||
end
|
||||
|
||||
#######
|
||||
private
|
||||
#######
|
||||
|
||||
# Process JSON-RPC request
|
||||
def self.post_rpc
|
||||
lambda {
|
||||
warden.authenticate!
|
||||
begin
|
||||
body = request.body.read
|
||||
tmp_params = sanitize_params(params)
|
||||
data = get_dispatcher(settings.dispatchers, tmp_params[:version].to_sym, settings.framework).process(body)
|
||||
set_raw_response(data)
|
||||
rescue => e
|
||||
print_error("There was an error executing the RPC: #{e.message}.", e)
|
||||
error = Msf::RPC::JSON::Dispatcher.create_error_response(Msf::RPC::JSON::InternalError.new(e))
|
||||
data = Msf::RPC::JSON::Dispatcher.to_json(error)
|
||||
set_raw_response(data, code: 500)
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# msf-json-rpc.ru
|
||||
# Start using thin:
|
||||
# thin --rackup msf-json-rpc.ru --address localhost --port 8081 --environment development --tag msf-json-rpc start
|
||||
#
|
||||
|
||||
require 'pathname'
|
||||
@framework_path = '.'
|
||||
root = Pathname.new(@framework_path).expand_path
|
||||
@framework_lib_path = root.join('lib')
|
||||
$LOAD_PATH << @framework_lib_path unless $LOAD_PATH.include?(@framework_lib_path)
|
||||
|
||||
require 'msfenv'
|
||||
|
||||
if ENV['MSF_LOCAL_LIB']
|
||||
$LOAD_PATH << ENV['MSF_LOCAL_LIB'] unless $LOAD_PATH.include?(ENV['MSF_LOCAL_LIB'])
|
||||
end
|
||||
|
||||
# Note: setup Rails environment before calling require
|
||||
require 'msf/core/web_services/json_rpc_app'
|
||||
|
||||
run JsonRpcApp
|
|
@ -0,0 +1,301 @@
|
|||
require 'spec_helper'
|
||||
require 'json'
|
||||
|
||||
require 'msf/core/rpc'
|
||||
|
||||
RSpec.describe Msf::RPC::JSON::Dispatcher do
|
||||
include_context 'Msf::Simple::Framework'
|
||||
|
||||
def to_json(data)
|
||||
return nil if data.nil?
|
||||
|
||||
json = data.to_json
|
||||
return json.to_s
|
||||
end
|
||||
|
||||
describe '#process' do
|
||||
|
||||
before(:each) do
|
||||
# prepare a dispatcher for all of the tests
|
||||
@dispatcher = Msf::RPC::JSON::Dispatcher.new(framework)
|
||||
end
|
||||
|
||||
context 'invalid JSON-RPC request' do
|
||||
|
||||
before(:each) do
|
||||
# mock RpcCommand behavior as it isn't relevant for JSON-RPC validation
|
||||
cmd = instance_double('RpcCommand')
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_return({})
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_return({})
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_return({})
|
||||
@dispatcher.set_command(cmd)
|
||||
end
|
||||
|
||||
context 'is not valid JSON' do
|
||||
it 'contains only a string' do
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32700,
|
||||
message: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
|
||||
},
|
||||
id: nil
|
||||
}
|
||||
expect(@dispatcher.process("Ce n'est pas un JSON")).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'is not a valid request object' do
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32600,
|
||||
message: 'The JSON sent is not a valid Request object.'
|
||||
},
|
||||
id: nil
|
||||
}
|
||||
|
||||
it 'does not contain required jsonrpc member' do
|
||||
request = '{ "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'does not contain required method member' do
|
||||
request = '{ "jsonrpc": "2.0" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'does not contain valid JSON-RPC version number' do
|
||||
request = '{ "jsonrpc": "1.0", "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty JSON object' do
|
||||
expect(@dispatcher.process('{}')).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an array with an empty JSON object' do
|
||||
expect(@dispatcher.process('[{}]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with an empty array' do
|
||||
expect(@dispatcher.process('[[]]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with a string' do
|
||||
expect(@dispatcher.process('["bad"]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with a number' do
|
||||
expect(@dispatcher.process('[123456]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with true' do
|
||||
expect(@dispatcher.process('[true]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with false' do
|
||||
expect(@dispatcher.process('[false]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
it 'is an array with null' do
|
||||
expect(@dispatcher.process('[null]')).to eq([expected_response].to_json)
|
||||
end
|
||||
|
||||
context 'contains incorrect data type' do
|
||||
context 'jsonrpc' do
|
||||
it 'is a number' do
|
||||
request = '{ "jsonrpc": 2.0, "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty JSON object' do
|
||||
request = '{ "jsonrpc": {}, "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty array' do
|
||||
request = '{ "jsonrpc": [], "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is null' do
|
||||
request = '{ "jsonrpc": null, "method": "unit-test" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'method' do
|
||||
it 'is a number' do
|
||||
request = '{ "jsonrpc": "2.0", "method": 123456 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty JSON object' do
|
||||
request = '{ "jsonrpc": "2.0", "method": {} }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty array' do
|
||||
request = '{ "jsonrpc": "2.0", "method": [] }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is null' do
|
||||
request = '{ "jsonrpc": "2.0", "method": null }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'params' do
|
||||
it 'is a number' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": 123456 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is a string' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": "bad-params" }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": true }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": false }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is null' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": null }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'id' do
|
||||
it 'is an empty JSON object' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": {} }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an empty array' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": [] }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is an array that contains a number' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": [1] }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is a number that contain fractional parts' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 3.14 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": true }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": false }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'errors on JSON-RPC method execute' do
|
||||
it 'does not contain valid method name' do
|
||||
# mock RpcCommand behavior for MethodNotFound exception
|
||||
method_name = 'DNE'
|
||||
cmd = instance_double('RpcCommand')
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::JSON::MethodNotFound.new(method_name))
|
||||
@dispatcher.set_command(cmd)
|
||||
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32601,
|
||||
message: 'The method %<name>s does not exist.' % { name: method_name }
|
||||
},
|
||||
id: 1
|
||||
}
|
||||
request = '{ "jsonrpc": "2.0", "method": "DNE", "params": [], "id": 1 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'does not contain valid method params' do
|
||||
# mock RpcCommand behavior for InvalidParams exception
|
||||
cmd = instance_double('RpcCommand')
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Array)).and_raise(ArgumentError)
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), instance_of(Hash)).and_raise(ArgumentError)
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(ArgumentError)
|
||||
@dispatcher.set_command(cmd)
|
||||
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32602,
|
||||
message: 'Invalid method parameter(s).'
|
||||
},
|
||||
id: 1
|
||||
}
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "params": ["method-has-no-params"], "id": 1 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'throws Msf::RPC::Exception' do
|
||||
# mock RpcCommand behavior for Msf::RPC::Exception exception
|
||||
error_code = 123
|
||||
error_msg = 'unit-test'
|
||||
cmd = instance_double('RpcCommand')
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(Msf::RPC::Exception.new(error_code, error_msg))
|
||||
@dispatcher.set_command(cmd)
|
||||
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Application server error: %<msg>s' % { msg: error_msg },
|
||||
data: {
|
||||
code: error_code
|
||||
}
|
||||
},
|
||||
id: 1
|
||||
}
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 1 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
|
||||
it 'throws StandardError' do
|
||||
# mock RpcCommand behavior for StandardError exception
|
||||
error_msg = 'unit-test'
|
||||
cmd = instance_double('RpcCommand')
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
|
||||
allow(cmd).to receive(:execute).with(instance_of(String), nil).and_raise(StandardError.new(error_msg))
|
||||
@dispatcher.set_command(cmd)
|
||||
|
||||
expected_response = {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Application server error: %<msg>s' % { msg: error_msg }
|
||||
},
|
||||
id: 1
|
||||
}
|
||||
request = '{ "jsonrpc": "2.0", "method": "unit-test", "id": 1 }'
|
||||
expect(@dispatcher.process(request)).to eq(expected_response.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue