diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 723bcd0d..445570be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,10 +23,12 @@ jobs: '3.3', '3.4', '4.0', - 'head' + 'head', + 'jruby-10.1', + 'jruby-head' ] # Rake replaces RUBYOPT for the test subprocess; keep it in Bundler's context. - ruby-opt: ["-rbundler/setup"] + ruby-opt: ["-rbundler/setup --enable-frozen-string-literal --debug-frozen-string-literal"] include: # Ruby 2.5 through 3.1 have no prebuilt binaries for Ubuntu 26.04. - ruby-version: '2.5' @@ -49,39 +51,24 @@ jobs: os: ubuntu-latest - ruby-version: 'head' os: ubuntu-latest - - ruby-version: '3.4' - ruby-opt: '-rbundler/setup --enable-frozen-string-literal --debug-frozen-string-literal' + - ruby-version: 'jruby-10.1' + os: ubuntu-latest + - ruby-version: 'jruby-head' os: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true - cache-version: 2 - - name: Run tests - run: bundle exec rake --trace RUBYOPT="${{ matrix.ruby-opt }}" - - jruby: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Set up Java + if: startsWith(matrix.ruby-version, 'jruby-') uses: actions/setup-java@v6 with: distribution: temurin - java-version: '21' - - name: Set up JRuby + java-version: '25' + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: jruby-10.1.2.0 + ruby-version: ${{ matrix.ruby-version }} bundler-cache: true cache-version: 2 - name: Run tests - run: bundle exec rake --trace RUBYOPT="-rbundler/setup" - - name: Run JRuby-specific tests - run: bundle exec ruby -Ilib test/jruby_ssl_socket/test_pemutils.rb - - name: Run JRuby socket timeout tests - run: bundle exec ruby -Ilib test/jruby_ssl_socket/test_socket_timeout.rb + run: bundle exec rake --trace RUBYOPT="${{ matrix.ruby-opt }}" diff --git a/Rakefile b/Rakefile index 73d219ba..324b2952 100644 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ ENV['CI_REPORTS'] = File.expand_path('./reports', File.dirname(__FILE__)) Rake::TestTask.new('test') do |test| test.libs << 'lib' test.verbose = true - test.test_files = Dir.glob('test/test_*.rb') + test.test_files = Dir.glob('test/**/test_*.rb') end unless ENV['CI'] diff --git a/lib/httpclient/jruby_ssl_socket.rb b/lib/httpclient/jruby_ssl_socket.rb index ae2a8b0d..d4b56731 100644 --- a/lib/httpclient/jruby_ssl_socket.rb +++ b/lib/httpclient/jruby_ssl_socket.rb @@ -42,7 +42,7 @@ def initialize(socket, debug_dev = nil) @outstr = @socket.getOutputStream @instr = BufferedInputStream.new(@socket.getInputStream) @buf = (' ' * BUF_SIZE).to_java_bytes - @bufstr = '' + @bufstr = ''.dup end def close @@ -352,7 +352,7 @@ def add(cert_source) File.read(cert_source).each_line do |line| case line when /-----BEGIN CERTIFICATE-----/ - pem = '' + pem = ''.dup when /-----END CERTIFICATE-----/ load_pem(pem) # keep parsing in case where multiple certificates in a file diff --git a/lib/httpclient/session.rb b/lib/httpclient/session.rb index 8cb09d4d..29c6f93c 100644 --- a/lib/httpclient/session.rb +++ b/lib/httpclient/session.rb @@ -400,6 +400,7 @@ def debug(str) if str && @debug_dev if str.index("\0") require 'hexdump' + str = str.dup str.force_encoding('BINARY') if str.respond_to?(:force_encoding) @debug_dev << HexDump.encode(str).join("\n") else diff --git a/test/helper.rb b/test/helper.rb index e58c2eb7..7a3c3ba3 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -6,10 +6,15 @@ require 'webrick/httpproxy.rb' require 'logger' require 'stringio' -require 'cgi' +begin + require 'cgi/escape' +rescue LoadError + require 'cgi' +end require 'webrick/httputils' if RUBY_ENGINE == 'jruby' + require 'webrick/https' require 'delegate' module JRubyWEBrickSSLReadBuffer @@ -36,9 +41,24 @@ def run(sock) sock.singleton_class.remove_method(:to_io) if readable_io end end + + module HTTPRequestPatch + def parse(socket = nil) + skip_peer_cert_chain = socket.respond_to?(:peer_cert_chain) && + socket.respond_to?(:peer_cert) && socket.peer_cert.nil? + if skip_peer_cert_chain + # WEBrick asks JRuby for a peer chain even when no client certificate was sent. + socket.define_singleton_method(:peer_cert_chain) { nil } + end + super + ensure + socket.singleton_class.remove_method(:peer_cert_chain) if skip_peer_cert_chain + end + end end WEBrick::HTTPServer.prepend(JRubyWEBrickSSLReadBuffer::HTTPServerPatch) + WEBrick::HTTPRequest.prepend(JRubyWEBrickSSLReadBuffer::HTTPRequestPatch) end diff --git a/test/jruby_ssl_socket/test_mutable_buffers.rb b/test/jruby_ssl_socket/test_mutable_buffers.rb new file mode 100644 index 00000000..b8dcd029 --- /dev/null +++ b/test/jruby_ssl_socket/test_mutable_buffers.rb @@ -0,0 +1,30 @@ +require File.expand_path('../helper', File.dirname(__FILE__)) + + +class JRubyMutableBuffersTest < Test::Unit::TestCase + def setup + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' + end + + def test_socket_reads_using_mutable_buffer + input = Java::JavaIo::ByteArrayInputStream.new("hello\nworld".to_java_bytes) + output = Java::JavaIo::ByteArrayOutputStream.new + socket = Object.new + socket.define_singleton_method(:getInputStream) { input } + socket.define_singleton_method(:getOutputStream) { output } + wrapped_socket = HTTPClient::JavaSocketWrap.new(socket) + + assert_equal("hello\n", wrapped_socket.gets("\n")) + assert_equal('wo', wrapped_socket.read(2)) + assert_equal('rld', wrapped_socket.readpartial(3)) + assert_raise(EOFError) { wrapped_socket.readpartial(1) } + end + + def test_trust_store_loads_multiple_pem_certificates + loader = HTTPClient::JRubySSLSocket::TrustStoreLoader.new + loader.add(File.expand_path('../ca-chain.pem', File.dirname(__FILE__))) + + assert_equal(2, loader.size) + assert_equal(2, loader.trust_store.size) + end +end diff --git a/test/jruby_ssl_socket/test_pemutils.rb b/test/jruby_ssl_socket/test_pemutils.rb index 560a1c6e..66915446 100644 --- a/test/jruby_ssl_socket/test_pemutils.rb +++ b/test/jruby_ssl_socket/test_pemutils.rb @@ -5,26 +5,27 @@ class PEMUtilsTest < Test::Unit::TestCase include Helper def setup + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' @raw_cert = "-----BEGIN CERTIFICATE-----\nMIIDOTCCAiGgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPyLGQB\nGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdSdWJ5\nIENBMB4XDTE2MDgxMDE3MjEzNFoXDTE3MDgxMDE3MjEzNFowSzETMBEGCgmSJomT\n8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEZMBcGA1UEAwwQ\nUnVieSBjZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJCfsSXpSMpmZCVa+ZCM+QDgomnhDlvnrGDq6pasTaIspGTXgws+7r8Dt/cNe6EH\nHJpRH2cGRiO4yPcfcT9eS4X7k8OC4f33wHfACOmLu6LeoNE8ujmSk6L6WzLUI+sE\nnLZbFrXxoAo4XHsm8vEG9C+jEoXZ1p+47wrAGaDwDQTnzlMy4dT9pRQEJP2G/Rry\nUkuZn8SUWmh3/YS78iaSzsNF1cgE1ealHOrPPFDjiCGDaH/LHyUPYlbFSLZ/B7Qx\nLxi5sePLcywWq/EJrmWpgeVTDjtNijsdKv/A3qkY+fm/oD0pzt7XsfJaP9YKNyJO\nQFdxWZeiPcDF+Hwf+IwSr+kCAwEAAaMxMC8wDgYDVR0PAQH/BAQDAgeAMB0GA1Ud\nDgQWBBQNvzYzJyXemGhxbA8NMXLolDnPyjANBgkqhkiG9w0BAQsFAAOCAQEARIJV\noKejGlOTn71QutnNnu07UtTu0IHs6YqjYzzND+m4JXLN+wvYm72AFUG0b1L7dRg0\niK8XjQrlNQNVqP1Mc6tffchy20neOPOHeiO6qTdRU8P2S8D3Uwe+1qhgxjfE+cWc\nwZmWxYK4HA8c58PxWMqrkr2QqXDplG9KWLvOgrtPGiLLZcQSKhvvB63QzItHBDU6\nRayiJY3oPkK/HrIvFlySqFqzWmuyknkciOFywEHQMz/tcSFJ2QFpPj/tBz9VXohH\nZ8KscmfhZrTPBjo+ky1lz/WraWoz4LMiLnkC2ABczWLRSawu+v3Irx1NFJngt05e\npqwtqIUeg7j+JLiTaA==\n-----END CERTIFICATE-----" end def test_read_certificate assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(@raw_cert) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(@raw_cert) end end def test_read_certificate_works_with_random_ascii_text_outside_begin_end raw_cert_with_ascii = "some text before begin\n" + @raw_cert + "\nsome text after end" assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(raw_cert_with_ascii) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(raw_cert_with_ascii) end end def test_read_certificate_uses_all_content_if_missing_begin_end cert = @raw_cert.sub(/-----BEGIN CERTIFICATE-----/, '').sub(/-----END CERTIFICATE-----/, '') assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(cert) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(cert) end end diff --git a/test/jruby_ssl_socket/test_socket_timeout.rb b/test/jruby_ssl_socket/test_socket_timeout.rb index 8b854431..9202a6f9 100644 --- a/test/jruby_ssl_socket/test_socket_timeout.rb +++ b/test/jruby_ssl_socket/test_socket_timeout.rb @@ -3,6 +3,7 @@ class JRubySocketTimeoutTest < Test::Unit::TestCase def test_read_timeout_is_reported_as_receive_timeout + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' socket = HTTPClient::JavaSocketWrap.allocate input = Object.new input.define_singleton_method(:read) do |_buffer| diff --git a/test/test_auth.rb b/test/test_auth.rb index 95c17188..8b97e914 100644 --- a/test/test_auth.rb +++ b/test/test_auth.rb @@ -2,7 +2,14 @@ require 'digest/md5' require 'rack' require 'rack/lint' -require 'rack-ntlm' +# rack-ntlm-test-service 0.0.1 assigns an unused rescue variable while loading. +original_verbose = $VERBOSE +begin + $VERBOSE = nil + require 'rack-ntlm' +ensure + $VERBOSE = original_verbose +end class TestAuth < Test::Unit::TestCase include Helper diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index 74406ef2..d1d664ca 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -65,6 +65,21 @@ def test_debug_dev_stream assert(!str.empty?) end + data('mutable' => false, 'frozen' => true) + def test_debug_socket_does_not_mutate_binary_input(freeze_payload) + debug_dev = StringIO.new + socket = Object.new + socket.extend(HTTPClient::DebugSocket) + socket.debug_dev = debug_dev + payload = "\x00".dup + payload.freeze if freeze_payload + + assert_nothing_raised { socket.send(:debug, payload) } + assert_equal("\x00", payload) + assert_equal(Encoding::UTF_8, payload.encoding) + assert(!debug_dev.string.empty?) + end + def test_protocol_version_http09 @client.protocol_version = 'HTTP/0.9' @client.debug_dev = str = ''.dup @@ -768,7 +783,8 @@ def test_get_async_for_largebody if RUBY_VERSION > "1.9" def test_post_async_with_default_internal original_encoding = Encoding.default_internal - Encoding.default_internal = Encoding::UTF_8 + # This test covers the legacy setter even though JRuby deprecates it. + silent { Encoding.default_internal = Encoding::UTF_8 } begin post_body = StringIO.new("こんにちは") conn = @client.post_async(serverurl + 'servlet', post_body) @@ -776,7 +792,7 @@ def test_post_async_with_default_internal res = conn.pop assert_equal 'post,こんにちは', res.content.read ensure - Encoding.default_internal = original_encoding + silent { Encoding.default_internal = original_encoding } end end end @@ -934,7 +950,7 @@ def test_post_with_file STDOUT.sync = true File.open(__FILE__) do |file| res = @client.post(serverurl + 'servlet', {1=>2, 3=>file}) - assert_match(/^Content-Disposition: form-data; name="1"\r\n/nm, res.content) + assert_match(/^Content-Disposition: form-data; name="1"\r\n/m, res.content.b) assert_match(/^Content-Disposition: form-data; name="3";/, res.content) assert_match(/FIND_TAG_IN_THIS_FILE/, res.content) end