diff --git a/lib/msf/core/rpc/json/v1_0/rpc_command.rb b/lib/msf/core/rpc/json/v1_0/rpc_command.rb index ce1451ab90..8d723ef460 100644 --- a/lib/msf/core/rpc/json/v1_0/rpc_command.rb +++ b/lib/msf/core/rpc/json/v1_0/rpc_command.rb @@ -148,4 +148,4 @@ module Msf::RPC::JSON end end end -end \ No newline at end of file +end diff --git a/lib/msf/core/rpc/v10/client.rb b/lib/msf/core/rpc/v10/client.rb index 46c7025702..752738a136 100644 --- a/lib/msf/core/rpc/v10/client.rb +++ b/lib/msf/core/rpc/v10/client.rb @@ -91,7 +91,7 @@ class Client do_logout_cleanup end - unless meth == "auth.login" + if meth != 'auth.login' && meth != 'health.check' unless self.token raise RuntimeError, "client not authenticated" end diff --git a/lib/msf/core/rpc/v10/health.rb b/lib/msf/core/rpc/v10/health.rb new file mode 100644 index 0000000000..8b742c2ae6 --- /dev/null +++ b/lib/msf/core/rpc/v10/health.rb @@ -0,0 +1,35 @@ +# -*- coding: binary -*- +module Msf + module RPC + class Health + + # Returns whether the framework object is currently healthy and ready to accept + # requests + # + # @return [Hash] + # + def self.check(framework) + # A couple of rudimentary checks to ensure that nothing breaks when interacting + # with framework object + is_healthy = ( + !framework.version.to_s.empty? && + # Ensure that the db method can be invoked and returns a truthy value as + # the rpc clients interact with framework's database object which raises can + # raise an exception + framework.db + ) + + unless is_healthy + return { status: 'DOWN' } + end + + { status: 'UP' } + rescue => e + elog('Health status failing', error: e) + + { status: 'DOWN' } + end + + end + end +end diff --git a/lib/msf/core/rpc/v10/rpc_health.rb b/lib/msf/core/rpc/v10/rpc_health.rb new file mode 100644 index 0000000000..96acd4ea45 --- /dev/null +++ b/lib/msf/core/rpc/v10/rpc_health.rb @@ -0,0 +1,18 @@ +# -*- coding: binary -*- +module Msf +module RPC +class RPC_Health < RPC_Base + + # Returns whether the service is currently healthy and ready to accept + # requests. This endpoint is not authenticated. + # + # @return [Hash] + # @example Here's how you would use this from the client: + # rpc.call('health.check') + def rpc_check_noauth + Msf::RPC::Health.check(framework) + end + +end +end +end diff --git a/lib/msf/core/rpc/v10/service.rb b/lib/msf/core/rpc/v10/service.rb index d54f6ce305..90f8c3b3e6 100644 --- a/lib/msf/core/rpc/v10/service.rb +++ b/lib/msf/core/rpc/v10/service.rb @@ -36,6 +36,7 @@ class Service self.users = self.options[:users] || [] self.job_status_tracker = Msf::RPC::RpcJobStatusTracker.new + add_handler("health", Msf::RPC::RPC_Health.new(self)) add_handler("core", Msf::RPC::RPC_Core.new(self)) add_handler("auth", Msf::RPC::RPC_Auth.new(self)) add_handler("console", Msf::RPC::RPC_Console.new(self)) diff --git a/lib/msf/core/web_services/json_rpc_app.rb b/lib/msf/core/web_services/json_rpc_app.rb index 0f913d3623..2414671782 100644 --- a/lib/msf/core/web_services/json_rpc_app.rb +++ b/lib/msf/core/web_services/json_rpc_app.rb @@ -14,6 +14,7 @@ module Msf::WebServices # Servlet registration register AuthServlet + register HealthServlet register JsonRpcServlet # Custom error handling diff --git a/lib/msf/core/web_services/servlet/health_servlet.rb b/lib/msf/core/web_services/servlet/health_servlet.rb new file mode 100644 index 0000000000..1062009791 --- /dev/null +++ b/lib/msf/core/web_services/servlet/health_servlet.rb @@ -0,0 +1,22 @@ +module Msf::WebServices::HealthServlet + + def self.api_path + '/api/v1/health' + end + + def self.registered(app) + app.get self.api_path, &health_check + end + + ####### + private + ####### + + def self.health_check + lambda { + health_check = Msf::RPC::Health.check(framework) + is_success = health_check[:status] == 'UP' + set_json_data_response(response: health_check, code: is_success ? 200 : 503) + } + end +end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index 62461afe35..2be7e57e30 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -181,6 +181,7 @@ class MsfAutoload 'rpc_auth' => 'RPC_Auth', 'rpc_job' => 'RPC_Job', 'rpc_core' => 'RPC_Core', + 'rpc_health' => 'RPC_Health', 'rpc_module' => 'RPC_Module', 'cli' => 'CLI', 'sqlitei' => 'SQLitei', diff --git a/msf-json-rpc.ru b/msf-json-rpc.ru index fd83955513..905cf68c1a 100644 --- a/msf-json-rpc.ru +++ b/msf-json-rpc.ru @@ -24,29 +24,17 @@ run Msf::WebServices::JsonRpcApp # warmup do |app| client = Rack::MockRequest.new(app) - response = client.post( - '/api/v1/json-rpc', - input: { - jsonrpc: '2.0', - method: 'core.version', - id: 1, - params: [] - }.to_json - ) + response = client.get('/api/v1/health') - warmup_error_message = "Metasploit JSON RPC did not successfully start up. Unexpected response returned: #{response.body}" + warmup_error_message = "Metasploit JSON RPC did not successfully start up. Unexpected response returned: '#{response.body}'" begin parsed_response = JSON.parse(response.body) rescue JSON::ParserError => e raise warmup_error_message, e end - is_valid_response = ( - parsed_response['jsonrpc'] == '2.0' && - parsed_response['id'] == 1 && - !parsed_response.dig('result', 'version').to_s.empty? && - !parsed_response.dig('result', 'ruby').to_s.empty? - ) + expected_response = { 'data' => { 'status' => 'UP' } } + is_valid_response = parsed_response == expected_response unless is_valid_response raise warmup_error_message diff --git a/spec/api/json_rpc_spec.rb b/spec/api/json_rpc_spec.rb index b2099ab127..1eb331a8c1 100644 --- a/spec/api/json_rpc_spec.rb +++ b/spec/api/json_rpc_spec.rb @@ -13,7 +13,8 @@ RSpec.describe "Metasploit's json-rpc" do include_context 'Msf::Framework#threads cleaner' let(:app) { subject } - let(:api_url) { '/api/v1/json-rpc' } + let(:health_check_url) { '/api/v1/health' } + let(:rpc_url) { '/api/v1/json-rpc' } let(:framework) { app.settings.framework } let(:module_name) { 'scanner/ssl/openssl_heartbleed' } let(:a_valid_result_uuid) { { 'result' => hash_including({ 'uuid' => match(/\w+/) }) } } @@ -27,7 +28,7 @@ RSpec.describe "Metasploit's json-rpc" do end def create_job - post api_url, { + post rpc_url, { 'jsonrpc': '2.0', 'method': 'module.check', 'id': 1, @@ -42,7 +43,7 @@ RSpec.describe "Metasploit's json-rpc" do end def get_job_results(uuid) - post api_url, { + post rpc_url, { 'jsonrpc': '2.0', 'method': 'module.results', 'id': 1, @@ -52,6 +53,19 @@ RSpec.describe "Metasploit's json-rpc" do }.to_json end + def get_rpc_health_check + post rpc_url, { + 'jsonrpc': '2.0', + 'method': 'health.check', + 'id': 1, + 'params': [] + }.to_json + end + + def get_rest_health_check + get health_check_url + end + def last_json_response JSON.parse(last_response.body) end @@ -96,6 +110,80 @@ RSpec.describe "Metasploit's json-rpc" do end end + describe 'health status' do + context 'when using the REST health check functionality' do + it 'passes the health check' do + expected_response = { + "data" => { + "status"=>"UP" + } + } + + get_rest_health_check + expect(last_response).to be_ok + expect(last_json_response).to eq(expected_response) + end + end + + context 'when there is an issue' do + before(:each) do + allow(framework).to receive(:version).and_raise 'Mock error' + end + + it 'fails the health check' do + expected_response = { + "data" => { + "status"=>"DOWN" + } + } + + get_rest_health_check + + expect(last_response.status).to be 503 + expect(last_json_response).to eq(expected_response) + end + end + + context 'when using the RPC health check functionality' do + context 'when the service is healthy' do + it 'passes the health check' do + expected_response = { + "id"=>1, + "jsonrpc"=>"2.0", + "result"=> { + "status"=>"UP" + } + } + + get_rpc_health_check + expect(last_response).to be_ok + expect(last_json_response).to eq(expected_response) + end + end + + context 'when there is an issue' do + before(:each) do + allow(framework).to receive(:version).and_raise 'Mock error' + end + + it 'fails the health check' do + expected_response = { + "id"=>1, + "jsonrpc"=>"2.0", + "result"=> { + "status"=>"DOWN" + } + } + + get_rpc_health_check + + expect(last_response).to be_ok + expect(last_json_response).to eq(expected_response) + end + end + end + end + describe 'Running a check job and verifying results' do context 'when the module returns check code safe' do before(:each) do