mirror of
				https://github.com/HoneyryderChuck/httpx.git
				synced 2025-11-04 00:01:41 -05:00 
			
		
		
		
	it was previously only handling 421 status codes for the same effect; this achieves parity with the frame-driven redirection
		
			
				
	
	
		
			255 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
require_relative "support/http_helpers"
 | 
						|
 | 
						|
class HTTPSTest < Minitest::Test
 | 
						|
  include HTTPHelpers
 | 
						|
  include Requests
 | 
						|
  include Get
 | 
						|
  include Compression
 | 
						|
  include Head
 | 
						|
  include WithBody
 | 
						|
  include Multipart
 | 
						|
  include Headers
 | 
						|
  include ResponseBody
 | 
						|
  include IO
 | 
						|
  include Callbacks
 | 
						|
  include Errors if RUBY_ENGINE == "ruby"
 | 
						|
  include Resolvers if ENV.key?("HTTPX_RESOLVER_URI")
 | 
						|
  # TODO: uncomment as soon as nghttpx supports altsvc for HTTP/2
 | 
						|
  # include AltSvc if ENV.key?("HTTPBIN_ALTSVC_HOST")
 | 
						|
 | 
						|
  include Plugins::Proxy unless ENV.key?("HTTPX_NO_PROXY")
 | 
						|
  include Plugins::Authentication
 | 
						|
  include Plugins::OAuth
 | 
						|
  include Plugins::FollowRedirects
 | 
						|
  include Plugins::ContentDigest
 | 
						|
  include Plugins::Cookies
 | 
						|
  include Plugins::PushPromise if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
 | 
						|
  include Plugins::Retries
 | 
						|
  include Plugins::Expect
 | 
						|
  include Plugins::RateLimiter
 | 
						|
  include Plugins::Persistent
 | 
						|
  include Plugins::Stream
 | 
						|
  include Plugins::AWSAuthentication
 | 
						|
  include Plugins::Upgrade
 | 
						|
  include Plugins::GRPC if RUBY_ENGINE == "ruby"
 | 
						|
  include Plugins::ResponseCache
 | 
						|
  include Plugins::CircuitBreaker
 | 
						|
  include Plugins::WebDav
 | 
						|
  include Plugins::Brotli if RUBY_ENGINE == "ruby"
 | 
						|
  include Plugins::SsrfFilter
 | 
						|
  include Plugins::XML
 | 
						|
 | 
						|
  def test_ssl_session_resumption
 | 
						|
    uri = build_uri("/get")
 | 
						|
    HTTPX.with(ssl: { ssl_version: :TLSv1_2, alpn_protocols: %w[http1.1] }).plugin(SessionWithPool).wrap do |http|
 | 
						|
      http.get(uri)
 | 
						|
      conn1 = http.connections.last
 | 
						|
 | 
						|
      http.get(uri)
 | 
						|
      conn2 = http.connections.last
 | 
						|
 | 
						|
      # because there's reconnection
 | 
						|
      assert conn1 == conn2
 | 
						|
 | 
						|
      assert conn2.io.instance_variable_get(:@io).session_reused?
 | 
						|
    end
 | 
						|
  end unless RUBY_ENGINE == "jruby"
 | 
						|
 | 
						|
  def test_connection_coalescing
 | 
						|
    coalesced_origin = "https://#{ENV["HTTPBIN_COALESCING_HOST"]}"
 | 
						|
    HTTPX.plugin(SessionWithPool).wrap do |http|
 | 
						|
      response1 = http.get(origin)
 | 
						|
      verify_status(response1, 200)
 | 
						|
      response2 = http.get(coalesced_origin)
 | 
						|
      verify_status(response2, 200)
 | 
						|
      # introspection time
 | 
						|
      connections = http.connections
 | 
						|
      origins = connections.map(&:origins)
 | 
						|
      assert origins.any? { |orgs| orgs.sort == [origin, coalesced_origin].sort },
 | 
						|
             "connections for #{[origin, coalesced_origin]} didn't coalesce (expected connection with both origins (#{origins}))"
 | 
						|
 | 
						|
      unsafe_origin = URI(origin)
 | 
						|
      unsafe_origin.scheme = "http"
 | 
						|
      response3 = http.get(unsafe_origin)
 | 
						|
      verify_status(response3, 200)
 | 
						|
 | 
						|
      # introspection time
 | 
						|
      connections = http.connections
 | 
						|
      origins = connections.map(&:origins)
 | 
						|
      refute origins.any?([origin]),
 | 
						|
             "connection coalesced inexpectedly (expected connection with both origins (#{origins}))"
 | 
						|
    end
 | 
						|
  end if ENV.key?("HTTPBIN_COALESCING_HOST")
 | 
						|
 | 
						|
  def test_verbose_log
 | 
						|
    log = StringIO.new
 | 
						|
    uri = build_uri("/get")
 | 
						|
    response = HTTPX.get(uri, debug: log, debug_level: 3)
 | 
						|
    verify_status(response, 200)
 | 
						|
    log_output = log.string
 | 
						|
    # assert tls output
 | 
						|
    assert log_output.include?("SSL connection using TLSv")
 | 
						|
    assert log_output.include?("ALPN, server accepted to use h2")
 | 
						|
    assert log_output.include?("Server certificate:")
 | 
						|
    assert log_output.include?(" subject: ")
 | 
						|
    assert log_output.include?(" start date: ")
 | 
						|
    assert log_output.include?(" expire date: ")
 | 
						|
    assert log_output.include?(" issuer: ")
 | 
						|
    assert log_output.include?(" SSL certificate verify ok")
 | 
						|
 | 
						|
    # assert request headers
 | 
						|
    assert log_output.include?("HEADER: :scheme: https")
 | 
						|
    assert log_output.include?("HEADER: :method: GET")
 | 
						|
    assert log_output.include?("HEADER: :path: ")
 | 
						|
    assert log_output.include?("HEADER: :authority: ")
 | 
						|
    assert log_output.include?("HEADER: accept: */*")
 | 
						|
    # assert response headers
 | 
						|
    assert log_output.include?("HEADER: :status: 200")
 | 
						|
    assert log_output.include?("HEADER: content-type: ")
 | 
						|
    assert log_output.include?("HEADER: content-length: ")
 | 
						|
  end
 | 
						|
 | 
						|
  # HTTP/2-specific tests
 | 
						|
 | 
						|
  {
 | 
						|
    # TODO: turn this on when CI does keep-alive on HTTP/1.1
 | 
						|
    # http1: { uri: "https://httpbin.org/get", ssl: { alpn_protocols: %w[http/1.1] }, max_concurrent_requests: 1 },
 | 
						|
    http2: {},
 | 
						|
  }.each do |proto, proto_options|
 | 
						|
    define_method :"test_multiple_get_max_requests_#{proto}" do
 | 
						|
      uri = proto_options.delete(:uri) || URI(build_uri("/get"))
 | 
						|
      options = { max_requests: 2, **proto_options }
 | 
						|
 | 
						|
      HTTPX.plugin(SessionWithPool).with(options).wrap do |http|
 | 
						|
        response1, response2, response3 = http.get(uri, uri, uri)
 | 
						|
        verify_status(response1, 200)
 | 
						|
        verify_body_length(response1)
 | 
						|
        verify_status(response2, 200)
 | 
						|
        verify_body_length(response2)
 | 
						|
        verify_status(response3, 200)
 | 
						|
        verify_body_length(response3)
 | 
						|
        connection_count = http.connection_count
 | 
						|
        assert connection_count == 2, "expected to have 2 connections, instead have #{connection_count}"
 | 
						|
        assert http.connections.size == 1, "expected connection to have been reused on exhaustion"
 | 
						|
 | 
						|
        # ssl session ought to be reused
 | 
						|
        conn = http.connections.first
 | 
						|
        assert conn.io.instance_variable_get(:@io).session_reused? unless RUBY_ENGINE == "jruby"
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def test_http2_uncoalesce_on_misdirected
 | 
						|
    uri = build_uri("/status/421")
 | 
						|
    HTTPX.plugin(SessionWithPool).wrap do |http|
 | 
						|
      response = http.get(uri)
 | 
						|
      verify_status(response, 421)
 | 
						|
      connection_count = http.connection_count
 | 
						|
      assert connection_count == 2, "expected to have 2 connections, instead have #{connection_count}"
 | 
						|
      assert response.version == "1.1", "request should have been retried with HTTP/1.1"
 | 
						|
    end
 | 
						|
 | 
						|
    start_test_servlet(MisdirectedServer) do |server|
 | 
						|
      HTTPX.plugin(SessionWithPool).with(ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE }).wrap do |http|
 | 
						|
        uri = "#{server.origin}/"
 | 
						|
        response = http.get(uri)
 | 
						|
        verify_status(response, 200)
 | 
						|
        connection_count = http.connection_count
 | 
						|
        assert connection_count == 2, "expected to have 2 connections, instead have #{connection_count}"
 | 
						|
        assert response.version == "1.1", "request should have been retried with HTTP/1.1"
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def test_http2_settings_timeout
 | 
						|
    uri = build_uri("/get")
 | 
						|
    HTTPX.plugin(SessionWithPool).plugin(SessionWithFrameDelay).wrap do |http|
 | 
						|
      response = http.get(uri)
 | 
						|
      verify_error_response(response, /settings_timeout/)
 | 
						|
    end
 | 
						|
  end unless RUBY_ENGINE == "jruby"
 | 
						|
 | 
						|
  def test_http2_request_trailers
 | 
						|
    uri = build_uri("/post")
 | 
						|
    log = StringIO.new
 | 
						|
 | 
						|
    HTTPX.wrap do |http|
 | 
						|
      total_time = start_time = nil
 | 
						|
      trailered = false
 | 
						|
      request = http.build_request("POST", uri, body: %w[this is chunked], debug: log, debug_level: 3)
 | 
						|
      request.on(:headers) do |_written_request|
 | 
						|
        start_time = HTTPX::Utils.now
 | 
						|
      end
 | 
						|
      request.on(:trailers) do |written_request|
 | 
						|
        total_time = HTTPX::Utils.elapsed_time(start_time)
 | 
						|
        written_request.trailers["x-time-spent"] = total_time
 | 
						|
        trailered = true
 | 
						|
      end
 | 
						|
      response = http.request(request)
 | 
						|
      verify_status(response, 200)
 | 
						|
      body = json_body(response)
 | 
						|
      # verify_header(body["headers"], "x-time-spent", total_time.to_s)
 | 
						|
      assert body.key?("data")
 | 
						|
      assert trailered, "trailer callback wasn't called"
 | 
						|
 | 
						|
      # assert response headers
 | 
						|
      log_output = log.string
 | 
						|
      assert log_output.include?("HEADER: x-time-spent: #{total_time}")
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def test_http2_client_sends_settings_timeout
 | 
						|
    test_server = nil
 | 
						|
    start_test_servlet(SettingsTimeoutServer) do |server|
 | 
						|
      test_server = server
 | 
						|
      uri = "#{server.origin}/"
 | 
						|
      http = HTTPX.plugin(SessionWithPool).with(timeout: { settings_timeout: 3 }, ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE })
 | 
						|
      response = http.get(uri)
 | 
						|
      verify_error_response(response, HTTPX::SettingsTimeoutError)
 | 
						|
    end
 | 
						|
    last_frame = test_server.frames.last
 | 
						|
    assert last_frame[:error] == :settings_timeout
 | 
						|
  end
 | 
						|
 | 
						|
  def test_http2_client_goaway_with_no_response
 | 
						|
    start_test_servlet(KeepAlivePongServer) do |server|
 | 
						|
      uri = "#{server.origin}/"
 | 
						|
      HTTPX.plugin(SessionWithPool).with(ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE }) do |http|
 | 
						|
        response = http.get(uri)
 | 
						|
        verify_status(response, 200)
 | 
						|
        response = http.get(uri)
 | 
						|
        verify_error_response(response, HTTPX::Connection::HTTP2::GoawayError)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  def test_ssl_wrong_hostname
 | 
						|
    uri = build_uri("/get")
 | 
						|
    response = HTTPX.with(ssl: { hostname: "great-gatsby.com" }).get(uri)
 | 
						|
    verify_error_response(response, /certificate verify failed|does not match the server certificate/)
 | 
						|
  end
 | 
						|
 | 
						|
  def test_https_request_with_ip_not_set_sni
 | 
						|
    # # server conf
 | 
						|
    ca_store = OpenSSL::X509::Store.new
 | 
						|
    ca_store.set_default_paths
 | 
						|
    ca_store.add_file(File.join(ByIpCertServer::CERTS_DIR, "ca-bundle.crt"))
 | 
						|
 | 
						|
    start_test_servlet(ByIpCertServer) do |server|
 | 
						|
      uri = "#{server.origin}/"
 | 
						|
      HTTPX.plugin(SessionWithPool).with(ssl: { cert_store: ca_store }) do |http|
 | 
						|
        response = http.get(uri)
 | 
						|
        verify_status(response, 200)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  private
 | 
						|
 | 
						|
  def scheme
 | 
						|
    "https://"
 | 
						|
  end
 | 
						|
end
 |