diff --git a/lib/web_console.rb b/lib/web_console.rb index d4d1dd97..ef50089e 100644 --- a/lib/web_console.rb +++ b/lib/web_console.rb @@ -1,6 +1,7 @@ require 'binding_of_caller' require 'active_support/lazy_load_hooks' +require 'active_support/logger' require 'web_console/core_ext/exception' require 'web_console/engine' @@ -13,7 +14,11 @@ require 'web_console/middleware' require 'web_console/whitelist' require 'web_console/request' +require 'web_console/whiny_request' module WebConsole + mattr_accessor :logger + @@logger = ActiveSupport::Logger.new($stderr) + ActiveSupport.run_load_hooks(:web_console, self) end diff --git a/lib/web_console/middleware.rb b/lib/web_console/middleware.rb index 9052542c..59e7391e 100644 --- a/lib/web_console/middleware.rb +++ b/lib/web_console/middleware.rb @@ -7,13 +7,16 @@ class Middleware binding_change_re: %r{/repl_sessions/(?.+?)/trace\z} } + cattr_accessor :whiny_requests + @@whiny_requests = true + def initialize(app, options = {}) @app = app @options = DEFAULT_OPTIONS.merge(options) end def call(env) - request = Request.new(env) + request = create_request(env) return @app.call(env) unless request.from_whitelited_ip? if id = id_for_repl_session_update(request) @@ -43,6 +46,11 @@ def call(env) private + def create_request(env) + request = Request.new(env) + whiny_requests ? WhinyRequest.new(request) : request + end + def update_re @options[:update_re] end diff --git a/lib/web_console/request.rb b/lib/web_console/request.rb index a7e5c6b4..7917855a 100644 --- a/lib/web_console/request.rb +++ b/lib/web_console/request.rb @@ -3,9 +3,10 @@ module WebConsole class Request < ActionDispatch::Request # While most of the servers will return blank content type if none given, # Puma will return text/plain. - ACCEPTABLE_CONTENT_TYPE = [Mime::HTML, Mime::TEXT] + cattr_accessor :acceptable_content_types + @@acceptable_content_types = [Mime::HTML, Mime::TEXT] - # Configurable whitelisted IPs. + # Configurable set of whitelisted networks. cattr_accessor :whitelisted_ips @@whitelisted_ips = Whitelist.new @@ -19,10 +20,11 @@ def from_whitelited_ip? # Returns whether the request is from an acceptable content type. # - # We can render a console for HTML and TEXT. If a client didn't - # specified any content type, we'll render it as well. + # We can render a console for HTML and TEXT by default. If a client didn't + # specified any content type and the server returned it as blank, we'll + # render it as well. def acceptable_content_type? - content_type.blank? || content_type.in?(ACCEPTABLE_CONTENT_TYPE) + content_type.blank? || content_type.in?(acceptable_content_types) end end end diff --git a/lib/web_console/whiny_request.rb b/lib/web_console/whiny_request.rb new file mode 100644 index 00000000..87a6b5ba --- /dev/null +++ b/lib/web_console/whiny_request.rb @@ -0,0 +1,38 @@ +module WebConsole + # Noisy wrapper around +Request+. + # + # If any calls to +from_whitelisted_ip?+ and +acceptable_content_type?+ + # return false, an info log message will be displayed in users' logs. + class WhinyRequest < SimpleDelegator + def from_whitelited_ip? + whine_unless request.from_whitelited_ip? do + "Cannot render console from #{request.remote_ip}! " \ + "Allowed networks: #{request.whitelisted_ips}" + end + end + + def acceptable_content_type? + whine_unless request.acceptable_content_type? do + "Cannot render console with content type #{request.content_type}" \ + "Allowed content types: #{request.acceptable_content_types}" + end + end + + private + + def whine_unless(condition) + unless condition + logger.info { yield } + end + condition + end + + def logger + env['action_dispatch.logger'] || WebConsole.logger + end + + def request + __getobj__ + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 43c2793a..822f306c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,6 +23,28 @@ def assert_select(*) include SilenceRailsDomTesting end +# A copy of Kernel#capture in active_support/core_ext/kernel/reporting.rb as +# its getting deprecated past 4.2. Its not thread safe, but I don't need it to +# be in the tests +def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read +ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) +end + +alias silence capture + # Load fixtures from the engine if ActiveSupport::TestCase.method_defined?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) diff --git a/test/web_console/middleware_test.rb b/test/web_console/middleware_test.rb index 3b45b8cf..422099b3 100644 --- a/test/web_console/middleware_test.rb +++ b/test/web_console/middleware_test.rb @@ -58,7 +58,9 @@ def call(env) test "doesn't render console from non whitelisted IP" do Request.stubs(:whitelisted_ips).returns(IPAddr.new('127.0.0.1')) - get '/', nil, 'CONTENT_TYPE' => 'text/html', 'REMOTE_ADDR' => '1.1.1.1', 'web-console.binding' => binding + silence(:stderr) do + get '/', nil, 'CONTENT_TYPE' => 'text/html', 'REMOTE_ADDR' => '1.1.1.1', 'web-console.binding' => binding + end assert_select '#console', 0 end diff --git a/test/web_console/whiny_request_test.rb b/test/web_console/whiny_request_test.rb new file mode 100644 index 00000000..f2436ec3 --- /dev/null +++ b/test/web_console/whiny_request_test.rb @@ -0,0 +1,33 @@ +require 'test_helper' + +module WebConsole + class WhinyRequestTest < ActiveSupport::TestCase + test '#from_whitelited_ip? logs out to stderr' do + Request.stubs(:whitelisted_ips).returns(IPAddr.new('127.0.0.1')) + assert_output_to_stderr do + req = request('http://example.com', 'REMOTE_ADDR' => '0.0.0.0') + assert_not req.from_whitelited_ip? + end + end + + test '#acceptable_content_type? logs out to stderr' do + Request.stubs(:acceptable_content_types).returns([]) + assert_output_to_stderr do + req = request('http://example.com', 'CONTENT_TYPE' => 'application/json') + assert_not req.acceptable_content_type? + end + end + + private + + def assert_output_to_stderr + output = capture(:stderr) { yield } + assert_not output.blank? + end + + def request(*args) + request = Request.new(Rack::MockRequest.env_for(*args)) + WhinyRequest.new(request) + end + end +end