# 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, "expecting the last frame error to carry a settings timeout: (#{last_frame.inspect})" 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