diff --git a/lib/net/http.rb b/lib/net/http.rb index 6c43e62..5ab4cf9 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -688,6 +688,9 @@ class HTTPHeaderSyntaxError < StandardError; end # Returns the flags for server the certification verification at the beginning of the SSL/TLS session. # - {:verify_mode=}[rdoc-ref:Net::HTTP#verify_mode=]: # Sets the flags for server the certification verification at the beginning of the SSL/TLS session. + # - {#set_debug_output}[rdoc-ref:Net::HTTP#set_debug_output]: + # Sets the output stream for debugging, which logs credentials in plain text; + # never use it in production code. # # === Addresses and Ports # @@ -1260,8 +1263,22 @@ def inspect # read 2 bytes # Conn keep-alive # + # Debug output contains credentials, so enabling it writes a warning to + # $stderr. The warning is written at most once per thread, and is + # not written when $DEBUG is set, which already declares that the + # process is being debugged, or when +output+ is +nil+, which disables debug + # output. Like any other Ruby warning it can be intercepted by overriding + # Warning.warn. + # def set_debug_output(output) warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started? + if output and !$DEBUG and !Thread.current.thread_variable_get(:net_http_debug_output_warned) + Thread.current.thread_variable_set(:net_http_debug_output_warned, true) + warn 'Net::HTTP#set_debug_output: every request and response, including ' + + 'Authorization and Cookie headers and message bodies, will be written ' + + 'to the given stream in plain text; never enable this in production', + uplevel: 1 + end @debug_output = output end diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb index e5028d4..a1bd6a8 100644 --- a/test/net/http/test_http.rb +++ b/test/net/http/test_http.rb @@ -7,6 +7,10 @@ class TestNetHTTP < Test::Unit::TestCase + def setup + Thread.current.thread_variable_set(:net_http_debug_output_warned, nil) + end + def test_class_Proxy no_proxy_class = Net::HTTP.Proxy nil @@ -266,6 +270,76 @@ def test_default_configuration Net::HTTP.default_configuration = nil end + def test_set_debug_output_warns + http = Net::HTTP.new 'hostname.example' + + assert_warning(/#{__FILE__}:#{__LINE__+1}: warning: Net::HTTP#set_debug_output: /) do + http.set_debug_output StringIO.new + end + end + + def test_set_debug_output_names_what_is_logged + http = Net::HTTP.new 'hostname.example' + + assert_warning(/Authorization and Cookie headers and message bodies/) do + http.set_debug_output StringIO.new + end + end + + def test_set_debug_output_nil_does_not_warn + http = Net::HTTP.new 'hostname.example' + + assert_warning('') do + http.set_debug_output nil + end + end + + def test_set_debug_output_DEBUG_does_not_warn + debug, $DEBUG = $DEBUG, true + + http = Net::HTTP.new 'hostname.example' + + assert_warning('') do + http.set_debug_output StringIO.new + end + ensure + $DEBUG = debug + end + + def test_set_debug_output_warns_once_per_thread + http = Net::HTTP.new 'hostname.example' + + assert_warning(/Net::HTTP#set_debug_output: /) do + http.set_debug_output StringIO.new + end + + assert_warning('') do + http.set_debug_output StringIO.new + end + end + + def test_set_debug_output_warns_once_across_objects + assert_warning(/Net::HTTP#set_debug_output: /) do + Net::HTTP.new('hostname.example').set_debug_output StringIO.new + end + + assert_warning('') do + Net::HTTP.new('hostname.example').set_debug_output StringIO.new + end + end + + def test_set_debug_output_warns_once_in_subclasses + assert_warning(/Net::HTTP#set_debug_output: /) do + Net::HTTP.new('hostname.example').set_debug_output StringIO.new + end + + proxy = Net::HTTP::Proxy('proxy.example', 8000) + + assert_warning('') do + proxy.new('hostname.example').set_debug_output StringIO.new + end + end + end module TestNetHTTP_version_1_1_methods @@ -1470,4 +1544,18 @@ def test_get assert_equal expected, ret RUBY end + + # The once-per-thread flag is kept in thread-local storage rather than on + # Net::HTTP, because non-main Ractors cannot set instance variables of + # classes. Recording it there would raise Ractor::IsolationError. + def test_set_debug_output + assert_ractor(<<~RUBY, require: 'net/http', ignore_stderr: true) + require 'stringio' + ret = Ractor.new { + Net::HTTP.new('hostname.example').set_debug_output(StringIO.new) + :ok + }.value + assert_equal :ok, ret + RUBY + end end if defined?(Ractor) && Ractor.method_defined?(:value)