Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dialyzer_ignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
lib/mint/tunnel_proxy.ex:49
lib/mint/tunnel_proxy.ex:50
lib/mint/http1.ex:999
lib/mint/unsafe_proxy.ex:173
lib/mint/unsafe_proxy.ex:198
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ services:
ports:
- "${TINYPROXY_AUTH_PORT:-8889}:8888"

proxy-https:
build: ./test/docker/squid-https
ports:
# Bound to loopback: this is an unauthenticated open proxy, it must not
# be reachable from other hosts while the test suite runs.
- "127.0.0.1:${SQUID_HTTPS_PORT:-8890}:3129"

httpbin:
image: docker.io/kennethreitz/httpbin:latest
platform: linux/amd64
Expand Down
23 changes: 21 additions & 2 deletions lib/mint/core/transport/ssl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ defmodule Mint.Core.Transport.SSL do
@default_versions [:"tlsv1.3", :"tlsv1.2"]
@default_timeout 30_000

@tunnel_cb_info {Mint.Core.Transport.SSL.Tunnel, :ssl, :ssl_closed, :ssl_error, :ssl_passive}

Record.defrecordp(
:certificate,
:Certificate,
Expand Down Expand Up @@ -347,6 +349,10 @@ defmodule Mint.Core.Transport.SSL do
end

@impl true
# :ssl.connect/3's spec doesn't cover upgrading an :ssl socket, but the
# function supports any socket type that the :cb_info callback module can
# handle.
@dialyzer {:nowarn_function, upgrade: 5}
def upgrade(socket, :http, hostname, _port, opts) do
hostname = String.to_charlist(hostname)
timeout = Keyword.get(opts, :timeout, @default_timeout)
Expand All @@ -357,8 +363,21 @@ defmodule Mint.Core.Transport.SSL do
wrap_err(:ssl.connect(socket, ssl_opts(hostname, opts), timeout))
end

def upgrade(_socket, :https, _hostname, _port, _opts) do
raise "nested SSL sessions are not supported"
def upgrade(socket, :https, hostname, _port, opts) do
hostname = String.to_charlist(hostname)
timeout = Keyword.get(opts, :timeout, @default_timeout)

with :ok <- setopts(socket, active: false) do
# The TLS session to the host is nested inside the TLS session to the
# proxy: the established :ssl socket acts as the transport for the new
# connection, so :ssl needs a callback module that speaks :ssl instead
# of :gen_tcp. Forced with Keyword.put/3 so that user-provided transport
# options cannot override it.
opts = Keyword.delete(opts, :hostname)
opts = Keyword.put(ssl_opts(hostname, opts), :cb_info, @tunnel_cb_info)

wrap_err(:ssl.connect(socket, opts, timeout))
end
end

@impl true
Expand Down
28 changes: 28 additions & 0 deletions lib/mint/core/transport/ssl/tunnel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Mint.Core.Transport.SSL.Tunnel do
@moduledoc false

# The transport callback module given to `:ssl.connect/3` through the
# `:cb_info` option when nesting a TLS session inside an established
# `:ssl` socket. `:ssl` requires the callback module to behave like
# `:gen_tcp`, with functions corresponding to `:inet.setopts/2`,
# `:inet.getopts/2`, `:inet.peername/1`, `:inet.sockname/1`, and
# `:inet.port/1`.

defdelegate setopts(socket, opts), to: :ssl
defdelegate getopts(socket, opts), to: :ssl
defdelegate send(socket, data), to: :ssl
defdelegate recv(socket, length), to: :ssl
defdelegate recv(socket, length, timeout), to: :ssl
defdelegate controlling_process(socket, pid), to: :ssl
defdelegate close(socket), to: :ssl
defdelegate shutdown(socket, how), to: :ssl
defdelegate getstat(socket, options), to: :ssl
defdelegate peername(socket), to: :ssl
defdelegate sockname(socket), to: :ssl

def port(socket) do
with {:ok, {_address, port}} <- :ssl.sockname(socket) do
{:ok, port}
end
end
end
24 changes: 14 additions & 10 deletions lib/mint/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,6 @@ defmodule Mint.HTTP do
**bytes**, of an HTTP/1 response header section or chunked trailer section. Defaults to
256 KiB. This option is only used for HTTP/1 connections. *Available since 1.9.2*.

The following options are HTTP/1-specific and will force the connection
to be an HTTP/1 connection.

* `:proxy` - a `{scheme, address, port, opts}` tuple that identifies a proxy to
connect to. See the "Proxying" section below for more information.

Expand Down Expand Up @@ -278,15 +275,17 @@ defmodule Mint.HTTP do
You can set up proxying through the `:proxy` option, which is a tuple
`{scheme, address, port, opts}` that identifies the proxy to connect to.
Once a proxied connection is returned, the proxy is transparent to you and you
can use the connection like a normal HTTP/1 connection.
can use the connection like a normal connection.

If the `scheme` is `:http`, we will connect to the host in the most compatible
way, supporting older proxy servers. Data will be sent in clear text.
If the connection scheme is `:http`, requests are forwarded through the proxy
in the most compatible way, supporting older proxy servers. Data will be sent
in clear text.

If the connection scheme is `:https`, we will connect to the host with a tunnel
through the proxy. Using `:https` for both the proxy and the connection scheme
is not supported, it is recommended to use `:https` for the end host connection
instead of the proxy.
If the connection scheme is `:https`, a tunnel through the proxy is established
with the `CONNECT` method. If the proxy `scheme` is `:http`, the `CONNECT`
request is sent in clear text; if it is `:https`, the connection to the proxy
uses TLS and the TLS session to the host is nested inside it. *HTTPS proxies
for HTTPS connections are available since v1.10.0*.

## Transport options

Expand Down Expand Up @@ -415,6 +414,11 @@ defmodule Mint.HTTP do
proxy = {:http, "myproxy.example.com", 80, []}
{:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443, proxy: proxy)

Using an HTTPS proxy:

proxy = {:https, "myproxy.example.com", 443, []}
{:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443, proxy: proxy)

Forcing the connection to be an HTTP/2 connection:

{:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443, protocols: [:http2])
Expand Down
3 changes: 3 additions & 0 deletions lib/mint/negotiate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ defmodule Mint.Negotiate do
{:ok, transport_state} ->
alpn_negotiate(new_scheme, transport_state, hostname, port, opts)

{:error, %TransportError{} = error} ->
{:error, error}

{:error, reason} ->
{:error, %TransportError{reason: reason}}
end
Expand Down
8 changes: 6 additions & 2 deletions lib/mint/tunnel_proxy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ defmodule Mint.TunnelProxy do

# Note that we may leak messages if the server sent data after the CONNECT response
case Negotiate.upgrade(proxy_scheme, socket, scheme, hostname, port, opts) do
{:ok, conn} -> {:ok, HTTP.put_proxy_headers(conn, proxy_headers)}
{:error, reason} -> {:error, wrap_in_proxy_error(reason)}
{:ok, conn} ->
{:ok, HTTP.put_proxy_headers(conn, proxy_headers)}

{:error, reason} ->
_ = Mint.Core.Util.scheme_to_transport(proxy_scheme).close(socket)
{:error, wrap_in_proxy_error(reason)}
end
end

Expand Down
16 changes: 16 additions & 0 deletions test/docker/squid-https/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM ubuntu:24.04

RUN apt-get update && \
apt-get install -y --no-install-recommends squid-openssl openssl && \
rm -rf /var/lib/apt/lists/*

# Self-signed certificate for the TLS listener. Tests connect to the proxy
# with verify: :verify_none.
RUN openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout /etc/squid/key.pem -out /etc/squid/cert.pem -subj "/CN=localhost"

COPY squid.conf /etc/squid/squid.conf

EXPOSE 3129

CMD ["squid", "-N", "-d", "1"]
3 changes: 3 additions & 0 deletions test/docker/squid-https/squid.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
visible_hostname squid-https
https_port 3129 tls-cert=/etc/squid/cert.pem tls-key=/etc/squid/key.pem
http_access allow all
8 changes: 0 additions & 8 deletions test/mint/core/transport/ssl_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,6 @@ defmodule Mint.Core.Transport.SSLTest do
end
end

describe "upgrade/4" do
test "raises an error if the scheme is :https" do
assert_raise RuntimeError, "nested SSL sessions are not supported", fn ->
SSL.upgrade(_fake_socket = nil, :https, ~c"localhost", _port = 0, _timeout = 5000)
end
end
end

defp cn_cert(_context) do
[cert: load_cert(@cn_cert)]
end
Expand Down
Loading
Loading