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)