mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-05 00:02:38 -04:00
255 lines
9.1 KiB
Ruby
255 lines
9.1 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, "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
|