├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Caddyfile ├── LICENSE.txt ├── README.md ├── caddy_storage └── .gitignore ├── coveralls.json ├── docker-compose.yml ├── lib └── mint │ ├── application.ex │ ├── core │ ├── conn.ex │ ├── headers.ex │ ├── transport.ex │ ├── transport │ │ ├── ssl.ex │ │ └── tcp.ex │ └── util.ex │ ├── http.ex │ ├── http1.ex │ ├── http1 │ ├── parse.ex │ ├── request.ex │ └── response.ex │ ├── http2.ex │ ├── http2 │ └── frame.ex │ ├── http_error.ex │ ├── negotiate.ex │ ├── transport_error.ex │ ├── tunnel_proxy.ex │ ├── types.ex │ └── unsafe_proxy.ex ├── mix.exs ├── mix.lock ├── pages ├── Architecture.md └── Decompression.md ├── plts └── .gitkeep ├── src └── mint_shims.erl └── test ├── docker ├── tinyproxy-auth │ ├── Dockerfile │ └── tinyproxy.conf └── tinyproxy │ ├── Dockerfile │ └── tinyproxy.conf ├── http_test.exs ├── mint ├── core │ └── transport │ │ ├── ssl_test.exs │ │ └── tcp_test.exs ├── http1 │ ├── conn_properties_test.exs │ ├── conn_test.exs │ ├── integration_test.exs │ ├── parse_test.exs │ └── request_test.exs ├── http2 │ ├── conn_test.exs │ ├── frame_test.exs │ └── integration_test.exs ├── integration_test.exs ├── transport_error_test.exs ├── tunnel_proxy_test.exs ├── unix_socket_test.exs └── unsafe_proxy_test.exs ├── support ├── empty_cacerts.pem └── mint │ ├── ca_store.pem │ ├── certificate.pem │ ├── chain.pem │ ├── dst_and_isrg.pem │ ├── http1 │ ├── test_helpers.ex │ └── test_server.ex │ ├── http2 │ ├── test_helpers.ex │ └── test_server.ex │ ├── http_bin.ex │ ├── key.pem │ ├── pkix_verify_hostname_cn.pem │ ├── pkix_verify_hostname_subjAltName.pem │ ├── pkix_verify_hostname_subjAltName_IP.pem │ ├── test_socket_server.ex │ └── wildcard_san.pem └── test_helper.exs /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"lib/mint/tunnel_proxy.ex", "Type mismatch in call with opaque term in put_proxy_headers."}, 3 | {"lib/mint/http1.ex", :improper_list_constr}, 4 | ~r{test/support}, 5 | ~r{Function ExUnit.Assertion.* does not exist}, 6 | ~r{Call to missing or private function :public_key.cacerts_get/0} 7 | ] 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{lib,test}/**/*.{ex,exs}"], 4 | import_deps: [:stream_data], 5 | locals_without_parens: [ 6 | assert_round_trip: 1, 7 | assert_recv_frames: 1, 8 | assert_http2_error: 2, 9 | assert_transport_error: 2 10 | ] 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Test (Elixir ${{ matrix.elixir }}, OTP ${{ matrix.erlang }}) 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - erlang: "27.2" 18 | elixir: "1.18" 19 | lint: true 20 | coverage: true 21 | dialyzer: true 22 | - erlang: "24.3" 23 | elixir: "1.12" 24 | - erlang: "23.3.1" 25 | elixir: "1.12" 26 | dialyzer: true 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | MIX_ENV: test 30 | 31 | steps: 32 | - name: Checkout this repository 33 | uses: actions/checkout@v3 34 | 35 | - name: Install OTP and Elixir 36 | uses: erlef/setup-beam@v1 37 | with: 38 | otp-version: ${{ matrix.erlang }} 39 | elixir-version: ${{ matrix.elixir }} 40 | 41 | - name: Cache dependencies 42 | id: cache-deps 43 | uses: actions/cache@v3 44 | with: 45 | path: | 46 | deps 47 | _build 48 | key: | 49 | ${{ runner.os }}-mix-otp${{ matrix.erlang }}-elixir${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 50 | restore-keys: | 51 | ${{ runner.os }}-mix-otp${{ matrix.erlang }}-elixir${{ matrix.elixir }}- 52 | 53 | - name: Install and compile dependencies 54 | if: steps.cache-deps.outputs.cache-hit != 'true' 55 | run: mix do deps.get --only test, deps.compile 56 | 57 | # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones 58 | # Cache key based on Elixir & Erlang version (also useful when running in matrix) 59 | - name: Cache Dialyzer's PLT 60 | uses: actions/cache@v3 61 | id: cache-plt 62 | with: 63 | path: plts 64 | key: | 65 | ${{ runner.os }}-plt-otp${{ matrix.erlang }}-elixir${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 66 | restore-keys: | 67 | ${{ runner.os }}-plt-otp${{ matrix.erlang }}-elixir${{ matrix.elixir }}- 68 | 69 | # Create PLTs if no cache was found 70 | - name: Create PLTs 71 | if: ${{ matrix.dialyzer && steps.cache-plt.outputs.cache-hit != 'true' }} 72 | run: mix dialyzer --plt 73 | 74 | - name: Start docker 75 | run: DOCKER_USER="$UID:$GID" docker compose up --detach 76 | 77 | - name: Check for unused dependencies 78 | run: mix do deps.get, deps.unlock --check-unused 79 | if: ${{ matrix.lint && steps.cache-deps.outputs.cache-hit != 'true'}} 80 | 81 | - name: Compile with --warnings-as-errors 82 | run: mix compile --warnings-as-errors 83 | if: ${{ matrix.lint }} 84 | 85 | - name: Check mix format 86 | run: mix format --check-formatted 87 | if: ${{ matrix.lint }} 88 | 89 | - name: Run tests 90 | run: mix test --trace --include proxy 91 | if: ${{ !matrix.coverage }} 92 | 93 | - name: Run tests with coverage 94 | run: mix coveralls.github --include proxy 95 | if: ${{ matrix.coverage }} 96 | 97 | - name: Run Dialyzer 98 | run: mix dialyzer 99 | if: ${{ matrix.dialyzer }} 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | mint-*.tar 24 | 25 | /caddy_storage 26 | 27 | /plts/*.plt 28 | /plts/*.plt.hash 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.7.1 4 | 5 | ### Bug Fixes and Improvements 6 | 7 | * Fix a bug introduced in the previous version that broke `Mint.HTTP.open?/1`. 8 | 9 | ## v1.7.0 10 | 11 | ### Bug Fixes and Improvements 12 | 13 | * 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). 14 | * Introduce `:skip_target_validation` option for HTTP/1.1 connections. 15 | * 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. 16 | * Fix compilation warning for the next Elixir release (1.19). 17 | 18 | ## v1.6.2 19 | 20 | ### Bug Fixes and Improvements 21 | 22 | * Allow for version `~> 1.0` of the `hpax` dependency. 23 | 24 | ## v1.6.1 25 | 26 | ### Bug Fixes and Improvements 27 | 28 | * 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). 29 | * 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). 30 | 31 | ## v1.6.0 32 | 33 | ### New features 34 | 35 | * Add `:case_sensitive_headers` option to `Mint.HTTP1.connect/4`. 36 | * Add `:inet4` option to `Mint.HTTP.connect/4`. 37 | 38 | ### Bug Fixes and Improvements 39 | 40 | * Require Elixir 1.11+. 41 | * Add `match_fun` clause to deal with IP addresses in TLS handshake. 42 | * Optimize creation of HTTP/2 requests. 43 | * Fix a compilation warning (unused `set_flag/2` function). 44 | * Improve performance of downcasing headers. 45 | * Deprecate `:read_write` option in `Mint.HTTP.open?/2`. 46 | * Improve performance of checking for the CAStore library. 47 | 48 | ## v1.5.2 49 | 50 | ### Bug Fixes and Improvements 51 | 52 | * Fix a memory leak with `Mint.HTTP1` connections which would stay open but 53 | report as closed on timeouts. 54 | 55 | ## v1.5.1 56 | 57 | ### Bug Fixes and Improvements 58 | 59 | * Fix a `FunctionClauseError` that would happen when calling 60 | `Mint.HTTP2.close/1` on an HTTP/2 connection that hadn't completed the 61 | handshake yet. This bug was introduced in v1.5.0. See [issue 62 | #392](https://github.com/elixir-mint/mint/issues/392). 63 | 64 | ## v1.5.0 65 | 66 | ### Bug Fixes and Improvements 67 | 68 | * Properly close sockets on erroneous connections. 69 | * Fix `Mint.HTTP.is_connection_message/2` to support proxy connections. 70 | * Add support for CAStore v1.0.0+. 71 | * Support all HTTP/2 settings for clients as well (see 72 | `Mint.HTTP2.put_settings/2`). 73 | * Handle the first `SETTINGS` frame sent by the server *asynchronously* in 74 | HTTP/2. This means lower latency between connecting to a server and being 75 | able to send data to it. 76 | * Add more logging and make logging configurable through the `:log` option 77 | (see `Mint.HTTP.connect/4`, `Mint.HTTP1.connect/4`, `Mint.HTTP2.connect/4`). 78 | 79 | ## v1.4.2 80 | 81 | ### Bug Fixes and Improvements 82 | 83 | * Properly handle interim responses (informational `1xx` status codes) in 84 | HTTP/2. Now you might get zero or more sequences of `:status` and `:headers` 85 | responses with status `1xx` before the *final response* (with status 86 | non-`1xx`). 87 | 88 | ## v1.4.1 89 | 90 | ### Bug Fixes and Improvements 91 | 92 | * Emit the remaining buffer as a `:data` response when switching protocols 93 | from HTTP/1. 94 | * Respect closed-for-writing when streaming data frames in HTTP/2. 95 | * Fix handling of HTTP/2 frames of an unknown type. 96 | 97 | ## v1.4.0 98 | 99 | ### Bug Fixes and Improvements 100 | 101 | * Add support for `SETTINGS_ENABLE_CONNECT_PROTOCOL` HTTP/2 server setting. 102 | * Omit the `:scheme` and `:path` pseudo headers for HTTP/2 CONNECT. 103 | * Fix invalid connection state when data can't be sent. 104 | * Skip expired certs in partial chain hook. 105 | * Add `Mint.HTTP.get_proxy_headers/1`. 106 | * Add `Mint.HTTP.module/1`. 107 | 108 | ## v1.3.0 109 | 110 | ### Bug Fixes and Improvements 111 | 112 | * Improve compatibility with OTP 24. 113 | * Support HTTP/1 pipelining when streaming requests. 114 | * Add `Mint.HTTP.get_socket/1` for returning the connection socket. 115 | * Improve compatibility with TLS 1.3. 116 | 117 | ## v1.2.1 118 | 119 | ### Bug Fixes and Improvements 120 | 121 | * Fix a bug where we were not ignoring the return value of `:ssl.close/1` and `:gen_tcp.close/1`. 122 | * Fix a bug where we were not properly handling transport errors when doing ALPN protocol negotiation. 123 | * Fix a bug where we were not handling connection closed errors in a few places. 124 | 125 | ## v1.2.0 126 | 127 | ### Bug Fixes and Improvements 128 | 129 | * Fix a few bugs with passing the Mint connection around. 130 | * Add IPv6 support with `inet6: true` in the transport options. 131 | * Cache the `:cacertfile` option for faster certificate lookup and decoding. 132 | * Add TLS 1.3 to default versions. 133 | 134 | ## v1.1.0 135 | 136 | ### Bug Fixes and Improvements 137 | 138 | * Concatenate values in one `cookie` header if the `cookie` header is provided more than once in HTTP/2. 139 | * Fix headers merging in `Mint.UnsafeProxy`. 140 | * Remove some `Logger.debug/1` calls from the codebase. 141 | * Assume the HTTP/2 protocol on TCP connections if using `Mint.HTTP2`. 142 | * Fix a bug where we would send `WINDOW_UPDATE` frames with an increment of `0` in HTTP/2. 143 | * Make the empty body chunk a no-op for `Mint.HTTP.stream_request_body/3` (only for HTTP/1). 144 | * Add the `Mint.HTTP.is_connection_message/2` guard. 145 | * Fix wildcard certificate verification in OTP 23. 146 | 147 | ## v1.0.0 148 | 149 | ### Breaking changes 150 | 151 | * Remove the deprecated `Mint.HTTP.request/4`, `Mint.HTTP1.request/4`, and `Mint.HTTP2.request/4`. 152 | 153 | ## v0.5.0 154 | 155 | ### Bug Fixes and Improvements 156 | 157 | * 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`. 158 | * Don't include port in the `authority` header if it's the default port for the used protocol. 159 | * Add a default `content-length` header in HTTP/2 160 | * Allow passing headers to proxies with the `:proxy_headers` option. 161 | * Fix a bug with HTTP/1 chunking. 162 | 163 | ## v0.4.0 164 | 165 | ### Bug Fixes and Improvements 166 | 167 | * Fix a small bug with double "wrapping" of some `Mint.TransportError`s. 168 | * Prevent unnecessary buffer allocations in the connections (less memory waste!). 169 | * Add support for chunked transfer-encoding in HTTP/1 requests when you don't use `content-encoding`/`transfer-encoding` yourself. 170 | * Add support for trailer headers in HTTP/* requests through `stream_request_body/3`. 171 | * Add a page about decompressing responses in the guides. 172 | 173 | ## v0.3.0 174 | 175 | ### Breaking changes 176 | 177 | * Remove `Mint.HTTP1.get_socket/1`, `Mint.HTTP2.get_socket/1`, and `Mint.HTTP.get_socket/1`. 178 | 179 | ### Bug Fixes and Improvements 180 | 181 | * Downcase all headers in HTTP/2 to mimic the behavior in HTTP/1.1. 182 | 183 | * 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. 184 | 185 | * Add a `:mode` option to the `connect/4` functions to start the socket in active or passive mode. 186 | 187 | * 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. 188 | 189 | * 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. 190 | 191 | * Support trailer response headers in HTTP/2 connections. 192 | 193 | ## v0.2.1 194 | 195 | ### Bug Fixes and Improvements 196 | 197 | * 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. 198 | 199 | * Fix a bug in the HTTP/2 handshake that would crash the connection in case the server sent unforeseen frames. 200 | 201 | * Improve buffering of body chunks in HTTP/1. 202 | 203 | ## v0.2.0 204 | 205 | ### Breaking changes 206 | 207 | * Add the `Mint.TransportError` and `Mint.HTTPError` exceptions. Change all the connection functions so that they return these error structs instead of generic terms. 208 | * Remove `Mint.HTTP2.get_setting/2` in favour of `Mint.HTTP2.get_server_setting/2` and `Mint.HTTP2.get_client_setting/2`. 209 | 210 | ### Bug fixes and enhancements 211 | 212 | * Add support for HTTP/2 server push with the new `:push_promise` response. 213 | * Add `Mint.HTTP2.cancel_request/5`. 214 | * Add `Mint.HTTP2.get_window_size/2`. 215 | * Add `open_request_count/1` function to `Mint.HTTP`, and `Mint.HTTP1`, `Mint.HTTP2`. 216 | * Add `open?/2` function to `Mint.HTTP`, and `Mint.HTTP1`, `Mint.HTTP2`. 217 | * Make the `Mint.HTTP2.HPACK` module private. 218 | * Take into account the max header list size advertised by the server in HTTP/2 connections. 219 | * Improve error handling in a bunch of `Mint.HTTP2` functions. 220 | * Fix flow control on `WINDOW_UPDATE` frames at the connection level in `Mint.HTTP2`. 221 | * Correctly return timeout errors when connecting. 222 | * Treat HTTP/1 header keys as case-insensitive. 223 | * Prohibit users from streaming on unknown requests in HTTP/2. 224 | * Prohibit the server from violating the client's max concurrent streams setting in HTTP/2. 225 | * Strip whitespace when parsing the `content-length` header in HTTP/1. 226 | * Fix path validation when building HTTP/1 requests, fixes paths with `%NN` escapes. 227 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | local_certs 3 | skip_install_trust 4 | storage file_system /caddy_storage 5 | } 6 | 7 | https://localhost:8443 { 8 | reverse_proxy httpbin:80 9 | } 10 | 11 | http://localhost:8080 { 12 | reverse_proxy httpbin:80 13 | } 14 | 15 | https://caddyhttpbin:8443 { 16 | reverse_proxy httpbin:80 17 | } 18 | 19 | http://caddyhttpbin:8080 { 20 | reverse_proxy httpbin:80 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mint 🌱 2 | 3 | [![CI badge](https://github.com/elixir-mint/mint/actions/workflows/main.yml/badge.svg)](https://github.com/elixir-mint/mint/actions/workflows/main.yml) 4 | [![Documentation badge](https://img.shields.io/badge/Documentation-ff69b4)][documentation] 5 | [![Hex.pm badge](https://img.shields.io/badge/Package%20on%20hex.pm-informational)](https://hex.pm/packages/mint) 6 | [![Coverage status badge](https://coveralls.io/repos/github/elixir-mint/mint/badge.svg?branch=main)](https://coveralls.io/github/elixir-mint/mint?branch=main) 7 | 8 | > Functional, low-level HTTP client for Elixir with support for HTTP/1 and HTTP/2. 9 | 10 | ## Installation 11 | 12 | To install Mint, add it to your `mix.exs` file: 13 | 14 | ```elixir 15 | defp deps do 16 | [ 17 | {:mint, "~> 1.0"} 18 | ] 19 | end 20 | ``` 21 | 22 | Then, run: 23 | 24 | ```sh 25 | mix deps.get 26 | ``` 27 | 28 | ## Usage 29 | 30 | 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. 31 | 32 | Let's see an example of a basic interaction with Mint. First, we start a connection through `Mint.HTTP.connect/3`: 33 | 34 | ```elixir 35 | iex> {:ok, conn} = Mint.HTTP.connect(:http, "httpbin.org", 80) 36 | ``` 37 | 38 | This transparently chooses between HTTP/1 and HTTP/2. Then, we can send requests with: 39 | 40 | ```elixir 41 | iex> {:ok, conn, request_ref} = Mint.HTTP.request(conn, "GET", "/", [], "") 42 | ``` 43 | 44 | 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): 45 | 46 | ```elixir 47 | iex> flush() 48 | {:tcp, #Port<0.8>, 49 | "HTTP/1.1 200 OK\r\n" <> _} 50 | ``` 51 | 52 | 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`. 53 | 54 | ```elixir 55 | iex> {:ok, conn} = Mint.HTTP.connect(:https, "httpbin.org", 443) 56 | iex> {:ok, conn, request_ref} = Mint.HTTP.request(conn, "GET", "/", [], "") 57 | iex> receive do 58 | ...> message -> 59 | ...> {:ok, conn, responses} = Mint.HTTP.stream(conn, message) 60 | ...> IO.inspect(responses) 61 | ...> end 62 | [ 63 | {:status, #Reference<...>, 200}, 64 | {:headers, #Reference<...>, [{"connection", "keep-alive"}, ...}, 65 | {:data, #Reference<...>, "..."}, 66 | {:done, #Reference<...>} 67 | ] 68 | ``` 69 | 70 | 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. 71 | 72 | The connection API is *stateless*, which means that you need to make sure to always save the connection that functions return: 73 | 74 | ```elixir 75 | # Wrong ❌ 76 | {:ok, _conn, ref} = Mint.HTTP.request(conn, "GET", "/foo", [], "") 77 | {:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/bar", [], "") 78 | 79 | # Correct ✅ 80 | {:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/foo", [], "") 81 | {:ok, conn, ref} = Mint.HTTP.request(conn, "GET", "/bar", [], "") 82 | ``` 83 | 84 | For more information, see [the documentation][documentation]. 85 | 86 | ### SSL certificates 87 | 88 | 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. 89 | 90 | ```elixir 91 | defp deps do 92 | [ 93 | # ..., 94 | {:castore, "~> 1.0.0"}, 95 | {:mint, "~> 0.4.0"} 96 | ] 97 | end 98 | ``` 99 | 100 | ### WebSocket Support 101 | 102 | 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]. 103 | 104 | ### Connection Management and Pooling 105 | 106 | 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. 107 | 108 | ## Contributing 109 | 110 | 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. 111 | 112 | 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. 113 | 114 | 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. 115 | 116 | Here are a few examples of running tests: 117 | 118 | Run all default tests which including `:requires_internet_connection` except `:proxy`: 119 | 120 | ```sh 121 | mix test 122 | ``` 123 | 124 | Local tests, if you don't have an Internet connection available: 125 | 126 | ```sh 127 | mix test --exclude requires_internet_connection 128 | ``` 129 | 130 | Run all tests: 131 | 132 | ```sh 133 | DOCKER_USER="$UID:$GID" docker compose up --detach # or podman-compose up --detach 134 | mix test --include proxy 135 | ``` 136 | 137 | ## License 138 | 139 | Copyright 2018 Eric Meadows-Jönsson and Andrea Leopardi 140 | 141 | Licensed under the Apache License, Version 2.0 (the "License"); 142 | you may not use this file except in compliance with the License. 143 | You may obtain a copy of the License at 144 | 145 | https://www.apache.org/licenses/LICENSE-2.0 146 | 147 | Unless required by applicable law or agreed to in writing, software 148 | distributed under the License is distributed on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 150 | See the License for the specific language governing permissions and 151 | limitations under the License. 152 | 153 | [documentation]: https://hexdocs.pm/mint 154 | [issues]: https://github.com/elixir-mint/mint/issues 155 | [mint_web_socket]: https://github.com/elixir-mint/mint_web_socket 156 | [Finch]: https://github.com/sneako/finch 157 | -------------------------------------------------------------------------------- /caddy_storage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "treat_no_relevant_lines_as_covered": true 4 | }, 5 | "skip_files": [ 6 | "test" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | proxy: 5 | build: ./test/docker/tinyproxy 6 | ports: 7 | - "8888:8888" 8 | 9 | proxy-auth: 10 | build: ./test/docker/tinyproxy-auth 11 | ports: 12 | - "8889:8888" 13 | 14 | httpbin: 15 | image: docker.io/kennethreitz/httpbin:latest 16 | platform: linux/amd64 17 | ports: 18 | - "8080:80" 19 | 20 | caddyhttpbin: 21 | image: docker.io/caddy:2.8.4-alpine 22 | # In GitHub Actions we want to access the files created 23 | # by Caddy. However, in Linux these end up being owned by root 24 | # because the container by default runs using the root user 25 | # and we are not root in the GH action so we cannot access them. 26 | user: "${DOCKER_USER}" 27 | volumes: 28 | # The :z mount option solves issues with SELinux. 29 | # See https://github.com/elixir-mint/mint/pull/406. 30 | - "./caddy_storage:/caddy_storage:z" 31 | - "./Caddyfile:/etc/caddy/Caddyfile:z" 32 | ports: 33 | - "8443:8443" 34 | -------------------------------------------------------------------------------- /lib/mint/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | persistent_term = 7 | Code.ensure_loaded?(:persistent_term) and function_exported?(:persistent_term, :get, 2) 8 | 9 | Application.put_env(:mint, :persistent_term, persistent_term) 10 | 11 | opts = [strategy: :one_for_one, name: Mint.Supervisor] 12 | Supervisor.start_link([], opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/mint/core/conn.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Conn do 2 | @moduledoc false 3 | 4 | alias Mint.Types 5 | 6 | @type conn() :: term() 7 | 8 | @callback initiate( 9 | module(), 10 | Mint.Types.socket(), 11 | String.t(), 12 | :inet.port_number(), 13 | keyword() 14 | ) :: {:ok, conn()} | {:error, Types.error()} 15 | 16 | @callback open?(conn(), :read | :write) :: boolean() 17 | 18 | @callback close(conn()) :: {:ok, conn()} 19 | 20 | @callback request( 21 | conn(), 22 | method :: String.t(), 23 | path :: String.t(), 24 | Types.headers(), 25 | body :: iodata() | nil | :stream 26 | ) :: 27 | {:ok, conn(), Types.request_ref()} 28 | | {:error, conn(), Types.error()} 29 | 30 | @callback stream_request_body( 31 | conn(), 32 | Types.request_ref(), 33 | body_chunk :: iodata() | :eof | {:eof, trailer_headers :: Types.headers()} 34 | ) :: 35 | {:ok, conn()} | {:error, conn(), Types.error()} 36 | 37 | @callback stream(conn(), term()) :: 38 | {:ok, conn(), [Types.response()]} 39 | | {:error, conn(), Types.error(), [Types.response()]} 40 | | :unknown 41 | 42 | @callback open_request_count(conn()) :: non_neg_integer() 43 | 44 | @callback recv(conn(), byte_count :: non_neg_integer(), timeout()) :: 45 | {:ok, conn(), [Types.response()]} 46 | | {:error, conn(), Types.error(), [Types.response()]} 47 | 48 | @callback set_mode(conn(), :active | :passive) :: {:ok, conn()} | {:error, Types.error()} 49 | 50 | @callback controlling_process(conn(), pid()) :: {:ok, conn()} | {:error, Types.error()} 51 | 52 | @callback put_private(conn(), key :: atom(), value :: term()) :: conn() 53 | 54 | @callback get_private(conn(), key :: atom(), default_value :: term()) :: term() 55 | 56 | @callback delete_private(conn(), key :: atom()) :: conn() 57 | 58 | @callback get_socket(conn()) :: Mint.Types.socket() 59 | 60 | @callback get_proxy_headers(conn()) :: Mint.Types.headers() 61 | 62 | @callback put_proxy_headers(conn(), Mint.Types.headers()) :: conn() 63 | 64 | @callback put_log(conn(), boolean()) :: conn() 65 | end 66 | -------------------------------------------------------------------------------- /lib/mint/core/headers.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Headers do 2 | @moduledoc false 3 | 4 | @type canonical() :: 5 | {original_name :: String.t(), canonical_name :: String.t(), value :: String.t()} 6 | @type raw() :: {original_name :: String.t(), value :: String.t()} 7 | 8 | @unallowed_trailers MapSet.new([ 9 | "content-encoding", 10 | "content-length", 11 | "content-range", 12 | "content-type", 13 | "trailer", 14 | "transfer-encoding", 15 | 16 | # Control headers (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.1) 17 | "cache-control", 18 | "expect", 19 | "host", 20 | "max-forwards", 21 | "pragma", 22 | "range", 23 | "te", 24 | 25 | # Conditionals (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.5.2) 26 | "if-match", 27 | "if-none-match", 28 | "if-modified-since", 29 | "if-unmodified-since", 30 | "if-range", 31 | 32 | # Authentication/authorization (https://tools.ietf.org/html/rfc7235#section-5.3) 33 | "authorization", 34 | "proxy-authenticate", 35 | "proxy-authorization", 36 | "www-authenticate", 37 | 38 | # Cookie management (https://tools.ietf.org/html/rfc6265) 39 | "cookie", 40 | "set-cookie", 41 | 42 | # Control data (https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7231.html#rfc.section.7.1) 43 | "age", 44 | "cache-control", 45 | "expires", 46 | "date", 47 | "location", 48 | "retry-after", 49 | "vary", 50 | "warning" 51 | ]) 52 | 53 | @spec from_raw([raw()]) :: [canonical()] 54 | def from_raw(headers) do 55 | Enum.map(headers, fn {name, value} -> {name, lower_raw(name), value} end) 56 | end 57 | 58 | @spec to_raw([canonical()], boolean()) :: [raw()] 59 | def to_raw(headers, _case_sensitive = true) do 60 | Enum.map(headers, fn {name, _canonical_name, value} -> {name, value} end) 61 | end 62 | 63 | def to_raw(headers, _case_sensitive = false) do 64 | Enum.map(headers, fn {_name, canonical_name, value} -> 65 | {canonical_name, value} 66 | end) 67 | end 68 | 69 | @spec find([canonical()], String.t()) :: {String.t(), String.t()} | nil 70 | def find(headers, name) do 71 | case List.keyfind(headers, name, 1) do 72 | nil -> nil 73 | {name, _canonical_name, value} -> {name, value} 74 | end 75 | end 76 | 77 | @spec replace([canonical()], String.t(), String.t(), String.t()) :: 78 | [canonical()] 79 | def replace(headers, new_name, canonical_name, value) do 80 | List.keyreplace(headers, canonical_name, 1, {new_name, canonical_name, value}) 81 | end 82 | 83 | @spec has?([canonical()], String.t()) :: boolean() 84 | def has?(headers, name) do 85 | List.keymember?(headers, name, 1) 86 | end 87 | 88 | @spec put_new([canonical()], String.t(), String.t(), String.t() | nil) :: 89 | [canonical()] 90 | def put_new(headers, _name, _canonical_name, nil) do 91 | headers 92 | end 93 | 94 | def put_new(headers, name, canonical_name, value) do 95 | if List.keymember?(headers, canonical_name, 1) do 96 | headers 97 | else 98 | [{name, canonical_name, value} | headers] 99 | end 100 | end 101 | 102 | @spec put_new([canonical()], String.t(), String.t(), (-> String.t())) :: 103 | [canonical()] 104 | def put_new_lazy(headers, name, canonical_name, fun) do 105 | if List.keymember?(headers, canonical_name, 1) do 106 | headers 107 | else 108 | [{name, canonical_name, fun.()} | headers] 109 | end 110 | end 111 | 112 | @spec find_unallowed_trailer([canonical()]) :: String.t() | nil 113 | def find_unallowed_trailer(headers) do 114 | Enum.find_value(headers, fn 115 | {raw_name, canonical_name, _value} -> 116 | if canonical_name in @unallowed_trailers do 117 | raw_name 118 | end 119 | end) 120 | end 121 | 122 | @spec remove_unallowed_trailer([raw()]) :: [raw()] 123 | def remove_unallowed_trailer(headers) do 124 | Enum.reject(headers, fn {name, _value} -> name in @unallowed_trailers end) 125 | end 126 | 127 | @spec lower_raw(String.t()) :: String.t() 128 | def lower_raw(name) do 129 | String.downcase(name, :ascii) 130 | end 131 | 132 | @spec lower_raws([raw()]) :: [raw()] 133 | def lower_raws(headers) do 134 | Enum.map(headers, fn {name, value} -> {lower_raw(name), value} end) 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/mint/core/transport.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Transport do 2 | @moduledoc false 3 | 4 | @type error() :: {:error, %Mint.TransportError{}} 5 | 6 | alias Mint.Types 7 | 8 | @callback connect(address :: Types.address(), port :: :inet.port_number(), opts :: keyword()) :: 9 | {:ok, Types.socket()} | error() 10 | 11 | @callback upgrade( 12 | Types.socket(), 13 | original_scheme :: Types.scheme(), 14 | hostname :: String.t(), 15 | :inet.port_number(), 16 | opts :: keyword() 17 | ) :: {:ok, Types.socket()} | error() 18 | 19 | @callback negotiated_protocol(Types.socket()) :: 20 | {:ok, protocol :: binary()} | {:error, :protocol_not_negotiated} 21 | 22 | @callback send(Types.socket(), payload :: iodata()) :: :ok | error() 23 | 24 | @callback close(Types.socket()) :: :ok | error() 25 | 26 | @callback recv(Types.socket(), bytes :: non_neg_integer(), timeout()) :: 27 | {:ok, binary()} | error() 28 | 29 | @callback controlling_process(Types.socket(), pid()) :: :ok | error() 30 | 31 | @callback setopts(Types.socket(), opts :: keyword()) :: :ok | error() 32 | 33 | @callback getopts(Types.socket(), opts :: keyword()) :: {:ok, opts :: keyword()} | error() 34 | 35 | @callback wrap_error(reason :: term()) :: %Mint.TransportError{} 36 | end 37 | -------------------------------------------------------------------------------- /lib/mint/core/transport/tcp.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Transport.TCP do 2 | @moduledoc false 3 | 4 | @behaviour Mint.Core.Transport 5 | 6 | @transport_opts [ 7 | packet: :raw, 8 | mode: :binary, 9 | active: false 10 | ] 11 | 12 | @default_timeout 30_000 13 | 14 | @impl true 15 | def connect(address, port, opts) when is_binary(address), 16 | do: connect(String.to_charlist(address), port, opts) 17 | 18 | def connect(address, port, opts) do 19 | opts = Keyword.delete(opts, :hostname) 20 | 21 | timeout = Keyword.get(opts, :timeout, @default_timeout) 22 | inet4? = Keyword.get(opts, :inet4, true) 23 | inet6? = Keyword.get(opts, :inet6, false) 24 | 25 | opts = 26 | opts 27 | |> Keyword.merge(@transport_opts) 28 | |> Keyword.drop([:alpn_advertised_protocols, :timeout, :inet4, :inet6]) 29 | 30 | if inet6? do 31 | # Try inet6 first, then fall back to the defaults provided by 32 | # gen_tcp if connection fails. 33 | case :gen_tcp.connect(address, port, [:inet6 | opts], timeout) do 34 | {:ok, socket} -> 35 | {:ok, socket} 36 | 37 | _error when inet4? -> 38 | wrap_err(:gen_tcp.connect(address, port, opts, timeout)) 39 | 40 | error -> 41 | wrap_err(error) 42 | end 43 | else 44 | # Use the defaults provided by gen_tcp. 45 | wrap_err(:gen_tcp.connect(address, port, opts, timeout)) 46 | end 47 | end 48 | 49 | @impl true 50 | def upgrade(socket, _scheme, _hostname, _port, _opts) do 51 | {:ok, socket} 52 | end 53 | 54 | @impl true 55 | def negotiated_protocol(_socket), do: wrap_err({:error, :protocol_not_negotiated}) 56 | 57 | @impl true 58 | def send(socket, payload) do 59 | wrap_err(:gen_tcp.send(socket, payload)) 60 | end 61 | 62 | @impl true 63 | defdelegate close(socket), to: :gen_tcp 64 | 65 | @impl true 66 | def recv(socket, bytes, timeout) do 67 | wrap_err(:gen_tcp.recv(socket, bytes, timeout)) 68 | end 69 | 70 | @impl true 71 | def controlling_process(socket, pid) do 72 | wrap_err(:gen_tcp.controlling_process(socket, pid)) 73 | end 74 | 75 | @impl true 76 | def setopts(socket, opts) do 77 | wrap_err(:inet.setopts(socket, opts)) 78 | end 79 | 80 | @impl true 81 | def getopts(socket, opts) do 82 | wrap_err(:inet.getopts(socket, opts)) 83 | end 84 | 85 | @impl true 86 | def wrap_error(reason) do 87 | %Mint.TransportError{reason: reason} 88 | end 89 | 90 | defp wrap_err({:error, reason}), do: {:error, wrap_error(reason)} 91 | defp wrap_err(other), do: other 92 | end 93 | -------------------------------------------------------------------------------- /lib/mint/core/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Util do 2 | @moduledoc false 3 | 4 | alias Mint.Types 5 | 6 | @spec hostname(keyword(), String.t()) :: String.t() 7 | def hostname(opts, address) when is_list(opts) do 8 | case Keyword.fetch(opts, :hostname) do 9 | {:ok, hostname} -> 10 | hostname 11 | 12 | :error when is_binary(address) -> 13 | address 14 | 15 | :error -> 16 | raise ArgumentError, "the :hostname option is required when address is not a binary" 17 | end 18 | end 19 | 20 | @spec inet_opts(:gen_tcp | :ssl, :gen_tcp.socket() | :ssl.sslsocket()) :: :ok | {:error, term()} 21 | def inet_opts(transport, socket) do 22 | with {:ok, opts} <- transport.getopts(socket, [:sndbuf, :recbuf, :buffer]), 23 | buffer = calculate_buffer(opts), 24 | :ok <- transport.setopts(socket, buffer: buffer) do 25 | :ok 26 | end 27 | end 28 | 29 | @spec scheme_to_transport(atom()) :: module() 30 | def scheme_to_transport(:http), do: Mint.Core.Transport.TCP 31 | def scheme_to_transport(:https), do: Mint.Core.Transport.SSL 32 | def scheme_to_transport(module) when is_atom(module), do: module 33 | 34 | defp calculate_buffer(opts) do 35 | Keyword.fetch!(opts, :buffer) 36 | |> max(Keyword.fetch!(opts, :sndbuf)) 37 | |> max(Keyword.fetch!(opts, :recbuf)) 38 | end 39 | 40 | # Adds a header to the list of headers unless it's nil or it's already there. 41 | @spec put_new_header(Types.headers(), String.t(), String.t() | nil) :: Types.headers() 42 | def put_new_header(headers, name, value) 43 | 44 | def put_new_header(headers, _name, nil) do 45 | headers 46 | end 47 | 48 | def put_new_header(headers, name, value) do 49 | if List.keymember?(headers, name, 0) do 50 | headers 51 | else 52 | [{name, value} | headers] 53 | end 54 | end 55 | 56 | @spec put_new_header_lazy(Types.headers(), String.t(), (-> String.t())) :: Types.headers() 57 | def put_new_header_lazy(headers, name, fun) do 58 | if List.keymember?(headers, name, 0) do 59 | headers 60 | else 61 | [{name, fun.()} | headers] 62 | end 63 | end 64 | 65 | # If the buffer is empty, reusing the incoming data saves 66 | # a potentially large allocation of memory. 67 | # This should be fixed in a subsequent OTP release. 68 | @spec maybe_concat(binary(), binary()) :: binary() 69 | def maybe_concat(<<>>, data), do: data 70 | def maybe_concat(buffer, data) when is_binary(buffer), do: buffer <> data 71 | end 72 | -------------------------------------------------------------------------------- /lib/mint/http1/parse.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.Parse do 2 | @moduledoc false 3 | 4 | defmacro is_digit(char), do: quote(do: unquote(char) in ?0..?9) 5 | defmacro is_alpha(char), do: quote(do: unquote(char) in ?a..?z or unquote(char) in ?A..?Z) 6 | defmacro is_whitespace(char), do: quote(do: unquote(char) in ~c"\s\t") 7 | defmacro is_comma(char), do: quote(do: unquote(char) == ?,) 8 | defmacro is_vchar(char), do: quote(do: unquote(char) in 33..126) 9 | 10 | defmacro is_tchar(char) do 11 | quote do 12 | is_digit(unquote(char)) or is_alpha(unquote(char)) or unquote(char) in ~c"!#$%&'*+-.^_`|~" 13 | end 14 | end 15 | 16 | def ignore_until_crlf(<<>>), do: :more 17 | def ignore_until_crlf(<<"\r\n", rest::binary>>), do: {:ok, rest} 18 | def ignore_until_crlf(<<_char, rest::binary>>), do: ignore_until_crlf(rest) 19 | 20 | def content_length_header(string) do 21 | case Integer.parse(String.trim_trailing(string)) do 22 | {length, ""} when length >= 0 -> {:ok, length} 23 | _other -> {:error, {:invalid_content_length_header, string}} 24 | end 25 | end 26 | 27 | def connection_header(string) do 28 | split_into_downcase_tokens(string) 29 | end 30 | 31 | def transfer_encoding_header(string) do 32 | split_into_downcase_tokens(string) 33 | end 34 | 35 | defp split_into_downcase_tokens(string) do 36 | case token_list_downcase(string) do 37 | {:ok, []} -> {:error, :empty_token_list} 38 | {:ok, list} -> {:ok, list} 39 | :error -> {:error, {:invalid_token_list, string}} 40 | end 41 | end 42 | 43 | # Made public for testing. 44 | def token_list_downcase(string), do: token_list_downcase(string, []) 45 | 46 | defp token_list_downcase(<<>>, acc), do: {:ok, :lists.reverse(acc)} 47 | 48 | # Skip all whitespace and commas. 49 | defp token_list_downcase(<>, acc) 50 | when is_whitespace(char) or is_comma(char), 51 | do: token_list_downcase(rest, acc) 52 | 53 | defp token_list_downcase(rest, acc), do: token_downcase(rest, _token_acc = <<>>, acc) 54 | 55 | defp token_downcase(<>, token_acc, acc) when is_tchar(char), 56 | do: token_downcase(rest, <>, acc) 57 | 58 | defp token_downcase(rest, token_acc, acc), do: token_list_sep_downcase(rest, [token_acc | acc]) 59 | 60 | defp token_list_sep_downcase(<<>>, acc), do: {:ok, :lists.reverse(acc)} 61 | 62 | defp token_list_sep_downcase(<>, acc) when is_whitespace(char), 63 | do: token_list_sep_downcase(rest, acc) 64 | 65 | defp token_list_sep_downcase(<>, acc) when is_comma(char), 66 | do: token_list_downcase(rest, acc) 67 | 68 | defp token_list_sep_downcase(_rest, _acc), do: :error 69 | 70 | defp downcase_ascii_char(char) when char in ?A..?Z, do: char + 32 71 | defp downcase_ascii_char(char) when char in 0..127, do: char 72 | end 73 | -------------------------------------------------------------------------------- /lib/mint/http1/request.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.Request do 2 | @moduledoc false 3 | 4 | import Mint.HTTP1.Parse 5 | 6 | def encode(method, target, headers, body) do 7 | body = [ 8 | encode_request_line(method, target), 9 | encode_headers(headers), 10 | "\r\n", 11 | encode_body(body) 12 | ] 13 | 14 | {:ok, body} 15 | catch 16 | {:mint, reason} -> {:error, reason} 17 | end 18 | 19 | defp encode_request_line(method, target) do 20 | [method, ?\s, target, " HTTP/1.1\r\n"] 21 | end 22 | 23 | defp encode_headers(headers) do 24 | Enum.reduce(headers, "", fn {name, value}, acc -> 25 | validate_header_name!(name) 26 | validate_header_value!(name, value) 27 | [acc, name, ": ", value, "\r\n"] 28 | end) 29 | end 30 | 31 | defp encode_body(nil), do: "" 32 | defp encode_body(:stream), do: "" 33 | defp encode_body(body), do: body 34 | 35 | def encode_chunk(:eof) do 36 | "0\r\n\r\n" 37 | end 38 | 39 | def encode_chunk({:eof, trailing_headers}) do 40 | ["0\r\n", encode_headers(trailing_headers), "\r\n"] 41 | end 42 | 43 | def encode_chunk(chunk) do 44 | length = IO.iodata_length(chunk) 45 | [Integer.to_string(length, 16), "\r\n", chunk, "\r\n"] 46 | end 47 | 48 | defp validate_header_name!(name) do 49 | _ = 50 | for <> do 51 | unless is_tchar(char) do 52 | throw({:mint, {:invalid_header_name, name}}) 53 | end 54 | end 55 | 56 | :ok 57 | end 58 | 59 | defp validate_header_value!(name, value) do 60 | _ = 61 | for <> do 62 | unless is_vchar(char) or char in ~c"\s\t" do 63 | throw({:mint, {:invalid_header_value, name, value}}) 64 | end 65 | end 66 | 67 | :ok 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/mint/http1/response.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.Response do 2 | @moduledoc false 3 | 4 | alias Mint.Core.Headers 5 | 6 | def decode_status_line(binary) do 7 | case :erlang.decode_packet(:http_bin, binary, []) do 8 | {:ok, {:http_response, version, status, reason}, rest} -> 9 | {:ok, {version, status, reason}, rest} 10 | 11 | {:ok, _other, _rest} -> 12 | :error 13 | 14 | {:more, _length} -> 15 | :more 16 | 17 | {:error, _reason} -> 18 | :error 19 | end 20 | end 21 | 22 | def decode_header(binary) do 23 | case :erlang.decode_packet(:httph_bin, binary, []) do 24 | {:ok, {:http_header, _unused, name, _reserved, value}, rest} -> 25 | {:ok, {header_name(name), value}, rest} 26 | 27 | {:ok, :http_eoh, rest} -> 28 | {:ok, :eof, rest} 29 | 30 | {:ok, _other, _rest} -> 31 | :error 32 | 33 | {:more, _length} -> 34 | :more 35 | 36 | {:error, _reason} -> 37 | :error 38 | end 39 | end 40 | 41 | defp header_name(atom) when is_atom(atom), do: atom |> Atom.to_string() |> header_name() 42 | defp header_name(binary) when is_binary(binary), do: Headers.lower_raw(binary) 43 | end 44 | -------------------------------------------------------------------------------- /lib/mint/http_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTPError do 2 | @moduledoc """ 3 | An HTTP error. 4 | 5 | This exception struct is used to represent HTTP errors of all sorts and for 6 | both HTTP/1 and HTTP/2. 7 | 8 | A `Mint.HTTPError` struct is an exception, so it can be raised as any 9 | other exception. 10 | 11 | ## Struct 12 | 13 | The `Mint.HTTPError` struct is opaque, that is, not all of its fields are public. 14 | The list of public fields is: 15 | 16 | * `:reason` - the error reason. Can be one of: 17 | 18 | * a term of type `t:Mint.HTTP1.error_reason/0`. See its documentation for 19 | more information. 20 | 21 | * a term of type `t:Mint.HTTP2.error_reason/0`. See its documentation for 22 | more information. 23 | 24 | * `{:proxy, reason}`, which is used when an HTTP error happens when connecting 25 | to a tunnel proxy. `reason` can be: 26 | 27 | * `:tunnel_timeout` - when the tunnel times out. 28 | 29 | * `{:unexpected_status, status}` - when the proxy returns an unexpected 30 | status `status`. 31 | 32 | * `{:unexpected_trailing_responses, responses}` - when the proxy returns 33 | unexpected responses (`responses`). 34 | 35 | ## Message representation 36 | 37 | If you want to convert an error reason to a human-friendly message (for example 38 | for using in logs), you can use `Exception.message/1`: 39 | 40 | iex> {:error, %Mint.HTTPError{} = error} = Mint.HTTP.connect(:http, "bad-response.com", 80) 41 | iex> Exception.message(error) 42 | "the response contains two or more Content-Length headers" 43 | 44 | """ 45 | 46 | alias Mint.{HTTP1, HTTP2} 47 | 48 | @type proxy_reason() :: 49 | {:proxy, 50 | HTTP1.error_reason() 51 | | HTTP2.error_reason() 52 | | :tunnel_timeout 53 | | {:unexpected_status, non_neg_integer()} 54 | | {:unexpected_trailing_responses, list()}} 55 | 56 | @type t() :: %__MODULE__{ 57 | reason: HTTP1.error_reason() | HTTP2.error_reason() | proxy_reason() | term() 58 | } 59 | 60 | defexception [:reason, :module] 61 | 62 | def message(%__MODULE__{reason: reason, module: module}) do 63 | module.format_error(reason) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/mint/negotiate.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Negotiate do 2 | @moduledoc false 3 | 4 | alias Mint.{ 5 | HTTP1, 6 | HTTP2, 7 | TransportError, 8 | Types 9 | } 10 | 11 | alias Mint.Core.Util 12 | 13 | @default_protocols [:http1, :http2] 14 | @transport_opts [alpn_advertised_protocols: ["http/1.1", "h2"]] 15 | 16 | @spec connect(Types.scheme(), Types.address(), :inet.port_number(), keyword()) :: 17 | {:ok, Mint.HTTP.t()} | {:error, Types.error()} 18 | def connect(scheme, address, port, opts \\ []) do 19 | {protocols, opts} = Keyword.pop(opts, :protocols, @default_protocols) 20 | 21 | case Enum.sort(protocols) do 22 | [:http1] -> 23 | HTTP1.connect(scheme, address, port, opts) 24 | 25 | [:http2] -> 26 | HTTP2.connect(scheme, address, port, opts) 27 | 28 | [:http1, :http2] -> 29 | transport_connect(scheme, address, port, opts) 30 | end 31 | end 32 | 33 | @spec upgrade( 34 | module(), 35 | Types.socket(), 36 | Types.scheme(), 37 | String.t(), 38 | :inet.port_number(), 39 | keyword() 40 | ) :: {:ok, Mint.HTTP.t()} | {:error, Types.error()} 41 | def upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts) do 42 | {protocols, opts} = Keyword.pop(opts, :protocols, @default_protocols) 43 | 44 | case Enum.sort(protocols) do 45 | [:http1] -> 46 | HTTP1.upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts) 47 | 48 | [:http2] -> 49 | HTTP2.upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts) 50 | 51 | [:http1, :http2] -> 52 | transport_upgrade(proxy_scheme, transport_state, scheme, hostname, port, opts) 53 | end 54 | end 55 | 56 | @spec initiate(module(), Types.socket(), String.t(), :inet.port_number(), keyword()) :: 57 | {:ok, Mint.HTTP.t()} | {:error, Types.error()} 58 | def initiate(transport, transport_state, hostname, port, opts), 59 | do: alpn_negotiate(transport, transport_state, hostname, port, opts) 60 | 61 | defp transport_connect(:http, address, port, opts) do 62 | # HTTP1 upgrade is not supported 63 | HTTP1.connect(:http, address, port, opts) 64 | end 65 | 66 | defp transport_connect(:https, address, port, opts) do 67 | connect_negotiate(:https, address, port, opts) 68 | end 69 | 70 | defp connect_negotiate(scheme, address, port, opts) do 71 | transport = Util.scheme_to_transport(scheme) 72 | hostname = Mint.Core.Util.hostname(opts, address) 73 | 74 | transport_opts = 75 | opts 76 | |> Keyword.get(:transport_opts, []) 77 | |> Keyword.merge(@transport_opts) 78 | |> Keyword.put(:hostname, hostname) 79 | 80 | with {:ok, transport_state} <- transport.connect(address, port, transport_opts) do 81 | alpn_negotiate(scheme, transport_state, hostname, port, opts) 82 | end 83 | end 84 | 85 | defp transport_upgrade( 86 | proxy_scheme, 87 | transport_state, 88 | :http, 89 | hostname, 90 | port, 91 | opts 92 | ) do 93 | # HTTP1 upgrade is not supported 94 | HTTP1.upgrade(proxy_scheme, transport_state, :http, hostname, port, opts) 95 | end 96 | 97 | defp transport_upgrade( 98 | proxy_scheme, 99 | transport_state, 100 | :https, 101 | hostname, 102 | port, 103 | opts 104 | ) do 105 | connect_upgrade(proxy_scheme, transport_state, :https, hostname, port, opts) 106 | end 107 | 108 | defp connect_upgrade(proxy_scheme, transport_state, new_scheme, hostname, port, opts) do 109 | transport = Util.scheme_to_transport(new_scheme) 110 | 111 | transport_opts = 112 | opts 113 | |> Keyword.get(:transport_opts, []) 114 | |> Keyword.merge(@transport_opts) 115 | 116 | case transport.upgrade(transport_state, proxy_scheme, hostname, port, transport_opts) do 117 | {:ok, transport_state} -> 118 | alpn_negotiate(new_scheme, transport_state, hostname, port, opts) 119 | 120 | {:error, reason} -> 121 | {:error, %TransportError{reason: reason}} 122 | end 123 | end 124 | 125 | defp alpn_negotiate(scheme, socket, hostname, port, opts) do 126 | transport = Util.scheme_to_transport(scheme) 127 | 128 | case transport.negotiated_protocol(socket) do 129 | {:ok, "http/1.1"} -> 130 | HTTP1.initiate(scheme, socket, hostname, port, opts) 131 | 132 | {:ok, "h2"} -> 133 | HTTP2.initiate(scheme, socket, hostname, port, opts) 134 | 135 | {:error, %TransportError{reason: :protocol_not_negotiated}} -> 136 | # Assume HTTP1 if ALPN is not supported 137 | HTTP1.initiate(scheme, socket, hostname, port, opts) 138 | 139 | {:ok, protocol} -> 140 | {:error, %TransportError{reason: {:bad_alpn_protocol, protocol}}} 141 | 142 | {:error, %TransportError{} = error} -> 143 | {:error, error} 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/mint/transport_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.TransportError do 2 | @moduledoc """ 3 | Represents an error with the transport used by an HTTP connection. 4 | 5 | A `Mint.TransportError` struct is an exception, so it can be raised as any 6 | other exception. 7 | 8 | ## Struct fields 9 | 10 | This exception represents an error with the transport (TCP or SSL) used 11 | by an HTTP connection. The exception struct itself is opaque, that is, 12 | not all fields are public. The following are the public fields: 13 | 14 | * `:reason` - a term representing the error reason. The value of this field 15 | can be: 16 | 17 | * `:timeout` - if there's a timeout in interacting with the socket. 18 | 19 | * `:closed` - if the connection has been closed. 20 | 21 | * `:protocol_not_negotiated` - if the ALPN protocol negotiation failed. 22 | 23 | * `{:bad_alpn_protocol, protocol}` - when the ALPN protocol is not 24 | one of the supported protocols, which are `http/1.1` and `h2`. 25 | 26 | * `t::inet.posix/0` - if there's any other error with the socket, 27 | such as `:econnrefused` or `:nxdomain`. 28 | 29 | * `t::ssl.error_alert/0` - if there's an SSL error. 30 | 31 | ## Message representation 32 | 33 | If you want to convert an error reason to a human-friendly message (for example 34 | for using in logs), you can use `Exception.message/1`: 35 | 36 | iex> {:error, %Mint.TransportError{} = error} = Mint.HTTP.connect(:http, "nonexistent", 80) 37 | iex> Exception.message(error) 38 | "non-existing domain" 39 | 40 | """ 41 | 42 | reason_type = 43 | quote do 44 | :timeout 45 | | :closed 46 | | :protocol_not_negotiated 47 | | {:bad_alpn_protocol, String.t()} 48 | | :inet.posix() 49 | end 50 | 51 | reason_type = 52 | if System.otp_release() >= "21" do 53 | quote do: unquote(reason_type) | :ssl.error_alert() 54 | else 55 | reason_type 56 | end 57 | 58 | @type t() :: %__MODULE__{reason: unquote(reason_type) | term()} 59 | 60 | defexception [:reason] 61 | 62 | def message(%__MODULE__{reason: reason}) do 63 | format_reason(reason) 64 | end 65 | 66 | ## Our reasons. 67 | 68 | defp format_reason(:protocol_not_negotiated) do 69 | "ALPN protocol not negotiated" 70 | end 71 | 72 | defp format_reason({:bad_alpn_protocol, protocol}) do 73 | "bad ALPN protocol #{inspect(protocol)}, supported protocols are \"http/1.1\" and \"h2\"" 74 | end 75 | 76 | defp format_reason(:closed) do 77 | "socket closed" 78 | end 79 | 80 | defp format_reason(:timeout) do 81 | "timeout" 82 | end 83 | 84 | # :ssl.format_error/1 falls back to :inet.format_error/1 when the error is not an SSL-specific 85 | # error (at least since OTP 19+), so we can just use that. 86 | defp format_reason(reason) do 87 | case :ssl.format_error(reason) do 88 | ~c"Unexpected error:" ++ _ -> inspect(reason) 89 | message -> List.to_string(message) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/mint/tunnel_proxy.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.TunnelProxy do 2 | @moduledoc false 3 | 4 | alias Mint.{HTTP, HTTP1, HTTPError, Negotiate, TransportError} 5 | 6 | @tunnel_timeout 30_000 7 | 8 | @spec connect(tuple(), tuple()) :: {:ok, Mint.HTTP.t()} | {:error, term()} 9 | def connect(proxy, host) do 10 | case establish_proxy(proxy, host) do 11 | {:ok, conn} -> upgrade_connection(conn, proxy, host) 12 | {:error, reason} -> {:error, reason} 13 | end 14 | end 15 | 16 | defp establish_proxy(proxy, host) do 17 | {proxy_scheme, proxy_address, proxy_port, proxy_opts} = proxy 18 | {_scheme, address, port, opts} = host 19 | hostname = Mint.Core.Util.hostname(opts, address) 20 | 21 | path = "#{hostname}:#{port}" 22 | 23 | with {:ok, conn} <- HTTP1.connect(proxy_scheme, proxy_address, proxy_port, proxy_opts), 24 | timeout_deadline = timeout_deadline(proxy_opts), 25 | headers = Keyword.get(opts, :proxy_headers, []), 26 | {:ok, conn, ref} <- HTTP1.request(conn, "CONNECT", path, headers, nil), 27 | {:ok, proxy_headers} <- receive_response(conn, ref, timeout_deadline) do 28 | {:ok, HTTP1.put_proxy_headers(conn, proxy_headers)} 29 | else 30 | {:error, reason} -> 31 | {:error, wrap_in_proxy_error(reason)} 32 | 33 | {:error, conn, reason} -> 34 | {:ok, _conn} = HTTP1.close(conn) 35 | {:error, wrap_in_proxy_error(reason)} 36 | end 37 | end 38 | 39 | defp upgrade_connection( 40 | conn, 41 | {proxy_scheme, _proxy_address, _proxy_port, _proxy_opts} = _proxy, 42 | {scheme, hostname, port, opts} = _host 43 | ) do 44 | proxy_headers = HTTP1.get_proxy_headers(conn) 45 | socket = HTTP1.get_socket(conn) 46 | 47 | # Note that we may leak messages if the server sent data after the CONNECT response 48 | case Negotiate.upgrade(proxy_scheme, socket, scheme, hostname, port, opts) do 49 | {:ok, conn} -> {:ok, HTTP.put_proxy_headers(conn, proxy_headers)} 50 | {:error, reason} -> {:error, wrap_in_proxy_error(reason)} 51 | end 52 | end 53 | 54 | defp receive_response(conn, ref, timeout_deadline) do 55 | timeout = timeout_deadline - System.monotonic_time(:millisecond) 56 | socket = HTTP1.get_socket(conn) 57 | 58 | receive do 59 | {tag, ^socket, _data} = msg when tag in [:tcp, :ssl] -> 60 | stream(conn, ref, timeout_deadline, msg) 61 | 62 | {tag, ^socket} = msg when tag in [:tcp_closed, :ssl_closed] -> 63 | stream(conn, ref, timeout_deadline, msg) 64 | 65 | {tag, ^socket, _reason} = msg when tag in [:tcp_error, :ssl_error] -> 66 | stream(conn, ref, timeout_deadline, msg) 67 | after 68 | timeout -> 69 | {:error, conn, wrap_error({:proxy, :tunnel_timeout})} 70 | end 71 | end 72 | 73 | defp stream(conn, ref, timeout_deadline, msg) do 74 | case HTTP1.stream(conn, msg) do 75 | {:ok, conn, responses} -> 76 | case handle_responses(ref, timeout_deadline, responses) do 77 | {:done, proxy_headers} -> {:ok, proxy_headers} 78 | :more -> receive_response(conn, ref, timeout_deadline) 79 | {:error, reason} -> {:error, conn, reason} 80 | end 81 | 82 | {:error, conn, reason, _responses} -> 83 | {:error, conn, wrap_in_proxy_error(reason)} 84 | end 85 | end 86 | 87 | defp handle_responses(ref, timeout_deadline, [response | responses]) do 88 | case response do 89 | {:status, ^ref, status} when status in 200..299 -> 90 | handle_responses(ref, timeout_deadline, responses) 91 | 92 | {:status, ^ref, status} -> 93 | {:error, wrap_error({:proxy, {:unexpected_status, status}})} 94 | 95 | {:headers, ^ref, headers} when responses == [] -> 96 | {:done, headers} 97 | 98 | {:headers, ^ref, _headers} -> 99 | {:error, wrap_error({:proxy, {:unexpected_trailing_responses, responses}})} 100 | 101 | {:error, ^ref, reason} -> 102 | {:error, wrap_in_proxy_error(reason)} 103 | end 104 | end 105 | 106 | defp handle_responses(_ref, _timeout_deadline, []) do 107 | :more 108 | end 109 | 110 | defp timeout_deadline(opts) do 111 | timeout = Keyword.get(opts, :tunnel_timeout, @tunnel_timeout) 112 | System.monotonic_time(:millisecond) + timeout 113 | end 114 | 115 | defp wrap_error(reason) do 116 | %HTTPError{module: __MODULE__, reason: reason} 117 | end 118 | 119 | defp wrap_in_proxy_error(%HTTPError{reason: {:proxy, _}} = error) do 120 | error 121 | end 122 | 123 | defp wrap_in_proxy_error(%HTTPError{reason: reason}) do 124 | %HTTPError{module: __MODULE__, reason: {:proxy, reason}} 125 | end 126 | 127 | defp wrap_in_proxy_error(%TransportError{} = error) do 128 | error 129 | end 130 | 131 | @doc false 132 | def format_error({:proxy, reason}) do 133 | case reason do 134 | :tunnel_timeout -> 135 | "proxy tunnel timeout" 136 | 137 | {:unexpected_status, status} -> 138 | "expected tunnel proxy to return a status between 200 and 299, got: #{inspect(status)}" 139 | 140 | {:unexpected_trailing_responses, responses} -> 141 | "tunnel proxy returned unexpected trailer responses: #{inspect(responses)}" 142 | 143 | http_reason -> 144 | "error when establishing the tunnel proxy connection: " <> 145 | HTTP1.format_error(http_reason) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/mint/types.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.Types do 2 | @moduledoc """ 3 | HTTP-related types. 4 | """ 5 | 6 | @typedoc """ 7 | A hostname, IP address, Unix domain socket path, `:loopback`, or any 8 | other term representing an internet address. 9 | """ 10 | @type address() :: :inet.socket_address() | String.t() 11 | 12 | @typedoc """ 13 | A request reference that uniquely identifies a request. 14 | 15 | Responses for a request are always tagged with a request reference so that you 16 | can connect each response to the right request. Also see `Mint.HTTP.request/5`. 17 | """ 18 | @type request_ref() :: reference() 19 | 20 | @typedoc """ 21 | An HTTP/2-specific response to a request. 22 | 23 | This type of response is only returned on HTTP/2 connections. See `t:response/0` for 24 | more response types. 25 | """ 26 | @type http2_response() :: 27 | {:pong, request_ref()} 28 | | {:push_promise, request_ref(), promised_request_ref :: request_ref(), headers()} 29 | 30 | @typedoc """ 31 | A response to a request. 32 | 33 | Terms of this type are returned as responses to requests. See `Mint.HTTP.stream/2` 34 | for more information. 35 | """ 36 | @type response() :: 37 | {:status, request_ref(), status()} 38 | | {:headers, request_ref(), headers()} 39 | | {:data, request_ref(), body_chunk :: binary()} 40 | | {:done, request_ref()} 41 | | {:error, request_ref(), reason :: term()} 42 | | http2_response() 43 | 44 | @typedoc """ 45 | An HTTP status code. 46 | 47 | The type for an HTTP is a generic non-negative integer since we don't formally check that 48 | the response code is in the "common" range (`200..599`). 49 | """ 50 | @type status() :: non_neg_integer() 51 | 52 | @typedoc """ 53 | HTTP headers. 54 | 55 | Headers are sent and received as lists of two-element tuples containing two strings, 56 | the header name and header value. 57 | """ 58 | @type headers() :: [{header_name :: String.t(), header_value :: String.t()}] 59 | 60 | @typedoc """ 61 | The scheme to use when connecting to an HTTP server. 62 | """ 63 | @type scheme() :: :http | :https 64 | 65 | @typedoc """ 66 | An error reason. 67 | """ 68 | @type error() :: Mint.TransportError.t() | Mint.HTTPError.t() 69 | 70 | @typedoc """ 71 | The connection socket. 72 | """ 73 | @type socket() :: term() 74 | end 75 | -------------------------------------------------------------------------------- /lib/mint/unsafe_proxy.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.UnsafeProxy do 2 | @moduledoc false 3 | 4 | alias Mint.{Types, UnsafeProxy} 5 | 6 | @behaviour Mint.Core.Conn 7 | 8 | defstruct [ 9 | :hostname, 10 | :port, 11 | :scheme, 12 | :module, 13 | :proxy_headers, 14 | :state 15 | ] 16 | 17 | @opaque t() :: %UnsafeProxy{} 18 | 19 | @type host_triple() :: {Types.scheme(), address :: Types.address(), :inet.port_number()} 20 | 21 | @spec connect(host_triple(), host_triple(), opts :: keyword()) :: 22 | {:ok, t()} | {:error, Types.error()} 23 | def connect(proxy, host, opts \\ []) do 24 | {proxy_scheme, proxy_address, proxy_port} = proxy 25 | {scheme, address, port} = host 26 | hostname = Mint.Core.Util.hostname(opts, address) 27 | 28 | with {:ok, state} <- Mint.HTTP1.connect(proxy_scheme, proxy_address, proxy_port, opts) do 29 | conn = %UnsafeProxy{ 30 | scheme: scheme, 31 | hostname: hostname, 32 | port: port, 33 | module: Mint.HTTP1, 34 | proxy_headers: Keyword.get(opts, :proxy_headers, []), 35 | state: state 36 | } 37 | 38 | {:ok, conn} 39 | end 40 | end 41 | 42 | @impl true 43 | @spec initiate( 44 | module(), 45 | Mint.Types.socket(), 46 | String.t(), 47 | :inet.port_number(), 48 | keyword() 49 | ) :: no_return() 50 | def initiate(_transport, _transport_state, _hostname, _port, _opts) do 51 | raise "initiate/5 does not apply for #{inspect(__MODULE__)}" 52 | end 53 | 54 | @impl true 55 | @spec close(t()) :: {:ok, t()} 56 | def close(%UnsafeProxy{module: module, state: state} = _conn) do 57 | module.close(state) 58 | end 59 | 60 | @impl true 61 | @spec open?(t(), :read | :write) :: boolean() 62 | def open?(%UnsafeProxy{module: module, state: state}, type \\ :write) do 63 | module.open?(state, type) 64 | end 65 | 66 | @impl true 67 | @spec request( 68 | t(), 69 | method :: String.t(), 70 | path :: String.t(), 71 | Types.headers(), 72 | body :: iodata() | nil | :stream 73 | ) :: 74 | {:ok, t(), Types.request_ref()} 75 | | {:error, t(), Types.error()} 76 | def request( 77 | %UnsafeProxy{module: module, state: state} = conn, 78 | method, 79 | path, 80 | headers, 81 | body \\ nil 82 | ) do 83 | path = request_line(conn, path) 84 | headers = headers ++ conn.proxy_headers 85 | 86 | case module.request(state, method, path, headers, body) do 87 | {:ok, state, request} -> {:ok, %{conn | state: state}, request} 88 | {:error, state, reason} -> {:error, %{conn | state: state}, reason} 89 | end 90 | end 91 | 92 | @impl true 93 | @spec stream_request_body( 94 | t(), 95 | Types.request_ref(), 96 | iodata() | :eof | {:eof, trailer_headers :: Types.headers()} 97 | ) :: 98 | {:ok, t()} | {:error, t(), Types.error()} 99 | def stream_request_body(%UnsafeProxy{module: module, state: state} = conn, ref, body) do 100 | case module.stream_request_body(state, ref, body) do 101 | {:ok, state} -> {:ok, %{conn | state: state}} 102 | {:error, state, reason} -> {:error, %{conn | state: state}, reason} 103 | end 104 | end 105 | 106 | @impl true 107 | @spec stream(t(), term()) :: 108 | {:ok, t(), [Types.response()]} 109 | | {:error, t(), Types.error(), [Types.response()]} 110 | | :unknown 111 | def stream(%UnsafeProxy{module: module, state: state} = conn, message) do 112 | case module.stream(state, message) do 113 | {:ok, state, responses} -> {:ok, %{conn | state: state}, responses} 114 | {:error, state, reason, responses} -> {:error, %{conn | state: state}, reason, responses} 115 | :unknown -> :unknown 116 | end 117 | end 118 | 119 | @impl true 120 | @spec open_request_count(t()) :: non_neg_integer() 121 | def open_request_count(%UnsafeProxy{module: module, state: state} = _conn) do 122 | module.open_request_count(state) 123 | end 124 | 125 | @impl true 126 | @spec recv(t(), non_neg_integer(), timeout()) :: 127 | {:ok, t(), [Types.response()]} 128 | | {:error, t(), Types.error(), [Types.response()]} 129 | def recv(%UnsafeProxy{module: module, state: state} = conn, byte_count, timeout) do 130 | case module.recv(state, byte_count, timeout) do 131 | {:ok, state, responses} -> {:ok, %{conn | state: state}, responses} 132 | {:error, state, reason, responses} -> {:error, %{conn | state: state}, reason, responses} 133 | end 134 | end 135 | 136 | @impl true 137 | @spec set_mode(t(), :active | :passive) :: {:ok, t()} | {:error, Types.error()} 138 | def set_mode(%UnsafeProxy{module: module, state: state} = conn, mode) do 139 | with {:ok, state} <- module.set_mode(state, mode) do 140 | {:ok, %{conn | state: state}} 141 | end 142 | end 143 | 144 | @impl true 145 | @spec controlling_process(t(), pid()) :: {:ok, t()} | {:error, Types.error()} 146 | def controlling_process(%UnsafeProxy{module: module, state: state} = conn, new_pid) do 147 | with {:ok, _} <- module.controlling_process(state, new_pid) do 148 | {:ok, conn} 149 | end 150 | end 151 | 152 | @impl true 153 | @spec put_private(t(), atom(), term()) :: t() 154 | def put_private(%UnsafeProxy{module: module, state: state} = conn, key, value) do 155 | state = module.put_private(state, key, value) 156 | %{conn | state: state} 157 | end 158 | 159 | @impl true 160 | @spec get_private(t(), atom(), term()) :: term() 161 | def get_private(%UnsafeProxy{module: module, state: state}, key, default \\ nil) do 162 | module.get_private(state, key, default) 163 | end 164 | 165 | @impl true 166 | @spec delete_private(t(), atom()) :: t() 167 | def delete_private(%UnsafeProxy{module: module, state: state} = conn, key) do 168 | state = module.delete_private(state, key) 169 | %{conn | state: state} 170 | end 171 | 172 | defp request_line(%UnsafeProxy{scheme: scheme, hostname: hostname, port: port}, path) do 173 | %URI{scheme: Atom.to_string(scheme), host: hostname, port: port, path: path} 174 | |> URI.to_string() 175 | end 176 | 177 | @impl true 178 | @spec get_socket(t()) :: Mint.Types.socket() 179 | def get_socket(%UnsafeProxy{module: module, state: state}) do 180 | module.get_socket(state) 181 | end 182 | 183 | @impl true 184 | @spec put_log(t(), boolean()) :: t() 185 | def put_log(%UnsafeProxy{module: module, state: state} = conn, log) do 186 | state = module.put_log(state, log) 187 | %{conn | state: state} 188 | end 189 | 190 | # The `%__MODULE__{proxy_headers: value}` here is the request headers, 191 | # not the proxy response ones. Unsafe proxy mixes its headers (if any) 192 | # with the regular response headers, so you can get them there. 193 | @impl true 194 | @spec get_proxy_headers(t()) :: Mint.Types.headers() 195 | def get_proxy_headers(%__MODULE__{}), do: [] 196 | 197 | @impl true 198 | @spec put_proxy_headers(t(), Mint.Types.headers()) :: t() 199 | def put_proxy_headers(%__MODULE__{}, _headers) do 200 | raise "invalid function for proxy unsafe proxy connections" 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.7.1" 5 | @repo_url "https://github.com/elixir-mint/mint" 6 | 7 | def project do 8 | [ 9 | app: :mint, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | start_permanent: Mix.env() == :prod, 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | deps: deps(), 15 | 16 | # Xref 17 | xref: [ 18 | exclude: [ 19 | :persistent_term, 20 | {:ssl, :cipher_suites, 1}, 21 | {:public_key, :cacerts_get, 0}, 22 | CAStore 23 | ] 24 | ], 25 | 26 | # Dialyxir 27 | dialyzer: [ 28 | plt_add_apps: [:castore], 29 | plt_local_path: "plts", 30 | plt_core_path: "plts" 31 | ], 32 | 33 | # Code coverage 34 | test_coverage: [tool: ExCoveralls], 35 | preferred_cli_env: ["coveralls.html": :test, coveralls: :test], 36 | 37 | # Hex 38 | package: package(), 39 | description: "Small and composable HTTP client.", 40 | 41 | # Docs 42 | name: "Mint", 43 | docs: [ 44 | source_ref: "v#{@version}", 45 | source_url: @repo_url, 46 | extras: [ 47 | "pages/Architecture.md", 48 | "pages/Decompression.md" 49 | ] 50 | ] 51 | ] 52 | end 53 | 54 | # Run "mix help compile.app" to learn about applications. 55 | def application do 56 | [ 57 | extra_applications: [:logger, :ssl], 58 | mod: {Mint.Application, []} 59 | ] 60 | end 61 | 62 | defp package do 63 | [ 64 | licenses: ["Apache-2.0"], 65 | links: %{"GitHub" => @repo_url} 66 | ] 67 | end 68 | 69 | defp elixirc_paths(:test), do: ["lib", "test/support"] 70 | defp elixirc_paths(_env), do: ["lib"] 71 | 72 | # Run "mix help deps" to learn about dependencies. 73 | defp deps do 74 | [ 75 | {:castore, "~> 0.1.0 or ~> 1.0", optional: true}, 76 | {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0"}, 77 | 78 | # Dev/test dependencies 79 | {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, 80 | {:ex_doc, "~> 0.20", only: :dev}, 81 | {:excoveralls, "~> 0.18.0", only: :test}, 82 | {:mox, "~> 1.0", only: :test}, 83 | {:stream_data, "~> 1.0", only: [:dev, :test]} 84 | ] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, 3 | "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 5 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 6 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 7 | "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, 8 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 13 | "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, 14 | "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 16 | "stream_data": {:hex, :stream_data, "1.1.2", "05499eaec0443349ff877aaabc6e194e82bda6799b9ce6aaa1aadac15a9fdb4d", [:mix], [], "hexpm", "129558d2c77cbc1eb2f4747acbbea79e181a5da51108457000020a906813a1a9"}, 17 | } 18 | -------------------------------------------------------------------------------- /pages/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Mint is an HTTP client with a process-less architecture. Mint provides an API where the HTTP connection is represented by a functional and immutable data structure, a **connection**. Every operation you do on the connection (like sending a request) returns an updated connection. A connection wraps a socket that is in active mode (with `active: :once`). Messages coming from the socket are delivered to the process that created the connection. You can hand those messages to Mint so that they can be parsed into responses. 4 | 5 | Having a process-less architecture makes Mint a powerful and composable HTTP client. The developer has more flexibility compared to a client that forces an interface that includes processes. A Mint connection can be stored inside any kind of process, such as GenServers or GenStage processes. 6 | 7 | Another important feature enabled by the Mint architecture is that a single process can store and manage multiple Mint connections since they are simple data structures. When a message comes from a socket, it's easy to identify which connection it belongs to since that's the only connection that will return a response different from `:unknown` when the messages are parsed. This enables developers to use Mint in fine-tailored ways that suit their needs. 8 | 9 | ## Pooling 10 | 11 | The Mint connection architecture is low level. This means that it doesn't provide things like connection pooling out of the box. This is by design: it's hard to write a general purpose HTTP connection pool that fits all use cases and it's often better to write simple pools that better fit your own use case. In this page, we'll see a few possible uses of Mint including some simple connection pools that you can use as a base for writing your own. 12 | 13 | ## Usage examples 14 | 15 | Let's see a few example of how to use Mint connections. 16 | 17 | ### Wrapping a Mint connection in a GenServer 18 | 19 | In this example we will look at wrapping a single connection in a GenServer. This architecture can be useful if you only need to issue a few requests to the same host, or if you plan to have many connections spread over just as many processes. 20 | 21 | The way this architecture works is that a GenServer process holds the connection. The connection is started when the `ConnectionProcess` GenServer starts (in the `init/1` callback). When a request is made, the request is sent but the GenServer keeps processing stuff and doesn't directly reply to the process that asked to send the request. The GenServer will only reply to the caller once a response comes from the server. This allows the GenServer to keep sending requests and processing responses while making the request blocking for the caller. 22 | 23 | This asynchronous architecture also makes the GenServer usable from different processes. If you use HTTP/1, requests will appear to be concurrent to the callers of our GenServer, but the GenServer will pipeline the requests. If you use HTTP/2, the requests will be actually concurrent. 24 | If you want to avoid pipelining requests you need to manually queue them or reject them in case there's already an ongoing request. 25 | 26 | In this code we don't handle closed connections and failed requests (for brevity). For example, you could handle closed connections by having the GenServer try to reconnect after a backoff time. 27 | 28 | ```elixir 29 | defmodule ConnectionProcess do 30 | use GenServer 31 | 32 | require Logger 33 | 34 | defstruct [:conn, requests: %{}] 35 | 36 | def start_link({scheme, host, port}) do 37 | GenServer.start_link(__MODULE__, {scheme, host, port}) 38 | end 39 | 40 | def request(pid, method, path, headers, body) do 41 | GenServer.call(pid, {:request, method, path, headers, body}) 42 | end 43 | 44 | ## Callbacks 45 | 46 | @impl true 47 | def init({scheme, host, port}) do 48 | case Mint.HTTP.connect(scheme, host, port) do 49 | {:ok, conn} -> 50 | state = %__MODULE__{conn: conn} 51 | {:ok, state} 52 | 53 | {:error, reason} -> 54 | {:stop, reason} 55 | end 56 | end 57 | 58 | @impl true 59 | def handle_call({:request, method, path, headers, body}, from, state) do 60 | # In both the successful case and the error case, we make sure to update the connection 61 | # struct in the state since the connection is an immutable data structure. 62 | case Mint.HTTP.request(state.conn, method, path, headers, body) do 63 | {:ok, conn, request_ref} -> 64 | state = put_in(state.conn, conn) 65 | # We store the caller this request belongs to and an empty map as the response. 66 | # The map will be filled with status code, headers, and so on. 67 | state = put_in(state.requests[request_ref], %{from: from, response: %{}}) 68 | {:noreply, state} 69 | 70 | {:error, conn, reason} -> 71 | state = put_in(state.conn, conn) 72 | {:reply, {:error, reason}, state} 73 | end 74 | end 75 | 76 | @impl true 77 | def handle_info(message, state) do 78 | # We should handle the error case here as well, but we're omitting it for brevity. 79 | case Mint.HTTP.stream(state.conn, message) do 80 | :unknown -> 81 | _ = Logger.error(fn -> "Received unknown message: " <> inspect(message) end) 82 | {:noreply, state} 83 | 84 | {:ok, conn, responses} -> 85 | state = put_in(state.conn, conn) 86 | state = Enum.reduce(responses, state, &process_response/2) 87 | {:noreply, state} 88 | end 89 | end 90 | 91 | defp process_response({:status, request_ref, status}, state) do 92 | put_in(state.requests[request_ref].response[:status], status) 93 | end 94 | 95 | defp process_response({:headers, request_ref, headers}, state) do 96 | put_in(state.requests[request_ref].response[:headers], headers) 97 | end 98 | 99 | defp process_response({:data, request_ref, new_data}, state) do 100 | update_in(state.requests[request_ref].response[:data], fn data -> (data || "") <> new_data end) 101 | end 102 | 103 | # When the request is done, we use GenServer.reply/2 to reply to the caller that was 104 | # blocked waiting on this request. 105 | defp process_response({:done, request_ref}, state) do 106 | {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) 107 | GenServer.reply(from, {:ok, response}) 108 | state 109 | end 110 | 111 | # A request can also error, but we're not handling the erroneous responses for 112 | # brevity. 113 | end 114 | ``` 115 | -------------------------------------------------------------------------------- /pages/Decompression.md: -------------------------------------------------------------------------------- 1 | # Decompression 2 | 3 | Many web servers use compression to reduce the size of the payload to speed up delivery to clients, expecting clients to decompress the body of the request. Some of the common compression algorithms used are [gzip], [brotli], [zstd], or no compression at all. 4 | 5 | Clients may specify acceptable compression algorithms through the [`accept-encoding`][accept-encoding] request header. It's common for clients to supply one or more values in `accept-encoding`, for example `accept-encoding: gzip, br` in the order of preference. 6 | 7 | Servers will read the `accept-encoding` and `TE` request headers, and respond appropriately indicating which compression is used in the response body through the [`content-encoding`][content-encoding] or [`transfer-encoding`][transfer-encoding] response headers respectively. It's not as common to use multiple compression algorithms, but it is possible: for example, `content-encoding: gzip` or `content-encoding: br, gzip` (meaning it was compressed with `br` first, and then `gzip`). 8 | 9 | Mint is a low-level client so it doesn't have built-in support for decompression. In this guide we'll explore how to add support for decompression when using Mint. 10 | 11 | ## Decompressing the response body 12 | 13 | Starting with the [architecture example](Architecture.md#content), we're going add some logic to handle a finished request's compressed body. With some compression algorithms, it's possible to decompress body chunks as they come (in a streaming way), but let's look at an example that works for every compression algorithm by decompressing the whole response body when the response is done. 14 | 15 | This is where we start: 16 | 17 | ```elixir 18 | defp process_response({:done, request_ref}, state) do 19 | {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) 20 | GenServer.reply(from, {:ok, response}) 21 | state 22 | end 23 | ``` 24 | 25 | This function handles the response back to the blocked process that's waiting for the HTTP response. You'll see that it returns `{:ok, response}` with `response` being a map with `:status`, `:headers`, and `:data` fields. 26 | 27 | We need to attempt to decompress the data if the `content-encoding` header is present. We're going to work with `content-encoding`, but the same applies if compression is used in `transfer-encoding`. Let's add a function that finds all applied compression algorithms. 28 | 29 | ```elixir 30 | # Returns a list of found compressions or [] if none found. 31 | defp get_content_encoding_header(headers) do 32 | headers 33 | |> Enum.flat_map(fn {name, value} -> 34 | if String.downcase(name, :ascii) == "content-encoding" do 35 | value 36 | |> String.downcase() 37 | |> String.split(",", trim: true) 38 | |> Stream.map(&String.trim/1) 39 | else 40 | [] 41 | end 42 | end) 43 | |> Enum.reverse() 44 | end 45 | ``` 46 | 47 | We use a combination of `Enum.flat_map/2` and `String.split/3` because the values can be comma-separated and spread over multiple headers. Now we should have a list like `["gzip"]`. We reversed the compression algorithms so that we decompress from the last one to the first one. Let's use this in another function that handles the decompression. Thankfully, Erlang ships with built-in support for gzip algorithm. 48 | 49 | ```elixir 50 | defp decompress_data(data, algorithms) do 51 | Enum.reduce(algorithms, data, &decompress_with_algorithm/2) 52 | end 53 | 54 | defp decompress_with_algorithm(gzip, data) when gzip in ["gzip", "x-gzip"], 55 | do: :zlib.gunzip(data) 56 | 57 | defp decompress_with_algorithm("identity", data), 58 | do: data 59 | 60 | defp decompress_with_algorithm(algorithm, data), 61 | do: raise "unsupported decompression algorithm: #{inspect(algorithm)}" 62 | ``` 63 | 64 | In case you come across an unsupported algorithm, you might want to log or raise an exception so you can see where you may be lacking support. 65 | 66 | Now let's put it together. We can use these new functions when the request is done and pass the result back to the client. 67 | 68 | ```elixir 69 | defp process_response({:done, request_ref}, state) do 70 | {%{response: response, from: from}, state} = pop_in(state.requests[request_ref]) 71 | 72 | # Handle compression here. 73 | compression_algorithms = get_content_encoding_header(response.headers) 74 | response = update_in(response.data, &decompress_data(&1, compression_algorithms)) 75 | 76 | GenServer.reply(from, {:ok, response}) 77 | 78 | state 79 | end 80 | ``` 81 | 82 | Now you can decompress responses! Above is a simple approach to a potentially complex response, so there is room for error. For example, this guide does not handle decompression errors or compression through `transfer-encoding` (although the code stays very similar in that case). 83 | 84 | 85 | [gzip]: https://tools.ietf.org/html/rfc1952 86 | [brotli]: https://tools.ietf.org/html/rfc7932 87 | [zstd]: https://tools.ietf.org/html/rfc8478 88 | [accept-encoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding 89 | [content-encoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding 90 | [transfer-encoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding 91 | -------------------------------------------------------------------------------- /plts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-mint/mint/763e70cdaf63bfec144b71c641206c02821b3210/plts/.gitkeep -------------------------------------------------------------------------------- /src/mint_shims.erl: -------------------------------------------------------------------------------- 1 | %% Shims for functions introduced in recent Erlang/OTP releases, 2 | %% to enable use of Mint on older releases. The code in this module 3 | %% was taken directly from the Erlang/OTP project. 4 | %% 5 | %% File: lib/public_key/src/public_key.erl 6 | %% Tag: OTP-20.3.4 7 | %% Commit: f2c1d537dc28ffbde5d42aedec70bf4c6574c3ea 8 | %% Changes from original file: 9 | %% - extracted pkix_verify_hostname/2 and /3, and any private 10 | %% functions they depend upon 11 | %% - replaced local calls to other public functions in the 12 | %% 'public_key' module with fully qualified equivalents 13 | %% - replaced local type references with fully qualified equivalents 14 | %% 15 | %% The original license follows: 16 | 17 | %% %CopyrightBegin% 18 | %% 19 | %% Copyright Ericsson AB 2013-2017. All Rights Reserved. 20 | %% 21 | %% Licensed under the Apache License, Version 2.0 (the "License"); 22 | %% you may not use this file except in compliance with the License. 23 | %% You may obtain a copy of the License at 24 | %% 25 | %% http://www.apache.org/licenses/LICENSE-2.0 26 | %% 27 | %% Unless required by applicable law or agreed to in writing, software 28 | %% distributed under the License is distributed on an "AS IS" BASIS, 29 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | %% See the License for the specific language governing permissions and 31 | %% limitations under the License. 32 | %% 33 | %% %CopyrightEnd% 34 | %% 35 | -module(mint_shims). 36 | 37 | -include_lib("public_key/include/public_key.hrl"). 38 | 39 | -export([pkix_verify_hostname/2, pkix_verify_hostname/3]). 40 | 41 | %-------------------------------------------------------------------- 42 | -spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(), 43 | ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | public_key:oid(), string()}]) -> boolean(). 44 | 45 | -spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(), 46 | ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | public_key:oid(), string()}], 47 | Options :: proplists:proplist()) -> boolean(). 48 | 49 | %% Description: Validates a hostname to RFC 6125 50 | %%-------------------------------------------------------------------- 51 | pkix_verify_hostname(Cert, ReferenceIDs) -> 52 | pkix_verify_hostname(Cert, ReferenceIDs, []). 53 | 54 | pkix_verify_hostname(BinCert, ReferenceIDs, Options) when is_binary(BinCert) -> 55 | pkix_verify_hostname(public_key:pkix_decode_cert(BinCert,otp), ReferenceIDs, Options); 56 | 57 | pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, ReferenceIDs0, Opts) -> 58 | MatchFun = proplists:get_value(match_fun, Opts, undefined), 59 | FailCB = proplists:get_value(fail_callback, Opts, fun(_Cert) -> false end), 60 | FqdnFun = proplists:get_value(fqdn_fun, Opts, fun verify_hostname_extract_fqdn_default/1), 61 | 62 | ReferenceIDs = [{T,to_string(V)} || {T,V} <- ReferenceIDs0], 63 | PresentedIDs = 64 | try lists:keyfind(?'id-ce-subjectAltName', 65 | #'Extension'.extnID, 66 | TbsCert#'OTPTBSCertificate'.extensions) 67 | of 68 | #'Extension'{extnValue = ExtVals} -> 69 | [{T,to_string(V)} || {T,V} <- ExtVals]; 70 | false -> 71 | [] 72 | catch 73 | _:_ -> [] 74 | end, 75 | %% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]} 76 | case PresentedIDs of 77 | [] -> 78 | %% Fallback to CN-ids [rfc6125, ch6] 79 | case TbsCert#'OTPTBSCertificate'.subject of 80 | {rdnSequence,RDNseq} -> 81 | PresentedCNs = 82 | [{cn, to_string(V)} 83 | || ATVs <- RDNseq, % RDNseq is list-of-lists 84 | #'AttributeTypeAndValue'{type = ?'id-at-commonName', 85 | value = {_T,V}} <- ATVs 86 | % _T = kind of string (teletexString etc) 87 | ], 88 | %% Example of PresentedCNs: [{cn,"www.ericsson.se"}] 89 | %% match ReferenceIDs to PresentedCNs 90 | verify_hostname_match_loop(verify_hostname_fqnds(ReferenceIDs, FqdnFun), 91 | PresentedCNs, 92 | MatchFun, FailCB, Cert); 93 | 94 | _ -> 95 | false 96 | end; 97 | _ -> 98 | %% match ReferenceIDs to PresentedIDs 99 | case verify_hostname_match_loop(ReferenceIDs, PresentedIDs, 100 | MatchFun, FailCB, Cert) of 101 | false -> 102 | %% Try to extract DNS-IDs from URIs etc 103 | DNS_ReferenceIDs = 104 | [{dns_id,X} || X <- verify_hostname_fqnds(ReferenceIDs, FqdnFun)], 105 | verify_hostname_match_loop(DNS_ReferenceIDs, PresentedIDs, 106 | MatchFun, FailCB, Cert); 107 | true -> 108 | true 109 | end 110 | end. 111 | 112 | %%%---------------------------------------------------------------- 113 | %%% pkix_verify_hostname help functions 114 | verify_hostname_extract_fqdn_default({dns_id,S}) -> 115 | S; 116 | verify_hostname_extract_fqdn_default({uri_id,URI}) -> 117 | % Modified from original to remove dependency on http_uri:parse/1 from inets 118 | #{scheme := <<"https">>, host := Host} = 'Elixir.URI':parse(list_to_binary(URI)), 119 | binary_to_list(Host). 120 | 121 | 122 | verify_hostname_fqnds(L, FqdnFun) -> 123 | [E || E0 <- L, 124 | E <- [try case FqdnFun(E0) of 125 | default -> verify_hostname_extract_fqdn_default(E0); 126 | undefined -> undefined; % will make the "is_list(E)" test fail 127 | Other -> Other 128 | end 129 | catch _:_-> undefined % will make the "is_list(E)" test fail 130 | end], 131 | is_list(E), 132 | E =/= "", 133 | {error,einval} == inet:parse_address(E) 134 | ]. 135 | 136 | 137 | -define(srvName_OID, {1,3,6,1,4,1,434,2,2,1,37,0}). 138 | 139 | verify_hostname_match_default(Ref, Pres) -> 140 | verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)). 141 | 142 | verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> 143 | not lists:member($*, FQDN); 144 | verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> 145 | [F1|Fs] = string:tokens(FQDN, "."), 146 | [N1|Ns] = string:tokens(Name, "."), 147 | match_wild(F1,N1) andalso Fs==Ns; 148 | verify_hostname_match_default0({dns_id,R}, {dNSName,P}) -> 149 | R==P; 150 | verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) -> 151 | R==P; 152 | verify_hostname_match_default0({ip,R}, {iPAddress,P}) when length(P) == 4 -> 153 | %% IPv4 154 | try 155 | list_to_tuple(P) 156 | == if is_tuple(R), size(R)==4 -> R; 157 | is_list(R) -> ok(inet:parse_ipv4strict_address(R)) 158 | end 159 | catch 160 | _:_ -> 161 | false 162 | end; 163 | 164 | verify_hostname_match_default0({ip,R}, {iPAddress,P}) when length(P) == 16 -> 165 | %% IPv6. The length 16 is due to the certificate specification. 166 | try 167 | l16_to_tup(P) 168 | == if is_tuple(R), size(R)==8 -> R; 169 | is_list(R) -> ok(inet:parse_ipv6strict_address(R)) 170 | end 171 | catch 172 | _:_ -> 173 | false 174 | end; 175 | verify_hostname_match_default0({srv_id,R}, {srvName,P}) -> 176 | R==P; 177 | verify_hostname_match_default0({srv_id,R}, {?srvName_OID,P}) -> 178 | R==P; 179 | verify_hostname_match_default0(_, _) -> 180 | false. 181 | 182 | ok({ok,X}) -> X. 183 | 184 | l16_to_tup(L) -> list_to_tuple(l16_to_tup(L, [])). 185 | %% 186 | l16_to_tup([A,B|T], Acc) -> l16_to_tup(T, [(A bsl 8) bor B | Acc]); 187 | l16_to_tup([], Acc) -> lists:reverse(Acc). 188 | 189 | match_wild(A, [$*|B]) -> match_wild_suffixes(A, B); 190 | match_wild([C|A], [ C|B]) -> match_wild(A, B); 191 | match_wild([], []) -> true; 192 | match_wild(_, _) -> false. 193 | 194 | %% Match the parts after the only wildcard by comparing them from the end 195 | match_wild_suffixes(A, B) -> match_wild_sfx(lists:reverse(A), lists:reverse(B)). 196 | 197 | match_wild_sfx([$*|_], _) -> false; % Bad name (no wildcards allowed) 198 | match_wild_sfx(_, [$*|_]) -> false; % Bad pattern (no more wildcards allowed) 199 | match_wild_sfx([A|Ar], [A|Br]) -> match_wild_sfx(Ar, Br); 200 | match_wild_sfx(Ar, []) -> not lists:member($*, Ar); % Chk for bad name (= wildcards) 201 | match_wild_sfx(_, _) -> false. 202 | 203 | 204 | verify_hostname_match_loop(Refs0, Pres0, undefined, FailCB, Cert) -> 205 | Pres = lists:map(fun to_lower_ascii/1, Pres0), 206 | Refs = lists:map(fun to_lower_ascii/1, Refs0), 207 | lists:any( 208 | fun(R) -> 209 | lists:any(fun(P) -> 210 | verify_hostname_match_default(R,P) orelse FailCB(Cert) 211 | end, Pres) 212 | end, Refs); 213 | verify_hostname_match_loop(Refs, Pres, MatchFun, FailCB, Cert) -> 214 | lists:any( 215 | fun(R) -> 216 | lists:any(fun(P) -> 217 | (case MatchFun(R,P) of 218 | default -> verify_hostname_match_default(R,P); 219 | Bool -> Bool 220 | end) orelse FailCB(Cert) 221 | end, 222 | Pres) 223 | end, 224 | Refs). 225 | 226 | 227 | to_lower_ascii({ip,_}=X) -> X; 228 | to_lower_ascii({iPAddress,_}=X) -> X; 229 | to_lower_ascii(S) when is_list(S) -> lists:map(fun to_lower_ascii/1, S); 230 | to_lower_ascii({T,S}) -> {T, to_lower_ascii(S)}; 231 | to_lower_ascii(C) when $A =< C,C =< $Z -> C + ($a-$A); 232 | to_lower_ascii(C) -> C. 233 | 234 | to_string(S) when is_list(S) -> S; 235 | to_string(B) when is_binary(B) -> binary_to_list(B); 236 | to_string(X) -> X. 237 | -------------------------------------------------------------------------------- /test/docker/tinyproxy-auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/alpine:3.20.1 2 | 3 | RUN apk add --no-cache \ 4 | tinyproxy 5 | 6 | RUN chown -R nobody: /home 7 | WORKDIR /home 8 | USER nobody 9 | 10 | RUN mkdir -p /home/var/log/tinyproxy && mkdir -p /home/var/run/tinyproxy 11 | 12 | COPY ./tinyproxy.conf /home/etc/tinyproxy/tinyproxy.conf 13 | 14 | ENTRYPOINT ["tinyproxy", "-d", "-c", "/home/etc/tinyproxy/tinyproxy.conf"] 15 | -------------------------------------------------------------------------------- /test/docker/tinyproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/alpine:3.20.1 2 | 3 | RUN apk add --no-cache \ 4 | tinyproxy 5 | 6 | RUN chown -R nobody: /home 7 | WORKDIR /home 8 | USER nobody 9 | 10 | RUN mkdir -p /home/var/log/tinyproxy && mkdir -p /home/var/run/tinyproxy 11 | 12 | COPY ./tinyproxy.conf /home/etc/tinyproxy/tinyproxy.conf 13 | 14 | ENTRYPOINT ["tinyproxy", "-d", "-c", "/home/etc/tinyproxy/tinyproxy.conf"] 15 | -------------------------------------------------------------------------------- /test/http_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTPTest do 2 | use ExUnit.Case, async: true 3 | doctest Mint.HTTP 4 | end 5 | -------------------------------------------------------------------------------- /test/mint/core/transport/tcp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.Core.Transport.TCPTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Mint.Core.Transport.TCP 5 | 6 | describe "connect/3" do 7 | test "can connect to IPv6 addresses" do 8 | tcp_opts = [ 9 | :inet6, 10 | mode: :binary, 11 | packet: :raw, 12 | active: false, 13 | reuseaddr: true, 14 | nodelay: true 15 | ] 16 | 17 | {:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts) 18 | {:ok, {_address, port}} = :inet.sockname(listen_socket) 19 | 20 | task = 21 | Task.async(fn -> 22 | {:ok, _socket} = :gen_tcp.accept(listen_socket) 23 | end) 24 | 25 | assert {:ok, _socket} = 26 | TCP.connect({127, 0, 0, 1}, port, 27 | active: false, 28 | inet6: true, 29 | timeout: 1000 30 | ) 31 | 32 | assert {:ok, _server_socket} = Task.await(task) 33 | end 34 | 35 | test "can fall back to IPv4 if IPv6 fails" do 36 | tcp_opts = [ 37 | :inet6, 38 | mode: :binary, 39 | packet: :raw, 40 | active: false, 41 | reuseaddr: true, 42 | nodelay: true 43 | ] 44 | 45 | {:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts) 46 | {:ok, {_address, port}} = :inet.sockname(listen_socket) 47 | 48 | task = 49 | Task.async(fn -> 50 | {:ok, _socket} = :gen_tcp.accept(listen_socket) 51 | end) 52 | 53 | assert {:ok, _socket} = 54 | TCP.connect("localhost", port, 55 | active: false, 56 | inet6: true, 57 | timeout: 1000 58 | ) 59 | 60 | assert {:ok, _server_socket} = Task.await(task) 61 | end 62 | 63 | test "does not fall back to IPv4 if IPv4 is disabled" do 64 | tcp_opts = [ 65 | :inet, 66 | mode: :binary, 67 | packet: :raw, 68 | active: false, 69 | reuseaddr: true, 70 | nodelay: true 71 | ] 72 | 73 | {:ok, listen_socket} = :gen_tcp.listen(0, tcp_opts) 74 | {:ok, {_address, port}} = :inet.sockname(listen_socket) 75 | 76 | Task.async(fn -> 77 | {:ok, _socket} = :gen_tcp.accept(listen_socket) 78 | end) 79 | 80 | assert {:error, %Mint.TransportError{reason: :econnrefused}} = 81 | TCP.connect("localhost", port, 82 | active: false, 83 | inet6: true, 84 | inet4: false, 85 | timeout: 1000 86 | ) 87 | end 88 | end 89 | 90 | describe "controlling_process/2" do 91 | @describetag :capture_log 92 | 93 | setup do 94 | parent = self() 95 | ref = make_ref() 96 | 97 | ssl_opts = [ 98 | mode: :binary, 99 | packet: :raw, 100 | active: false, 101 | reuseaddr: true, 102 | nodelay: true 103 | ] 104 | 105 | spawn_link(fn -> 106 | {:ok, listen_socket} = :gen_tcp.listen(0, ssl_opts) 107 | {:ok, {_address, port}} = :inet.sockname(listen_socket) 108 | send(parent, {ref, port}) 109 | 110 | {:ok, socket} = :gen_tcp.accept(listen_socket) 111 | 112 | send(parent, {ref, socket}) 113 | 114 | # Keep the server alive forever. 115 | :ok = Process.sleep(:infinity) 116 | end) 117 | 118 | assert_receive {^ref, port} when is_integer(port), 500 119 | 120 | {:ok, socket} = TCP.connect("localhost", port, []) 121 | assert_receive {^ref, server_socket}, 200 122 | 123 | {:ok, server_port: port, socket: socket, server_socket: server_socket} 124 | end 125 | 126 | test "changing the controlling process of a active: :once socket", 127 | %{socket: socket, server_socket: server_socket} do 128 | parent = self() 129 | ref = make_ref() 130 | 131 | # Send two SSL messages (that get translated to Erlang messages right 132 | # away because of "nodelay: true"), but wait after each one so that 133 | # it actually arrives and we can set the socket back to active: :once. 134 | :ok = TCP.setopts(socket, active: :once) 135 | :ok = :gen_tcp.send(server_socket, "some data 1") 136 | Process.sleep(100) 137 | 138 | :ok = TCP.setopts(socket, active: :once) 139 | :ok = :gen_tcp.send(server_socket, "some data 2") 140 | 141 | wait_until_passes(500, fn -> 142 | {:messages, messages} = Process.info(self(), :messages) 143 | assert {:tcp, socket, "some data 1"} in messages 144 | assert {:tcp, socket, "some data 2"} in messages 145 | end) 146 | 147 | other_process = spawn_link(fn -> process_mirror(parent, ref) end) 148 | 149 | assert :ok = TCP.controlling_process(socket, other_process) 150 | 151 | assert_receive {^ref, {:tcp, ^socket, "some data 1"}} 152 | assert_receive {^ref, {:tcp, ^socket, "some data 2"}} 153 | 154 | refute_received _message 155 | end 156 | 157 | test "changing the controlling process of a passive socket", 158 | %{socket: socket, server_socket: server_socket} do 159 | parent = self() 160 | ref = make_ref() 161 | 162 | :ok = :gen_tcp.send(server_socket, "some data") 163 | 164 | other_process = 165 | spawn_link(fn -> 166 | assert_receive message, 500 167 | send(parent, {ref, message}) 168 | end) 169 | 170 | assert :ok = TCP.controlling_process(socket, other_process) 171 | assert {:ok, [active: false]} = TCP.getopts(socket, [:active]) 172 | :ok = TCP.setopts(socket, active: :once) 173 | 174 | assert_receive {^ref, {:tcp, ^socket, "some data"}}, 500 175 | 176 | refute_received _message 177 | end 178 | 179 | test "changing the controlling process of a closed socket", 180 | %{socket: socket} do 181 | other_process = spawn_link(fn -> :ok = Process.sleep(:infinity) end) 182 | 183 | :ok = TCP.close(socket) 184 | 185 | assert {:error, _error} = TCP.controlling_process(socket, other_process) 186 | end 187 | end 188 | 189 | defp process_mirror(parent, ref) do 190 | receive do 191 | message -> 192 | send(parent, {ref, message}) 193 | process_mirror(parent, ref) 194 | end 195 | end 196 | 197 | defp wait_until_passes(time_left, fun) when time_left <= 0 do 198 | fun.() 199 | end 200 | 201 | defp wait_until_passes(time_left, fun) do 202 | fun.() 203 | rescue 204 | _exception -> 205 | Process.sleep(10) 206 | wait_until_passes(time_left - 10, fun) 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /test/mint/http1/conn_properties_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.PropertiesTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | 5 | import Mint.HTTP1.TestHelpers 6 | 7 | alias Mint.{HTTP1, HTTP1.TestServer} 8 | 9 | setup do 10 | {:ok, port, server_ref} = TestServer.start() 11 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", port) 12 | assert_receive {^server_ref, _server_socket} 13 | [conn: conn] 14 | end 15 | 16 | property "body with content-length", %{conn: conn} do 17 | {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil) 18 | response = "HTTP/1.1 200 OK\r\ncontent-length: 10\r\n\r\n0123456789" 19 | 20 | check all byte_chunks <- random_chunks(response) do 21 | {conn, responses} = 22 | Enum.reduce(byte_chunks, {conn, []}, fn bytes, {conn, responses} -> 23 | assert {:ok, conn, new_responses} = HTTP1.stream(conn, {:tcp, conn.socket, bytes}) 24 | 25 | {conn, responses ++ new_responses} 26 | end) 27 | 28 | assert [status, headers | rest] = responses 29 | assert {:status, ^ref, 200} = status 30 | assert {:headers, ^ref, [{"content-length", "10"}]} = headers 31 | assert merge_body(rest, ref) == "0123456789" 32 | assert conn.buffer == "" 33 | end 34 | end 35 | 36 | property "body with chunked transfer-encoding split on every byte", %{conn: conn} do 37 | {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil) 38 | 39 | response = 40 | "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n" <> 41 | "2meta\r\n01\r\n2\r\n23\r\n0meta\r\nmy-trailer: value\r\n\r\n" 42 | 43 | check all byte_chunks <- random_chunks(response) do 44 | {conn, responses} = 45 | Enum.reduce(byte_chunks, {conn, []}, fn bytes, {conn, responses} -> 46 | assert {:ok, conn, new_responses} = HTTP1.stream(conn, {:tcp, conn.socket, bytes}) 47 | 48 | {conn, responses ++ new_responses} 49 | end) 50 | 51 | assert [status, headers | rest] = responses 52 | assert {:status, ^ref, 200} = status 53 | assert {:headers, ^ref, [{"transfer-encoding", "chunked"}]} = headers 54 | assert merge_body_with_trailers(rest, ref) == {"0123", [{"my-trailer", "value"}]} 55 | assert conn.buffer == "" 56 | end 57 | end 58 | 59 | property "pipeline with multiple responses in single message", %{conn: conn} do 60 | {:ok, conn, ref1} = HTTP1.request(conn, "GET", "/", [], nil) 61 | {:ok, conn, ref2} = HTTP1.request(conn, "GET", "/", [], nil) 62 | {:ok, conn, ref3} = HTTP1.request(conn, "GET", "/", [], nil) 63 | response = "HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nXXXXX" 64 | responses = for _ <- 1..3, do: response, into: "" 65 | 66 | check all byte_chunks <- random_chunks(responses) do 67 | {_conn, responses} = 68 | Enum.reduce(byte_chunks, {conn, []}, fn bytes, {conn, responses} -> 69 | assert {:ok, conn, new_responses} = HTTP1.stream(conn, {:tcp, conn.socket, bytes}) 70 | 71 | {conn, responses ++ new_responses} 72 | end) 73 | 74 | assert [{:status, ^ref1, _}, {:headers, ^ref1, _} | responses] = responses 75 | assert {"XXXXX", responses} = merge_pipelined_body(responses, ref1) 76 | assert [{:status, ^ref2, _}, {:headers, ^ref2, _} | responses] = responses 77 | assert {"XXXXX", responses} = merge_pipelined_body(responses, ref2) 78 | assert [{:status, ^ref3, _}, {:headers, ^ref3, _} | responses] = responses 79 | assert {"XXXXX", []} = merge_pipelined_body(responses, ref3) 80 | end 81 | end 82 | 83 | defp random_chunks(binary) do 84 | size = byte_size(binary) 85 | 86 | StreamData.bind(StreamData.integer(0..size), fn num_splits -> 87 | StreamData.integer(1..(size - 1)) 88 | |> Enum.take(num_splits) 89 | |> Enum.uniq() 90 | |> Enum.sort() 91 | |> Enum.reduce({[], binary, 0}, fn split, {chunks, rest, prev_split} -> 92 | length = split - prev_split 93 | <> = rest 94 | {[chunk | chunks], rest, split} 95 | end) 96 | |> join_last_chunk() 97 | |> Enum.reverse() 98 | |> StreamData.constant() 99 | end) 100 | end 101 | 102 | defp join_last_chunk({chunks, rest, _last_split}), do: [rest | chunks] 103 | end 104 | -------------------------------------------------------------------------------- /test/mint/http1/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.IntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mint.HTTP1.TestHelpers 5 | 6 | alias Mint.{TransportError, HTTP1, HttpBin} 7 | 8 | @moduletag :requires_internet_connection 9 | 10 | describe "local httpbin" do 11 | test "200 response" do 12 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 13 | assert {:ok, conn, request} = HTTP1.request(conn, "GET", "/", [], nil) 14 | assert {:ok, conn, responses} = receive_stream(conn) 15 | 16 | assert conn.buffer == "" 17 | assert [status, headers | responses] = responses 18 | assert {:status, ^request, 200} = status 19 | assert {:headers, ^request, headers} = headers 20 | assert get_header(headers, "connection") == ["keep-alive"] 21 | assert merge_body(responses, request) =~ "httpbin" 22 | end 23 | 24 | test "POST body" do 25 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 26 | assert {:ok, conn, request} = HTTP1.request(conn, "POST", "/post", [], "BODY") 27 | assert {:ok, conn, responses} = receive_stream(conn) 28 | 29 | assert conn.buffer == "" 30 | assert [status, headers | responses] = responses 31 | assert {:status, ^request, 200} = status 32 | assert {:headers, ^request, _} = headers 33 | assert merge_body(responses, request) =~ ~s("BODY") 34 | end 35 | 36 | test "POST body streaming" do 37 | headers = [{"content-length", "4"}] 38 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 39 | assert {:ok, conn, request} = HTTP1.request(conn, "POST", "/post", headers, :stream) 40 | assert {:ok, conn} = HTTP1.stream_request_body(conn, request, "BO") 41 | assert {:ok, conn} = HTTP1.stream_request_body(conn, request, "DY") 42 | assert {:ok, conn} = HTTP1.stream_request_body(conn, request, :eof) 43 | assert {:ok, conn, responses} = receive_stream(conn) 44 | 45 | assert conn.buffer == "" 46 | assert [status, headers | responses] = responses 47 | assert {:status, ^request, 200} = status 48 | assert {:headers, ^request, _} = headers 49 | assert merge_body(responses, request) =~ ~s("BODY") 50 | end 51 | 52 | test "pipelining" do 53 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 54 | assert {:ok, conn, request1} = HTTP1.request(conn, "GET", "/", [], nil) 55 | assert {:ok, conn, request2} = HTTP1.request(conn, "GET", "/", [], nil) 56 | assert {:ok, conn, request3} = HTTP1.request(conn, "GET", "/", [], nil) 57 | assert {:ok, conn, request4} = HTTP1.request(conn, "GET", "/", [], nil) 58 | 59 | assert {:ok, conn, [_status, _headers | responses1]} = receive_stream(conn) 60 | assert {:ok, conn, [_status, _headers | responses2]} = receive_stream(conn) 61 | assert {:ok, conn, [_status, _headers | responses3]} = receive_stream(conn) 62 | assert {:ok, _conn, [_status, _headers | responses4]} = receive_stream(conn) 63 | 64 | assert merge_body(responses1, request1) =~ "A simple HTTP Request & Response Service" 65 | 66 | assert merge_body(responses2, request2) =~ "A simple HTTP Request & Response Service" 67 | 68 | assert merge_body(responses3, request3) =~ "A simple HTTP Request & Response Service" 69 | 70 | assert merge_body(responses4, request4) =~ "A simple HTTP Request & Response Service" 71 | end 72 | 73 | test "chunked with no chunks" do 74 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 75 | assert {:ok, conn, request} = HTTP1.request(conn, "GET", "/stream-bytes/0", [], nil) 76 | 77 | assert {:ok, _conn, [_status, _headers | responses]} = receive_stream(conn) 78 | 79 | assert byte_size(merge_body(responses, request)) == 0 80 | end 81 | 82 | test "chunked with single chunk" do 83 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 84 | 85 | assert {:ok, conn, request} = 86 | HTTP1.request(conn, "GET", "/stream-bytes/1024?chunk_size=1024", [], nil) 87 | 88 | assert {:ok, _conn, [_status, _headers | responses]} = receive_stream(conn) 89 | 90 | assert byte_size(merge_body(responses, request)) == 1024 91 | end 92 | 93 | test "chunked with multiple chunks" do 94 | assert {:ok, conn} = HTTP1.connect(:http, "localhost", 8080) 95 | 96 | assert {:ok, conn, request} = 97 | HTTP1.request(conn, "GET", "/stream-bytes/1024?chunk_size=100", [], nil) 98 | 99 | assert {:ok, _conn, [_status, _headers | responses]} = receive_stream(conn) 100 | 101 | assert byte_size(merge_body(responses, request)) == 1024 102 | end 103 | end 104 | 105 | describe "twitter.com" do 106 | test "timeout with http" do 107 | assert {:error, %TransportError{reason: :timeout}} = 108 | HTTP1.connect(:http, "twitter.com", 80, transport_opts: [timeout: 0]) 109 | end 110 | 111 | test "timeout with https" do 112 | assert {:error, %TransportError{reason: :timeout}} = 113 | HTTP1.connect(:https, "twitter.com", 443, transport_opts: [timeout: 0]) 114 | end 115 | end 116 | 117 | describe "httpbin.org" do 118 | test "keep alive" do 119 | assert {:ok, conn} = 120 | HTTP1.connect(:https, HttpBin.host(), HttpBin.https_port(), 121 | transport_opts: HttpBin.https_transport_opts() 122 | ) 123 | 124 | assert {:ok, conn, request} = HTTP1.request(conn, "GET", "/", [], nil) 125 | assert {:ok, conn, responses} = receive_stream(conn) 126 | 127 | assert conn.buffer == "" 128 | assert [status, headers | responses] = responses 129 | assert {:status, ^request, 200} = status 130 | assert {:headers, ^request, _} = headers 131 | assert merge_body(responses, request) =~ "Other Utilities" 132 | 133 | assert {:ok, conn} = 134 | HTTP1.connect(:https, HttpBin.host(), HttpBin.https_port(), 135 | transport_opts: HttpBin.https_transport_opts() 136 | ) 137 | 138 | assert {:ok, conn, request} = HTTP1.request(conn, "GET", "/", [], nil) 139 | assert {:ok, conn, responses} = receive_stream(conn) 140 | 141 | assert conn.buffer == "" 142 | assert [status, headers | responses] = responses 143 | assert {:status, ^request, 200} = status 144 | assert {:headers, ^request, _} = headers 145 | assert merge_body(responses, request) =~ "Other Utilities" 146 | end 147 | 148 | test "SSL with missing CA cacertfile" do 149 | assert {:error, %TransportError{reason: reason}} = 150 | HTTP1.connect( 151 | :https, 152 | HttpBin.host(), 153 | HttpBin.https_port(), 154 | transport_opts: [ 155 | cacertfile: "test/support/empty_cacerts.pem", 156 | log_alert: false, 157 | reuse_sessions: false 158 | ] 159 | ) 160 | 161 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 162 | assert reason == {:tls_alert, ~c"unknown ca"} or 163 | match?({:tls_alert, {:unknown_ca, _}}, reason), 164 | "expecter reason to look like {:tls_alert, _}, got: #{inspect(reason)}" 165 | end 166 | 167 | test "SSL, path, long body" do 168 | assert {:ok, conn} = 169 | HTTP1.connect(:https, HttpBin.host(), HttpBin.https_port(), 170 | transport_opts: HttpBin.https_transport_opts() 171 | ) 172 | 173 | assert {:ok, conn, request} = HTTP1.request(conn, "GET", "/bytes/50000", [], nil) 174 | assert {:ok, conn, responses} = receive_stream(conn) 175 | 176 | assert conn.buffer == "" 177 | assert [status, headers | responses] = responses 178 | assert {:status, ^request, 200} = status 179 | assert {:headers, ^request, _} = headers 180 | assert byte_size(merge_body(responses, request)) == 50000 181 | end 182 | 183 | test "SSL with missing CA cacerts" do 184 | assert {:error, %TransportError{reason: reason}} = 185 | HTTP1.connect( 186 | :https, 187 | "localhost", 188 | Mint.HttpBin.https_port(), 189 | transport_opts: [cacerts: [], log_alert: false, reuse_sessions: false] 190 | ) 191 | 192 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 193 | # Newer OTP versions treat empty list for `cacerts` as if the option was not set 194 | assert reason == {:tls_alert, ~c"unknown ca"} or 195 | match?({:tls_alert, {:unknown_ca, _}}, reason) or 196 | reason == {:options, {:cacertfile, []}} or :closed 197 | end 198 | end 199 | 200 | describe "badssl.com" do 201 | @describetag :requires_internet_connection 202 | 203 | @tag :capture_log 204 | test "SSL with bad certificate" do 205 | assert {:error, %TransportError{reason: reason}} = 206 | HTTP1.connect(:https, "untrusted-root.badssl.com", 443, 207 | transport_opts: [log_alert: false, reuse_sessions: false] 208 | ) 209 | 210 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 211 | assert reason == {:tls_alert, ~c"unknown ca"} or 212 | match?({:tls_alert, {:unknown_ca, _}}, reason) 213 | 214 | assert {:ok, _conn} = 215 | HTTP1.connect(:https, "untrusted-root.badssl.com", 443, 216 | transport_opts: [verify: :verify_none] 217 | ) 218 | end 219 | 220 | @tag :capture_log 221 | test "SSL with bad hostname" do 222 | assert {:error, %TransportError{reason: reason}} = 223 | HTTP1.connect(:https, "wrong.host.badssl.com", 443, 224 | transport_opts: [log_alert: false, reuse_sessions: false] 225 | ) 226 | 227 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 228 | assert reason == {:tls_alert, ~c"handshake failure"} or 229 | match?({:tls_alert, {:handshake_failure, _}}, reason) 230 | 231 | assert {:ok, _conn} = 232 | HTTP1.connect(:https, "wrong.host.badssl.com", 443, 233 | transport_opts: [verify: :verify_none] 234 | ) 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /test/mint/http1/parse_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.ParseTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | 5 | import Mint.HTTP1.Parse 6 | 7 | test "content_length_header/1" do 8 | assert content_length_header("0") == {:ok, 0} 9 | assert content_length_header("100") == {:ok, 100} 10 | assert content_length_header("200 ") == {:ok, 200} 11 | 12 | assert content_length_header("foo") == 13 | {:error, {:invalid_content_length_header, "foo"}} 14 | 15 | assert content_length_header("-10") == 16 | {:error, {:invalid_content_length_header, "-10"}} 17 | end 18 | 19 | test "connection_header/1" do 20 | assert connection_header("close") == {:ok, ["close"]} 21 | assert connection_header("close ") == {:ok, ["close"]} 22 | assert connection_header("Keep-Alive") == {:ok, ["keep-alive"]} 23 | assert connection_header("keep-alive, Upgrade") == {:ok, ["keep-alive", "upgrade"]} 24 | assert connection_header("keep-alive, Upgrade ") == {:ok, ["keep-alive", "upgrade"]} 25 | 26 | assert connection_header("\n") == {:error, {:invalid_token_list, "\n"}} 27 | assert connection_header("") == {:error, :empty_token_list} 28 | end 29 | 30 | test "transfer_encoding_header/1" do 31 | assert transfer_encoding_header("deflate") == {:ok, ["deflate"]} 32 | assert transfer_encoding_header("deflate ") == {:ok, ["deflate"]} 33 | assert transfer_encoding_header("gzip, Chunked") == {:ok, ["gzip", "chunked"]} 34 | assert transfer_encoding_header("gzip, Chunked ") == {:ok, ["gzip", "chunked"]} 35 | 36 | assert transfer_encoding_header("\n") == {:error, {:invalid_token_list, "\n"}} 37 | assert transfer_encoding_header("") == {:error, :empty_token_list} 38 | end 39 | 40 | describe "token_list_downcase/1" do 41 | property "returns an empty list if there's no token" do 42 | no_tokens_generator = string([?\s, ?\t, ?,]) 43 | 44 | check all string <- no_tokens_generator, max_runs: 25 do 45 | assert token_list_downcase(string) == {:ok, []} 46 | end 47 | end 48 | 49 | property "parses lists of tokens and downcases them" do 50 | whitespace_generator = string([?\s, ?\t]) 51 | 52 | check all tokens <- list_of(string(:alphanumeric, min_length: 1)), 53 | whitespace <- whitespace_generator, 54 | string = Enum.join(tokens, whitespace <> "," <> whitespace), 55 | max_runs: 30 do 56 | assert token_list_downcase(string) == {:ok, Enum.map(tokens, &String.downcase/1)} 57 | end 58 | end 59 | 60 | test "parses practical examples" do 61 | assert token_list_downcase("foo") == {:ok, ["foo"]} 62 | assert token_list_downcase("foo, bar") == {:ok, ["foo", "bar"]} 63 | assert token_list_downcase("FOO,bAr") == {:ok, ["foo", "bar"]} 64 | assert token_list_downcase(" , ,,, foo , , ") == {:ok, ["foo"]} 65 | end 66 | 67 | test "throws {:mint, :invalid_token_list} for invalid tokens" do 68 | assert token_list_downcase("\n") == :error 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/mint/http1/request_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.RequestTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | 5 | alias Mint.HTTP1.Request 6 | 7 | describe "encode/5" do 8 | test "with header" do 9 | assert encode_request("GET", "/", [{"foo", "bar"}], nil) == 10 | request_string(""" 11 | GET / HTTP/1.1 12 | foo: bar 13 | 14 | """) 15 | end 16 | 17 | test "with body" do 18 | assert encode_request("GET", "/", [], "BODY") == 19 | request_string(""" 20 | GET / HTTP/1.1 21 | 22 | BODY\ 23 | """) 24 | end 25 | 26 | test "with body and headers" do 27 | assert encode_request("POST", "/some-url", [{"foo", "bar"}], "hello!") == 28 | request_string(""" 29 | POST /some-url HTTP/1.1 30 | foo: bar 31 | 32 | hello!\ 33 | """) 34 | end 35 | 36 | test "invalid header name" do 37 | assert Request.encode("GET", "/", [{"f oo", "bar"}], nil) == 38 | {:error, {:invalid_header_name, "f oo"}} 39 | end 40 | 41 | test "invalid header value" do 42 | assert Request.encode("GET", "/", [{"foo", "bar\r\n"}], nil) == 43 | {:error, {:invalid_header_value, "foo", "bar\r\n"}} 44 | end 45 | end 46 | 47 | describe "encode_chunk/1" do 48 | test ":eof" do 49 | assert IO.iodata_to_binary(Request.encode_chunk(:eof)) == "0\r\n\r\n" 50 | end 51 | 52 | test "iodata" do 53 | iodata = "foo" 54 | assert IO.iodata_to_binary(Request.encode_chunk(iodata)) == "3\r\nfoo\r\n" 55 | 56 | iodata = ["hello ", ?w, [["or"], ?l], ?d] 57 | assert IO.iodata_to_binary(Request.encode_chunk(iodata)) == "B\r\nhello world\r\n" 58 | end 59 | 60 | property "encoded chunk always contains at least two CRLFs" do 61 | check all iodata <- iodata() do 62 | encoded = iodata |> Request.encode_chunk() |> IO.iodata_to_binary() 63 | assert String.ends_with?(encoded, "\r\n") 64 | assert encoded |> String.replace_suffix("\r\n", "") |> String.contains?("\r\n") 65 | end 66 | end 67 | end 68 | 69 | defp encode_request(method, target, headers, body) do 70 | assert {:ok, iodata} = Request.encode(method, target, headers, body) 71 | IO.iodata_to_binary(iodata) 72 | end 73 | 74 | defp request_string(string) do 75 | String.replace(string, "\n", "\r\n") 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/mint/http2/frame_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP2.FrameTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | 5 | import Bitwise, only: [bor: 2] 6 | 7 | import Mint.HTTP2.Frame, except: [decode_next: 1, encode_raw: 4] 8 | 9 | alias Mint.HTTP2.Frame 10 | 11 | test "set_flags/2" do 12 | assert set_flags(:ping, [:ack]) == 0x01 13 | assert set_flags(:data, [:end_stream]) == 0x01 14 | assert_raise FunctionClauseError, fn -> set_flags(:data, [:ack]) end 15 | end 16 | 17 | test "set_flags/3" do 18 | assert set_flags(0x01, :data, [:padded]) == bor(0x01, 0x08) 19 | assert_raise FunctionClauseError, fn -> set_flags(0x00, :data, [:ack]) end 20 | end 21 | 22 | test "flag_set?/3" do 23 | assert flag_set?(0x08, :data, :padded) == true 24 | assert flag_set?(0x00, :data, :padded) == false 25 | 26 | # Typing violation if done at compile time. 27 | assert_raise FunctionClauseError, fn -> 28 | Code.eval_quoted(quote do: Mint.HTTP2.Frame.flag_set?(0x00, :data, :ack)) 29 | end 30 | end 31 | 32 | test "decode_next/1 with an incomplete frame" do 33 | assert Frame.decode_next(<<>>) == :more 34 | end 35 | 36 | describe "DATA" do 37 | test "without padding" do 38 | check all stream_id <- non_zero_stream_id(), 39 | data <- binary() do 40 | assert_round_trip data(stream_id: stream_id, flags: 0x00, data: data, padding: nil) 41 | end 42 | end 43 | 44 | test "with padding" do 45 | check all stream_id <- non_zero_stream_id(), 46 | data <- binary(), 47 | padding <- binary() do 48 | assert_round_trip data(stream_id: stream_id, flags: 0x08, data: data, padding: padding) 49 | end 50 | end 51 | 52 | test "with bad padding" do 53 | # "payload" is 4 bytes, the pad length is >= 5 bytes 54 | payload = <<5::8, "data">> 55 | debug_data = "the padding length of a :data frame is bigger than the payload length" 56 | 57 | assert Frame.decode_next(encode_raw(0x00, 0x08, 3, payload)) == 58 | {:error, {:protocol_error, debug_data}} 59 | end 60 | end 61 | 62 | describe "HEADERS" do 63 | test "with meaningful hbf" do 64 | headers = [{"foo", "bar"}, {"baz", "bong"}, {"foo", "badung"}] 65 | 66 | {encoded_headers, _} = 67 | headers 68 | |> Enum.map(fn {name, value} -> {:no_store, name, value} end) 69 | |> HPAX.encode(HPAX.new(100_000)) 70 | 71 | assert {:ok, headers(stream_id: 3, flags: 0x00, hbf: hbf, padding: nil), "rest"} = 72 | Frame.decode_next(encode_raw(0x01, 0x00, 3, encoded_headers) <> "rest") 73 | 74 | assert {:ok, ^headers, _} = HPAX.decode(hbf, HPAX.new(100_000)) 75 | end 76 | 77 | test "without padding and without priority" do 78 | check all stream_id <- non_zero_stream_id(), 79 | hbf <- binary() do 80 | assert_round_trip headers( 81 | stream_id: stream_id, 82 | flags: 0x00, 83 | exclusive?: nil, 84 | stream_dependency: nil, 85 | weight: nil, 86 | hbf: hbf, 87 | padding: nil 88 | ) 89 | end 90 | end 91 | 92 | test "with padding and priority" do 93 | check all stream_id <- non_zero_stream_id(), 94 | hbf <- binary(), 95 | padding <- binary() do 96 | assert_round_trip headers( 97 | stream_id: stream_id, 98 | flags: bor(0x08, 0x20), 99 | exclusive?: true, 100 | stream_dependency: 19, 101 | weight: 10, 102 | hbf: hbf, 103 | padding: padding 104 | ) 105 | end 106 | end 107 | end 108 | 109 | describe "PRIORITY" do 110 | test "regular" do 111 | check all stream_id <- non_zero_stream_id(), 112 | stream_dependency <- non_zero_stream_id(), 113 | weight <- positive_integer() do 114 | assert_round_trip priority( 115 | stream_id: stream_id, 116 | exclusive?: true, 117 | stream_dependency: stream_dependency, 118 | weight: weight, 119 | flags: 0x00 120 | ) 121 | end 122 | end 123 | 124 | test "with bad length" do 125 | assert Frame.decode_next(encode_raw(0x02, 0x00, 3, "")) == 126 | {:error, {:frame_size_error, :priority}} 127 | end 128 | end 129 | 130 | describe "RST_STREAM" do 131 | test "regular" do 132 | check all stream_id <- non_zero_stream_id(), 133 | error_code <- error_code() do 134 | assert_round_trip rst_stream( 135 | stream_id: stream_id, 136 | flags: 0x00, 137 | error_code: error_code 138 | ) 139 | end 140 | end 141 | 142 | test "with bad length" do 143 | assert Frame.decode_next(encode_raw(0x03, 0x00, 3, <<3::8>>)) == 144 | {:error, {:frame_size_error, :rst_stream}} 145 | end 146 | end 147 | 148 | describe "SETTINGS" do 149 | test "with empty settings" do 150 | assert_round_trip settings(stream_id: 0, flags: 0x00, params: []) 151 | end 152 | 153 | test "with parameters" do 154 | check all header_table_size <- positive_integer(), 155 | enable_push <- boolean(), 156 | max_concurrent_streams <- non_negative_integer(), 157 | initial_window_size <- positive_integer(), 158 | max_frame_size <- positive_integer(), 159 | max_header_list_size <- positive_integer(), 160 | enable_connect_protocol <- boolean() do 161 | params = [ 162 | header_table_size: header_table_size, 163 | enable_push: enable_push, 164 | max_concurrent_streams: max_concurrent_streams, 165 | initial_window_size: initial_window_size, 166 | max_frame_size: max_frame_size, 167 | max_header_list_size: max_header_list_size, 168 | enable_connect_protocol: enable_connect_protocol 169 | ] 170 | 171 | assert_round_trip settings(stream_id: 0, flags: 0x01, params: params) 172 | end 173 | end 174 | 175 | test "with bad length" do 176 | assert Frame.decode_next(encode_raw(0x04, 0x00, 0, <<_not_multiple_of_6 = 3::8>>)) == 177 | {:error, {:frame_size_error, :settings}} 178 | end 179 | end 180 | 181 | describe "PUSH_PROMISE" do 182 | test "without padding" do 183 | check all stream_id <- non_zero_stream_id(), 184 | promised_stream_id <- non_zero_stream_id(), 185 | hbf <- binary() do 186 | assert_round_trip push_promise( 187 | stream_id: stream_id, 188 | flags: 0x00, 189 | promised_stream_id: promised_stream_id, 190 | hbf: hbf, 191 | padding: nil 192 | ) 193 | end 194 | end 195 | 196 | test "with padding" do 197 | check all stream_id <- non_zero_stream_id(), 198 | promised_stream_id <- non_zero_stream_id(), 199 | hbf <- binary(), 200 | padding <- binary() do 201 | assert_round_trip push_promise( 202 | stream_id: stream_id, 203 | flags: 0x08, 204 | promised_stream_id: promised_stream_id, 205 | hbf: hbf, 206 | padding: padding 207 | ) 208 | end 209 | end 210 | end 211 | 212 | describe "PING" do 213 | test "regular" do 214 | check all opaque_data <- binary(length: 8) do 215 | assert_round_trip ping(stream_id: 0, flags: 0x01, opaque_data: opaque_data) 216 | end 217 | end 218 | 219 | test "with bad length" do 220 | assert Frame.decode_next(encode_raw(0x06, 0x00, 0, <<_not_multiple_of_6 = 3::8>>)) == 221 | {:error, {:frame_size_error, :ping}} 222 | end 223 | end 224 | 225 | describe "GOAWAY" do 226 | test "regular" do 227 | check all last_stream_id <- non_zero_stream_id(), 228 | error_code <- error_code(), 229 | debug_data <- binary() do 230 | assert_round_trip goaway( 231 | stream_id: 0, 232 | flags: 0x00, 233 | last_stream_id: last_stream_id, 234 | error_code: error_code, 235 | debug_data: debug_data 236 | ) 237 | end 238 | end 239 | end 240 | 241 | describe "WINDOW_UPDATE" do 242 | test "regular" do 243 | check all stream_id <- one_of([constant(0), non_zero_stream_id()]), 244 | wsi <- positive_integer() do 245 | assert_round_trip window_update( 246 | stream_id: stream_id, 247 | flags: 0x00, 248 | window_size_increment: wsi 249 | ) 250 | end 251 | end 252 | 253 | test "invalid window size increment" do 254 | assert Frame.decode_next(encode_raw(0x08, 0x00, 0, <<0::1, 0::31>>)) == 255 | {:error, {:protocol_error, "bad WINDOW_SIZE increment"}} 256 | end 257 | 258 | test "with bad length" do 259 | assert Frame.decode_next(encode_raw(0x08, 0x00, 0, <<>>)) == 260 | {:error, {:frame_size_error, :window_update}} 261 | end 262 | end 263 | 264 | describe "CONTINUATION" do 265 | test "regular" do 266 | check all stream_id <- non_zero_stream_id(), 267 | hbf <- binary() do 268 | assert_round_trip continuation(stream_id: stream_id, flags: 0x00, hbf: hbf) 269 | end 270 | end 271 | end 272 | 273 | defp assert_round_trip(frame) do 274 | encoded = frame |> Frame.encode() |> IO.iodata_to_binary() 275 | assert Frame.decode_next(encoded <> "rest") == {:ok, frame, "rest"} 276 | end 277 | 278 | defp encode_raw(type, flags, stream_id, payload) do 279 | IO.iodata_to_binary(Frame.encode_raw(type, flags, stream_id, payload)) 280 | end 281 | 282 | defp non_zero_stream_id() do 283 | map(positive_integer(), &(&1 * 2 + 1)) 284 | end 285 | 286 | defp error_code() do 287 | member_of([ 288 | :no_error, 289 | :protocol_error, 290 | :internal_error, 291 | :flow_control_error, 292 | :settings_timeout, 293 | :stream_closed, 294 | :frame_size_error, 295 | :refused_stream, 296 | :cancel, 297 | :compression_error, 298 | :connect_error, 299 | :enhance_your_calm, 300 | :inadequate_security, 301 | :http_1_1_required, 302 | {:custom_error, 0x11}, 303 | {:custom_error, 0xFF}, 304 | {:custom_error, 70007} 305 | ]) 306 | end 307 | end 308 | -------------------------------------------------------------------------------- /test/mint/http2/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HTTP2.IntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mint.HTTP2.TestHelpers 5 | 6 | alias Mint.HTTP2 7 | alias Mint.HttpBin 8 | 9 | @moduletag :requires_internet_connection 10 | 11 | setup context do 12 | transport_opts = 13 | if Mint.Core.Transport.SSL.ssl_version() >= [10, 2] do 14 | [{:versions, [:"tlsv1.2", :"tlsv1.3"]}] 15 | else 16 | [] 17 | end 18 | 19 | case Map.fetch(context, :connect) do 20 | {:ok, {host, port}} -> 21 | extra_transport_opts = Map.get(context, :transport_opts, []) 22 | 23 | assert {:ok, %HTTP2{} = conn} = 24 | HTTP2.connect(:https, host, port, 25 | transport_opts: transport_opts ++ extra_transport_opts 26 | ) 27 | 28 | [conn: conn] 29 | 30 | :error -> 31 | [] 32 | end 33 | end 34 | 35 | test "TCP - nghttp2.org" do 36 | assert {:ok, %HTTP2{} = conn} = HTTP2.connect(:http, "nghttp2.org", 80) 37 | 38 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/httpbin/", [], nil) 39 | 40 | # For some reason, on OTP 26+ we get an SSL message sneaking in here. Instead of going 41 | # crazy trying to debug it, for now let's just swallow it. 42 | if System.otp_release() >= "26" do 43 | assert_receive {:ssl, _socket, _data}, 1000 44 | end 45 | 46 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 47 | 48 | assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses 49 | assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1)) 50 | 51 | assert status == 200 52 | assert is_list(headers) 53 | 54 | assert conn.buffer == "" 55 | assert HTTP2.open?(conn) 56 | end 57 | 58 | describe "httpbin.org" do 59 | @describetag connect: {HttpBin.host(), HttpBin.https_port()}, 60 | transport_opts: HttpBin.https_transport_opts() 61 | 62 | test "GET /user-agent", %{conn: conn} do 63 | assert {:ok, %HTTP2{} = conn, req_id} = HTTP2.request(conn, "GET", "/user-agent", [], nil) 64 | 65 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 66 | 67 | assert [ 68 | {:status, ^req_id, 200}, 69 | {:headers, ^req_id, headers}, 70 | {:data, ^req_id, data}, 71 | {:done, ^req_id} 72 | ] = responses 73 | 74 | assert is_list(headers) 75 | assert data =~ "mint/" 76 | 77 | assert conn.buffer == "" 78 | assert HTTP2.open?(conn) 79 | end 80 | 81 | test "GET /image/png", %{conn: conn} do 82 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/image/png", [], nil) 83 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 84 | 85 | assert [ 86 | {:status, ^ref, 200}, 87 | {:headers, ^ref, headers}, 88 | {:data, ^ref, data1}, 89 | {:data, ^ref, data2}, 90 | {:done, ^ref} 91 | ] = responses 92 | 93 | assert is_list(headers) 94 | assert is_binary(data1) 95 | assert is_binary(data2) 96 | 97 | assert conn.buffer == "" 98 | assert HTTP2.open?(conn) 99 | end 100 | 101 | test "ping", %{conn: conn} do 102 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.ping(conn) 103 | assert {:ok, %HTTP2{} = conn, [{:pong, ^ref}]} = receive_stream(conn) 104 | assert conn.buffer == "" 105 | assert HTTP2.open?(conn) 106 | end 107 | end 108 | 109 | describe "twitter.com" do 110 | @moduletag connect: {"twitter.com", 443} 111 | @browser_user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 112 | 113 | test "ping", %{conn: conn} do 114 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.ping(conn) 115 | assert {:ok, %HTTP2{} = conn, [{:pong, ^ref}]} = receive_stream(conn) 116 | assert conn.buffer == "" 117 | assert HTTP2.open?(conn) 118 | end 119 | 120 | test "GET /", %{conn: conn} do 121 | assert {:ok, %HTTP2{} = conn, ref} = 122 | HTTP2.request(conn, "GET", "/", [{"user-agent", @browser_user_agent}], nil) 123 | 124 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 125 | 126 | assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses 127 | assert status in [200, 302] 128 | 129 | assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1)) 130 | 131 | assert is_list(headers) 132 | 133 | assert conn.buffer == "" 134 | assert HTTP2.open?(conn) 135 | end 136 | end 137 | 138 | describe "facebook.com" do 139 | @describetag connect: {"facebook.com", 443} 140 | 141 | test "ping", %{conn: conn} do 142 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.ping(conn) 143 | assert {:ok, %HTTP2{} = conn, [{:pong, ^ref}]} = receive_stream(conn) 144 | assert conn.buffer == "" 145 | assert HTTP2.open?(conn) 146 | end 147 | 148 | test "GET /", %{conn: conn} do 149 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/", [], nil) 150 | 151 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 152 | 153 | assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses 154 | assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1)) 155 | 156 | assert status == 301 157 | assert is_list(headers) 158 | 159 | assert conn.buffer == "" 160 | assert HTTP2.open?(conn) 161 | end 162 | end 163 | 164 | describe "nghttp2.org/httpbin" do 165 | @describetag connect: {"nghttp2.org", 443} 166 | 167 | test "ping", %{conn: conn} do 168 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.ping(conn) 169 | assert {:ok, %HTTP2{} = conn, [{:pong, ^ref}]} = receive_stream(conn) 170 | assert conn.buffer == "" 171 | assert HTTP2.open?(conn) 172 | end 173 | 174 | test "GET /", %{conn: conn} do 175 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/httpbin/", [], nil) 176 | 177 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 178 | 179 | assert [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] = responses 180 | assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1)) 181 | 182 | assert status == 200 183 | assert is_list(headers) 184 | 185 | assert conn.buffer == "" 186 | assert HTTP2.open?(conn) 187 | end 188 | end 189 | 190 | describe "robynthinks.wordpress.com" do 191 | @describetag connect: {"robynthinks.wordpress.com", 443} 192 | 193 | test "GET /feed/ - regression for #171", %{conn: conn} do 194 | # Using non-downcased header meant that HPACK wouldn't find it in the 195 | # static built-in headers table and so it wouldn't encode it correctly. 196 | headers = [{"If-Modified-Since", "Wed, 26 May 2019 07:43:40 GMT"}] 197 | assert {:ok, %HTTP2{} = conn, ref} = HTTP2.request(conn, "GET", "/feed/", headers, nil) 198 | 199 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 200 | 201 | assert [{:status, ^ref, status}, {:headers, ^ref, _headers} | rest] = responses 202 | assert {_, [{:done, ^ref}]} = Enum.split_while(rest, &match?({:data, ^ref, _}, &1)) 203 | 204 | assert status in [200, 304] 205 | 206 | assert conn.buffer == "" 207 | assert HTTP2.open?(conn) 208 | end 209 | end 210 | 211 | describe "www.shopify.com" do 212 | @describetag connect: {"www.shopify.com", 443} 213 | 214 | if List.to_integer(:erlang.system_info(:otp_release)) < 23 do 215 | @tag :skip 216 | end 217 | 218 | # Informational responses were the issue.s 219 | # https://github.com/elixir-mint/mint/issues/349 220 | test "GET / with specific User-Agent header - regression for #349", %{conn: conn} do 221 | assert %HTTP2{} = conn 222 | 223 | assert {:ok, %HTTP2{} = conn, ref} = 224 | HTTP2.request(conn, "GET", "/", [{"user-agent", "curl/7.68.0"}], nil) 225 | 226 | assert {:ok, %HTTP2{} = conn, responses} = receive_stream(conn) 227 | 228 | case responses do 229 | [ 230 | {:status, ^ref, informational_status}, 231 | {:headers, ^ref, informational_headers}, 232 | {:status, ^ref, status}, 233 | {:headers, ^ref, headers} 234 | | rest 235 | ] -> 236 | assert informational_status == 103 237 | assert {"link", _} = List.keyfind(informational_headers, "link", 0) 238 | assert status == 200 239 | assert is_list(headers) and length(headers) > 0 240 | 241 | assert Enum.count(rest, &match?({:data, ^ref, _data}, &1)) >= 1 242 | assert List.last(rest) == {:done, ref} 243 | 244 | [{:status, ^ref, status}, {:headers, ^ref, headers} | rest] -> 245 | assert status == 200 246 | assert is_list(headers) and length(headers) > 0 247 | assert Enum.count(rest, &match?({:data, ^ref, _data}, &1)) >= 1 248 | assert List.last(rest) == {:done, ref} 249 | 250 | _other -> 251 | flunk( 252 | "Unexpected responses. Expected status + headers + data, or informational " <> 253 | "response + status + headers + data, got:\n#{inspect(responses, pretty: true)}" 254 | ) 255 | end 256 | 257 | assert HTTP2.open?(conn) 258 | end 259 | end 260 | 261 | # TODO: certificate verification; badssl.com does not seem to support HTTP2 262 | end 263 | -------------------------------------------------------------------------------- /test/mint/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.IntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mint.HTTP1.TestHelpers 5 | 6 | alias Mint.{TransportError, HTTP, HttpBin} 7 | 8 | @moduletag :requires_internet_connection 9 | 10 | describe "nghttp2.org" do 11 | test "SSL - select HTTP1" do 12 | assert {:ok, conn} = 13 | HTTP.connect(:https, HttpBin.host(), HttpBin.https_port(), 14 | transport_opts: HttpBin.https_transport_opts(), 15 | protocols: [:http1] 16 | ) 17 | 18 | assert conn.__struct__ == Mint.HTTP1 19 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/bytes/1", [], nil) 20 | assert {:ok, _conn, responses} = receive_stream(conn) 21 | 22 | assert [ 23 | {:status, ^request, 200}, 24 | {:headers, ^request, _}, 25 | {:data, ^request, <<_>>}, 26 | {:done, ^request} 27 | ] = responses 28 | end 29 | 30 | test "SSL - select HTTP2" do 31 | assert {:ok, conn} = 32 | HTTP.connect(:https, HttpBin.host(), HttpBin.https_port(), 33 | transport_opts: HttpBin.https_transport_opts() 34 | ) 35 | 36 | assert conn.__struct__ == Mint.HTTP2 37 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/bytes/1", [], nil) 38 | assert {:ok, _conn, responses} = receive_stream(conn) 39 | 40 | assert [ 41 | {:status, ^request, 200}, 42 | {:headers, ^request, _}, 43 | {:data, ^request, <<_>>}, 44 | {:done, ^request} 45 | ] = responses 46 | end 47 | end 48 | 49 | describe "ssl certificate verification" do 50 | @tag :capture_log 51 | test "bad certificate - badssl.com" do 52 | assert {:error, %TransportError{reason: reason}} = 53 | HTTP.connect( 54 | :https, 55 | "untrusted-root.badssl.com", 56 | 443, 57 | transport_opts: [log_alert: false, reuse_sessions: false] 58 | ) 59 | 60 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 61 | assert reason == {:tls_alert, ~c"unknown ca"} or 62 | match?({:tls_alert, {:unknown_ca, _}}, reason) 63 | 64 | assert {:ok, _conn} = 65 | HTTP.connect( 66 | :https, 67 | "untrusted-root.badssl.com", 68 | 443, 69 | transport_opts: [verify: :verify_none] 70 | ) 71 | end 72 | 73 | @tag :capture_log 74 | test "bad hostname - badssl.com" do 75 | assert {:error, %TransportError{reason: reason}} = 76 | HTTP.connect( 77 | :https, 78 | "wrong.host.badssl.com", 79 | 443, 80 | transport_opts: [log_alert: false, reuse_sessions: false] 81 | ) 82 | 83 | # OTP 21.3 changes the format of SSL errors. Let's support both ways for now. 84 | assert reason == {:tls_alert, ~c"handshake failure"} or 85 | match?({:tls_alert, {:handshake_failure, _}}, reason) 86 | 87 | assert {:ok, _conn} = 88 | HTTP.connect( 89 | :https, 90 | "wrong.host.badssl.com", 91 | 443, 92 | transport_opts: [verify: :verify_none] 93 | ) 94 | end 95 | 96 | if List.to_integer(:erlang.system_info(:otp_release)) < 25 do 97 | @tag :skip 98 | end 99 | 100 | @tag :capture_log 101 | test "using :public_key.cacerts_get/0" do 102 | cacerts = apply(:public_key, :cacerts_get, []) 103 | 104 | assert {:error, %TransportError{}} = 105 | HTTP.connect( 106 | :https, 107 | "untrusted-root.badssl.com", 108 | 443, 109 | transport_opts: [ 110 | log_alert: false, 111 | reuse_sessions: false, 112 | cacerts: cacerts 113 | ] 114 | ) 115 | 116 | assert {:ok, _conn} = 117 | HTTP.connect( 118 | :https, 119 | "nghttp2.org", 120 | 443, 121 | transport_opts: [reuse_sessions: false, cacerts: cacerts] 122 | ) 123 | end 124 | end 125 | 126 | describe "partial chain handling" do 127 | @dst_and_isrg Path.expand("../support/mint/dst_and_isrg.pem", __DIR__) 128 | 129 | # OTP 18.3 fails to connect to letsencrypt.org, skip this test 130 | if Mint.Core.Transport.SSL.ssl_version() < [8, 0] do 131 | @tag skip: ":ssl version too old" 132 | end 133 | 134 | # This test assumes the letsencrypt.org server presents the 'long chain', 135 | # consisting of the following certificates: 136 | # 137 | # 0 s:/CN=lencr.org 138 | # i:/C=US/O=Let's Encrypt/CN=R3 139 | # 1 s:/C=US/O=Let's Encrypt/CN=R3 140 | # i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1 141 | # 2 s:/C=US/O=Internet Security Research Group/CN=ISRG Root X1 142 | # i:/O=Digital Signature Trust Co./CN=DST Root CA X3 143 | # 144 | # This is currently the case, but won't be the case after Sep 2024, or 145 | # possibly earlier. 146 | test "Let's Encrypt ISRG cross-signed by expired root" do 147 | assert {:ok, _conn} = 148 | HTTP.connect(:https, "letsencrypt.org", 443, 149 | transport_opts: [cacertfile: @dst_and_isrg, reuse_sessions: false] 150 | ) 151 | end 152 | end 153 | 154 | describe "proxy" do 155 | @describetag :proxy 156 | 157 | test "200 response - http://httpbin.org" do 158 | assert {:ok, conn} = 159 | HTTP.connect(:http, HttpBin.proxy_host(), HttpBin.http_port(), 160 | proxy: {:http, "localhost", 8888, []} 161 | ) 162 | 163 | assert conn.__struct__ == Mint.UnsafeProxy 164 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 165 | assert {:ok, _conn, responses} = receive_stream(conn) 166 | 167 | assert [status, headers | responses] = responses 168 | assert {:status, ^request, 200} = status 169 | assert {:headers, ^request, headers} = headers 170 | assert is_list(headers) 171 | assert merge_body(responses, request) =~ "httpbin" 172 | end 173 | 174 | test "200 response - https://httpbin.org" do 175 | assert {:ok, conn} = 176 | HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 177 | proxy: {:http, "localhost", 8888, []}, 178 | transport_opts: HttpBin.https_transport_opts() 179 | ) 180 | 181 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 182 | assert {:ok, _conn, responses} = receive_stream(conn) 183 | 184 | assert [status, headers | responses] = responses 185 | assert {:status, ^request, 200} = status 186 | assert {:headers, ^request, headers} = headers 187 | assert is_list(headers) 188 | assert merge_body(responses, request) =~ "httpbin.org" 189 | end 190 | 191 | test "200 response with explicit http2 - https://httpbin.org" do 192 | assert {:ok, conn} = 193 | HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 194 | proxy: {:http, "localhost", 8888, []}, 195 | protocols: [:http2], 196 | transport_opts: HttpBin.https_transport_opts() 197 | ) 198 | 199 | assert conn.__struct__ == Mint.HTTP2 200 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/user-agent", [], nil) 201 | assert {:ok, _conn, responses} = receive_stream(conn) 202 | 203 | assert [status, headers | responses] = responses 204 | assert {:status, ^request, 200} = status 205 | assert {:headers, ^request, headers} = headers 206 | assert is_list(headers) 207 | assert merge_body(responses, request) =~ "mint/" 208 | end 209 | 210 | test "200 response without explicit http2 - https://httpbin.org" do 211 | assert {:ok, conn} = 212 | HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 213 | proxy: {:http, "localhost", 8888, []}, 214 | protocols: [:http1, :http2], 215 | transport_opts: HttpBin.https_transport_opts() 216 | ) 217 | 218 | assert conn.__struct__ == Mint.HTTP2 219 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/user-agent", [], nil) 220 | assert {:ok, _conn, responses} = receive_stream(conn) 221 | 222 | assert [status, headers | responses] = responses 223 | assert {:status, ^request, 200} = status 224 | assert {:headers, ^request, headers} = headers 225 | assert is_list(headers) 226 | assert merge_body(responses, request) =~ "mint/" 227 | end 228 | end 229 | 230 | describe "information from connection's socket" do 231 | test "TLSv1.2 - badssl.com" do 232 | assert {:ok, conn} = 233 | HTTP.connect( 234 | :https, 235 | "tls-v1-2.badssl.com", 236 | 1012 237 | ) 238 | 239 | assert socket = Mint.HTTP.get_socket(conn) 240 | 241 | if Mint.Core.Transport.SSL.ssl_version() >= [10, 2] do 242 | assert {:ok, [{:keylog, _keylog_items}]} = :ssl.connection_information(socket, [:keylog]) 243 | else 244 | assert {:ok, [{:protocol, _protocol}]} = :ssl.connection_information(socket, [:protocol]) 245 | end 246 | end 247 | end 248 | 249 | describe "force TLS v1.3 only" do 250 | test "rabbitmq.com" do 251 | if Mint.Core.Transport.SSL.ssl_version() >= [10, 2] do 252 | ciphers = :ssl.filter_cipher_suites(:ssl.cipher_suites(:all, :"tlsv1.3"), []) 253 | 254 | opts = [ 255 | transport_opts: [ 256 | versions: [:"tlsv1.3"], 257 | ciphers: ciphers 258 | ] 259 | ] 260 | 261 | assert {:ok, _conn} = HTTP.connect(:https, "rabbitmq.com", 443, opts) 262 | else 263 | :ok 264 | end 265 | end 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /test/mint/transport_error_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.TransportErrorTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Mint.TransportError 5 | 6 | describe "Exception.message/1" do 7 | test "with one of our reasons" do 8 | error = %TransportError{reason: :closed} 9 | assert Exception.message(error) == "socket closed" 10 | 11 | error = %TransportError{reason: :timeout} 12 | assert Exception.message(error) == "timeout" 13 | 14 | error = %TransportError{reason: :protocol_not_negotiated} 15 | assert Exception.message(error) == "ALPN protocol not negotiated" 16 | end 17 | 18 | if System.otp_release() >= "26" do 19 | test "with an SSL reason" do 20 | # This error reason type is specific to OTP 26+. 21 | error = %TransportError{reason: {:tls_alert, {:unknown_ca, ~c"unknown ca"}}} 22 | assert Exception.message(error) == "unknown ca" 23 | end 24 | end 25 | 26 | test "with a POSIX reason" do 27 | error = %TransportError{reason: :econnrefused} 28 | assert Exception.message(error) == "connection refused" 29 | end 30 | 31 | test "with :bad_alpn_protocol sa the reason" do 32 | error = %TransportError{reason: {:bad_alpn_protocol, :h3}} 33 | 34 | assert Exception.message(error) == 35 | ~s(bad ALPN protocol :h3, supported protocols are "http/1.1" and "h2") 36 | end 37 | 38 | test "with an unknown reason" do 39 | error = %TransportError{reason: :unknown} 40 | assert Exception.message(error) == ":unknown" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/mint/tunnel_proxy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.TunnelProxyTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Mint.HTTP1.TestHelpers 5 | 6 | alias Mint.HTTP 7 | alias Mint.HttpBin 8 | 9 | @moduletag :proxy 10 | @moduletag :requires_internet_connection 11 | 12 | test "200 response - http://httpbin.org" do 13 | # Ensure we only match relevant messages 14 | send(self(), {:tcp, :not_my_socket, "DATA"}) 15 | 16 | assert {:ok, conn} = 17 | Mint.TunnelProxy.connect( 18 | {:http, "localhost", 8888, []}, 19 | {:http, HttpBin.proxy_host(), HttpBin.http_port(), []} 20 | ) 21 | 22 | assert conn.__struct__ == Mint.HTTP1 23 | 24 | assert [{"proxy-agent", <<"tinyproxy/", _version::binary>>}] = 25 | Mint.HTTP1.get_proxy_headers(conn) 26 | 27 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 28 | assert {:ok, _conn, responses} = receive_stream(conn) 29 | 30 | assert [status, headers | responses] = responses 31 | assert {:status, ^request, 200} = status 32 | assert {:headers, ^request, headers} = headers 33 | assert is_list(headers) 34 | assert merge_body(responses, request) =~ "httpbin" 35 | end 36 | 37 | test "200 response - https://httpbin.org" do 38 | assert {:ok, conn} = 39 | Mint.TunnelProxy.connect( 40 | {:http, "localhost", 8888, []}, 41 | {:https, HttpBin.proxy_host(), HttpBin.https_port(), 42 | transport_opts: HttpBin.https_transport_opts()} 43 | ) 44 | 45 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 46 | assert {:ok, _conn, responses} = receive_stream(conn) 47 | 48 | assert [status, headers | responses] = responses 49 | assert {:status, ^request, 200} = status 50 | assert {:headers, ^request, headers} = headers 51 | assert is_list(headers) 52 | assert merge_body(responses, request) =~ "httpbin" 53 | end 54 | 55 | test "407 response - proxy with missing authentication" do 56 | assert {:error, %Mint.HTTPError{reason: {:proxy, {:unexpected_status, 407}}}} = 57 | Mint.HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 58 | proxy: {:http, "localhost", 8889, []}, 59 | transport_opts: HttpBin.https_transport_opts() 60 | ) 61 | end 62 | 63 | test "401 response - proxy with invalid authentication" do 64 | invalid_auth64 = Base.encode64("test:wrong_password") 65 | 66 | assert {:error, %Mint.HTTPError{reason: {:proxy, {:unexpected_status, 401}}}} = 67 | Mint.HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 68 | proxy: {:http, "localhost", 8889, []}, 69 | proxy_headers: [{"proxy-authorization", "basic #{invalid_auth64}"}], 70 | transport_opts: HttpBin.https_transport_opts() 71 | ) 72 | end 73 | 74 | test "200 response - proxy with valid authentication" do 75 | auth64 = Base.encode64("test:password") 76 | 77 | assert {:ok, conn} = 78 | Mint.HTTP.connect(:https, HttpBin.proxy_host(), HttpBin.https_port(), 79 | proxy: {:http, "localhost", 8889, []}, 80 | proxy_headers: [{"proxy-authorization", "basic #{auth64}"}], 81 | transport_opts: HttpBin.https_transport_opts() 82 | ) 83 | 84 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 85 | assert {:ok, _conn, responses} = receive_stream(conn) 86 | 87 | assert [status, headers | responses] = responses 88 | assert {:status, ^request, 200} = status 89 | assert {:headers, ^request, headers} = headers 90 | assert is_list(headers) 91 | assert merge_body(responses, request) =~ "httpbin" 92 | end 93 | 94 | test "200 response with explicit http2 - https://httpbin.org" do 95 | assert {:ok, conn} = 96 | Mint.TunnelProxy.connect( 97 | {:http, "localhost", 8888, []}, 98 | {:https, HttpBin.proxy_host(), HttpBin.https_port(), 99 | [protocols: [:http2], transport_opts: HttpBin.https_transport_opts()]} 100 | ) 101 | 102 | assert conn.__struct__ == Mint.HTTP2 103 | 104 | assert [{"proxy-agent", <<"tinyproxy/", _version::binary>>}] = 105 | Mint.HTTP2.get_proxy_headers(conn) 106 | 107 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/user-agent", [], nil) 108 | assert {:ok, _conn, responses} = receive_stream(conn) 109 | 110 | assert [status, headers | responses] = responses 111 | assert {:status, ^request, 200} = status 112 | assert {:headers, ^request, headers} = headers 113 | assert is_list(headers) 114 | assert merge_body(responses, request) =~ "mint/" 115 | end 116 | 117 | test "200 response without explicit http2 - https://httpbin.org" do 118 | assert {:ok, conn} = 119 | Mint.TunnelProxy.connect( 120 | {:http, "localhost", 8888, []}, 121 | {:https, HttpBin.proxy_host(), HttpBin.https_port(), 122 | [protocols: [:http1, :http2], transport_opts: HttpBin.https_transport_opts()]} 123 | ) 124 | 125 | assert conn.__struct__ == Mint.HTTP2 126 | 127 | assert [{"proxy-agent", <<"tinyproxy/", _version::binary>>}] = 128 | Mint.HTTP.get_proxy_headers(conn) 129 | 130 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/user-agent", [], nil) 131 | assert {:ok, _conn, responses} = receive_stream(conn) 132 | 133 | assert [status, headers | responses] = responses 134 | assert {:status, ^request, 200} = status 135 | assert {:headers, ^request, headers} = headers 136 | assert is_list(headers) 137 | assert merge_body(responses, request) =~ "mint/" 138 | end 139 | 140 | @tag :skip 141 | test "do not support nested HTTPS connections - https://httpbin.org" do 142 | assert {:ok, conn} = 143 | Mint.TunnelProxy.connect( 144 | {:https, "localhost", 8888, []}, 145 | {:https, HttpBin.proxy_host(), HttpBin.https_port(), 146 | [transport_opts: HttpBin.https_transport_opts()]} 147 | ) 148 | 149 | assert conn.__struct__ == Mint.HTTP1 150 | 151 | assert [{"proxy-agent", <<"tinyproxy/", _version::binary>>}] = 152 | Mint.HTTP.get_proxy_headers(conn) 153 | 154 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 155 | assert {:ok, _conn, responses} = receive_stream(conn) 156 | 157 | assert [status, headers | responses] = responses 158 | assert {:status, ^request, 200} = status 159 | assert {:headers, ^request, headers} = headers 160 | assert is_list(headers) 161 | assert merge_body(responses, request) =~ "httpbin" 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/mint/unix_socket_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.UnixSocketTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Mint.{HTTP, TestSocketServer} 5 | 6 | require HTTP 7 | 8 | unix? = match?({:unix, _}, :os.type()) 9 | otp_19? = System.otp_release() >= "19" 10 | @moduletag skip: not (unix? and otp_19?) 11 | 12 | test "starting an HTTP connection to a Unix domain socket works" do 13 | {:ok, address, server_ref} = TestSocketServer.start(ssl: false) 14 | 15 | assert {:ok, conn} = HTTP.connect(:http, address, 0, mode: :passive, hostname: "localhost") 16 | 17 | assert_receive {^server_ref, server_socket} 18 | 19 | {:ok, conn, ref} = HTTP.request(conn, "GET", "/", [], nil) 20 | 21 | :ok = :gen_tcp.send(server_socket, "HTTP/1.1 200 OK\r\n") 22 | 23 | assert {:ok, _conn, responses} = HTTP.recv(conn, 0, 100) 24 | assert responses == [{:status, ref, 200}] 25 | end 26 | 27 | @tag :capture_log 28 | test "starting an https connection to a unix domain socket works" do 29 | {:ok, address, server_ref} = TestSocketServer.start(ssl: true) 30 | 31 | assert {:ok, conn} = 32 | HTTP.connect(:https, address, 0, 33 | mode: :passive, 34 | hostname: "localhost", 35 | transport_opts: [ 36 | verify: :verify_none 37 | ] 38 | ) 39 | 40 | assert_receive {^server_ref, server_socket} 41 | 42 | {:ok, conn, ref} = HTTP.request(conn, "GET", "/", [], nil) 43 | 44 | :ok = :ssl.send(server_socket, "HTTP/1.1 200 OK\r\n") 45 | 46 | assert {:ok, _conn, responses} = HTTP.recv(conn, 0, 100) 47 | assert responses == [{:status, ref, 200}] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/mint/unsafe_proxy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mint.UnsafeProxyTest do 2 | use ExUnit.Case, async: true 3 | import Mint.HTTP1.TestHelpers 4 | alias Mint.UnsafeProxy 5 | alias Mint.HTTP 6 | alias Mint.HttpBin 7 | 8 | @moduletag :proxy 9 | @moduletag :requires_internet_connection 10 | 11 | test "200 response - http://httpbin.org" do 12 | assert {:ok, conn} = 13 | UnsafeProxy.connect( 14 | {:http, "localhost", 8888}, 15 | {:http, HttpBin.proxy_host(), HttpBin.http_port()} 16 | ) 17 | 18 | assert {:ok, conn, request} = UnsafeProxy.request(conn, "GET", "/", [], nil) 19 | assert {:ok, _conn, responses} = receive_stream(conn) 20 | 21 | assert [status, headers | responses] = responses 22 | assert {:status, ^request, 200} = status 23 | assert {:headers, ^request, headers} = headers 24 | assert is_list(headers) 25 | assert merge_body(responses, request) =~ "httpbin" 26 | end 27 | 28 | test "407 response - proxy with missing authentication" do 29 | assert {:ok, conn} = 30 | HTTP.connect(:http, HttpBin.proxy_host(), HttpBin.http_port(), 31 | proxy: {:http, "localhost", 8889, []} 32 | ) 33 | 34 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 35 | assert {:ok, _conn, responses} = receive_stream(conn) 36 | assert [status, _headers | _responses] = responses 37 | assert {:status, ^request, 407} = status 38 | end 39 | 40 | test "401 response - proxy with invalid authentication" do 41 | invalid_auth64 = Base.encode64("test:wrong_password") 42 | 43 | assert {:ok, conn} = 44 | HTTP.connect(:http, HttpBin.proxy_host(), HttpBin.http_port(), 45 | proxy: {:http, "localhost", 8889, []}, 46 | proxy_headers: [{"proxy-authorization", "basic #{invalid_auth64}"}] 47 | ) 48 | 49 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 50 | assert {:ok, _conn, responses} = receive_stream(conn) 51 | assert [status, _headers | _responses] = responses 52 | assert {:status, ^request, 401} = status 53 | end 54 | 55 | test "200 response - proxy with valid authentication" do 56 | auth64 = Base.encode64("test:password") 57 | 58 | assert {:ok, conn} = 59 | HTTP.connect(:http, HttpBin.proxy_host(), HttpBin.http_port(), 60 | proxy: {:http, "localhost", 8889, []}, 61 | proxy_headers: [{"proxy-authorization", "basic #{auth64}"}] 62 | ) 63 | 64 | assert {:ok, conn, request} = HTTP.request(conn, "GET", "/", [], nil) 65 | assert {:ok, _conn, responses} = receive_stream(conn) 66 | assert [status, headers | responses] = responses 67 | assert {:status, ^request, 200} = status 68 | assert {:headers, ^request, headers} = headers 69 | assert is_list(headers) 70 | assert merge_body(responses, request) =~ "httpbin" 71 | end 72 | 73 | test "Mint.HTTP.protocol/1 on an unsafe proxy connection" do 74 | assert {:ok, %UnsafeProxy{} = conn} = 75 | UnsafeProxy.connect( 76 | {:http, "localhost", 8888}, 77 | {:http, HttpBin.proxy_host(), HttpBin.http_port()} 78 | ) 79 | 80 | assert Mint.HTTP.protocol(conn) == :http1 81 | end 82 | 83 | # Regression for #371 84 | test "Mint.HTTP.is_connection_message/2 works with unsafe proxy connections" do 85 | import Mint.HTTP, only: [is_connection_message: 2] 86 | 87 | assert {:ok, %UnsafeProxy{state: %{socket: socket}} = conn} = 88 | UnsafeProxy.connect( 89 | {:http, "localhost", 8888}, 90 | {:http, HttpBin.proxy_host(), HttpBin.http_port()} 91 | ) 92 | 93 | assert is_connection_message(conn, {:tcp, socket, "foo"}) == true 94 | assert is_connection_message(conn, {:tcp_closed, socket}) == true 95 | assert is_connection_message(conn, {:tcp_error, socket, :nxdomain}) == true 96 | 97 | assert is_connection_message(conn, {:tcp, :not_a_socket, "foo"}) == false 98 | assert is_connection_message(conn, {:tcp_closed, :not_a_socket}) == false 99 | 100 | assert is_connection_message(_conn = %UnsafeProxy{}, {:tcp, socket, "foo"}) == false 101 | 102 | # If the first argument is not a connection struct, we return false. 103 | assert is_connection_message(%{socket: socket}, {:tcp, socket, "foo"}) == false 104 | assert is_connection_message(%URI{}, {:tcp, socket, "foo"}) == false 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/support/empty_cacerts.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-mint/mint/763e70cdaf63bfec144b71c641206c02821b3210/test/support/empty_cacerts.pem -------------------------------------------------------------------------------- /test/support/mint/ca_store.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHDCCAgSgAwIBAgIIOv+oU5CKFNQwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UE 3 | AwwPRXhwaXJlZCBSb290IENBMB4XDTkxMDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1 4 | OVowGjEYMBYGA1UEAwwPRXhwaXJlZCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAzgT9rV+trFuWiiJ6PmEspLW70iX5khyWGTZJu745uQB8 6 | iba2al76dna05eItiwigPs6sAg3kWOeW6FOYeQzH89OlFfdKrDke2EazAz4aCghH 7 | pjRpTywQINAnT38Yn1UK63klC8a877s+NdzgE5jWAzKVuviY2NBKOqXVqg2fhh4m 8 | Bk1mYfzxmqeBoFRp/rVgz5fnNo78lkHsItdQPxi2GFiKjfWsyail1wz6GL4XCOpX 9 | 5KnulvDg124FaxUe4uuOWcRMTAtMBuYQ91gDLkcIpilpdfjnQOFfh067u1UGugw3 10 | clpVYHby0Wfgln2FHuNHCUOCkdEZEkslV/o7nhAqlQIDAQABo2YwZDASBgNVHRMB 11 | Af8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUFbNIcEBp4gRU 12 | LCQb5IUZJZyl+ykwHwYDVR0jBBgwFoAUFbNIcEBp4gRULCQb5IUZJZyl+ykwDQYJ 13 | KoZIhvcNAQELBQADggEBAApd3c0IMgguRiz0K2EnlgCst+a7x4LRLfAa4VSKzzDf 14 | knsR8BT16UahLT2b6jO/hG919tqokWqOC3CjC0PUp0C/zHKGI45waOJrryTfER+P 15 | spTQZFqTjVJIEhLOQi3nlnBynanrzkBlzH87YgSEzkmDudrJGw1Twar/ZJmtjpn7 16 | eGefvK9BH7PiM2ykHweIgWyzE+HU50CxLFzM2HOx5Tha8FCYgKukTehF0h1hM/Mn 17 | bSAPqcF1d40lcY28HTNbPLOIPKP66mEo3f1i6x5JVyQcrZZChUpsKl1CGmp1e/vy 18 | 9KPpXycvWIGzb6PmHBZ39XdKIv8JLBAdfPNHjuHmpa0= 19 | -----END CERTIFICATE----- 20 | 21 | -----BEGIN CERTIFICATE----- 22 | MIIDFjCCAf6gAwIBAgIIJ9/iR+IyCLwwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UE 23 | AwwLTmV3IFJvb3QgQ0EwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MTEyMzEyMzU5NTla 24 | MBYxFDASBgNVBAMMC05ldyBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 25 | MIIBCgKCAQEAxxrIbH4LRM87hwLN7CQbvauFsVgfSonoe2+P3RJcvpZhCwEPBUTB 26 | YRv15x+D2zJ/I25Pltx9e6X1QMSeBzifVYhELKBBkVB+AkHtOCp+cvUl8GrccZmC 27 | rKQUmPei/Z1/k+HvAhMzKKWNUgLXpRr+Nw+0uhxJoP90ynSxcjB14DrKHaC+Vu7h 28 | Fm/a8BGY+k2o0DKOmeKfMIcRmEhEwWwmRqBceBAmvCc6QJxHew/hCQe7/J9yJk6Y 29 | E5NjLUVFsJqt3EVfnWtVl0Ph8ruha7VioXFwA0zPN1qG84n66mBN+i86QlOugNFQ 30 | 8E9Mb6blgSry9x6cBUOGTASpfO9xsFWCXQIDAQABo2YwZDASBgNVHRMBAf8ECDAG 31 | AQH/AgEBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUy0Qn7LGa53gz9Hy1x9fE 32 | 93b20fwwHwYDVR0jBBgwFoAUy0Qn7LGa53gz9Hy1x9fE93b20fwwDQYJKoZIhvcN 33 | AQELBQADggEBADlWclXsV1iO6TzYF9Fs6LRJZthe2iHIYIh4e3rMLC8twpw1cyTq 34 | tTNRlQBadzZwhTaODjDB9oUPvu+cVfNt6T710tjb5kIgO9fCTIUmKqZ2NHRKdl5E 35 | kisdiqdIjmb4BLuMmm2i7zTYVAKfB9GTU6VeSud0FCn9PCMJgE3Cy4AE+zUEqw3i 36 | 4Nyd48SJO+z9fVkt852JJlYYodyuvhvW8Lk0HA2VQms7MvR7QP5PPOsfiB3ZxxVa 37 | fhR6yCqHaDsmE8/h4eA/jX3bJBgaDxkhgfdtVQhJzNzWOZj5OPM61LdZJFE4Ya7h 38 | ZpJO9CqeXHVhIecLFu7LTBIENXi672xgyxg= 39 | -----END CERTIFICATE----- 40 | -------------------------------------------------------------------------------- /test/support/mint/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICzjCCAbYCCQDM0i9xf9D8qTANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJJ 3 | VDELMAkGA1UECAwCUk0xDTALBgNVBAcMBFJvbWUwHhcNMTcxMjI4MTAzMTE1WhcN 4 | MTgxMjI4MTAzMTE1WjApMQswCQYDVQQGEwJJVDELMAkGA1UECAwCUk0xDTALBgNV 5 | BAcMBFJvbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6lBy3mpfB 6 | UrtclV/PFOM8OGiBNYVZmNJmZqCZCl2LQE5ekPrJ2Fh+zkcJcO19OxmseN+2F7VT 7 | zCYarty+h5ZXzNUmUcI2Ld60mfYwEfMjQRa4Tmp0K5PkphJ2gG9n9QOhFxky7KWz 8 | C84oe7Zm8iGni6wAQEEBOdo/qTCfGbPHzd39WUV+9Aft8HeDUcnpMhO6vXWDT3Yh 9 | 658p04rXLzj8auyAZpfSq61x9ZS4WQYWB5vRLsJ4/V51RVGfA5nJYFKJ+cwZR4Hz 10 | bTRVc34rXaZS9ggIp8ktqL14NO6jfo9/dRng4RcTmkKMxU+0pWdTNZ7iPJX46/xM 11 | 0XGxEd+7X4uHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABygqnZTQ4dsd5EFQKF7 12 | UT7ZfKi9a65e+iDGHikhUjHc+hSUMXFyP5RjpN6Z+4igi9LuWhJFZ0dqSZxDwCYG 13 | RdrMZSM/2yTBLKVdgcKXPuiV5eFPXHmYm39ru/WpNEqR/P28Q50xz/HJRoFhg3Qe 14 | AIlncG+v6AaUAKD8Qj6IZOLIVJuMaT8ONsDaa2LJiAz5uzKwgijEWiw7m83dvqGi 15 | FHkrj9/l5SQQVLGej/74Av+OFmMRI6nPc5lIu39atMRxsiPubrcQOVZmXZxRSEg8 16 | P7k3nBjtxCUhAnokjRqv/4rYfm8hvbqiRnL3rmtLlM1IF2L37nOqnfGo2NikjV+G 17 | 1hU= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/support/mint/chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHDCCAgSgAwIBAgIIOv+oU5CKFNQwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UE 3 | AwwPRXhwaXJlZCBSb290IENBMB4XDTkxMDEwMTAwMDAwMFoXDTIwMTIzMTIzNTk1 4 | OVowGjEYMBYGA1UEAwwPRXhwaXJlZCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEAzgT9rV+trFuWiiJ6PmEspLW70iX5khyWGTZJu745uQB8 6 | iba2al76dna05eItiwigPs6sAg3kWOeW6FOYeQzH89OlFfdKrDke2EazAz4aCghH 7 | pjRpTywQINAnT38Yn1UK63klC8a877s+NdzgE5jWAzKVuviY2NBKOqXVqg2fhh4m 8 | Bk1mYfzxmqeBoFRp/rVgz5fnNo78lkHsItdQPxi2GFiKjfWsyail1wz6GL4XCOpX 9 | 5KnulvDg124FaxUe4uuOWcRMTAtMBuYQ91gDLkcIpilpdfjnQOFfh067u1UGugw3 10 | clpVYHby0Wfgln2FHuNHCUOCkdEZEkslV/o7nhAqlQIDAQABo2YwZDASBgNVHRMB 11 | Af8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUFbNIcEBp4gRU 12 | LCQb5IUZJZyl+ykwHwYDVR0jBBgwFoAUFbNIcEBp4gRULCQb5IUZJZyl+ykwDQYJ 13 | KoZIhvcNAQELBQADggEBAApd3c0IMgguRiz0K2EnlgCst+a7x4LRLfAa4VSKzzDf 14 | knsR8BT16UahLT2b6jO/hG919tqokWqOC3CjC0PUp0C/zHKGI45waOJrryTfER+P 15 | spTQZFqTjVJIEhLOQi3nlnBynanrzkBlzH87YgSEzkmDudrJGw1Twar/ZJmtjpn7 16 | eGefvK9BH7PiM2ykHweIgWyzE+HU50CxLFzM2HOx5Tha8FCYgKukTehF0h1hM/Mn 17 | bSAPqcF1d40lcY28HTNbPLOIPKP66mEo3f1i6x5JVyQcrZZChUpsKl1CGmp1e/vy 18 | 9KPpXycvWIGzb6PmHBZ39XdKIv8JLBAdfPNHjuHmpa0= 19 | -----END CERTIFICATE----- 20 | 21 | -----BEGIN CERTIFICATE----- 22 | MIIDOzCCAiOgAwIBAgIICGNdx0mQQh0wDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UE 23 | AwwPRXhwaXJlZCBSb290IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIwNTExMjMxMjM1 24 | OTU5WjAWMRQwEgYDVQQDDAtOZXcgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD 25 | ggEPADCCAQoCggEBAMcayGx+C0TPO4cCzewkG72rhbFYH0qJ6Htvj90SXL6WYQsB 26 | DwVEwWEb9ecfg9syfyNuT5bcfXul9UDEngc4n1WIRCygQZFQfgJB7TgqfnL1JfBq 27 | 3HGZgqykFJj3ov2df5Ph7wITMyiljVIC16Ua/jcPtLocSaD/dMp0sXIwdeA6yh2g 28 | vlbu4RZv2vARmPpNqNAyjpninzCHEZhIRMFsJkagXHgQJrwnOkCcR3sP4QkHu/yf 29 | ciZOmBOTYy1FRbCardxFX51rVZdD4fK7oWu1YqFxcANMzzdahvOJ+upgTfovOkJT 30 | roDRUPBPTG+m5YEq8vcenAVDhkwEqXzvcbBVgl0CAwEAAaOBhjCBgzASBgNVHRMB 31 | Af8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcD 32 | AQYIKwYBBQUHAwIwHQYDVR0OBBYEFMtEJ+yxmud4M/R8tcfXxPd29tH8MB8GA1Ud 33 | IwQYMBaAFBWzSHBAaeIEVCwkG+SFGSWcpfspMA0GCSqGSIb3DQEBCwUAA4IBAQCp 34 | hs3difsZKhg5htEYyINdvcZKT2ujZVXGalrdFt7Jjlnn9OFPTNQQkJZ/nD1bnmJ2 35 | oalSa5Gd1XW89IQp0Jo0CcLDJ2fKWFH05Oj/gq35GZ6qqjydLfQ7yUuQdh+F7MEG 36 | IrJwyR/qRdIQJSspIr6HM1D6FAVbbztvQyMgfuTVfjDu+mG78J6R05Kc+SFmzob9 37 | o5evG2DIdgaYp+uwZ/7rSzSymsZIEXwMtbCPkeI1IVMC/pZbUHcbWsCrVMRasyHg 38 | mnIFeBrg0/fs9xXYHZ/shx7G1EfSuLkcAkk41sOcfH3cBSTWM3OqlbyCkdVMK1qU 39 | A3q4haNcXe3HkO1IfICx 40 | -----END CERTIFICATE----- 41 | 42 | -----BEGIN CERTIFICATE----- 43 | MIIDOTCCAiGgAwIBAgIIfYtvBLi9/sAwDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UE 44 | AwwLTmV3IFJvb3QgQ0EwHhcNMjAwMTAxMDAwMDAwWhcNMzkxMjMxMjM1OTU5WjAa 45 | MRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB 46 | DwAwggEKAoIBAQCrgTp/u3fM0ID0UqAjcOw2NwVCzlcZ7n79+6O1N82xBAZu2YhP 47 | aHuuJO6IAL845y+Kzx5S01FSuNGi/ULt0XMSzFAmG11fhJm6ee01NzocrPFofEeh 48 | 83zXH/3098Iu2PPh2P2L2tK0O3Or05EbFRnQd6JqBVmrfqnFZtzjzFEIP6jBNUhr 49 | +BlZN5FeC2oI5kjhg3DojolRy1duF7tj37S9c6vbKyjajcKkeOkRPhiPX2q/zTUc 50 | mcL8+q4Vv1eEKlgyVaXMZLJjF5S8xZ09Y9J6U6m8OqOeeCAVrYzC7EqLFMXQybOr 51 | NK5l4QCz/SOqCcNc31dtL/O1ujdrWPB3DQ/xAgMBAAGjgYYwgYMwEgYDVR0TAQH/ 52 | BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 53 | CCsGAQUFBwMCMB0GA1UdDgQWBBQK1jjin3KVPrIn8w9Gi0sfPHnRLzAfBgNVHSME 54 | GDAWgBTLRCfssZrneDP0fLXH18T3dvbR/DANBgkqhkiG9w0BAQsFAAOCAQEAFrfR 55 | puO3qfvyN57norKOL7UsLsNQwKVRCbfLuFwBITFJuflzSzTeY6byf0br/01wf8He 56 | 0Bbs7nKqWCC0FA8Feqield68ohFo8n0ZFNhWwq2ZB+oA9frJED2XHQ5JWbH1CXbZ 57 | mdk3jT0weWB+VnvsnKYXpD0x0CKdAM4I71ikH0fOiOnHtqRGFUJ11s7kb+Ns55du 58 | 9m5Gsw8ebc+vnEUVi7WZWBcgZdX7N3UDutLWnCEBBQxHdlQK2Q3M5gXRwQbDekEt 59 | 1QSpAD7keDagVQjbRj+Ly1hpWSpg0P6sTyf0QRdSG0kqkH/+K6syQOa4TofScKVJ 60 | nLJUkRMcKsvVd+weEg== 61 | -----END CERTIFICATE----- 62 | 63 | -----BEGIN CERTIFICATE----- 64 | MIIDKjCCAhKgAwIBAgIJAKwUSyPXOrFVMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV 65 | BAMMD0ludGVybWVkaWF0ZSBDQTAeFw0yMDAxMDEwMDAwMDBaFw0zOTEyMzEyMzU5 66 | NTlaMBExDzANBgNVBAMMBlNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 67 | AQoCggEBALRa/e8acLATiP4DpbR3+Hzz6TOZ+oPgedl/fxizd1BIKOYFuYA2y/QO 68 | y76xvkq5A4ZZSU0UG9RZ4fV6LBRr9rJBAS0SaoMWGRuw7HbDyHj0heLd8i9omfpd 69 | 29wEg6ok5WVIYDsEA9LRf/GfPw48i5x0uitafpcomt1onpBsajAoYWoITSgYXWU2 70 | Eqco73VFJtJHey7vkfFBwHc67g+9eNSM3FcsWLBB+E62ZeWF3+Pm/e7plac3/ywK 71 | 5672h5/wR9ihsv1bM0KCqFHWPjulq00TAYDVBTVMgA5SGN8HpBf6zAup302ozAck 72 | ad7CNTHPNtd3xtepv4VnEzTzKmFkc3kCAwEAAaN8MHowCQYDVR0TBAIwADAOBgNV 73 | HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1Ud 74 | DgQWBBTkfN4ApwUkcV56j0kBZMjGAD1AjzAfBgNVHSMEGDAWgBQK1jjin3KVPrIn 75 | 8w9Gi0sfPHnRLzANBgkqhkiG9w0BAQsFAAOCAQEAL2KvOziXCt5a9cdbN9TZntqV 76 | xJXElHzr9K9DGFg7YZaBMaYYlytih/9BQFy2tWkrI0UY+aVsndNhUpfOlCAJ7MSs 77 | X2sS8n2oZ0prvaKLzBtnkZpMPy9NwwfIrLp5S6sfFQv/IT7DBTv2+0n7y0thRdZ1 78 | 2OFQ5jNIXH6guSSHphDwBnYOz5ecsTbCvHcqt4eYykrZLnM7vw8+6QWY6rzJ4dsh 79 | GypEX0Zcog73Mgv2V4RGYogxkDKVD783fFjRm/NEfF6j67DjDaqgivhSjt8B/GQQ 80 | JRCuVxXz/K6gpy9kdI5zqi0nmYkQJsGxo4njLZ6fSHbhH4bFpgWtYf5mdrG3IA== 81 | -----END CERTIFICATE----- 82 | -------------------------------------------------------------------------------- /test/support/mint/dst_and_isrg.pem: -------------------------------------------------------------------------------- 1 | ### Digital Signature Trust Co. 2 | 3 | === /O=Digital Signature Trust Co./CN=DST Root CA X3 4 | Certificate: 5 | Data: 6 | Version: 3 (0x2) 7 | Serial Number: 8 | 44:af:b0:80:d6:a3:27:ba:89:30:39:86:2e:f8:40:6b 9 | Signature Algorithm: sha1WithRSAEncryption 10 | Validity 11 | Not Before: Sep 30 21:12:19 2000 GMT 12 | Not After : Sep 30 14:01:15 2021 GMT 13 | Subject: O=Digital Signature Trust Co., CN=DST Root CA X3 14 | X509v3 extensions: 15 | X509v3 Basic Constraints: critical 16 | CA:TRUE 17 | X509v3 Key Usage: critical 18 | Certificate Sign, CRL Sign 19 | X509v3 Subject Key Identifier: 20 | C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10 21 | SHA1 Fingerprint=DA:C9:02:4F:54:D8:F6:DF:94:93:5F:B1:73:26:38:CA:6A:D7:7C:13 22 | SHA256 Fingerprint=06:87:26:03:31:A7:24:03:D9:09:F1:05:E6:9B:CF:0D:32:E1:BD:24:93:FF:C6:D9:20:6D:11:BC:D6:77:07:39 23 | -----BEGIN CERTIFICATE----- 24 | MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ 25 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT 26 | DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow 27 | PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD 28 | Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 29 | AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O 30 | rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq 31 | OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b 32 | xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 33 | 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD 34 | aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV 35 | HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG 36 | SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 37 | ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr 38 | AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz 39 | R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 40 | JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo 41 | Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ 42 | -----END CERTIFICATE----- 43 | 44 | ### Internet Security Research Group 45 | 46 | === /C=US/O=Internet Security Research Group/CN=ISRG Root X1 47 | Certificate: 48 | Data: 49 | Version: 3 (0x2) 50 | Serial Number: 51 | 82:10:cf:b0:d2:40:e3:59:44:63:e0:bb:63:82:8b:00 52 | Signature Algorithm: sha256WithRSAEncryption 53 | Validity 54 | Not Before: Jun 4 11:04:38 2015 GMT 55 | Not After : Jun 4 11:04:38 2035 GMT 56 | Subject: C=US, O=Internet Security Research Group, CN=ISRG Root X1 57 | X509v3 extensions: 58 | X509v3 Key Usage: critical 59 | Certificate Sign, CRL Sign 60 | X509v3 Basic Constraints: critical 61 | CA:TRUE 62 | X509v3 Subject Key Identifier: 63 | 79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E 64 | SHA1 Fingerprint=CA:BD:2A:79:A1:07:6A:31:F2:1D:25:36:35:CB:03:9D:43:29:A5:E8 65 | SHA256 Fingerprint=96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6 66 | -----BEGIN CERTIFICATE----- 67 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw 68 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 69 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 70 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu 71 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY 72 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc 73 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 74 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U 75 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW 76 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH 77 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC 78 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv 79 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn 80 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn 81 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw 82 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI 83 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV 84 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq 85 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL 86 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 87 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK 88 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 89 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur 90 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC 91 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc 92 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 93 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA 94 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d 95 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= 96 | -----END CERTIFICATE----- 97 | -------------------------------------------------------------------------------- /test/support/mint/http1/test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.TestHelpers do 2 | import ExUnit.Assertions 3 | 4 | def merge_body(responses, request) do 5 | merge_body(responses, request, "") 6 | end 7 | 8 | defp merge_body([{:data, request, new_body} | responses], request, body) do 9 | merge_body(responses, request, body <> new_body) 10 | end 11 | 12 | defp merge_body([{:done, request}], request, body) do 13 | body 14 | end 15 | 16 | def merge_body_with_trailers(responses, request) do 17 | merge_body_with_trailers(responses, request, "") 18 | end 19 | 20 | defp merge_body_with_trailers([{:data, request, new_body} | responses], request, body) do 21 | merge_body_with_trailers(responses, request, body <> new_body) 22 | end 23 | 24 | defp merge_body_with_trailers([{:headers, request, trailers}, {:done, request}], request, body) do 25 | {body, trailers} 26 | end 27 | 28 | def merge_pipelined_body(responses, request) do 29 | merge_pipelined_body(responses, request, "") 30 | end 31 | 32 | defp merge_pipelined_body([{:data, request, new_body} | responses], request, body) do 33 | merge_pipelined_body(responses, request, body <> new_body) 34 | end 35 | 36 | defp merge_pipelined_body([{:done, request} | rest], request, body) do 37 | {body, rest} 38 | end 39 | 40 | def receive_stream(conn) do 41 | receive do 42 | {:rest, previous} -> 43 | maybe_done(conn, previous) 44 | after 45 | 0 -> 46 | receive_stream(conn, []) 47 | end 48 | end 49 | 50 | def receive_stream(conn, acc) do 51 | socket = Mint.HTTP.get_socket(conn) 52 | 53 | receive do 54 | {tag, ^socket, _data} = message when tag in [:tcp, :ssl] -> 55 | assert {:ok, conn, responses} = conn.__struct__.stream(conn, message) 56 | maybe_done(conn, acc ++ responses) 57 | 58 | {tag, ^socket} = message when tag in [:tcp_closed, :ssl_closed] -> 59 | assert {:ok, conn, responses} = conn.__struct__.stream(conn, message) 60 | maybe_done(conn, acc ++ responses) 61 | 62 | {tag, ^socket, _reason} = message when tag in [:tcp_error, :ssl_error] -> 63 | assert {:error, _conn, _reason, _responses} = conn.__struct__.stream(conn, message) 64 | after 65 | 10000 -> 66 | flunk("receive_stream timeout") 67 | end 68 | end 69 | 70 | def maybe_done(conn, responses) do 71 | {all, rest} = Enum.split_while(responses, &(not match?({:done, _}, &1))) 72 | 73 | case {all, rest} do 74 | {all, []} -> 75 | receive_stream(conn, all) 76 | 77 | {all, [done | rest]} -> 78 | if rest != [], do: send(self(), {:rest, rest}) 79 | {:ok, conn, all ++ [done]} 80 | end 81 | end 82 | 83 | def get_header(headers, name) do 84 | for {n, v} <- headers, n == name, do: v 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/support/mint/http1/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP1.TestServer do 2 | def start do 3 | {:ok, listen_socket} = :gen_tcp.listen(0, mode: :binary, packet: :raw) 4 | server_ref = make_ref() 5 | parent = self() 6 | 7 | spawn_link(fn -> loop(listen_socket, parent, server_ref) end) 8 | 9 | with {:ok, port} <- :inet.port(listen_socket) do 10 | {:ok, port, server_ref} 11 | end 12 | end 13 | 14 | defp loop(listen_socket, parent, server_ref) do 15 | case :gen_tcp.accept(listen_socket) do 16 | {:ok, socket} -> 17 | send(parent, {server_ref, socket}) 18 | 19 | # :einval started showing up with Erlang 23 and Ubuntu 18. 20 | case :gen_tcp.controlling_process(socket, parent) do 21 | :ok -> :ok 22 | {:error, :einval} -> :ok 23 | end 24 | 25 | loop(listen_socket, parent, server_ref) 26 | 27 | {:error, :closed} -> 28 | :ok 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/mint/http2/test_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP2.TestHelpers do 2 | import ExUnit.Assertions 3 | 4 | @spec receive_stream(Mint.HTTP2.t()) :: {:ok, Mint.HTTP2.t(), [Mint.Types.response()]} 5 | def receive_stream(%Mint.HTTP2{} = conn) do 6 | receive_stream(conn, []) 7 | end 8 | 9 | defp receive_stream(conn, responses) do 10 | assert_receive message, 10_000 11 | 12 | {tag, closed_tag, error_tag} = 13 | case conn.transport do 14 | Mint.Core.Transport.TCP -> {:tcp, :tcp_closed, :tcp_error} 15 | Mint.Core.Transport.SSL -> {:ssl, :ssl_closed, :ssl_error} 16 | end 17 | 18 | case message do 19 | {:rest, conn, rest_responses} -> 20 | maybe_done(conn, rest_responses, responses) 21 | 22 | {^tag, _socket, _data} = message -> 23 | assert {:ok, %Mint.HTTP2{} = conn, new_responses} = Mint.HTTP2.stream(conn, message) 24 | maybe_done(conn, new_responses, responses) 25 | 26 | {^closed_tag, _socket} = message -> 27 | assert {:error, %Mint.HTTP2{}, :closed} = Mint.HTTP2.stream(conn, message) 28 | 29 | {^error_tag, _reason} = message -> 30 | assert {:error, %Mint.HTTP2{}, _reason} = Mint.HTTP2.stream(conn, message) 31 | 32 | other -> 33 | flunk("Received unexpected message: #{inspect(other)}") 34 | end 35 | end 36 | 37 | defp maybe_done(conn, [{:done, _} = done | rest], acc) do 38 | if rest != [] do 39 | send(self(), {:rest, conn, rest}) 40 | end 41 | 42 | {:ok, conn, acc ++ [done]} 43 | end 44 | 45 | defp maybe_done(conn, [{:pong, _} = pong_resp | rest], acc) do 46 | if rest != [] do 47 | send(self(), {:rest, conn, rest}) 48 | end 49 | 50 | {:ok, conn, acc ++ [pong_resp]} 51 | end 52 | 53 | defp maybe_done(conn, [resp | rest], acc) do 54 | maybe_done(conn, rest, acc ++ [resp]) 55 | end 56 | 57 | defp maybe_done(conn, [], acc) do 58 | receive_stream(conn, acc) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/support/mint/http2/test_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HTTP2.TestServer do 2 | import ExUnit.Assertions 3 | import Mint.HTTP2.Frame, only: [settings: 1, goaway: 1, ping: 1] 4 | 5 | alias Mint.HTTP2.Frame 6 | 7 | defstruct [:socket, :encode_table, :decode_table] 8 | 9 | @ssl_opts [ 10 | mode: :binary, 11 | packet: :raw, 12 | active: false, 13 | reuseaddr: true, 14 | next_protocols_advertised: ["h2"], 15 | alpn_preferred_protocols: ["h2"], 16 | certfile: Path.absname("../certificate.pem", __DIR__), 17 | keyfile: Path.absname("../key.pem", __DIR__) 18 | ] 19 | 20 | @recv_timeout 300 21 | 22 | @spec new(:ssl.sslsocket()) :: %__MODULE__{} 23 | def new(socket) do 24 | %__MODULE__{ 25 | socket: socket, 26 | encode_table: HPAX.new(4096), 27 | decode_table: HPAX.new(4096) 28 | } 29 | end 30 | 31 | @spec recv_next_frames(%__MODULE__{}, pos_integer()) :: [frame :: term(), ...] 32 | def recv_next_frames(%__MODULE__{} = server, frame_count) when frame_count > 0 do 33 | recv_next_frames(server, frame_count, [], "") 34 | end 35 | 36 | defp recv_next_frames(_server, 0, frames, buffer) do 37 | if buffer == "" do 38 | Enum.reverse(frames) 39 | else 40 | flunk(""" 41 | Expected no more data, got: #{inspect(buffer)} 42 | This decodes to: #{inspect(Frame.decode_next(buffer))}} 43 | """) 44 | end 45 | end 46 | 47 | defp recv_next_frames(%{socket: server_socket} = server, n, frames, buffer) do 48 | assert_receive {:ssl, ^server_socket, data}, 49 | @recv_timeout, 50 | "Expected to receive another #{n} frames from the server, but got no data after #{@recv_timeout}ms" 51 | 52 | decode_next_frames(server, n, frames, buffer <> data) 53 | end 54 | 55 | defp decode_next_frames(_server, 0, frames, buffer) do 56 | if buffer == "" do 57 | Enum.reverse(frames) 58 | else 59 | flunk(""" 60 | Expected no more data, got: #{inspect(buffer)} 61 | This decodes to: #{inspect(Frame.decode_next(buffer))}} 62 | """) 63 | end 64 | end 65 | 66 | defp decode_next_frames(server, n, frames, data) do 67 | case Frame.decode_next(data) do 68 | {:ok, frame, rest} -> 69 | decode_next_frames(server, n - 1, [frame | frames], rest) 70 | 71 | :more -> 72 | recv_next_frames(server, n, frames, data) 73 | 74 | other -> 75 | flunk("Error decoding frame: #{inspect(other)}") 76 | end 77 | end 78 | 79 | @spec encode_frames(%__MODULE__{}, [frame :: term(), ...]) :: {%__MODULE__{}, binary()} 80 | def encode_frames(%__MODULE__{} = server, frames) when is_list(frames) and frames != [] do 81 | import Mint.HTTP2.Frame, only: [headers: 1] 82 | 83 | {data, server} = 84 | Enum.map_reduce(frames, server, fn 85 | {frame_type, stream_id, headers, flags}, server 86 | when frame_type in [:headers, :push_promise] -> 87 | {server, hbf} = encode_headers(server, headers) 88 | flags = Frame.set_flags(frame_type, flags) 89 | frame = headers(stream_id: stream_id, hbf: hbf, flags: flags) 90 | {Frame.encode(frame), server} 91 | 92 | frame, server -> 93 | {Frame.encode(frame), server} 94 | end) 95 | 96 | {server, IO.iodata_to_binary(data)} 97 | end 98 | 99 | @spec encode_headers(%__MODULE__{}, Mint.Types.headers()) :: {%__MODULE__{}, hbf :: binary()} 100 | def encode_headers(%__MODULE__{} = server, headers) when is_list(headers) do 101 | headers = for {name, value} <- headers, do: {:store_name, name, value} 102 | {hbf, encode_table} = HPAX.encode(headers, server.encode_table) 103 | server = put_in(server.encode_table, encode_table) 104 | {server, IO.iodata_to_binary(hbf)} 105 | end 106 | 107 | @spec decode_headers(%__MODULE__{}, binary()) :: {%__MODULE__{}, Mint.Types.headers()} 108 | def decode_headers(%__MODULE__{} = server, hbf) when is_binary(hbf) do 109 | assert {:ok, headers, decode_table} = HPAX.decode(hbf, server.decode_table) 110 | server = put_in(server.decode_table, decode_table) 111 | {server, headers} 112 | end 113 | 114 | @spec listen_and_accept() :: {:ok, :inet.port_number(), Task.t()} 115 | def listen_and_accept do 116 | {:ok, listen_socket} = :ssl.listen(0, @ssl_opts) 117 | {:ok, {_address, port}} = :ssl.sockname(listen_socket) 118 | parent = self() 119 | 120 | task = 121 | Task.async(fn -> 122 | # Let's accept a new connection. 123 | {:ok, socket} = :ssl.transport_accept(listen_socket) 124 | 125 | if function_exported?(:ssl, :handshake, 1) do 126 | {:ok, _} = apply(:ssl, :handshake, [socket]) 127 | else 128 | :ok = apply(:ssl, :ssl_accept, [socket]) 129 | end 130 | 131 | :ok = :ssl.controlling_process(socket, parent) 132 | {:ok, socket} 133 | end) 134 | 135 | {:ok, port, task} 136 | end 137 | 138 | connection_preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 139 | 140 | @spec perform_http2_handshake(:ssl.sslsocket()) :: :ok 141 | def perform_http2_handshake(socket) do 142 | no_flags = Frame.set_flags(:settings, []) 143 | 144 | # First we get the connection preface. 145 | {:ok, unquote(connection_preface) <> rest} = :ssl.recv(socket, 0, 100) 146 | 147 | # Then we get a SETTINGS frame. 148 | assert {:ok, frame, ""} = Frame.decode_next(rest) 149 | assert settings(flags: ^no_flags, params: _params) = frame 150 | 151 | :ok 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/support/mint/http_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.HttpBin do 2 | def host() do 3 | "localhost" 4 | end 5 | 6 | def proxy_host() do 7 | # the proxy runs in docker so we use the 8 | # docker compose name to connect 9 | "caddyhttpbin" 10 | end 11 | 12 | def http_port() do 13 | 8080 14 | end 15 | 16 | def https_port() do 17 | 8443 18 | end 19 | 20 | def https_transport_opts() do 21 | [cacertfile: "caddy_storage/pki/authorities/local/root.crt"] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/support/mint/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6lBy3mpfBUrtc 3 | lV/PFOM8OGiBNYVZmNJmZqCZCl2LQE5ekPrJ2Fh+zkcJcO19OxmseN+2F7VTzCYa 4 | rty+h5ZXzNUmUcI2Ld60mfYwEfMjQRa4Tmp0K5PkphJ2gG9n9QOhFxky7KWzC84o 5 | e7Zm8iGni6wAQEEBOdo/qTCfGbPHzd39WUV+9Aft8HeDUcnpMhO6vXWDT3Yh658p 6 | 04rXLzj8auyAZpfSq61x9ZS4WQYWB5vRLsJ4/V51RVGfA5nJYFKJ+cwZR4HzbTRV 7 | c34rXaZS9ggIp8ktqL14NO6jfo9/dRng4RcTmkKMxU+0pWdTNZ7iPJX46/xM0XGx 8 | Ed+7X4uHAgMBAAECggEAW+VNi6UJ778m5z/vU5iPH38NAe7xgiLCJouPuDEhx89h 9 | ijRQQZBcbgB9fonvfwnX6FoUnaRpvB9F+Uh9Ex7HDvGlXl1Qkczf7wYR+rUskwWh 10 | AiAlUJiSHEErwNAbjxFfuz0cPTfPmTNMVCYyvduudc5WZj0/hzIOa+KSPxqysMq+ 11 | kVEuoKm4yzVpbsvfVjGFhYyetFfqbURZ1d0EONGNiYCtwVPTP+gvcjeMTdgGMno4 12 | cpPHFF5RYsfnG/koaARHbOBtFrnkERpmABfsSL6DzwWtQys/ghxwaL2ZhjhdgbXO 13 | 9vjstnXso9mjCAxVZaATyCs6QKXCn//T9BLRkutz+QKBgQDhlwuz2Df7TZwsCOqM 14 | PoYn0GhsY/hHupifEiWvDDtkGCAPuiw48HtCe1aCXFCtbSN/6E3Wdm7ZnG2q7RMY 15 | R5jxEEhnrBGEN7JQhAZPdktndVbupOXMgnGdiz/7nwHPCCghr3X/mdPWY0isMqx/ 16 | T7Bl1bv2C5ipOAqHUER7AKd6awKBgQDTus7n3mwYQusGBqhygmpqOr/5JmTjOcFb 17 | fGbyuOE9JntLSGR6mJhlfOYQiESvFhBZEOtw4eDh/n7r8LnV48x6D009466hBY3+ 18 | Fs5jTq+Ah2a9gxCRhSfUdEXhrJct+YHEjI+BNvXlUs/2D/y3rvc/2f+qa6nzegTI 19 | 1NcmtlEyVQKBgE2dPjV+KqSXqyerWacuy9Fe7s58BqwHEwOHptd3CegCNOW0VAqz 20 | EnVpIfZv9IH2jsQvFLi4vqK4IzMvpeYwm/o0c/TXSp+G2h7BjbpBJOhPgr1Qlo+q 21 | QZTGmBjmOCUW1VfhmmN6dVvJhPNZ6+dRb4tZ4fVhQADYeybbAvSe4QBJAoGAbn1V 22 | 6/pOPnrtWr+ut9MG5VizRbmbfFhvZuaMcq24HMkwHiExDikDnjKHfKkf7p58+X2y 23 | 372ANW8xnL6Ku+uckTXbASkHwE+9wZL1MS2muFPwcYUr6ESsfFoQ/aurWPqTlZYk 24 | bTHZMEr+61F8d/5+WHvSx4RXtA9A3+zyOel6heECgYEAxqgiod1asd9g6Ao0vQ5Q 25 | YIQihF9Qq15stxj5H0qnBskvDKwS+He8GKjMUJjJss0gD43KCYhRvE8bmMD+rgVJ 26 | oy8Hp5oGeMVUHMG7ly8vdAj01RWxU2oIL5wlk3hX+4Pb6XyAN+NuCTMCQ8XukfTf 27 | AyfpMLycjN7uWN86CdTv/YY= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/support/mint/pkix_verify_hostname_cn.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICsjCCAhugAwIBAgIJAMCGx1ezaJFRMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV 3 | BAYTAlNFMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEaMBgGA1UEAwwRKi5mb28uZXhh 4 | bXBsZS5jb20xHDAaBgNVBAMME2EqYi5iYXIuZXhhbXBsZS5jb20xEzARBgNVBAoM 5 | CmVybGFuZy5vcmcwHhcNMTYxMjIwMTUwNDUyWhcNMTcwMTE5MTUwNDUyWjByMQsw 6 | CQYDVQQGEwJTRTEUMBIGA1UEAwwLZXhhbXBsZS5jb20xGjAYBgNVBAMMESouZm9v 7 | LmV4YW1wbGUuY29tMRwwGgYDVQQDDBNhKmIuYmFyLmV4YW1wbGUuY29tMRMwEQYD 8 | VQQKDAplcmxhbmcub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVGJgZ 9 | defGucvMXf0RrEm6Hb18IfVUo9IV6swSP/kwAu/608ZIZdzlfp2pxC0e72a4E3WN 10 | 4vrGxAr2wMMQOiyoy4qlAeLX27THJ6Q4Vl82fc6QuOJbScKIydSZ4KoB+luGlBu5 11 | b6xYh2pBbneKFpsecmK5rsWtTactjD4n1tKjUwIDAQABo1AwTjAdBgNVHQ4EFgQU 12 | OCtzidUeaDva7qp12T0CQrgfLW4wHwYDVR0jBBgwFoAUOCtzidUeaDva7qp12T0C 13 | QrgfLW4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCAz+ComCMo9Qbu 14 | PHxG7pv3mQvoxrMFva/Asg4o9mW2mDyrk0DwI4zU8vMHbSRKSBYGm4TATXsQkDQT 15 | gJw/bxhISnhZZtPC7Yup8kJCkJ6S6EDLYrlzgsRqfeU6jWim3nbfaLyMi9dHFDMk 16 | HULnyNNW3qxTEKi8Wo2sCMej4l7KFg== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/support/mint/pkix_verify_hostname_subjAltName.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICEjCCAXugAwIBAgIJANwliLph5EiAMA0GCSqGSIb3DQEBCwUAMCMxCzAJBgNV 3 | BAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNjEyMjAxNTEyMjRaFw0x 4 | NzAxMTkxNTEyMjRaMCMxCzAJBgNVBAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNv 5 | bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAydstIN157w8QxkVaOl3wm81j 6 | fgZ8gqO3BXkECPF6bw5ewLlmePL6Qs4RypsaRe7cKJ9rHFlwhpdcYkxWSWEt2N7Z 7 | Ry3N4SjuU04ohWbYgy3ijTt7bJg7jOV1Dh56BnI4hwhQj0oNFizNZOeRRfEzdMnS 8 | +uk03t/Qre2NS7KbwnUCAwEAAaNOMEwwSgYDVR0RBEMwQYIOa2IuZXhhbXBsZS5v 9 | cmeGFmh0dHA6Ly93d3cuZXhhbXBsZS5vcmeGF2h0dHBzOi8vd3dzLmV4YW1wbGUu 10 | b3JnMA0GCSqGSIb3DQEBCwUAA4GBAKqFqW5gCso422bXriCBJoygokOTTOw1Rzpq 11 | K8Mm0B8W9rrW9OTkoLEcjekllZcUCZFin2HovHC5HlHZz+mQvBI1M6sN2HVQbSzS 12 | EgL66U9gwJVnn9/U1hXhJ0LO28aGbyE29DxnewNR741dWN3oFxCdlNaO6eMWaEsO 13 | gduJ5sDl 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /test/support/mint/pkix_verify_hostname_subjAltName_IP.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICBzCCAXCgAwIBAgIJAJgbo5FL73LuMA0GCSqGSIb3DQEBCwUAMCMxCzAJBgNV 3 | BAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNzEwMTExMDM0NDJaFw0x 4 | NzExMTAxMDM0NDJaMCMxCzAJBgNVBAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNv 5 | bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5muN8NIRHuqXgtAFpaJ4EPnd 6 | SD+hnzMiiWQ9qAsS8P4xFsl5aNH74BTgst6Rcq33qAw+4BtKFXMt7JbWMuZklFV3 7 | fzRSx099MVJSH3f2LDMNLfyDiSJnhBEv1rLPaosi91ZLvI5LiGTxzRLi3qftZBft 8 | Ryw1OempB4chLcBy2rsCAwEAAaNDMEEwPwYDVR0RBDgwNoIHMS4yLjMuNIcECkMQ 9 | S4cQq80A7wAAAAAAAAAAAAAAAYYTaHR0cHM6Ly8xMC4xMS4xMi4xMzANBgkqhkiG 10 | 9w0BAQsFAAOBgQDMn8aqs/5FkkWhspvN2n+D2l87M+33a5My54ZVZhayZ/KRmhCN 11 | Gix/BiVYJ3UlmWmGcnQXb3MLt/LQHaD3S2whDaLN3xJ8BbnX7A4ZTybitdyeFhDw 12 | K3iDVUM3bSsBJ4EcBPWIMnow3ALP5HlGRMlH/87Qt+uVPXuwNh9pmyIhRQ== 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /test/support/mint/test_socket_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mint.TestSocketServer do 2 | @ssl_opts [ 3 | active: false, 4 | reuseaddr: true, 5 | nodelay: true, 6 | certfile: Path.expand("certificate.pem", __DIR__), 7 | keyfile: Path.expand("key.pem", __DIR__) 8 | ] 9 | 10 | def start(options) when is_list(options) do 11 | ssl? = Keyword.fetch!(options, :ssl) 12 | socket_path = Path.join(System.tmp_dir!(), "mint_http1_test_socket_server.sock") 13 | 14 | _ = File.rm(socket_path) 15 | 16 | server_ref = make_ref() 17 | parent = self() 18 | 19 | {:ok, listen_socket} = listen(socket_path, ssl?) 20 | 21 | spawn_link(fn -> 22 | {:ok, socket} = accept(listen_socket, ssl?) 23 | send(parent, {server_ref, socket}) 24 | :ok = Process.sleep(:infinity) 25 | end) 26 | 27 | {:ok, {:local, socket_path}, server_ref} 28 | end 29 | 30 | defp listen(socket_path, _ssl? = false) do 31 | :gen_tcp.listen(0, mode: :binary, packet: :raw, ifaddr: {:local, socket_path}) 32 | end 33 | 34 | defp listen(socket_path, _ssl? = true) do 35 | opts = [mode: :binary, packet: :raw, ifaddr: {:local, socket_path}] 36 | :ssl.listen(0, opts ++ @ssl_opts) 37 | end 38 | 39 | defp accept(listen_socket, _ssl? = false) do 40 | {:ok, _socket} = :gen_tcp.accept(listen_socket) 41 | end 42 | 43 | defp accept(listen_socket, _ssl? = true) do 44 | {:ok, socket} = :ssl.transport_accept(listen_socket) 45 | 46 | if function_exported?(:ssl, :handshake, 1) do 47 | {:ok, _} = apply(:ssl, :handshake, [socket]) 48 | else 49 | :ok = apply(:ssl, :ssl_accept, [socket]) 50 | end 51 | 52 | {:ok, socket} 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/support/mint/wildcard_san.pem: -------------------------------------------------------------------------------- 1 | # Retrieved from outlook.office365.com on 18/05/2018 2 | 3 | # X509v3 Subject Alternative Name: 4 | # DNS:*.clo.footprintdns.com, DNS:*.nrb.footprintdns.com, DNS:*.hotmail.com, 5 | # DNS:*.internal.outlook.com, DNS:*.live.com, DNS:*.office.com, 6 | # DNS:*.office365.com, DNS:*.outlook.com, DNS:*.outlook.office365.com, 7 | # DNS:attachment.outlook.live.net, DNS:attachment.outlook.office.net, 8 | # DNS:attachment.outlook.officeppe.net, DNS:ccs.login.microsoftonline.com, 9 | # DNS:ccs-sdf.login.microsoftonline.com, DNS:hotmail.com, 10 | # DNS:mail.services.live.com, DNS:office365.com, DNS:outlook.com, 11 | # DNS:outlook.office.com, DNS:substrate.office.com, 12 | # DNS:substrate-sdf.office.com 13 | 14 | -----BEGIN CERTIFICATE----- 15 | MIIG/jCCBeagAwIBAgIQDs2Q7J6KkeHe1d6ecU8P9DANBgkqhkiG9w0BAQsFADBL 16 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSUwIwYDVQQDExxE 17 | aWdpQ2VydCBDbG91ZCBTZXJ2aWNlcyBDQS0xMB4XDTE3MDkxMzAwMDAwMFoXDTE4 18 | MDkxMzEyMDAwMFowajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x 19 | EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv 20 | bjEUMBIGA1UEAxMLb3V0bG9vay5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 21 | ggEKAoIBAQC38AIorGo1Jr6unjupBpQXrKPztgJyyuQIz5WywwxDow4FPx6sdOni 22 | GSRKbaBpokFQ0sBaAw3rzJNBGlNRm2dEdgLsvW+lUbSixKz6W+beVYjbEIsKTRL0 23 | 2YCxMfkgH8pCiLn7hxZ+iizkC4czbtMQ3FYN4f8gr7Lg+T++Pe86p7p2nGylGH8f 24 | aQvdEeWAPWZv9Xn2Yzd198n2trCIjRfG+EAtWIYf2+D3Rt7WKz7jL8Jtw/7aBF9r 25 | BYRp0ZDeSBqZmh5hxuQv8SEkykRRXW9I9hTr+qj0MDmvgoqlDnGn2Eq8wfRxe3KL 26 | tlFkvJCYnB/4geNzQXd0qWF0t1ek9uSDAgMBAAGjggO9MIIDuTAfBgNVHSMEGDAW 27 | gBTdUdCiMXOpc66PtAF+XYxXy5/w9zAdBgNVHQ4EFgQU/LTuF7+AgdxpZFZ7/uCm 28 | hm21hNQwggHcBgNVHREEggHTMIIBz4IWKi5jbG8uZm9vdHByaW50ZG5zLmNvbYIW 29 | Ki5ucmIuZm9vdHByaW50ZG5zLmNvbYINKi5ob3RtYWlsLmNvbYIWKi5pbnRlcm5h 30 | bC5vdXRsb29rLmNvbYIKKi5saXZlLmNvbYIMKi5vZmZpY2UuY29tgg8qLm9mZmlj 31 | ZTM2NS5jb22CDSoub3V0bG9vay5jb22CFyoub3V0bG9vay5vZmZpY2UzNjUuY29t 32 | ghthdHRhY2htZW50Lm91dGxvb2subGl2ZS5uZXSCHWF0dGFjaG1lbnQub3V0bG9v 33 | ay5vZmZpY2UubmV0giBhdHRhY2htZW50Lm91dGxvb2sub2ZmaWNlcHBlLm5ldIId 34 | Y2NzLmxvZ2luLm1pY3Jvc29mdG9ubGluZS5jb22CIWNjcy1zZGYubG9naW4ubWlj 35 | cm9zb2Z0b25saW5lLmNvbYILaG90bWFpbC5jb22CFm1haWwuc2VydmljZXMubGl2 36 | ZS5jb22CDW9mZmljZTM2NS5jb22CC291dGxvb2suY29tghJvdXRsb29rLm9mZmlj 37 | ZS5jb22CFHN1YnN0cmF0ZS5vZmZpY2UuY29tghhzdWJzdHJhdGUtc2RmLm9mZmlj 38 | ZS5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF 39 | BQcDAjCBjQYDVR0fBIGFMIGCMD+gPaA7hjlodHRwOi8vY3JsMy5kaWdpY2VydC5j 40 | b20vRGlnaUNlcnRDbG91ZFNlcnZpY2VzQ0EtMS1nMS5jcmwwP6A9oDuGOWh0dHA6 41 | Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydENsb3VkU2VydmljZXNDQS0xLWcx 42 | LmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUFBwIBFhxodHRw 43 | czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjB8BggrBgEFBQcBAQRw 44 | MG4wJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NweC5kaWdpY2VydC5jb20wRQYIKwYB 45 | BQUHMAKGOWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydENsb3Vk 46 | U2VydmljZXNDQS0xLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IB 47 | AQBipSHmHGuSR5TgrmqCORJMtwvNEUhRQ3lMy6h/CBqCS/0dKVw8J28cVoQBY8Z4 48 | lRDx4vTHGQiUzHY6EwgJ5iGRqgxUX/NmO4dJnC7IpKbdY/v1b9KKGzFpw27OkXqk 49 | nGhseM2tJfwa2HMwUpuuo5029u4Dd40qvD0cMz33cOvBLRGkTPbXCFw24ZBdQrkt 50 | SC5TAWzHFyT2tLC17LeSb7d0g+fuj41L6y4a9och8cPiv9IAP4sftzYupO99h4qg 51 | 7UXP7o3AOOGqrPS3INhO4068Z63indstanIHYM0IUHa3A2xrcz7ZbEuw1HiGH/Ba 52 | HMz/gTSd2c0BXNiPeM7gdOK3 53 | -----END CERTIFICATE----- 54 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: :proxy) 2 | Application.ensure_all_started(:ssl) 3 | Logger.configure(level: :info) 4 | 5 | Mox.defmock(TransportMock, for: Mint.Core.Transport) 6 | --------------------------------------------------------------------------------