diff --git a/lib/mint/http.ex b/lib/mint/http.ex index 05ee649c..45d65f78 100644 --- a/lib/mint/http.ex +++ b/lib/mint/http.ex @@ -287,6 +287,15 @@ defmodule Mint.HTTP do uses TLS and the TLS session to the host is nested inside it. *HTTPS proxies for HTTPS connections are available since v1.10.0*. + The `opts` in the `:proxy` tuple are the options of the connection to the proxy + itself and support the same options as `connect/4`. Tunnel proxies additionally + support: + + * `:tunnel_timeout` - the maximum time (in milliseconds) to wait for the proxy + to reply to the `CONNECT` request. If the tunnel is not established within + this time, `connect/4` returns a `Mint.HTTPError` with reason + `{:proxy, :tunnel_timeout}`. Defaults to `30_000` (30 seconds). + ## Transport options The options specified in `:transport_opts` are passed to the module that diff --git a/lib/mint/tunnel_proxy.ex b/lib/mint/tunnel_proxy.ex index f2e3c012..55b95fb8 100644 --- a/lib/mint/tunnel_proxy.ex +++ b/lib/mint/tunnel_proxy.ex @@ -56,7 +56,7 @@ defmodule Mint.TunnelProxy do end defp receive_response(conn, ref, timeout_deadline) do - timeout = timeout_deadline - System.monotonic_time(:millisecond) + timeout = max(timeout_deadline - System.monotonic_time(:millisecond), 0) socket = HTTP1.get_socket(conn) receive do diff --git a/test/mint/tunnel_proxy_connect_test.exs b/test/mint/tunnel_proxy_connect_test.exs index 1cb68f04..74d93aa2 100644 --- a/test/mint/tunnel_proxy_connect_test.exs +++ b/test/mint/tunnel_proxy_connect_test.exs @@ -37,6 +37,24 @@ defmodule Mint.TunnelProxyConnectTest do assert merge_body(rest, request) == "hello" end + test "a proxy that stalls the CONNECT response yields a tunnel timeout error" do + proxy_port = start_silent_proxy() + + assert {:error, %Mint.HTTPError{reason: {:proxy, :tunnel_timeout}}} = + HTTP.connect(:https, "localhost", 443, + proxy: {:http, "localhost", proxy_port, [tunnel_timeout: 50]} + ) + end + + test "an already-elapsed tunnel deadline yields a tunnel timeout error instead of crashing" do + proxy_port = start_silent_proxy() + + assert {:error, %Mint.HTTPError{reason: {:proxy, :tunnel_timeout}}} = + HTTP.connect(:https, "localhost", 443, + proxy: {:http, "localhost", proxy_port, [tunnel_timeout: -1]} + ) + end + test "IPv6 address targets produce a bracketed CONNECT authority" do {proxy_port, proxy_ref} = start_capturing_proxy() @@ -82,6 +100,25 @@ defmodule Mint.TunnelProxyConnectTest do {port, ref} end + # Starts a one-shot proxy that accepts a single connection and never + # responds. + defp start_silent_proxy do + {:ok, listen_socket} = + :gen_tcp.listen(0, mode: :binary, packet: :raw, active: false, reuseaddr: true) + + {:ok, port} = :inet.port(listen_socket) + + spawn_link(fn -> + {:ok, socket} = :gen_tcp.accept(listen_socket) + + receive do + :stop -> :gen_tcp.close(socket) + end + end) + + port + end + # Starts a one-shot server that accepts a single connection, reports the raw # request head to the test process, and closes the connection. defp start_capturing_proxy do