This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"],
import_deps: [:stream_data],
locals_without_parens: [
assert_round_trip: 1,
assert_recv_frames: 1,
assert_http2_error: 2,
assert_transport_error: 2
]
]

BIN
phoenix/deps/mint/.hex Normal file

Binary file not shown.

View File

@@ -0,0 +1,226 @@
# Changelog
## v1.7.1
### Bug Fixes and Improvements
* Fix a bug introduced in the previous version that broke `Mint.HTTP.open?/1`.
## v1.7.0
### Bug Fixes and Improvements
* Fix a bug with double-wrapping tunnel proxy errors. This only affected HTTP/1 connections using proxies when upgrade errors would happen—see [#438](https://github.com/elixir-mint/mint/issues/438).
* Introduce `:skip_target_validation` option for HTTP/1.1 connections.
* Add generic `:custom_error` to HTTP/2 frames. This can be returned by HTTP/2 servers in compliance with the HTTP/2 spec. Before, Mint would error out in such cases, while now it just returns the unaltered custom error code.
* Fix compilation warning for the next Elixir release (1.19).
## v1.6.2
### Bug Fixes and Improvements
* Allow for version `~> 1.0` of the `hpax` dependency.
## v1.6.1
### Bug Fixes and Improvements
* Default to using Erlang certificate store (see [`public_key:cacerts_get/0`](https://www.erlang.org/doc/apps/public_key/public_key.html#cacerts_get-0) and friends) if available, instead of [CAStore](https://github.com/elixir-mint/castore).
* Don't send `RST_STREAM` frames in HTTP/2 if they are not needed (this is a network optimization, not visible to users of Mint).
## v1.6.0
### New features
* Add `:case_sensitive_headers` option to `Mint.HTTP1.connect/4`.
* Add `:inet4` option to `Mint.HTTP.connect/4`.
### Bug Fixes and Improvements
* Require Elixir 1.11+.
* Add `match_fun` clause to deal with IP addresses in TLS handshake.
* Optimize creation of HTTP/2 requests.
* Fix a compilation warning (unused `set_flag/2` function).
* Improve performance of downcasing headers.
* Deprecate `:read_write` option in `Mint.HTTP.open?/2`.
* Improve performance of checking for the CAStore library.
## v1.5.2
### Bug Fixes and Improvements
* Fix a memory leak with `Mint.HTTP1` connections which would stay open but
report as closed on timeouts.
## v1.5.1
### Bug Fixes and Improvements
* Fix a `FunctionClauseError` that would happen when calling
`Mint.HTTP2.close/1` on an HTTP/2 connection that hadn't completed the
handshake yet. This bug was introduced in v1.5.0. See [issue
#392](https://github.com/elixir-mint/mint/issues/392).
## v1.5.0
### Bug Fixes and Improvements
* Properly close sockets on erroneous connections.
* Fix `Mint.HTTP.is_connection_message/2` to support proxy connections.
* Add support for CAStore v1.0.0+.
* Support all HTTP/2 settings for clients as well (see
`Mint.HTTP2.put_settings/2`).
* Handle the first `SETTINGS` frame sent by the server *asynchronously* in
HTTP/2. This means lower latency between connecting to a server and being
able to send data to it.
* Add more logging and make logging configurable through the `:log` option
(see `Mint.HTTP.connect/4`, `Mint.HTTP1.connect/4`, `Mint.HTTP2.connect/4`).
## v1.4.2
### Bug Fixes and Improvements
* Properly handle interim responses (informational `1xx` status codes) in
HTTP/2. Now you might get zero or more sequences of `:status` and `:headers`
responses with status `1xx` before the *final response* (with status
non-`1xx`).
## v1.4.1
### Bug Fixes and Improvements
* Emit the remaining buffer as a `:data` response when switching protocols
from HTTP/1.
* Respect closed-for-writing when streaming data frames in HTTP/2.
* Fix handling of HTTP/2 frames of an unknown type.
## v1.4.0
### Bug Fixes and Improvements
* Add support for `SETTINGS_ENABLE_CONNECT_PROTOCOL` HTTP/2 server setting.
* Omit the `:scheme` and `:path` pseudo headers for HTTP/2 CONNECT.
* Fix invalid connection state when data can't be sent.
* Skip expired certs in partial chain hook.
* Add `Mint.HTTP.get_proxy_headers/1`.
* Add `Mint.HTTP.module/1`.
## v1.3.0
### Bug Fixes and Improvements
* Improve compatibility with OTP 24.
* Support HTTP/1 pipelining when streaming requests.
* Add `Mint.HTTP.get_socket/1` for returning the connection socket.
* Improve compatibility with TLS 1.3.
## v1.2.1
### Bug Fixes and Improvements
* Fix a bug where we were not ignoring the return value of `:ssl.close/1` and `:gen_tcp.close/1`.
* Fix a bug where we were not properly handling transport errors when doing ALPN protocol negotiation.
* Fix a bug where we were not handling connection closed errors in a few places.
## v1.2.0
### Bug Fixes and Improvements
* Fix a few bugs with passing the Mint connection around.
* Add IPv6 support with `inet6: true` in the transport options.
* Cache the `:cacertfile` option for faster certificate lookup and decoding.
* Add TLS 1.3 to default versions.
## v1.1.0
### Bug Fixes and Improvements
* Concatenate values in one `cookie` header if the `cookie` header is provided more than once in HTTP/2.
* Fix headers merging in `Mint.UnsafeProxy`.
* Remove some `Logger.debug/1` calls from the codebase.
* Assume the HTTP/2 protocol on TCP connections if using `Mint.HTTP2`.
* Fix a bug where we would send `WINDOW_UPDATE` frames with an increment of `0` in HTTP/2.
* Make the empty body chunk a no-op for `Mint.HTTP.stream_request_body/3` (only for HTTP/1).
* Add the `Mint.HTTP.is_connection_message/2` guard.
* Fix wildcard certificate verification in OTP 23.
## v1.0.0
### Breaking changes
* Remove the deprecated `Mint.HTTP.request/4`, `Mint.HTTP1.request/4`, and `Mint.HTTP2.request/4`.
## v0.5.0
### Bug Fixes and Improvements
* Deprecate `Mint.HTTP.request/4` in favor of explicitly passing the body every time in `Mint.HTTP.request/5`. Same for `Mint.HTTP1` and `Mint.HTTP2`.
* Don't include port in the `authority` header if it's the default port for the used protocol.
* Add a default `content-length` header in HTTP/2
* Allow passing headers to proxies with the `:proxy_headers` option.
* Fix a bug with HTTP/1 chunking.
## v0.4.0
### Bug Fixes and Improvements
* Fix a small bug with double "wrapping" of some `Mint.TransportError`s.
* Prevent unnecessary buffer allocations in the connections (less memory waste!).
* Add support for chunked transfer-encoding in HTTP/1 requests when you don't use `content-encoding`/`transfer-encoding` yourself.
* Add support for trailer headers in HTTP/* requests through `stream_request_body/3`.
* Add a page about decompressing responses in the guides.
## v0.3.0
### Breaking changes
* Remove `Mint.HTTP1.get_socket/1`, `Mint.HTTP2.get_socket/1`, and `Mint.HTTP.get_socket/1`.
### Bug Fixes and Improvements
* Downcase all headers in HTTP/2 to mimic the behavior in HTTP/1.1.
* Add `Mint.HTTP.set_mode/2`, `Mint.HTTP1.set_mode/2`, and `Mint.HTTP2.set_mode/2` to change the mode of a socket between active and passive.
* Add a `:mode` option to the `connect/4` functions to start the socket in active or passive mode.
* Add `Mint.HTTP.recv/3`, `Mint.HTTP1.recv/3`, and `Mint.HTTP2.recv/3` to receive data from a passive socket in a blocking way.
* Add `Mint.HTTP.controlling_process/2`, `Mint.HTTP1.controlling_process/2`, and `Mint.HTTP2.controlling_process/2` to change the controlling process of a connection.
* Support trailer response headers in HTTP/2 connections.
## v0.2.1
### Bug Fixes and Improvements
* Fix a bug with requests exceeding the window size in HTTP/2. We were sending the headers of a request even if the body was larger than the window size. Now, if the body is larger than the window size, we error out right away.
* Fix a bug in the HTTP/2 handshake that would crash the connection in case the server sent unforeseen frames.
* Improve buffering of body chunks in HTTP/1.
## v0.2.0
### Breaking changes
* Add the `Mint.TransportError` and `Mint.HTTPError` exceptions. Change all the connection functions so that they return these error structs instead of generic terms.
* Remove `Mint.HTTP2.get_setting/2` in favour of `Mint.HTTP2.get_server_setting/2` and `Mint.HTTP2.get_client_setting/2`.
### Bug fixes and enhancements
* Add support for HTTP/2 server push with the new `:push_promise` response.
* Add `Mint.HTTP2.cancel_request/5`.
* Add `Mint.HTTP2.get_window_size/2`.
* Add `open_request_count/1` function to `Mint.HTTP`, and `Mint.HTTP1`, `Mint.HTTP2`.
* Add `open?/2` function to `Mint.HTTP`, and `Mint.HTTP1`, `Mint.HTTP2`.
* Make the `Mint.HTTP2.HPACK` module private.
* Take into account the max header list size advertised by the server in HTTP/2 connections.
* Improve error handling in a bunch of `Mint.HTTP2` functions.
* Fix flow control on `WINDOW_UPDATE` frames at the connection level in `Mint.HTTP2`.
* Correctly return timeout errors when connecting.
* Treat HTTP/1 header keys as case-insensitive.
* Prohibit users from streaming on unknown requests in HTTP/2.
* Prohibit the server from violating the client's max concurrent streams setting in HTTP/2.
* Strip whitespace when parsing the `content-length` header in HTTP/1.
* Fix path validation when building HTTP/1 requests, fixes paths with `%NN` escapes.

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

158
phoenix/deps/mint/README.md Normal file
View File

@@ -0,0 +1,158 @@
# Mint 🌱
[![CI badge](https://github.com/elixir-mint/mint/actions/workflows/main.yml/badge.svg)](https://github.com/elixir-mint/mint/actions/workflows/main.yml)
[![Documentation badge](https://img.shields.io/badge/Documentation-ff69b4)][documentation]
[![Hex.pm badge](https://img.shields.io/badge/Package%20on%20hex.pm-informational)](https://hex.pm/packages/mint)
[![Coverage status badge](https://coveralls.io/repos/github/elixir-mint/mint/badge.svg?branch=main)](https://coveralls.io/github/elixir-mint/mint?branch=main)
> Functional, low-level HTTP client for Elixir with support for HTTP/1 and HTTP/2.
## Installation
To install Mint, add it to your `mix.exs` file:
```elixir
defp deps do
[
{:mint, "~> 1.0"}
]
end
```
Then, run:
```sh
mix deps.get
```
## Usage
Mint is different from most Erlang and Elixir HTTP clients because it provides a *process-less architecture*. Instead, Mint is based on a functional and immutable data structure that represents an HTTP connection. This data structure wraps a TCP or SSL socket. This allows for more fine-tailored architectures where the developer is responsible for wrapping the connection struct, such as having one process handle multiple connections or having different kinds of processes handle connections. You can think of Mint as [`:gen_tcp`](https://erlang.org/doc/man/gen_tcp.html) and [`:ssl`](https://www.erlang.org/doc/man/ssl.html), but with an understanding of the HTTP/1.1 and HTTP/2 protocols.
Let's see an example of a basic interaction with Mint. First, we start a connection through `Mint.HTTP.connect/3`:
```elixir
iex> {:ok, conn} = Mint.HTTP.connect(:http, "httpbin.org", 80)
```
This transparently chooses between HTTP/1 and HTTP/2. Then, we can send requests with:
```elixir
iex> {:ok, conn, request_ref} = Mint.HTTP.request(conn, "GET", "/", [], "")
```
The connection socket runs in [*active mode*](http://erlang.org/doc/man/inet.html#setopts-2) (with `active: :once`), which means that the user of the library needs to handle [TCP messages](http://erlang.org/doc/man/gen_tcp.html#connect-4) and [SSL messages](http://erlang.org/doc/man/ssl.html#id66002):
```elixir
iex> flush()
{:tcp, #Port<0.8>,
"HTTP/1.1 200 OK\r\n" <> _}
```
Users are not supposed to examine these messages. Instead, Mint provides a `stream/2` function that turns messages into HTTP responses. Mint streams responses back to the user in parts through response parts such as `:status`, `:headers`, `:data`, and `:done`.
```elixir
iex> {:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443)
iex> {:ok, conn, request_ref} = Mint.HTTP.request(conn, "GET", "/", [], "")
iex> receive do
...> message ->
...> {:ok, conn, responses} = Mint.HTTP.stream(conn, message)
...> IO.inspect(responses)
...> end
[
{:status, #Reference<...>, 200},
{:headers, #Reference<...>, [{"connection", "keep-alive"}, ...},
{:data, #Reference<...>, "<!DOCTYPE html>..."},
{:done, #Reference<...>}
]
```
In the example above, we get all the responses as a single SSL message, but that might not always be the case. This means that `Mint.HTTP.stream/2` might not always return responses.
The connection API is *stateless*, which means that you need to make sure to always save the connection that functions return:
```elixir
# Wrong ❌
{:ok, _conn, ref} = Mint.HTTP.request(conn, "GET", "/foo", [], "")
{:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/bar", [], "")
# Correct ✅
{:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/foo", [], "")
{:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/bar", [], "")
```
For more information, see [the documentation][documentation].
### SSL certificates
When using SSL, you can pass in your own CA certificate store. If one is not provided, Mint will use the one in your system, as long as you are using Erlang/OTP 25+. If none of these conditions are true, just add `:castore` to your dependencies.
```elixir
defp deps do
[
# ...,
{:castore, "~> 1.0.0"},
{:mint, "~> 0.4.0"}
]
end
```
### WebSocket Support
Mint itself does not support the WebSocket protocol, but it can be used as the foundation to build a WebSocket client on top of. If you need WebSocket support, you can use [mint_web_socket].
### Connection Management and Pooling
Mint is a low-level client. If you need higher-level features such as connection management, pooling, metrics, and more, check out [Finch], a project built on top of Mint that provides those things.
## Contributing
If you wish to contribute, check out the [issue list][issues] and let us know what you want to work on, so that we can discuss it and reduce duplicate work.
Tests are organized with tags. Integration tests that hit real websites over the internet are tagged with `:requires_internet_connection`. Proxy tests are tagged with `:proxy` (excluded by default) and require that you start local proxy instance through Docker Compose (or Podman Compose) from the Mint root directory in order to run.
If you encounter `{:error, %Mint.TransportError{reason: :nxdomain}}` error during the integration test, this suggests your DNS-based adblocker (self-hosted or VPN) might be blocking social media sites used in the integration test cases.
Here are a few examples of running tests:
Run all default tests which including `:requires_internet_connection` except `:proxy`:
```sh
mix test
```
Local tests, if you don't have an Internet connection available:
```sh
mix test --exclude requires_internet_connection
```
Run all tests:
```sh
DOCKER_USER="$UID:$GID" docker compose up --detach # or podman-compose up --detach
mix test --include proxy
```
## License
Copyright 2018 Eric Meadows-Jönsson and Andrea Leopardi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
[castore]: https://github.com/elixir-mint/castore
[documentation]: https://hexdocs.pm/mint
[issues]: https://github.com/elixir-mint/mint/issues
[mint_web_socket]: https://github.com/elixir-mint/mint_web_socket
[Finch]: https://github.com/sneako/finch

View File

@@ -0,0 +1,35 @@
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-mint/mint">>}]}.
{<<"name">>,<<"mint">>}.
{<<"version">>,<<"1.7.1">>}.
{<<"description">>,<<"Small and composable HTTP client.">>}.
{<<"elixir">>,<<"~> 1.12">>}.
{<<"app">>,<<"mint">>}.
{<<"licenses">>,[<<"Apache-2.0">>]}.
{<<"requirements">>,
[[{<<"name">>,<<"castore">>},
{<<"app">>,<<"castore">>},
{<<"optional">>,true},
{<<"requirement">>,<<"~> 0.1.0 or ~> 1.0">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"hpax">>},
{<<"app">>,<<"hpax">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 0.1.1 or ~> 0.2.0 or ~> 1.0">>},
{<<"repository">>,<<"hexpm">>}]]}.
{<<"files">>,
[<<"lib">>,<<"lib/mint">>,<<"lib/mint/http1">>,
<<"lib/mint/http1/response.ex">>,<<"lib/mint/http1/request.ex">>,
<<"lib/mint/http1/parse.ex">>,<<"lib/mint/http2.ex">>,
<<"lib/mint/tunnel_proxy.ex">>,<<"lib/mint/core">>,
<<"lib/mint/core/util.ex">>,<<"lib/mint/core/transport">>,
<<"lib/mint/core/transport/ssl.ex">>,<<"lib/mint/core/transport/tcp.ex">>,
<<"lib/mint/core/transport.ex">>,<<"lib/mint/core/headers.ex">>,
<<"lib/mint/core/conn.ex">>,<<"lib/mint/types.ex">>,
<<"lib/mint/http_error.ex">>,<<"lib/mint/http2">>,
<<"lib/mint/http2/frame.ex">>,<<"lib/mint/negotiate.ex">>,
<<"lib/mint/http1.ex">>,<<"lib/mint/application.ex">>,
<<"lib/mint/http.ex">>,<<"lib/mint/unsafe_proxy.ex">>,
<<"lib/mint/transport_error.ex">>,<<".formatter.exs">>,<<"mix.exs">>,
<<"README.md">>,<<"LICENSE.txt">>,<<"CHANGELOG.md">>,<<"src">>,
<<"src/mint_shims.erl">>]}.
{<<"build_tools">>,[<<"mix">>]}.

View File

@@ -0,0 +1,14 @@
defmodule Mint.Application do
@moduledoc false
use Application
def start(_type, _args) do
persistent_term =
Code.ensure_loaded?(:persistent_term) and function_exported?(:persistent_term, :get, 2)
Application.put_env(:mint, :persistent_term, persistent_term)
opts = [strategy: :one_for_one, name: Mint.Supervisor]
Supervisor.start_link([], opts)
end
end

View File

@@ -0,0 +1,65 @@
defmodule Mint.Core.Conn do
@moduledoc false
alias Mint.Types
@type conn() :: term()
@callback initiate(
module(),
Mint.Types.socket(),
String.t(),
:inet.port_number(),
keyword()
) :: {:ok, conn()} | {:error, Types.error()}
@callback open?(conn(), :read | :write) :: boolean()
@callback close(conn()) :: {:ok, conn()}
@callback request(
conn(),
method :: String.t(),
path :: String.t(),
Types.headers(),
body :: iodata() | nil | :stream
) ::
{:ok, conn(), Types.request_ref()}
| {:error, conn(), Types.error()}
@callback stream_request_body(
conn(),
Types.request_ref(),
body_chunk :: iodata() | :eof | {:eof, trailer_headers :: Types.headers()}
) ::
{:ok, conn()} | {:error, conn(), Types.error()}
@callback stream(conn(), term()) ::
{:ok, conn(), [Types.response()]}
| {:error, conn(), Types.error(), [Types.response()]}
| :unknown
@callback open_request_count(conn()) :: non_neg_integer()
@callback recv(conn(), byte_count :: non_neg_integer(), timeout()) ::
{:ok, conn(), [Types.response()]}
| {:error, conn(), Types.error(), [Types.response()]}
@callback set_mode(conn(), :active | :passive) :: {:ok, conn()} | {:error, Types.error()}
@callback controlling_process(conn(), pid()) :: {:ok, conn()} | {:error, Types.error()}
@callback put_private(conn(), key :: atom(), value :: term()) :: conn()
@callback get_private(conn(), key :: atom(), default_value :: term()) :: term()
@callback delete_private(conn(), key :: atom()) :: conn()
@callback get_socket(conn()) :: Mint.Types.socket()
@callback get_proxy_headers(conn()) :: Mint.Types.headers()
@callback put_proxy_headers(conn(), Mint.Types.headers()) :: conn()
@callback put_log(conn(), boolean()) :: conn()
end

View File

@@ -0,0 +1,136 @@
defmodule Mint.Core.Headers do
@moduledoc false
@type canonical() ::
{original_name :: String.t(), canonical_name :: String.t(), value :: String.t()}
@type raw() :: {original_name :: String.t(), value :: String.t()}
@unallowed_trailers MapSet.new([
"content-encoding",
"content-length",
"content-range",
"content-type",
"trailer",
"transfer-encoding",
# Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1)
"cache-control",
"expect",
"host",
"max-forwards",
"pragma",
"range",
"te",
# Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2)
"if-match",
"if-none-match",
"if-modified-since",
"if-unmodified-since",
"if-range",
# Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3)
"authorization",
"proxy-authenticate",
"proxy-authorization",
"www-authenticate",
# Cookie management (https://tools.ietf.org/html/rfc6265)
"cookie",
"set-cookie",
# Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1)
"age",
"cache-control",
"expires",
"date",
"location",
"retry-after",
"vary",
"warning"
])
@spec from_raw([raw()]) :: [canonical()]
def from_raw(headers) do
Enum.map(headers, fn {name, value} -> {name, lower_raw(name), value} end)
end
@spec to_raw([canonical()], boolean()) :: [raw()]
def to_raw(headers, _case_sensitive = true) do
Enum.map(headers, fn {name, _canonical_name, value} -> {name, value} end)
end
def to_raw(headers, _case_sensitive = false) do
Enum.map(headers, fn {_name, canonical_name, value} ->
{canonical_name, value}
end)
end
@spec find([canonical()], String.t()) :: {String.t(), String.t()} | nil
def find(headers, name) do
case List.keyfind(headers, name, 1) do
nil -> nil
{name, _canonical_name, value} -> {name, value}
end
end
@spec replace([canonical()], String.t(), String.t(), String.t()) ::
[canonical()]
def replace(headers, new_name, canonical_name, value) do
List.keyreplace(headers, canonical_name, 1, {new_name, canonical_name, value})
end
@spec has?([canonical()], String.t()) :: boolean()
def has?(headers, name) do
List.keymember?(headers, name, 1)
end
@spec put_new([canonical()], String.t(), String.t(), String.t() | nil) ::
[canonical()]
def put_new(headers, _name, _canonical_name, nil) do
headers
end
def put_new(headers, name, canonical_name, value) do
if List.keymember?(headers, canonical_name, 1) do
headers
else
[{name, canonical_name, value} | headers]
end
end
@spec put_new([canonical()], String.t(), String.t(), (-> String.t())) ::
[canonical()]
def put_new_lazy(headers, name, canonical_name, fun) do
if List.keymember?(headers, canonical_name, 1) do
headers
else
[{name, canonical_name, fun.()} | headers]
end
end
@spec find_unallowed_trailer([canonical()]) :: String.t() | nil
def find_unallowed_trailer(headers) do
Enum.find_value(headers, fn
{raw_name, canonical_name, _value} ->
if canonical_name in @unallowed_trailers do
raw_name
end
end)
end
@spec remove_unallowed_trailer([raw()]) :: [raw()]
def remove_unallowed_trailer(headers) do
Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailers end)
end
@spec lower_raw(String.t()) :: String.t()
def lower_raw(name) do
String.downcase(name, :ascii)
end
@spec lower_raws([raw()]) :: [raw()]
def lower_raws(headers) do
Enum.map(headers, fn {name, value} -> {lower_raw(name), value} end)
end
end

View File

@@ -0,0 +1,36 @@
defmodule Mint.Core.Transport do
@moduledoc false
@type error() :: {:error, %Mint.TransportError{}}
alias Mint.Types
@callback connect(address :: Types.address(), port :: :inet.port_number(), opts :: keyword()) ::
{:ok, Types.socket()} | error()
@callback upgrade(
Types.socket(),
original_scheme :: Types.scheme(),
hostname :: String.t(),
:inet.port_number(),
opts :: keyword()
) :: {:ok, Types.socket()} | error()
@callback negotiated_protocol(Types.socket()) ::
{:ok, protocol :: binary()} | {:error, :protocol_not_negotiated}
@callback send(Types.socket(), payload :: iodata()) :: :ok | error()
@callback close(Types.socket()) :: :ok | error()
@callback recv(Types.socket(), bytes :: non_neg_integer(), timeout()) ::
{:ok, binary()} | error()
@callback controlling_process(Types.socket(), pid()) :: :ok | error()
@callback setopts(Types.socket(), opts :: keyword()) :: :ok | error()
@callback getopts(Types.socket(), opts :: keyword()) :: {:ok, opts :: keyword()} | error()
@callback wrap_error(reason :: term()) :: %Mint.TransportError{}
end

View File

@@ -0,0 +1,755 @@
defmodule Mint.Core.Transport.SSL do
@moduledoc false
require Logger
require Record
@behaviour Mint.Core.Transport
# From RFC7540 appendix A
@blocked_ciphers MapSet.new([
{:null, :null, :null},
{:rsa, :null, :md5},
{:rsa, :null, :sha},
{:rsa_export, :rc4_40, :md5},
{:rsa, :rc4_128, :md5},
{:rsa, :rc4_128, :sha},
{:rsa_export, :rc2_cbc_40, :md5},
{:rsa, :idea_cbc, :sha},
{:rsa_export, :des40_cbc, :sha},
{:rsa, :des_cbc, :sha},
{:rsa, :"3des_ede_cbc", :sha},
{:dh_dss_export, :des40_cbc, :sha},
{:dh_dss, :des_cbc, :sha},
{:dh_dss, :"3des_ede_cbc", :sha},
{:dh_rsa_export, :des40_cbc, :sha},
{:dh_rsa, :des_cbc, :sha},
{:dh_rsa, :"3des_ede_cbc", :sha},
{:dhe_dss_export, :des40_cbc, :sha},
{:dhe_dss, :des_cbc, :sha},
{:dhe_dss, :"3des_ede_cbc", :sha},
{:dhe_rsa_export, :des40_cbc, :sha},
{:dhe_rsa, :des_cbc, :sha},
{:dhe_rsa, :"3des_ede_cbc", :sha},
{:dh_anon_export, :rc4_40, :md5},
{:dh_anon, :rc4_128, :md5},
{:dh_anon_export, :des40_cbc, :sha},
{:dh_anon, :des_cbc, :sha},
{:dh_anon, :"3des_ede_cbc", :sha},
{:krb5, :des_cbc, :sha},
{:krb5, :"3des_ede_cbc", :sha},
{:krb5, :rc4_128, :sha},
{:krb5, :idea_cbc, :sha},
{:krb5, :des_cbc, :md5},
{:krb5, :"3des_ede_cbc", :md5},
{:krb5, :rc4_128, :md5},
{:krb5, :idea_cbc, :md5},
{:krb5_export, :des_cbc_40, :sha},
{:krb5_export, :rc2_cbc_40, :sha},
{:krb5_export, :rc4_40, :sha},
{:krb5_export, :des_cbc_40, :md5},
{:krb5_export, :rc2_cbc_40, :md5},
{:krb5_export, :rc4_40, :md5},
{:psk, :null, :sha},
{:dhe_psk, :null, :sha},
{:rsa_psk, :null, :sha},
{:rsa, :aes_128_cbc, :sha},
{:dh_dss, :aes_128_cbc, :sha},
{:dh_rsa, :aes_128_cbc, :sha},
{:dhe_dss, :aes_128_cbc, :sha},
{:dhe_rsa, :aes_128_cbc, :sha},
{:dh_anon, :aes_128_cbc, :sha},
{:rsa, :aes_256_cbc, :sha},
{:dh_dss, :aes_256_cbc, :sha},
{:dh_rsa, :aes_256_cbc, :sha},
{:dhe_dss, :aes_256_cbc, :sha},
{:dhe_rsa, :aes_256_cbc, :sha},
{:dh_anon, :aes_256_cbc, :sha},
{:rsa, :null, :sha256},
{:rsa, :aes_128_cbc, :sha256},
{:rsa, :aes_256_cbc, :sha256},
{:dh_dss, :aes_128_cbc, :sha256},
{:dh_rsa, :aes_128_cbc, :sha256},
{:dhe_dss, :aes_128_cbc, :sha256},
{:rsa, :camellia_128_cbc, :sha},
{:dh_dss, :camellia_128_cbc, :sha},
{:dh_rsa, :camellia_128_cbc, :sha},
{:dhe_dss, :camellia_128_cbc, :sha},
{:dhe_rsa, :camellia_128_cbc, :sha},
{:dh_anon, :camellia_128_cbc, :sha},
{:dhe_rsa, :aes_128_cbc, :sha256},
{:dh_dss, :aes_256_cbc, :sha256},
{:dh_rsa, :aes_256_cbc, :sha256},
{:dhe_dss, :aes_256_cbc, :sha256},
{:dhe_rsa, :aes_256_cbc, :sha256},
{:dh_anon, :aes_128_cbc, :sha256},
{:dh_anon, :aes_256_cbc, :sha256},
{:rsa, :camellia_256_cbc, :sha},
{:dh_dss, :camellia_256_cbc, :sha},
{:dh_rsa, :camellia_256_cbc, :sha},
{:dhe_dss, :camellia_256_cbc, :sha},
{:dhe_rsa, :camellia_256_cbc, :sha},
{:dh_anon, :camellia_256_cbc, :sha},
{:psk, :rc4_128, :sha},
{:psk, :"3des_ede_cbc", :sha},
{:psk, :aes_128_cbc, :sha},
{:psk, :aes_256_cbc, :sha},
{:dhe_psk, :rc4_128, :sha},
{:dhe_psk, :"3des_ede_cbc", :sha},
{:dhe_psk, :aes_128_cbc, :sha},
{:dhe_psk, :aes_256_cbc, :sha},
{:rsa_psk, :rc4_128, :sha},
{:rsa_psk, :"3des_ede_cbc", :sha},
{:rsa_psk, :aes_128_cbc, :sha},
{:rsa_psk, :aes_256_cbc, :sha},
{:rsa, :seed_cbc, :sha},
{:dh_dss, :seed_cbc, :sha},
{:dh_rsa, :seed_cbc, :sha},
{:dhe_dss, :seed_cbc, :sha},
{:dhe_rsa, :seed_cbc, :sha},
{:dh_anon, :seed_cbc, :sha},
{:rsa, :aes_128_gcm, :sha256},
{:rsa, :aes_256_gcm, :sha384},
{:dh_rsa, :aes_128_gcm, :sha256},
{:dh_rsa, :aes_256_gcm, :sha384},
{:dh_dss, :aes_128_gcm, :sha256},
{:dh_dss, :aes_256_gcm, :sha384},
{:dh_anon, :aes_128_gcm, :sha256},
{:dh_anon, :aes_256_gcm, :sha384},
{:psk, :aes_128_gcm, :sha256},
{:psk, :aes_256_gcm, :sha384},
{:rsa_psk, :aes_128_gcm, :sha256},
{:rsa_psk, :aes_256_gcm, :sha384},
{:psk, :aes_128_cbc, :sha256},
{:psk, :aes_256_cbc, :sha384},
{:psk, :null, :sha256},
{:psk, :null, :sha384},
{:dhe_psk, :aes_128_cbc, :sha256},
{:dhe_psk, :aes_256_cbc, :sha384},
{:dhe_psk, :null, :sha256},
{:dhe_psk, :null, :sha384},
{:rsa_psk, :aes_128_cbc, :sha256},
{:rsa_psk, :aes_256_cbc, :sha384},
{:rsa_psk, :null, :sha256},
{:rsa_psk, :null, :sha384},
{:rsa, :camellia_128_cbc, :sha256},
{:dh_dss, :camellia_128_cbc, :sha256},
{:dh_rsa, :camellia_128_cbc, :sha256},
{:dhe_dss, :camellia_128_cbc, :sha256},
{:dhe_rsa, :camellia_128_cbc, :sha256},
{:dh_anon, :camellia_128_cbc, :sha256},
{:rsa, :camellia_256_cbc, :sha256},
{:dh_dss, :camellia_256_cbc, :sha256},
{:dh_rsa, :camellia_256_cbc, :sha256},
{:dhe_dss, :camellia_256_cbc, :sha256},
{:dhe_rsa, :camellia_256_cbc, :sha256},
{:dh_anon, :camellia_256_cbc, :sha256},
{:ecdh_ecdsa, :null, :sha},
{:ecdh_ecdsa, :rc4_128, :sha},
{:ecdh_ecdsa, :"3des_ede_cbc", :sha},
{:ecdh_ecdsa, :aes_128_cbc, :sha},
{:ecdh_ecdsa, :aes_256_cbc, :sha},
{:ecdhe_ecdsa, :null, :sha},
{:ecdhe_ecdsa, :rc4_128, :sha},
{:ecdhe_ecdsa, :"3des_ede_cbc", :sha},
{:ecdhe_ecdsa, :aes_128_cbc, :sha},
{:ecdhe_ecdsa, :aes_256_cbc, :sha},
{:ecdh_rsa, :null, :sha},
{:ecdh_rsa, :rc4_128, :sha},
{:ecdh_rsa, :"3des_ede_cbc", :sha},
{:ecdh_rsa, :aes_128_cbc, :sha},
{:ecdh_rsa, :aes_256_cbc, :sha},
{:ecdhe_rsa, :null, :sha},
{:ecdhe_rsa, :rc4_128, :sha},
{:ecdhe_rsa, :"3des_ede_cbc", :sha},
{:ecdhe_rsa, :aes_128_cbc, :sha},
{:ecdhe_rsa, :aes_256_cbc, :sha},
{:ecdh_anon, :null, :sha},
{:ecdh_anon, :rc4_128, :sha},
{:ecdh_anon, :"3des_ede_cbc", :sha},
{:ecdh_anon, :aes_128_cbc, :sha},
{:ecdh_anon, :aes_256_cbc, :sha},
{:srp_sha, :"3des_ede_cbc", :sha},
{:srp_sha_rsa, :"3des_ede_cbc", :sha},
{:srp_sha_dss, :"3des_ede_cbc", :sha},
{:srp_sha, :aes_128_cbc, :sha},
{:srp_sha_rsa, :aes_128_cbc, :sha},
{:srp_sha_dss, :aes_128_cbc, :sha},
{:srp_sha, :aes_256_cbc, :sha},
{:srp_sha_rsa, :aes_256_cbc, :sha},
{:srp_sha_dss, :aes_256_cbc, :sha},
{:ecdhe_ecdsa, :aes_128_cbc, :sha256},
{:ecdhe_ecdsa, :aes_256_cbc, :sha384},
{:ecdh_ecdsa, :aes_128_cbc, :sha256},
{:ecdh_ecdsa, :aes_256_cbc, :sha384},
{:ecdhe_rsa, :aes_128_cbc, :sha256},
{:ecdhe_rsa, :aes_256_cbc, :sha384},
{:ecdh_rsa, :aes_128_cbc, :sha256},
{:ecdh_rsa, :aes_256_cbc, :sha384},
{:ecdh_ecdsa, :aes_128_gcm, :sha256},
{:ecdh_ecdsa, :aes_256_gcm, :sha384},
{:ecdh_rsa, :aes_128_gcm, :sha256},
{:ecdh_rsa, :aes_256_gcm, :sha384},
{:ecdhe_psk, :rc4_128, :sha},
{:ecdhe_psk, :"3des_ede_cbc", :sha},
{:ecdhe_psk, :aes_128_cbc, :sha},
{:ecdhe_psk, :aes_256_cbc, :sha},
{:ecdhe_psk, :aes_128_cbc, :sha256},
{:ecdhe_psk, :aes_256_cbc, :sha384},
{:ecdhe_psk, :null, :sha},
{:ecdhe_psk, :null, :sha256},
{:ecdhe_psk, :null, :sha384},
{:rsa, :aria_128_cbc, :sha256},
{:rsa, :aria_256_cbc, :sha384},
{:dh_dss, :aria_128_cbc, :sha256},
{:dh_dss, :aria_256_cbc, :sha384},
{:dh_rsa, :aria_128_cbc, :sha256},
{:dh_rsa, :aria_256_cbc, :sha384},
{:dhe_dss, :aria_128_cbc, :sha256},
{:dhe_dss, :aria_256_cbc, :sha384},
{:dhe_rsa, :aria_128_cbc, :sha256},
{:dhe_rsa, :aria_256_cbc, :sha384},
{:dh_anon, :aria_128_cbc, :sha256},
{:dh_anon, :aria_256_cbc, :sha384},
{:ecdhe_ecdsa, :aria_128_cbc, :sha256},
{:ecdhe_ecdsa, :aria_256_cbc, :sha384},
{:ecdh_ecdsa, :aria_128_cbc, :sha256},
{:ecdh_ecdsa, :aria_256_cbc, :sha384},
{:ecdhe_rsa, :aria_128_cbc, :sha256},
{:ecdhe_rsa, :aria_256_cbc, :sha384},
{:ecdh_rsa, :aria_128_cbc, :sha256},
{:ecdh_rsa, :aria_256_cbc, :sha384},
{:rsa, :aria_128_gcm, :sha256},
{:rsa, :aria_256_gcm, :sha384},
{:dh_rsa, :aria_128_gcm, :sha256},
{:dh_rsa, :aria_256_gcm, :sha384},
{:dh_dss, :aria_128_gcm, :sha256},
{:dh_dss, :aria_256_gcm, :sha384},
{:dh_anon, :aria_128_gcm, :sha256},
{:dh_anon, :aria_256_gcm, :sha384},
{:ecdh_ecdsa, :aria_128_gcm, :sha256},
{:ecdh_ecdsa, :aria_256_gcm, :sha384},
{:ecdh_rsa, :aria_128_gcm, :sha256},
{:ecdh_rsa, :aria_256_gcm, :sha384},
{:psk, :aria_128_cbc, :sha256},
{:psk, :aria_256_cbc, :sha384},
{:dhe_psk, :aria_128_cbc, :sha256},
{:dhe_psk, :aria_256_cbc, :sha384},
{:rsa_psk, :aria_128_cbc, :sha256},
{:rsa_psk, :aria_256_cbc, :sha384},
{:psk, :aria_128_gcm, :sha256},
{:psk, :aria_256_gcm, :sha384},
{:rsa_psk, :aria_128_gcm, :sha256},
{:rsa_psk, :aria_256_gcm, :sha384},
{:ecdhe_psk, :aria_128_cbc, :sha256},
{:ecdhe_psk, :aria_256_cbc, :sha384},
{:ecdhe_ecdsa, :camellia_128_cbc, :sha256},
{:ecdhe_ecdsa, :camellia_256_cbc, :sha384},
{:ecdh_ecdsa, :camellia_128_cbc, :sha256},
{:ecdh_ecdsa, :camellia_256_cbc, :sha384},
{:ecdhe_rsa, :camellia_128_cbc, :sha256},
{:ecdhe_rsa, :camellia_256_cbc, :sha384},
{:ecdh_rsa, :camellia_128_cbc, :sha256},
{:ecdh_rsa, :camellia_256_cbc, :sha384},
{:rsa, :camellia_128_gcm, :sha256},
{:rsa, :camellia_256_gcm, :sha384},
{:dh_rsa, :camellia_128_gcm, :sha256},
{:dh_rsa, :camellia_256_gcm, :sha384},
{:dh_dss, :camellia_128_gcm, :sha256},
{:dh_dss, :camellia_256_gcm, :sha384},
{:dh_anon, :camellia_128_gcm, :sha256},
{:dh_anon, :camellia_256_gcm, :sha384},
{:ecdh_ecdsa, :camellia_128_gcm, :sha256},
{:ecdh_ecdsa, :camellia_256_gcm, :sha384},
{:ecdh_rsa, :camellia_128_gcm, :sha256},
{:ecdh_rsa, :camellia_256_gcm, :sha384},
{:psk, :camellia_128_gcm, :sha256},
{:psk, :camellia_256_gcm, :sha384},
{:rsa_psk, :camellia_128_gcm, :sha256},
{:rsa_psk, :camellia_256_gcm, :sha384},
{:psk, :camellia_128_cbc, :sha256},
{:psk, :camellia_256_cbc, :sha384},
{:dhe_psk, :camellia_128_cbc, :sha256},
{:dhe_psk, :camellia_256_cbc, :sha384},
{:rsa_psk, :camellia_128_cbc, :sha256},
{:rsa_psk, :camellia_256_cbc, :sha384},
{:ecdhe_psk, :camellia_128_cbc, :sha256},
{:ecdhe_psk, :camellia_256_cbc, :sha384},
{:rsa, :aes_128, :ccm},
{:rsa, :aes_256, :ccm},
{:rsa, :aes_128, :ccm_8},
{:rsa, :aes_256, :ccm_8},
{:psk, :aes_128, :ccm},
{:psk, :aes_256, :ccm},
{:psk, :aes_128, :ccm_8},
{:psk, :aes_256, :ccm_8}
])
@transport_opts [
packet: :raw,
mode: :binary,
active: false
]
@default_versions [:"tlsv1.3", :"tlsv1.2"]
@default_timeout 30_000
Record.defrecordp(
:certificate,
:Certificate,
Record.extract(:Certificate, from_lib: "public_key/include/OTP-PUB-KEY.hrl")
)
Record.defrecordp(
:tbs_certificate,
:OTPTBSCertificate,
Record.extract(:OTPTBSCertificate, from_lib: "public_key/include/OTP-PUB-KEY.hrl")
)
# TODO: Document how to enable revocation checking:
# crl_check: true
# crl_cache: {:ssl_crl_cache, {:internal, [http: 30_000]}}
@impl true
def connect(address, port, opts) do
hostname = Mint.Core.Util.hostname(opts, address)
opts = Keyword.delete(opts, :hostname)
connect(address, hostname, port, opts)
end
defp connect(address, hostname, port, opts) when is_binary(address),
do: connect(String.to_charlist(address), hostname, port, opts)
defp connect(address, hostname, port, opts) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
inet4? = Keyword.get(opts, :inet4, true)
inet6? = Keyword.get(opts, :inet6, false)
opts = ssl_opts(String.to_charlist(hostname), opts)
if inet6? do
# Try inet6 first, then fall back to the defaults provided by
# ssl/gen_tcp if connection fails.
case :ssl.connect(address, port, [:inet6 | opts], timeout) do
{:ok, sslsocket} ->
{:ok, sslsocket}
_error when inet4? ->
wrap_err(:ssl.connect(address, port, opts, timeout))
error ->
wrap_err(error)
end
else
# Use the defaults provided by ssl/gen_tcp.
wrap_err(:ssl.connect(address, port, opts, timeout))
end
end
@impl true
def upgrade(socket, :http, hostname, _port, opts) do
hostname = String.to_charlist(hostname)
timeout = Keyword.get(opts, :timeout, @default_timeout)
# Seems like this is not set in :ssl.connect/2 correctly, so set it explicitly
Mint.Core.Transport.TCP.setopts(socket, active: false)
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"
end
@impl true
def negotiated_protocol(socket) do
wrap_err(:ssl.negotiated_protocol(socket))
end
@impl true
def send(socket, payload) do
wrap_err(:ssl.send(socket, payload))
end
@impl true
def close(socket) do
wrap_err(:ssl.close(socket))
end
@impl true
def recv(socket, bytes, timeout) do
wrap_err(:ssl.recv(socket, bytes, timeout))
end
@impl true
def controlling_process(socket, pid) do
# We do this dance because it's what gen_tcp does in Erlang. However, ssl
# doesn't do this so we need to do it ourselves. Implementation roughly
# taken from this:
# https://github.com/erlang/otp/blob/fc1f0444e32b039194189af97fb3d5358a2b91e3/lib/kernel/src/inet.erl#L1696-L1754
with {:ok, active: active} <- getopts(socket, [:active]),
:ok <- setopts(socket, active: false),
:ok <- forward_messages_to_new_controlling_process(socket, pid),
:ok <- wrap_err(:ssl.controlling_process(socket, pid)) do
if(active == :once, do: setopts(socket, active: :once), else: :ok)
end
end
defp forward_messages_to_new_controlling_process(socket, pid) do
receive do
{:ssl, ^socket, _data} = message ->
Kernel.send(pid, message)
forward_messages_to_new_controlling_process(socket, pid)
{:ssl_error, ^socket, error} ->
{:error, error}
{:ssl_closed, ^socket} ->
{:error, :closed}
after
0 ->
:ok
end
end
@impl true
def setopts(socket, opts) do
wrap_err(:ssl.setopts(socket, opts))
end
@impl true
def getopts(socket, opts) do
wrap_err(:ssl.getopts(socket, opts))
end
@impl true
def wrap_error(reason) do
%Mint.TransportError{reason: reason}
end
defp ssl_opts(hostname, opts) do
default_ssl_opts(hostname)
|> Keyword.merge(opts)
|> Keyword.merge(@transport_opts)
|> Keyword.drop([:timeout, :inet4, :inet6])
|> add_verify_opts(hostname)
|> remove_incompatible_ssl_opts()
|> add_ciphers_opt()
end
defp add_verify_opts(opts, hostname) do
verify = Keyword.get(opts, :verify)
if verify == :verify_peer do
opts
|> add_cacerts()
|> add_partial_chain_fun()
|> customize_hostname_check(hostname)
else
opts
end
end
defp remove_incompatible_ssl_opts(opts) do
# These are the TLS versions that are compatible with :reuse_sessions and :secure_renegotiate
# If none of the compatible TLS versions are present in the transport options, then
# :reuse_sessions and :secure_renegotiate will be removed from the transport options.
compatible_versions = [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
versions_opt = Keyword.get(opts, :versions, [])
if Enum.any?(compatible_versions, &(&1 in versions_opt)) do
opts
else
opts
|> Keyword.delete(:reuse_sessions)
|> Keyword.delete(:secure_renegotiate)
end
end
defp customize_hostname_check(opts, host_or_ip) do
if ssl_version() >= [9, 0] do
# From OTP 20.0 use built-in support for custom hostname checks
add_customize_hostname_check(opts)
else
# Before OTP 20.0 use mint_shims for hostname check, from a custom
# verify_fun
add_verify_fun(opts, host_or_ip)
end
end
defp add_customize_hostname_check(opts) do
Keyword.put_new(opts, :customize_hostname_check, match_fun: &match_fun/2)
end
defp add_verify_fun(opts, host_or_ip) do
Keyword.put_new_lazy(opts, :verify_fun, fn ->
reference_ids = [dns_id: host_or_ip, ip: host_or_ip]
{&verify_fun/3, reference_ids}
end)
end
def verify_fun(_, {:bad_cert, _} = reason, _), do: {:fail, reason}
def verify_fun(_, {:extension, _}, state), do: {:unknown, state}
def verify_fun(_, :valid, state), do: {:valid, state}
def verify_fun(cert, :valid_peer, state) do
if :mint_shims.pkix_verify_hostname(cert, state, match_fun: &match_fun/2) do
{:valid, state}
else
{:fail, {:bad_cert, :hostname_check_failed}}
end
end
# Wildcard domain handling for DNS ID entries in the subjectAltName X.509
# extension. Note that this is a subset of the wildcard patterns implemented
# by OTP when matching against the subject CN attribute, but this is the only
# wildcard usage defined by the CA/Browser Forum's Baseline Requirements, and
# therefore the only pattern used in commercially issued certificates.
defp match_fun({:dns_id, reference}, {:dNSName, [?*, ?. | presented]}) do
case domain_without_host(reference) do
~c"" -> :default
domain -> :string.casefold(domain) == :string.casefold(presented)
end
end
# Workaround for a bug that was fixed in OTP 27:
# Before OTP 27 when connecting to an IP address and the server offers a
# certificate with its IP address in the "subject alternate names" extension,
# the TLS handshake fails with a `{:bad_cert, :hostname_check_failed}`.
# This clause can be removed once we depend on OTP 27+.
defp match_fun({:dns_id, hostname}, {:iPAddress, ip}) do
with {:ok, ip_tuple} <- :inet.parse_address(hostname),
^ip <- Tuple.to_list(ip_tuple) do
true
else
_ -> :default
end
end
defp match_fun(_reference, _presented), do: :default
defp domain_without_host([]), do: []
defp domain_without_host([?. | domain]), do: domain
defp domain_without_host([_ | more]), do: domain_without_host(more)
defp add_ciphers_opt(opts) do
Keyword.put_new_lazy(opts, :ciphers, fn ->
versions = opts[:versions]
get_ciphers_for_versions(versions)
end)
end
defp default_ssl_opts(hostname) do
# TODO: Add revocation check
# Note: the :ciphers option is added once the :versions option
# has been merged with the user-specified value
[
server_name_indication: hostname,
versions: ssl_versions(),
verify: :verify_peer,
depth: 4,
secure_renegotiate: true,
reuse_sessions: true
]
end
@doc false
def ssl_versions() do
available_versions = :ssl.versions()[:available]
versions = Enum.filter(@default_versions, &(&1 in available_versions))
# Remove buggy TLS 1.3 versions
if ssl_version() < [10, 0] do
versions -- [:"tlsv1.3"]
else
versions
end
end
defp add_cacerts(opts) do
if Keyword.has_key?(opts, :cacertfile) or Keyword.has_key?(opts, :cacerts) do
opts
else
try do
Keyword.put(opts, :cacerts, :public_key.cacerts_get())
rescue
_ ->
raise_on_missing_castore!()
Keyword.put(opts, :cacertfile, CAStore.file_path())
end
end
end
defp add_partial_chain_fun(opts) do
if Keyword.has_key?(opts, :partial_chain) do
opts
else
case Keyword.fetch(opts, :cacerts) do
{:ok, cacerts} ->
cacerts = decode_cacerts(cacerts)
fun = &partial_chain(cacerts, &1)
Keyword.put(opts, :partial_chain, fun)
:error ->
path = Keyword.fetch!(opts, :cacertfile)
cacerts = get_cacertfile(path)
fun = &partial_chain(cacerts, &1)
Keyword.put(opts, :partial_chain, fun)
end
end
end
defp get_cacertfile(path) do
if Application.get_env(:mint, :persistent_term) do
case :persistent_term.get({:mint, {:cacertfile, path}}, :error) do
{:ok, cacerts} ->
cacerts
:error ->
cacerts = decode_cacertfile(path)
:persistent_term.put({:mint, {:cacertfile, path}}, {:ok, cacerts})
cacerts
end
else
decode_cacertfile(path)
end
end
defp decode_cacertfile(path) do
path
|> File.read!()
|> :public_key.pem_decode()
|> Enum.filter(&match?({:Certificate, _, :not_encrypted}, &1))
|> Enum.map(&:public_key.pem_entry_decode/1)
end
defp decode_cacerts(certs) do
Enum.map(certs, fn
cert when is_binary(cert) -> :public_key.pkix_decode_cert(cert, :plain)
{:cert, _, otp_certificate} -> otp_certificate
end)
end
def partial_chain(cacerts, certs) do
# TODO: Shim this with OTP 21.1 implementation?
certs =
certs
|> Enum.map(&{&1, :public_key.pkix_decode_cert(&1, :plain)})
|> Enum.drop_while(&cert_expired?/1)
trusted =
Enum.find_value(certs, fn {der, cert} ->
trusted? =
Enum.find(cacerts, fn cacert ->
extract_public_key_info(cacert) == extract_public_key_info(cert)
end)
if trusted?, do: der
end)
if trusted do
{:trusted_ca, trusted}
else
:unknown_ca
end
end
defp cert_expired?({_der, cert}) do
now = DateTime.utc_now()
{not_before, not_after} = extract_validity(cert)
DateTime.compare(now, not_before) == :lt or
DateTime.compare(now, not_after) == :gt
end
defp extract_validity(cert) do
{:Validity, not_before, not_after} =
cert
|> certificate(:tbsCertificate)
|> tbs_certificate(:validity)
{to_datetime!(not_before), to_datetime!(not_after)}
end
defp extract_public_key_info(cert) do
cert
|> certificate(:tbsCertificate)
|> tbs_certificate(:subjectPublicKeyInfo)
end
defp to_datetime!({:utcTime, time}) do
"20#{time}"
|> to_datetime!()
end
defp to_datetime!({:generalTime, time}) do
time
|> to_string()
|> to_datetime!()
end
defp to_datetime!(
<<year::binary-size(4), month::binary-size(2), day::binary-size(2), hour::binary-size(2),
minute::binary-size(2), second::binary-size(2), "Z"::binary>>
) do
{:ok, datetime, _} =
DateTime.from_iso8601("#{year}-#{month}-#{day}T#{hour}:#{minute}:#{second}Z")
datetime
end
defp blocked_cipher?(%{cipher: cipher, key_exchange: kex, prf: prf}),
do: blocked_cipher?({kex, cipher, prf})
defp blocked_cipher?({kex, cipher, _mac, prf}), do: blocked_cipher?({kex, cipher, prf})
defp blocked_cipher?({_kex, _cipher, _prf} = suite), do: suite in @blocked_ciphers
if Code.ensure_loaded?(CAStore) do
defp raise_on_missing_castore! do
:ok
end
else
defp raise_on_missing_castore! do
raise """
default CA trust store not available; please add `:castore` to your project's \
dependencies or specify the trust store using the :cacertfile/:cacerts option \
within :transport_options. From OTP 25, you can also use:
* :public_key.cacerts_get/0 to get certificates that you loaded from files or
* from the OS with :public_key.cacerts_load/0,1
See: https://www.erlang.org/blog/my-otp-25-highlights/#ca-certificates-can-be-fetched-from-the-os-standard-place
"""
end
end
defp wrap_err({:error, reason}), do: {:error, wrap_error(reason)}
defp wrap_err(other), do: other
@doc false
def ssl_version() do
Application.spec(:ssl, :vsn)
|> List.to_string()
|> String.split(".")
|> Enum.map(&String.to_integer/1)
end
# Dialyzer warns on :ssl.cipher_suites/1 for now.
@dialyzer {:nowarn_function, get_ciphers_for_versions: 1}
@doc false
def get_ciphers_for_versions(versions) do
if ssl_version() >= [8, 2, 4] do
# :ssl.filter_cipher_suites/2 is available in ssl v8.2.4+
versions
|> Enum.flat_map(&:ssl.filter_cipher_suites(:ssl.cipher_suites(:all, &1), []))
|> Enum.uniq()
else
:ssl.cipher_suites(:all)
end
|> Enum.reject(&blocked_cipher?/1)
end
end

View File

@@ -0,0 +1,92 @@
defmodule Mint.Core.Transport.TCP do
@moduledoc false
@behaviour Mint.Core.Transport
@transport_opts [
packet: :raw,
mode: :binary,
active: false
]
@default_timeout 30_000
@impl true
def connect(address, port, opts) when is_binary(address),
do: connect(String.to_charlist(address), port, opts)
def connect(address, port, opts) do
opts = Keyword.delete(opts, :hostname)
timeout = Keyword.get(opts, :timeout, @default_timeout)
inet4? = Keyword.get(opts, :inet4, true)
inet6? = Keyword.get(opts, :inet6, false)
opts =
opts
|> Keyword.merge(@transport_opts)
|> Keyword.drop([:alpn_advertised_protocols, :timeout, :inet4, :inet6])
if inet6? do
# Try inet6 first, then fall back to the defaults provided by
# gen_tcp if connection fails.
case :gen_tcp.connect(address, port, [:inet6 | opts], timeout) do
{:ok, socket} ->
{:ok, socket}
_error when inet4? ->
wrap_err(:gen_tcp.connect(address, port, opts, timeout))
error ->
wrap_err(error)
end
else
# Use the defaults provided by gen_tcp.
wrap_err(:gen_tcp.connect(address, port, opts, timeout))
end
end
@impl true
def upgrade(socket, _scheme, _hostname, _port, _opts) do
{:ok, socket}
end
@impl true
def negotiated_protocol(_socket), do: wrap_err({:error, :protocol_not_negotiated})
@impl true
def send(socket, payload) do
wrap_err(:gen_tcp.send(socket, payload))
end
@impl true
defdelegate close(socket), to: :gen_tcp
@impl true
def recv(socket, bytes, timeout) do
wrap_err(:gen_tcp.recv(socket, bytes, timeout))
end
@impl true
def controlling_process(socket, pid) do
wrap_err(:gen_tcp.controlling_process(socket, pid))
end
@impl true
def setopts(socket, opts) do
wrap_err(:inet.setopts(socket, opts))
end
@impl true
def getopts(socket, opts) do
wrap_err(:inet.getopts(socket, opts))
end
@impl true
def wrap_error(reason) do
%Mint.TransportError{reason: reason}
end
defp wrap_err({:error, reason}), do: {:error, wrap_error(reason)}
defp wrap_err(other), do: other
end

View File

@@ -0,0 +1,71 @@
defmodule Mint.Core.Util do
@moduledoc false
alias Mint.Types
@spec hostname(keyword(), String.t()) :: String.t()
def hostname(opts, address) when is_list(opts) do
case Keyword.fetch(opts, :hostname) do
{:ok, hostname} ->
hostname
:error when is_binary(address) ->
address
:error ->
raise ArgumentError, "the :hostname option is required when address is not a binary"
end
end
@spec inet_opts(:gen_tcp | :ssl, :gen_tcp.socket() | :ssl.sslsocket()) :: :ok | {:error, term()}
def inet_opts(transport, socket) do
with {:ok, opts} <- transport.getopts(socket, [:sndbuf, :recbuf, :buffer]),
buffer = calculate_buffer(opts),
:ok <- transport.setopts(socket, buffer: buffer) do
:ok
end
end
@spec scheme_to_transport(atom()) :: module()
def scheme_to_transport(:http), do: Mint.Core.Transport.TCP
def scheme_to_transport(:https), do: Mint.Core.Transport.SSL
def scheme_to_transport(module) when is_atom(module), do: module
defp calculate_buffer(opts) do
Keyword.fetch!(opts, :buffer)
|> max(Keyword.fetch!(opts, :sndbuf))
|> max(Keyword.fetch!(opts, :recbuf))
end
# Adds a header to the list of headers unless it's nil or it's already there.
@spec put_new_header(Types.headers(), String.t(), String.t() | nil) :: Types.headers()
def put_new_header(headers, name, value)
def put_new_header(headers, _name, nil) do
headers
end
def put_new_header(headers, name, value) do
if List.keymember?(headers, name, 0) do
headers
else
[{name, value} | headers]
end
end
@spec put_new_header_lazy(Types.headers(), String.t(), (-> String.t())) :: Types.headers()
def put_new_header_lazy(headers, name, fun) do
if List.keymember?(headers, name, 0) do
headers
else
[{name, fun.()} | headers]
end
end
# If the buffer is empty, reusing the incoming data saves
# a potentially large allocation of memory.
# This should be fixed in a subsequent OTP release.
@spec maybe_concat(binary(), binary()) :: binary()
def maybe_concat(<<>>, data), do: data
def maybe_concat(buffer, data) when is_binary(buffer), do: buffer <> data
end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
defmodule Mint.HTTP1.Parse do
@moduledoc false
defmacro is_digit(char), do: quote(do: unquote(char) in ?0..?9)
defmacro is_alpha(char), do: quote(do: unquote(char) in ?a..?z or unquote(char) in ?A..?Z)
defmacro is_whitespace(char), do: quote(do: unquote(char) in ~c"\s\t")
defmacro is_comma(char), do: quote(do: unquote(char) == ?,)
defmacro is_vchar(char), do: quote(do: unquote(char) in 33..126)
defmacro is_tchar(char) do
quote do
is_digit(unquote(char)) or is_alpha(unquote(char)) or unquote(char) in ~c"!#$%&'*+-.^_`|~"
end
end
def ignore_until_crlf(<<>>), do: :more
def ignore_until_crlf(<<"\r\n", rest::binary>>), do: {:ok, rest}
def ignore_until_crlf(<<_char, rest::binary>>), do: ignore_until_crlf(rest)
def content_length_header(string) do
case Integer.parse(String.trim_trailing(string)) do
{length, ""} when length >= 0 -> {:ok, length}
_other -> {:error, {:invalid_content_length_header, string}}
end
end
def connection_header(string) do
split_into_downcase_tokens(string)
end
def transfer_encoding_header(string) do
split_into_downcase_tokens(string)
end
defp split_into_downcase_tokens(string) do
case token_list_downcase(string) do
{:ok, []} -> {:error, :empty_token_list}
{:ok, list} -> {:ok, list}
:error -> {:error, {:invalid_token_list, string}}
end
end
# Made public for testing.
def token_list_downcase(string), do: token_list_downcase(string, [])
defp token_list_downcase(<<>>, acc), do: {:ok, :lists.reverse(acc)}
# Skip all whitespace and commas.
defp token_list_downcase(<<char, rest::binary>>, acc)
when is_whitespace(char) or is_comma(char),
do: token_list_downcase(rest, acc)
defp token_list_downcase(rest, acc), do: token_downcase(rest, _token_acc = <<>>, acc)
defp token_downcase(<<char, rest::binary>>, token_acc, acc) when is_tchar(char),
do: token_downcase(rest, <<token_acc::binary, downcase_ascii_char(char)>>, acc)
defp token_downcase(rest, token_acc, acc), do: token_list_sep_downcase(rest, [token_acc | acc])
defp token_list_sep_downcase(<<>>, acc), do: {:ok, :lists.reverse(acc)}
defp token_list_sep_downcase(<<char, rest::binary>>, acc) when is_whitespace(char),
do: token_list_sep_downcase(rest, acc)
defp token_list_sep_downcase(<<char, rest::binary>>, acc) when is_comma(char),
do: token_list_downcase(rest, acc)
defp token_list_sep_downcase(_rest, _acc), do: :error
defp downcase_ascii_char(char) when char in ?A..?Z, do: char + 32
defp downcase_ascii_char(char) when char in 0..127, do: char
end

View File

@@ -0,0 +1,69 @@
defmodule Mint.HTTP1.Request do
@moduledoc false
import Mint.HTTP1.Parse
def encode(method, target, headers, body) do
body = [
encode_request_line(method, target),
encode_headers(headers),
"\r\n",
encode_body(body)
]
{:ok, body}
catch
{:mint, reason} -> {:error, reason}
end
defp encode_request_line(method, target) do
[method, ?\s, target, " HTTP/1.1\r\n"]
end
defp encode_headers(headers) do
Enum.reduce(headers, "", fn {name, value}, acc ->
validate_header_name!(name)
validate_header_value!(name, value)
[acc, name, ": ", value, "\r\n"]
end)
end
defp encode_body(nil), do: ""
defp encode_body(:stream), do: ""
defp encode_body(body), do: body
def encode_chunk(:eof) do
"0\r\n\r\n"
end
def encode_chunk({:eof, trailing_headers}) do
["0\r\n", encode_headers(trailing_headers), "\r\n"]
end
def encode_chunk(chunk) do
length = IO.iodata_length(chunk)
[Integer.to_string(length, 16), "\r\n", chunk, "\r\n"]
end
defp validate_header_name!(name) do
_ =
for <<char <- name>> do
unless is_tchar(char) do
throw({:mint, {:invalid_header_name, name}})
end
end
:ok
end
defp validate_header_value!(name, value) do
_ =
for <<char <- value>> do
unless is_vchar(char) or char in ~c"\s\t" do
throw({:mint, {:invalid_header_value, name, value}})
end
end
:ok
end
end

View File

@@ -0,0 +1,43 @@
defmodule Mint.HTTP1.Response do
@moduledoc false
alias Mint.Core.Headers
def decode_status_line(binary) do
case :erlang.decode_packet(:http_bin, binary, []) do
{:ok, {:http_response, version, status, reason}, rest} ->
{:ok, {version, status, reason}, rest}
{:ok, _other, _rest} ->
:error
{:more, _length} ->
:more
{:error, _reason} ->
:error
end
end
def decode_header(binary) do
case :erlang.decode_packet(:httph_bin, binary, []) do
{:ok, {:http_header, _unused, name, _reserved, value}, rest} ->
{:ok, {header_name(name), value}, rest}
{:ok, :http_eoh, rest} ->
{:ok, :eof, rest}
{:ok, _other, _rest} ->
:error
{:more, _length} ->
:more
{:error, _reason} ->
:error
end
end
defp header_name(atom) when is_atom(atom), do: atom |> Atom.to_string() |> header_name()
defp header_name(binary) when is_binary(binary), do: Headers.lower_raw(binary)
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
defmodule Mint.HTTP2.Frame do
@moduledoc false
import Bitwise, only: [band: 2, bor: 2]
import Kernel, except: [inspect: 1]
import Record
shared_stream = [:stream_id, {:flags, 0x00}]
shared_conn = [stream_id: 0, flags: 0x00]
defrecord :data, shared_stream ++ [:data, :padding]
defrecord :headers, shared_stream ++ [:exclusive?, :stream_dependency, :weight, :hbf, :padding]
defrecord :priority, shared_stream ++ [:exclusive?, :stream_dependency, :weight]
defrecord :rst_stream, shared_stream ++ [:error_code]
defrecord :settings, shared_conn ++ [:params]
defrecord :push_promise, shared_stream ++ [:promised_stream_id, :hbf, :padding]
defrecord :ping, shared_conn ++ [:opaque_data]
defrecord :goaway, shared_conn ++ [:last_stream_id, :error_code, :debug_data]
defrecord :window_update, shared_stream ++ [:window_size_increment]
defrecord :continuation, shared_stream ++ [:hbf]
defrecord :unknown, []
@types %{
data: 0x00,
headers: 0x01,
priority: 0x02,
rst_stream: 0x03,
settings: 0x04,
push_promise: 0x05,
ping: 0x06,
goaway: 0x07,
window_update: 0x08,
continuation: 0x09
}
## Inspecting
@spec inspect(tuple()) :: String.t()
for {type, _code} <- @types do
def inspect(frame) when is_record(frame, unquote(type)) do
unquote(String.upcase(Atom.to_string(type))) <> Kernel.inspect(unquote(type)(frame))
end
end
## Flag handling
@flags %{
data: [end_stream: 0x01, padded: 0x08],
headers: [end_stream: 0x01, end_headers: 0x04, padded: 0x08, priority: 0x20],
settings: [ack: 0x01],
push_promise: [end_headers: 0x04, padded: 0x08],
ping: [ack: 0x01],
continuation: [end_headers: 0x04]
}
@spec set_flags(byte(), atom(), [flag_name :: atom()]) :: byte()
def set_flags(initial_flags \\ 0x00, frame_name, flags_to_set)
when is_integer(initial_flags) and is_list(flags_to_set) do
Enum.reduce(flags_to_set, initial_flags, &set_flag(&2, frame_name, &1))
end
@spec flag_set?(byte(), atom(), atom()) :: boolean()
def flag_set?(flags, frame, flag_name)
for {frame, flags} <- @flags,
{flag_name, flag_value} <- flags do
defp set_flag(flags, unquote(frame), unquote(flag_name)), do: bor(flags, unquote(flag_value))
def flag_set?(flags, unquote(frame), unquote(flag_name)),
do: band(flags, unquote(flag_value)) == unquote(flag_value)
end
defmacrop is_flag_set(flags, flag) do
quote do
band(unquote(flags), unquote(flag)) == unquote(flag)
end
end
## Parsing
@doc """
Decodes the next frame of the given binary.
Returns `{:ok, frame, rest}` if successful, `{:error, reason}` if not.
"""
@spec decode_next(binary()) :: {:ok, tuple(), binary()} | :more | {:error, reason}
when reason:
{:frame_size_error, atom()}
| {:protocol_error, binary()}
| :payload_too_big
def decode_next(bin, max_frame_size \\ 16_384) when is_binary(bin) do
case decode_next_raw(bin) do
{:ok, {_type, _flags, _stream_id, payload}, _rest}
when byte_size(payload) > max_frame_size ->
{:error, :payload_too_big}
{:ok, {type, flags, stream_id, payload}, rest} ->
{:ok, decode_contents(type, flags, stream_id, payload), rest}
:more ->
:more
end
catch
:throw, {:mint, reason} -> {:error, reason}
end
defp decode_next_raw(<<
length::24,
type,
flags,
_reserved::1,
stream_id::31,
payload::size(length)-binary,
rest::binary
>>) do
{:ok, {type, flags, stream_id, payload}, rest}
end
defp decode_next_raw(_other) do
:more
end
for {frame, type} <- @types do
function = :"decode_#{frame}"
defp decode_contents(unquote(type), flags, stream_id, payload) do
unquote(function)(flags, stream_id, payload)
end
end
defp decode_contents(_type, _flags, _stream_id, _payload) do
unknown()
end
# Parsing of specific frames
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.1
defp decode_data(flags, stream_id, payload) do
{data, padding} = decode_padding(:data, flags, payload)
data(stream_id: stream_id, flags: flags, data: data, padding: padding)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.2
defp decode_headers(flags, stream_id, payload) do
{data, padding} = decode_padding(:headers, flags, payload)
{exclusive?, stream_dependency, weight, data} =
if flag_set?(flags, :headers, :priority) do
<<exclusive::1, stream_dependency::31, weight::8, rest::binary>> = data
{exclusive == 1, stream_dependency, weight + 1, rest}
else
{nil, nil, nil, data}
end
headers(
stream_id: stream_id,
flags: flags,
padding: padding,
exclusive?: exclusive?,
stream_dependency: stream_dependency,
weight: weight,
hbf: data
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.3
defp decode_priority(_flags, _stream_id, payload) when byte_size(payload) != 5 do
throw({:mint, {:frame_size_error, :priority}})
end
defp decode_priority(flags, stream_id, payload) do
<<exclusive::1, stream_dependency::31, weight::8>> = payload
priority(
stream_id: stream_id,
flags: flags,
exclusive?: exclusive == 1,
stream_dependency: stream_dependency,
weight: weight + 1
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.4
defp decode_rst_stream(_flags, _stream_id, payload) when byte_size(payload) != 4 do
throw({:mint, {:frame_size_error, :rst_stream}})
end
defp decode_rst_stream(flags, stream_id, <<error_code::32>>) do
rst_stream(
stream_id: stream_id,
flags: flags,
error_code: humanize_error_code(error_code)
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.5
defp decode_settings(_flags, _stream_id, payload) when rem(byte_size(payload), 6) != 0 do
throw({:mint, {:frame_size_error, :settings}})
end
defp decode_settings(flags, stream_id, payload) do
settings(stream_id: stream_id, flags: flags, params: decode_settings_params(payload))
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.6
defp decode_push_promise(flags, stream_id, payload) do
{data, padding} = decode_padding(:push_promise, flags, payload)
<<_reserved::1, promised_stream_id::31, header_block_fragment::binary>> = data
push_promise(
stream_id: stream_id,
flags: flags,
promised_stream_id: promised_stream_id,
hbf: header_block_fragment,
padding: padding
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.7
defp decode_ping(_flags, _stream_id, payload) when byte_size(payload) != 8 do
throw({:mint, {:frame_size_error, :ping}})
end
defp decode_ping(flags, stream_id, payload) do
ping(stream_id: stream_id, flags: flags, opaque_data: payload)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.8
defp decode_goaway(flags, stream_id, payload) do
<<_reserved::1, last_stream_id::31, error_code::32, debug_data::binary>> = payload
goaway(
stream_id: stream_id,
flags: flags,
last_stream_id: last_stream_id,
error_code: humanize_error_code(error_code),
debug_data: debug_data
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.9
defp decode_window_update(_flags, _stream_id, payload) when byte_size(payload) != 4 do
throw({:mint, {:frame_size_error, :window_update}})
end
defp decode_window_update(_flags, _stream_id, <<_reserved::1, 0::31>>) do
throw({:mint, {:protocol_error, "bad WINDOW_SIZE increment"}})
end
defp decode_window_update(flags, stream_id, <<_reserved::1, window_size_increment::31>>) do
window_update(
stream_id: stream_id,
flags: flags,
window_size_increment: window_size_increment
)
end
# http://httpwg.org/specs/rfc7540.html#rfc.section.6.10
defp decode_continuation(flags, stream_id, payload) do
continuation(stream_id: stream_id, flags: flags, hbf: payload)
end
defp decode_padding(frame, flags, <<pad_length, rest::binary>> = payload)
when is_flag_set(flags, unquote(@flags[:data][:padded])) do
if pad_length >= byte_size(payload) do
debug_data =
"the padding length of a #{Kernel.inspect(frame)} frame is bigger than the payload length"
throw({:mint, {:protocol_error, debug_data}})
else
# 1 byte is for the space taken by pad_length
data_length = byte_size(payload) - pad_length - 1
<<data::size(data_length)-binary, padding::size(pad_length)-binary>> = rest
{data, padding}
end
end
defp decode_padding(_frame, _flags, payload) do
{payload, nil}
end
defp decode_settings_params(payload) do
decode_settings_params(payload, _acc = [])
end
defp decode_settings_params(<<>>, acc) do
Enum.reverse(acc)
end
defp decode_settings_params(<<identifier::16, value::32, rest::binary>>, acc) do
# From http://httpwg.org/specs/rfc7540.html#SettingValues:
# An endpoint that receives a SETTINGS frame with any unknown or unsupported identifier MUST
# ignore that setting.
acc =
case identifier do
0x01 -> [{:header_table_size, value} | acc]
0x02 -> [{:enable_push, value == 1} | acc]
0x03 -> [{:max_concurrent_streams, value} | acc]
0x04 -> [{:initial_window_size, value} | acc]
0x05 -> [{:max_frame_size, value} | acc]
0x06 -> [{:max_header_list_size, value} | acc]
0x08 -> [{:enable_connect_protocol, value == 1} | acc]
_other -> acc
end
decode_settings_params(rest, acc)
end
## Encoding
@doc """
Encodes the given `frame`.
"""
@spec encode(tuple()) :: iodata()
def encode(frame)
def encode(data(stream_id: stream_id, flags: flags, data: data, padding: nil)) do
encode_raw(@types[:data], flags, stream_id, data)
end
def encode(data(stream_id: stream_id, flags: flags, data: data, padding: padding)) do
flags = set_flags(flags, :data, [:padded])
payload = [byte_size(padding), data, padding]
encode_raw(@types[:data], flags, stream_id, payload)
end
def encode(headers() = frame) do
headers(
flags: flags,
stream_id: stream_id,
exclusive?: exclusive?,
stream_dependency: stream_dependency,
weight: weight,
hbf: hbf,
padding: padding
) = frame
payload = hbf
{payload, flags} =
if stream_dependency && weight && is_boolean(exclusive?) do
{
[<<if(exclusive?, do: 1, else: 0)::1, stream_dependency::31>>, weight - 1, payload],
set_flags(flags, :headers, [:priority])
}
else
{payload, flags}
end
{payload, flags} =
if padding do
{[byte_size(padding), payload, padding], set_flags(flags, :headers, [:padded])}
else
{payload, flags}
end
encode_raw(@types[:headers], flags, stream_id, payload)
end
def encode(priority() = frame) do
priority(
stream_id: stream_id,
flags: flags,
exclusive?: exclusive?,
stream_dependency: stream_dependency,
weight: weight
) = frame
payload = [
<<if(exclusive?, do: 1, else: 0)::1, stream_dependency::31>>,
weight - 1
]
encode_raw(@types[:priority], flags, stream_id, payload)
end
def encode(rst_stream(stream_id: stream_id, flags: flags, error_code: error_code)) do
payload = <<dehumanize_error_code(error_code)::32>>
encode_raw(@types[:rst_stream], flags, stream_id, payload)
end
def encode(settings(stream_id: stream_id, flags: flags, params: params)) do
payload =
Enum.map(params, fn
{:header_table_size, value} -> <<0x01::16, value::32>>
{:enable_push, value} -> <<0x02::16, if(value, do: 1, else: 0)::32>>
{:max_concurrent_streams, value} -> <<0x03::16, value::32>>
{:initial_window_size, value} -> <<0x04::16, value::32>>
{:max_frame_size, value} -> <<0x05::16, value::32>>
{:max_header_list_size, value} -> <<0x06::16, value::32>>
{:enable_connect_protocol, value} -> <<0x08::16, if(value, do: 1, else: 0)::32>>
end)
encode_raw(@types[:settings], flags, stream_id, payload)
end
def encode(push_promise() = frame) do
push_promise(
stream_id: stream_id,
flags: flags,
promised_stream_id: promised_stream_id,
hbf: hbf,
padding: padding
) = frame
payload = [<<0::1, promised_stream_id::31>>, hbf]
{payload, flags} =
if padding do
{
[byte_size(padding), payload, padding],
set_flags(flags, :push_promise, [:padded])
}
else
{payload, flags}
end
encode_raw(@types[:push_promise], flags, stream_id, payload)
end
def encode(ping(stream_id: 0, flags: flags, opaque_data: opaque_data)) do
encode_raw(@types[:ping], flags, 0, opaque_data)
end
def encode(goaway() = frame) do
goaway(
stream_id: 0,
flags: flags,
last_stream_id: last_stream_id,
error_code: error_code,
debug_data: debug_data
) = frame
payload = [<<0::1, last_stream_id::31, dehumanize_error_code(error_code)::32>>, debug_data]
encode_raw(@types[:goaway], flags, 0, payload)
end
def encode(window_update(stream_id: stream_id, flags: flags, window_size_increment: wsi)) do
payload = <<0::1, wsi::31>>
encode_raw(@types[:window_update], flags, stream_id, payload)
end
def encode(continuation(stream_id: stream_id, flags: flags, hbf: hbf)) do
encode_raw(@types[:continuation], flags, stream_id, _payload = hbf)
end
def encode_raw(type, flags, stream_id, payload) do
[<<IO.iodata_length(payload)::24>>, type, flags, <<0::1, stream_id::31>>, payload]
end
## Helpers
error_codes = %{
0x00 => :no_error,
0x01 => :protocol_error,
0x02 => :internal_error,
0x03 => :flow_control_error,
0x04 => :settings_timeout,
0x05 => :stream_closed,
0x06 => :frame_size_error,
0x07 => :refused_stream,
0x08 => :cancel,
0x09 => :compression_error,
0x0A => :connect_error,
0x0B => :enhance_your_calm,
0x0C => :inadequate_security,
0x0D => :http_1_1_required
}
for {code, human_code} <- error_codes do
defp humanize_error_code(unquote(code)), do: unquote(human_code)
defp dehumanize_error_code(unquote(human_code)), do: unquote(code)
end
defp humanize_error_code(code), do: {:custom_error, code}
defp dehumanize_error_code({:custom_error, code}), do: code
end

View File

@@ -0,0 +1,65 @@
defmodule Mint.HTTPError do
@moduledoc """
An HTTP error.
This exception struct is used to represent HTTP errors of all sorts and for
both HTTP/1 and HTTP/2.
A `Mint.HTTPError` struct is an exception, so it can be raised as any
other exception.
## Struct
The `Mint.HTTPError` struct is opaque, that is, not all of its fields are public.
The list of public fields is:
* `:reason` - the error reason. Can be one of:
* a term of type `t:Mint.HTTP1.error_reason/0`. See its documentation for
more information.
* a term of type `t:Mint.HTTP2.error_reason/0`. See its documentation for
more information.
* `{:proxy, reason}`, which is used when an HTTP error happens when connecting
to a tunnel proxy. `reason` can be:
* `:tunnel_timeout` - when the tunnel times out.
* `{:unexpected_status, status}` - when the proxy returns an unexpected
status `status`.
* `{:unexpected_trailing_responses, responses}` - when the proxy returns
unexpected responses (`responses`).
## Message representation
If you want to convert an error reason to a human-friendly message (for example
for using in logs), you can use `Exception.message/1`:
iex> {:error, %Mint.HTTPError{} = error} = Mint.HTTP.connect(:http, "bad-response.com", 80)
iex> Exception.message(error)
"the response contains two or more Content-Length headers"
"""
alias Mint.{HTTP1, HTTP2}
@type proxy_reason() ::
{:proxy,
HTTP1.error_reason()
| HTTP2.error_reason()
| :tunnel_timeout
| {:unexpected_status, non_neg_integer()}
| {:unexpected_trailing_responses, list()}}
@type t() :: %__MODULE__{
reason: HTTP1.error_reason() | HTTP2.error_reason() | proxy_reason() | term()
}
defexception [:reason, :module]
def message(%__MODULE__{reason: reason, module: module}) do
module.format_error(reason)
end
end

View File

@@ -0,0 +1,146 @@
defmodule Mint.Negotiate do
@moduledoc false
alias Mint.{
HTTP1,
HTTP2,
TransportError,
Types
}
alias Mint.Core.Util
@default_protocols [:http1, :http2]
@transport_opts [alpn_advertised_protocols: ["http/1.1", "h2"]]
@spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) ::
{:ok, Mint.HTTP.t()} | {:error, Types.error()}
def connect(scheme, address, port, opts \\ []) do
{protocols, opts} = Keyword.pop(opts, :protocols, @default_protocols)
case Enum.sort(protocols) do
[:http1] ->
HTTP1.connect(scheme, address, port, opts)
[:http2] ->
HTTP2.connect(scheme, address, port, opts)
[:http1, :http2] ->
transport_connect(scheme, address, port, opts)
end
end
@spec upgrade(
module(),
Types.socket(),
Types.scheme(),
String.t(),
:inet.port_number(),
keyword()
) :: {:ok, Mint.HTTP.t()} | {:error, Types.error()}
def upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts) do
{protocols, opts} = Keyword.pop(opts, :protocols, @default_protocols)
case Enum.sort(protocols) do
[:http1] ->
HTTP1.upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts)
[:http2] ->
HTTP2.upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts)
[:http1, :http2] ->
transport_upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts)
end
end
@spec initiate(module(), Types.socket(), String.t(), :inet.port_number(), keyword()) ::
{:ok, Mint.HTTP.t()} | {:error, Types.error()}
def initiate(transport, transport_state, hostname, port, opts),
do: alpn_negotiate(transport, transport_state, hostname, port, opts)
defp transport_connect(:http, address, port, opts) do
# HTTP1 upgrade is not supported
HTTP1.connect(:http, address, port, opts)
end
defp transport_connect(:https, address, port, opts) do
connect_negotiate(:https, address, port, opts)
end
defp connect_negotiate(scheme, address, port, opts) do
transport = Util.scheme_to_transport(scheme)
hostname = Mint.Core.Util.hostname(opts, address)
transport_opts =
opts
|> Keyword.get(:transport_opts, [])
|> Keyword.merge(@transport_opts)
|> Keyword.put(:hostname, hostname)
with {:ok, transport_state} <- transport.connect(address, port, transport_opts) do
alpn_negotiate(scheme, transport_state, hostname, port, opts)
end
end
defp transport_upgrade(
proxy_scheme,
transport_state,
:http,
hostname,
port,
opts
) do
# HTTP1 upgrade is not supported
HTTP1.upgrade(proxy_scheme, transport_state, :http, hostname, port, opts)
end
defp transport_upgrade(
proxy_scheme,
transport_state,
:https,
hostname,
port,
opts
) do
connect_upgrade(proxy_scheme, transport_state, :https, hostname, port, opts)
end
defp connect_upgrade(proxy_scheme, transport_state, new_scheme, hostname, port, opts) do
transport = Util.scheme_to_transport(new_scheme)
transport_opts =
opts
|> Keyword.get(:transport_opts, [])
|> Keyword.merge(@transport_opts)
case transport.upgrade(transport_state, proxy_scheme, hostname, port, transport_opts) do
{:ok, transport_state} ->
alpn_negotiate(new_scheme, transport_state, hostname, port, opts)
{:error, reason} ->
{:error, %TransportError{reason: reason}}
end
end
defp alpn_negotiate(scheme, socket, hostname, port, opts) do
transport = Util.scheme_to_transport(scheme)
case transport.negotiated_protocol(socket) do
{:ok, "http/1.1"} ->
HTTP1.initiate(scheme, socket, hostname, port, opts)
{:ok, "h2"} ->
HTTP2.initiate(scheme, socket, hostname, port, opts)
{:error, %TransportError{reason: :protocol_not_negotiated}} ->
# Assume HTTP1 if ALPN is not supported
HTTP1.initiate(scheme, socket, hostname, port, opts)
{:ok, protocol} ->
{:error, %TransportError{reason: {:bad_alpn_protocol, protocol}}}
{:error, %TransportError{} = error} ->
{:error, error}
end
end
end

View File

@@ -0,0 +1,92 @@
defmodule Mint.TransportError do
@moduledoc """
Represents an error with the transport used by an HTTP connection.
A `Mint.TransportError` struct is an exception, so it can be raised as any
other exception.
## Struct fields
This exception represents an error with the transport (TCP or SSL) used
by an HTTP connection. The exception struct itself is opaque, that is,
not all fields are public. The following are the public fields:
* `:reason` - a term representing the error reason. The value of this field
can be:
* `:timeout` - if there's a timeout in interacting with the socket.
* `:closed` - if the connection has been closed.
* `:protocol_not_negotiated` - if the ALPN protocol negotiation failed.
* `{:bad_alpn_protocol, protocol}` - when the ALPN protocol is not
one of the supported protocols, which are `http/1.1` and `h2`.
* `t::inet.posix/0` - if there's any other error with the socket,
such as `:econnrefused` or `:nxdomain`.
* `t::ssl.error_alert/0` - if there's an SSL error.
## Message representation
If you want to convert an error reason to a human-friendly message (for example
for using in logs), you can use `Exception.message/1`:
iex> {:error, %Mint.TransportError{} = error} = Mint.HTTP.connect(:http, "nonexistent", 80)
iex> Exception.message(error)
"non-existing domain"
"""
reason_type =
quote do
:timeout
| :closed
| :protocol_not_negotiated
| {:bad_alpn_protocol, String.t()}
| :inet.posix()
end
reason_type =
if System.otp_release() >= "21" do
quote do: unquote(reason_type) | :ssl.error_alert()
else
reason_type
end
@type t() :: %__MODULE__{reason: unquote(reason_type) | term()}
defexception [:reason]
def message(%__MODULE__{reason: reason}) do
format_reason(reason)
end
## Our reasons.
defp format_reason(:protocol_not_negotiated) do
"ALPN protocol not negotiated"
end
defp format_reason({:bad_alpn_protocol, protocol}) do
"bad ALPN protocol #{inspect(protocol)}, supported protocols are \"http/1.1\" and \"h2\""
end
defp format_reason(:closed) do
"socket closed"
end
defp format_reason(:timeout) do
"timeout"
end
# :ssl.format_error/1 falls back to :inet.format_error/1 when the error is not an SSL-specific
# error (at least since OTP 19+), so we can just use that.
defp format_reason(reason) do
case :ssl.format_error(reason) do
~c"Unexpected error:" ++ _ -> inspect(reason)
message -> List.to_string(message)
end
end
end

View File

@@ -0,0 +1,148 @@
defmodule Mint.TunnelProxy do
@moduledoc false
alias Mint.{HTTP, HTTP1, HTTPError, Negotiate, TransportError}
@tunnel_timeout 30_000
@spec connect(tuple(), tuple()) :: {:ok, Mint.HTTP.t()} | {:error, term()}
def connect(proxy, host) do
case establish_proxy(proxy, host) do
{:ok, conn} -> upgrade_connection(conn, proxy, host)
{:error, reason} -> {:error, reason}
end
end
defp establish_proxy(proxy, host) do
{proxy_scheme, proxy_address, proxy_port, proxy_opts} = proxy
{_scheme, address, port, opts} = host
hostname = Mint.Core.Util.hostname(opts, address)
path = "#{hostname}:#{port}"
with {:ok, conn} <- HTTP1.connect(proxy_scheme, proxy_address, proxy_port, proxy_opts),
timeout_deadline = timeout_deadline(proxy_opts),
headers = Keyword.get(opts, :proxy_headers, []),
{:ok, conn, ref} <- HTTP1.request(conn, "CONNECT", path, headers, nil),
{:ok, proxy_headers} <- receive_response(conn, ref, timeout_deadline) do
{:ok, HTTP1.put_proxy_headers(conn, proxy_headers)}
else
{:error, reason} ->
{:error, wrap_in_proxy_error(reason)}
{:error, conn, reason} ->
{:ok, _conn} = HTTP1.close(conn)
{:error, wrap_in_proxy_error(reason)}
end
end
defp upgrade_connection(
conn,
{proxy_scheme, _proxy_address, _proxy_port, _proxy_opts} = _proxy,
{scheme, hostname, port, opts} = _host
) do
proxy_headers = HTTP1.get_proxy_headers(conn)
socket = HTTP1.get_socket(conn)
# 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)}
end
end
defp receive_response(conn, ref, timeout_deadline) do
timeout = timeout_deadline - System.monotonic_time(:millisecond)
socket = HTTP1.get_socket(conn)
receive do
{tag, ^socket, _data} = msg when tag in [:tcp, :ssl] ->
stream(conn, ref, timeout_deadline, msg)
{tag, ^socket} = msg when tag in [:tcp_closed, :ssl_closed] ->
stream(conn, ref, timeout_deadline, msg)
{tag, ^socket, _reason} = msg when tag in [:tcp_error, :ssl_error] ->
stream(conn, ref, timeout_deadline, msg)
after
timeout ->
{:error, conn, wrap_error({:proxy, :tunnel_timeout})}
end
end
defp stream(conn, ref, timeout_deadline, msg) do
case HTTP1.stream(conn, msg) do
{:ok, conn, responses} ->
case handle_responses(ref, timeout_deadline, responses) do
{:done, proxy_headers} -> {:ok, proxy_headers}
:more -> receive_response(conn, ref, timeout_deadline)
{:error, reason} -> {:error, conn, reason}
end
{:error, conn, reason, _responses} ->
{:error, conn, wrap_in_proxy_error(reason)}
end
end
defp handle_responses(ref, timeout_deadline, [response | responses]) do
case response do
{:status, ^ref, status} when status in 200..299 ->
handle_responses(ref, timeout_deadline, responses)
{:status, ^ref, status} ->
{:error, wrap_error({:proxy, {:unexpected_status, status}})}
{:headers, ^ref, headers} when responses == [] ->
{:done, headers}
{:headers, ^ref, _headers} ->
{:error, wrap_error({:proxy, {:unexpected_trailing_responses, responses}})}
{:error, ^ref, reason} ->
{:error, wrap_in_proxy_error(reason)}
end
end
defp handle_responses(_ref, _timeout_deadline, []) do
:more
end
defp timeout_deadline(opts) do
timeout = Keyword.get(opts, :tunnel_timeout, @tunnel_timeout)
System.monotonic_time(:millisecond) + timeout
end
defp wrap_error(reason) do
%HTTPError{module: __MODULE__, reason: reason}
end
defp wrap_in_proxy_error(%HTTPError{reason: {:proxy, _}} = error) do
error
end
defp wrap_in_proxy_error(%HTTPError{reason: reason}) do
%HTTPError{module: __MODULE__, reason: {:proxy, reason}}
end
defp wrap_in_proxy_error(%TransportError{} = error) do
error
end
@doc false
def format_error({:proxy, reason}) do
case reason do
:tunnel_timeout ->
"proxy tunnel timeout"
{:unexpected_status, status} ->
"expected tunnel proxy to return a status between 200 and 299, got: #{inspect(status)}"
{:unexpected_trailing_responses, responses} ->
"tunnel proxy returned unexpected trailer responses: #{inspect(responses)}"
http_reason ->
"error when establishing the tunnel proxy connection: " <>
HTTP1.format_error(http_reason)
end
end
end

View File

@@ -0,0 +1,74 @@
defmodule Mint.Types do
@moduledoc """
HTTP-related types.
"""
@typedoc """
A hostname, IP address, Unix domain socket path, `:loopback`, or any
other term representing an internet address.
"""
@type address() :: :inet.socket_address() | String.t()
@typedoc """
A request reference that uniquely identifies a request.
Responses for a request are always tagged with a request reference so that you
can connect each response to the right request. Also see `Mint.HTTP.request/5`.
"""
@type request_ref() :: reference()
@typedoc """
An HTTP/2-specific response to a request.
This type of response is only returned on HTTP/2 connections. See `t:response/0` for
more response types.
"""
@type http2_response() ::
{:pong, request_ref()}
| {:push_promise, request_ref(), promised_request_ref :: request_ref(), headers()}
@typedoc """
A response to a request.
Terms of this type are returned as responses to requests. See `Mint.HTTP.stream/2`
for more information.
"""
@type response() ::
{:status, request_ref(), status()}
| {:headers, request_ref(), headers()}
| {:data, request_ref(), body_chunk :: binary()}
| {:done, request_ref()}
| {:error, request_ref(), reason :: term()}
| http2_response()
@typedoc """
An HTTP status code.
The type for an HTTP is a generic non-negative integer since we don't formally check that
the response code is in the "common" range (`200..599`).
"""
@type status() :: non_neg_integer()
@typedoc """
HTTP headers.
Headers are sent and received as lists of two-element tuples containing two strings,
the header name and header value.
"""
@type headers() :: [{header_name :: String.t(), header_value :: String.t()}]
@typedoc """
The scheme to use when connecting to an HTTP server.
"""
@type scheme() :: :http | :https
@typedoc """
An error reason.
"""
@type error() :: Mint.TransportError.t() | Mint.HTTPError.t()
@typedoc """
The connection socket.
"""
@type socket() :: term()
end

View File

@@ -0,0 +1,202 @@
defmodule Mint.UnsafeProxy do
@moduledoc false
alias Mint.{Types, UnsafeProxy}
@behaviour Mint.Core.Conn
defstruct [
:hostname,
:port,
:scheme,
:module,
:proxy_headers,
:state
]
@opaque t() :: %UnsafeProxy{}
@type host_triple() :: {Types.scheme(), address :: Types.address(), :inet.port_number()}
@spec connect(host_triple(), host_triple(), opts :: keyword()) ::
{:ok, t()} | {:error, Types.error()}
def connect(proxy, host, opts \\ []) do
{proxy_scheme, proxy_address, proxy_port} = proxy
{scheme, address, port} = host
hostname = Mint.Core.Util.hostname(opts, address)
with {:ok, state} <- Mint.HTTP1.connect(proxy_scheme, proxy_address, proxy_port, opts) do
conn = %UnsafeProxy{
scheme: scheme,
hostname: hostname,
port: port,
module: Mint.HTTP1,
proxy_headers: Keyword.get(opts, :proxy_headers, []),
state: state
}
{:ok, conn}
end
end
@impl true
@spec initiate(
module(),
Mint.Types.socket(),
String.t(),
:inet.port_number(),
keyword()
) :: no_return()
def initiate(_transport, _transport_state, _hostname, _port, _opts) do
raise "initiate/5 does not apply for #{inspect(__MODULE__)}"
end
@impl true
@spec close(t()) :: {:ok, t()}
def close(%UnsafeProxy{module: module, state: state} = _conn) do
module.close(state)
end
@impl true
@spec open?(t(), :read | :write) :: boolean()
def open?(%UnsafeProxy{module: module, state: state}, type \\ :write) do
module.open?(state, type)
end
@impl true
@spec request(
t(),
method :: String.t(),
path :: String.t(),
Types.headers(),
body :: iodata() | nil | :stream
) ::
{:ok, t(), Types.request_ref()}
| {:error, t(), Types.error()}
def request(
%UnsafeProxy{module: module, state: state} = conn,
method,
path,
headers,
body \\ nil
) do
path = request_line(conn, path)
headers = headers ++ conn.proxy_headers
case module.request(state, method, path, headers, body) do
{:ok, state, request} -> {:ok, %{conn | state: state}, request}
{:error, state, reason} -> {:error, %{conn | state: state}, reason}
end
end
@impl true
@spec stream_request_body(
t(),
Types.request_ref(),
iodata() | :eof | {:eof, trailer_headers :: Types.headers()}
) ::
{:ok, t()} | {:error, t(), Types.error()}
def stream_request_body(%UnsafeProxy{module: module, state: state} = conn, ref, body) do
case module.stream_request_body(state, ref, body) do
{:ok, state} -> {:ok, %{conn | state: state}}
{:error, state, reason} -> {:error, %{conn | state: state}, reason}
end
end
@impl true
@spec stream(t(), term()) ::
{:ok, t(), [Types.response()]}
| {:error, t(), Types.error(), [Types.response()]}
| :unknown
def stream(%UnsafeProxy{module: module, state: state} = conn, message) do
case module.stream(state, message) do
{:ok, state, responses} -> {:ok, %{conn | state: state}, responses}
{:error, state, reason, responses} -> {:error, %{conn | state: state}, reason, responses}
:unknown -> :unknown
end
end
@impl true
@spec open_request_count(t()) :: non_neg_integer()
def open_request_count(%UnsafeProxy{module: module, state: state} = _conn) do
module.open_request_count(state)
end
@impl true
@spec recv(t(), non_neg_integer(), timeout()) ::
{:ok, t(), [Types.response()]}
| {:error, t(), Types.error(), [Types.response()]}
def recv(%UnsafeProxy{module: module, state: state} = conn, byte_count, timeout) do
case module.recv(state, byte_count, timeout) do
{:ok, state, responses} -> {:ok, %{conn | state: state}, responses}
{:error, state, reason, responses} -> {:error, %{conn | state: state}, reason, responses}
end
end
@impl true
@spec set_mode(t(), :active | :passive) :: {:ok, t()} | {:error, Types.error()}
def set_mode(%UnsafeProxy{module: module, state: state} = conn, mode) do
with {:ok, state} <- module.set_mode(state, mode) do
{:ok, %{conn | state: state}}
end
end
@impl true
@spec controlling_process(t(), pid()) :: {:ok, t()} | {:error, Types.error()}
def controlling_process(%UnsafeProxy{module: module, state: state} = conn, new_pid) do
with {:ok, _} <- module.controlling_process(state, new_pid) do
{:ok, conn}
end
end
@impl true
@spec put_private(t(), atom(), term()) :: t()
def put_private(%UnsafeProxy{module: module, state: state} = conn, key, value) do
state = module.put_private(state, key, value)
%{conn | state: state}
end
@impl true
@spec get_private(t(), atom(), term()) :: term()
def get_private(%UnsafeProxy{module: module, state: state}, key, default \\ nil) do
module.get_private(state, key, default)
end
@impl true
@spec delete_private(t(), atom()) :: t()
def delete_private(%UnsafeProxy{module: module, state: state} = conn, key) do
state = module.delete_private(state, key)
%{conn | state: state}
end
defp request_line(%UnsafeProxy{scheme: scheme, hostname: hostname, port: port}, path) do
%URI{scheme: Atom.to_string(scheme), host: hostname, port: port, path: path}
|> URI.to_string()
end
@impl true
@spec get_socket(t()) :: Mint.Types.socket()
def get_socket(%UnsafeProxy{module: module, state: state}) do
module.get_socket(state)
end
@impl true
@spec put_log(t(), boolean()) :: t()
def put_log(%UnsafeProxy{module: module, state: state} = conn, log) do
state = module.put_log(state, log)
%{conn | state: state}
end
# The `%__MODULE__{proxy_headers: value}` here is the request headers,
# not the proxy response ones. Unsafe proxy mixes its headers (if any)
# with the regular response headers, so you can get them there.
@impl true
@spec get_proxy_headers(t()) :: Mint.Types.headers()
def get_proxy_headers(%__MODULE__{}), do: []
@impl true
@spec put_proxy_headers(t(), Mint.Types.headers()) :: t()
def put_proxy_headers(%__MODULE__{}, _headers) do
raise "invalid function for proxy unsafe proxy connections"
end
end

86
phoenix/deps/mint/mix.exs Normal file
View File

@@ -0,0 +1,86 @@
defmodule Mint.MixProject do
use Mix.Project
@version "1.7.1"
@repo_url "https://github.com/elixir-mint/mint"
def project do
[
app: :mint,
version: @version,
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
deps: deps(),
# Xref
xref: [
exclude: [
:persistent_term,
{:ssl, :cipher_suites, 1},
{:public_key, :cacerts_get, 0},
CAStore
]
],
# Dialyxir
dialyzer: [
plt_add_apps: [:castore],
plt_local_path: "plts",
plt_core_path: "plts"
],
# Code coverage
test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test, coveralls: :test],
# Hex
package: package(),
description: "Small and composable HTTP client.",
# Docs
name: "Mint",
docs: [
source_ref: "v#{@version}",
source_url: @repo_url,
extras: [
"pages/Architecture.md",
"pages/Decompression.md"
]
]
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger, :ssl],
mod: {Mint.Application, []}
]
end
defp package do
[
licenses: ["Apache-2.0"],
links: %{"GitHub" => @repo_url}
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_env), do: ["lib"]
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:castore, "~> 0.1.0 or ~> 1.0", optional: true},
{:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0"},
# Dev/test dependencies
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.20", only: :dev},
{:excoveralls, "~> 0.18.0", only: :test},
{:mox, "~> 1.0", only: :test},
{:stream_data, "~> 1.0", only: [:dev, :test]}
]
end
end

View File

@@ -0,0 +1,236 @@
%% Shims for functions introduced in recent Erlang/OTP releases,
%% to enable use of Mint on older releases. The code in this module
%% was taken directly from the Erlang/OTP project.
%%
%% File: lib/public_key/src/public_key.erl
%% Tag: OTP-20.3.4
%% Commit: f2c1d537dc28ffbde5d42aedec70bf4c6574c3ea
%% Changes from original file:
%% - extracted pkix_verify_hostname/2 and /3, and any private
%% functions they depend upon
%% - replaced local calls to other public functions in the
%% 'public_key' module with fully qualified equivalents
%% - replaced local type references with fully qualified equivalents
%%
%% The original license follows:
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2013-2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(mint_shims).
-include_lib("public_key/include/public_key.hrl").
-export([pkix_verify_hostname/2, pkix_verify_hostname/3]).
%--------------------------------------------------------------------
-spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(),
ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | public_key:oid(), string()}]) -> boolean().
-spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(),
ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | public_key:oid(), string()}],
Options :: proplists:proplist()) -> boolean().
%% Description: Validates a hostname to RFC 6125
%%--------------------------------------------------------------------
pkix_verify_hostname(Cert, ReferenceIDs) ->
pkix_verify_hostname(Cert, ReferenceIDs, []).
pkix_verify_hostname(BinCert, ReferenceIDs, Options) when is_binary(BinCert) ->
pkix_verify_hostname(public_key:pkix_decode_cert(BinCert,otp), ReferenceIDs, Options);
pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, ReferenceIDs0, Opts) ->
MatchFun = proplists:get_value(match_fun, Opts, undefined),
FailCB = proplists:get_value(fail_callback, Opts, fun(_Cert) -> false end),
FqdnFun = proplists:get_value(fqdn_fun, Opts, fun verify_hostname_extract_fqdn_default/1),
ReferenceIDs = [{T,to_string(V)} || {T,V} <- ReferenceIDs0],
PresentedIDs =
try lists:keyfind(?'id-ce-subjectAltName',
#'Extension'.extnID,
TbsCert#'OTPTBSCertificate'.extensions)
of
#'Extension'{extnValue = ExtVals} ->
[{T,to_string(V)} || {T,V} <- ExtVals];
false ->
[]
catch
_:_ -> []
end,
%% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]}
case PresentedIDs of
[] ->
%% Fallback to CN-ids [rfc6125, ch6]
case TbsCert#'OTPTBSCertificate'.subject of
{rdnSequence,RDNseq} ->
PresentedCNs =
[{cn, to_string(V)}
|| ATVs <- RDNseq, % RDNseq is list-of-lists
#'AttributeTypeAndValue'{type = ?'id-at-commonName',
value = {_T,V}} <- ATVs
% _T = kind of string (teletexString etc)
],
%% Example of PresentedCNs: [{cn,"www.ericsson.se"}]
%% match ReferenceIDs to PresentedCNs
verify_hostname_match_loop(verify_hostname_fqnds(ReferenceIDs, FqdnFun),
PresentedCNs,
MatchFun, FailCB, Cert);
_ ->
false
end;
_ ->
%% match ReferenceIDs to PresentedIDs
case verify_hostname_match_loop(ReferenceIDs, PresentedIDs,
MatchFun, FailCB, Cert) of
false ->
%% Try to extract DNS-IDs from URIs etc
DNS_ReferenceIDs =
[{dns_id,X} || X <- verify_hostname_fqnds(ReferenceIDs, FqdnFun)],
verify_hostname_match_loop(DNS_ReferenceIDs, PresentedIDs,
MatchFun, FailCB, Cert);
true ->
true
end
end.
%%%----------------------------------------------------------------
%%% pkix_verify_hostname help functions
verify_hostname_extract_fqdn_default({dns_id,S}) ->
S;
verify_hostname_extract_fqdn_default({uri_id,URI}) ->
% Modified from original to remove dependency on http_uri:parse/1 from inets
#{scheme := <<"https">>, host := Host} = 'Elixir.URI':parse(list_to_binary(URI)),
binary_to_list(Host).
verify_hostname_fqnds(L, FqdnFun) ->
[E || E0 <- L,
E <- [try case FqdnFun(E0) of
default -> verify_hostname_extract_fqdn_default(E0);
undefined -> undefined; % will make the "is_list(E)" test fail
Other -> Other
end
catch _:_-> undefined % will make the "is_list(E)" test fail
end],
is_list(E),
E =/= "",
{error,einval} == inet:parse_address(E)
].
-define(srvName_OID, {1,3,6,1,4,1,434,2,2,1,37,0}).
verify_hostname_match_default(Ref, Pres) ->
verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)).
verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) ->
not lists:member($*, FQDN);
verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) ->
[F1|Fs] = string:tokens(FQDN, "."),
[N1|Ns] = string:tokens(Name, "."),
match_wild(F1,N1) andalso Fs==Ns;
verify_hostname_match_default0({dns_id,R}, {dNSName,P}) ->
R==P;
verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) ->
R==P;
verify_hostname_match_default0({ip,R}, {iPAddress,P}) when length(P) == 4 ->
%% IPv4
try
list_to_tuple(P)
== if is_tuple(R), size(R)==4 -> R;
is_list(R) -> ok(inet:parse_ipv4strict_address(R))
end
catch
_:_ ->
false
end;
verify_hostname_match_default0({ip,R}, {iPAddress,P}) when length(P) == 16 ->
%% IPv6. The length 16 is due to the certificate specification.
try
l16_to_tup(P)
== if is_tuple(R), size(R)==8 -> R;
is_list(R) -> ok(inet:parse_ipv6strict_address(R))
end
catch
_:_ ->
false
end;
verify_hostname_match_default0({srv_id,R}, {srvName,P}) ->
R==P;
verify_hostname_match_default0({srv_id,R}, {?srvName_OID,P}) ->
R==P;
verify_hostname_match_default0(_, _) ->
false.
ok({ok,X}) -> X.
l16_to_tup(L) -> list_to_tuple(l16_to_tup(L, [])).
%%
l16_to_tup([A,B|T], Acc) -> l16_to_tup(T, [(A bsl 8) bor B | Acc]);
l16_to_tup([], Acc) -> lists:reverse(Acc).
match_wild(A, [$*|B]) -> match_wild_suffixes(A, B);
match_wild([C|A], [ C|B]) -> match_wild(A, B);
match_wild([], []) -> true;
match_wild(_, _) -> false.
%% Match the parts after the only wildcard by comparing them from the end
match_wild_suffixes(A, B) -> match_wild_sfx(lists:reverse(A), lists:reverse(B)).
match_wild_sfx([$*|_], _) -> false; % Bad name (no wildcards allowed)
match_wild_sfx(_, [$*|_]) -> false; % Bad pattern (no more wildcards allowed)
match_wild_sfx([A|Ar], [A|Br]) -> match_wild_sfx(Ar, Br);
match_wild_sfx(Ar, []) -> not lists:member($*, Ar); % Chk for bad name (= wildcards)
match_wild_sfx(_, _) -> false.
verify_hostname_match_loop(Refs0, Pres0, undefined, FailCB, Cert) ->
Pres = lists:map(fun to_lower_ascii/1, Pres0),
Refs = lists:map(fun to_lower_ascii/1, Refs0),
lists:any(
fun(R) ->
lists:any(fun(P) ->
verify_hostname_match_default(R,P) orelse FailCB(Cert)
end, Pres)
end, Refs);
verify_hostname_match_loop(Refs, Pres, MatchFun, FailCB, Cert) ->
lists:any(
fun(R) ->
lists:any(fun(P) ->
(case MatchFun(R,P) of
default -> verify_hostname_match_default(R,P);
Bool -> Bool
end) orelse FailCB(Cert)
end,
Pres)
end,
Refs).
to_lower_ascii({ip,_}=X) -> X;
to_lower_ascii({iPAddress,_}=X) -> X;
to_lower_ascii(S) when is_list(S) -> lists:map(fun to_lower_ascii/1, S);
to_lower_ascii({T,S}) -> {T, to_lower_ascii(S)};
to_lower_ascii(C) when $A =< C,C =< $Z -> C + ($a-$A);
to_lower_ascii(C) -> C.
to_string(S) when is_list(S) -> S;
to_string(B) when is_binary(B) -> binary_to_list(B);
to_string(X) -> X.